@picovoice/porcupine-node
Version:
Picovoice Porcupine Node.js binding
248 lines (215 loc) • 7.26 kB
text/typescript
//
// Copyright 2020-2023 Picovoice Inc.
//
// You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE"
// file accompanying this source.
//
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.
//
;
import * as fs from "fs";
import Porcupine from "../src/porcupine";
import { checkWaveFile, getInt16Frames } from "../src/wave_util";
import { WaveFile } from "wavefile";
import { PorcupineInvalidArgumentError } from "../src/errors";
import { BuiltinKeyword } from "../src/builtin_keywords";
import {
getAudioFileByLanguage,
getKeywordPathsByLanguage,
getModelPathByLanguage,
getMultipleKeywordParameters,
getSingleKeywordParameters
} from "./test_utils";
const SINGLE_KEYWORD_PARAMETERS = getSingleKeywordParameters();
const MULTIPLE_KEYWORDS_PARAMETERS = getMultipleKeywordParameters();
const ACCESS_KEY = process.argv.filter(x => x.startsWith('--access_key='))[0]?.split('--access_key=')[1] ?? "";
function testPorcupineDetection(
language: string,
keywords: string[] | BuiltinKeyword[],
groundTruth: number[] | number,
audioFileName: string | null = null): void {
const keywordPaths = keywords.map(keyword => getKeywordPathsByLanguage(language, keyword)
);
const modelPath = getModelPathByLanguage(language);
const engineInstance = new Porcupine(
ACCESS_KEY,
keywordPaths,
Array(keywordPaths.length).fill(0.5),
modelPath,
);
const waveFilePath = getAudioFileByLanguage(language, audioFileName);
const waveBuffer = fs.readFileSync(waveFilePath);
const waveAudioFile = new WaveFile(waveBuffer);
if (!checkWaveFile(waveAudioFile, engineInstance.sampleRate)) {
fail(
"Audio file did not meet requirements. Wave file must be 16KHz, 16-bit, linear PCM (mono)."
);
}
const frames = getInt16Frames(waveAudioFile, engineInstance.frameLength);
for (let iteration = 0; iteration < 2; iteration++) {
const results = [];
for (let i = 0; i < frames.length; i++) {
const frame = frames[i];
const keywordIndex = engineInstance.process(frame);
if (keywordIndex !== -1) {
results.push(keywordIndex);
}
}
if (Array.isArray(groundTruth)) {
expect(results).toEqual(groundTruth);
} else {
expect(results.length).toBe(groundTruth);
}
}
engineInstance.release();
}
describe("error message stack", () => {
test("message stack cleared after read", () => {
let error: string[] = [];
try {
new Porcupine(
"invalid access key",
[BuiltinKeyword.PORCUPINE],
[0.5]);
} catch (e: any) {
error = e.messageStack;
}
expect(error.length).toBeGreaterThan(0);
expect(error.length).toBeLessThanOrEqual(8);
try {
new Porcupine(
"invalid access key",
[BuiltinKeyword.PORCUPINE],
[0.5]);
} catch (e: any) {
for (let i = 0; i < error.length; i++) {
expect(error[i]).toEqual(e.messageStack[i]);
}
}
});
});
describe("successful keyword detections", () => {
it.each(SINGLE_KEYWORD_PARAMETERS)(
'testing single keyword for %p with %p', (language: string, keyword: string, filename: string) => {
testPorcupineDetection(language, [keyword], [0], filename);
});
it.each(MULTIPLE_KEYWORDS_PARAMETERS)(
'testing multiple for %p with %p', (language: string, keywords: string[], result: number[]) => {
testPorcupineDetection(language, keywords, result);
});
test("builtin keyword 'GRASSHOPPER'", () => {
testPorcupineDetection("en", [BuiltinKeyword.GRASSHOPPER], 1);
});
test("single keyword multiple detection", () => {
testPorcupineDetection("en", ['porcupine'], 2);
});
}
);
describe("Non ascii characters", () => {
test("single non ascii character in the model name", () => {
new Porcupine(
ACCESS_KEY,
[getKeywordPathsByLanguage('es', 'murciélago')],
[0.5],
getModelPathByLanguage('es')
);
});
});
describe("basic parameter validation", () => {
test("num of keywords does not match num of sensitivities", () => {
expect(() => {
new Porcupine(
ACCESS_KEY,
[getKeywordPathsByLanguage('en', 'porcupine')],
[0.1, 0.2,]);
}).toThrow(PorcupineInvalidArgumentError);
});
test("keywords is not an array", () => {
expect(() => {
new Porcupine(
ACCESS_KEY,
// @ts-expect-error
getKeywordPathsByLanguage('en', 'porcupine'),
[0.1]);
}).toThrow(PorcupineInvalidArgumentError);
});
test("sensitivity is not in range", () => {
expect(() => {
new Porcupine(
ACCESS_KEY,
[getKeywordPathsByLanguage('en', 'porcupine')],
[4.2]);
}).toThrow(RangeError);
});
test("sensitivity is not a number", () => {
expect(() => {
new Porcupine(
ACCESS_KEY,
[getKeywordPathsByLanguage('en', 'porcupine')],
// @ts-expect-error
"porcupine");
}).toThrow(RangeError);
});
test("invalid keyword path", () => {
expect(() => {
new Porcupine(
ACCESS_KEY,
["to be or not to be"],
[0.5]);
}).toThrow(PorcupineInvalidArgumentError);
});
});
describe("frame validation", () => {
test("accepts non Int16Array if array is valid", () => {
const porcupineEngine = new Porcupine(
ACCESS_KEY,
[getKeywordPathsByLanguage('en', 'porcupine')],
[0.5]);
const emptyArray = Array.apply(null, Array(porcupineEngine.frameLength)).map((x, i) => i);
// @ts-expect-error
porcupineEngine.process(emptyArray);
porcupineEngine.release();
});
test("mismatched frameLength throws error", () => {
const porcupineEngine = new Porcupine(
ACCESS_KEY,
[getKeywordPathsByLanguage('en', 'porcupine')],
[0.5]);
expect(() => {
// @ts-expect-error
porcupineEngine.process([1, 2, 3]);
}).toThrow(PorcupineInvalidArgumentError);
porcupineEngine.release();
});
test("null/undefined frames throws error", () => {
const porcupineEngine = new Porcupine(
ACCESS_KEY,
[getKeywordPathsByLanguage('en', 'porcupine')],
[0.5]);
expect(() => {
// @ts-expect-error
porcupineEngine.process(null);
}).toThrow(PorcupineInvalidArgumentError);
expect(() => {
// @ts-expect-error
porcupineEngine.process(undefined);
}).toThrow(PorcupineInvalidArgumentError);
porcupineEngine.release();
});
test("passing floating point frame values throws PorcupineInvalidArgumentError", () => {
const porcupineEngine = new Porcupine(
ACCESS_KEY,
[getKeywordPathsByLanguage('en', 'porcupine')],
[0.5]);
const floatFrames = Array.from({ length: porcupineEngine.frameLength }).map(
() => 3.1415
);
expect(() => {
// @ts-expect-error
porcupineEngine.process(floatFrames);
}).toThrow(PorcupineInvalidArgumentError);
porcupineEngine.release();
});
});