UNPKG

@picovoice/porcupine-node

Version:

Picovoice Porcupine Node.js binding

248 lines (215 loc) 7.26 kB
// // 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. // "use strict"; 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(); }); });