diff --git a/sdk/web/scripts/setup_test.js b/sdk/web/scripts/setup_test.js index 259811a06..35d734edd 100644 --- a/sdk/web/scripts/setup_test.js +++ b/sdk/web/scripts/setup_test.js @@ -1,5 +1,6 @@ const fs = require('fs'); const { join } = require('path'); +const { createHash } = require('crypto'); console.log('Copying the porcupine and rhino models...'); @@ -8,28 +9,18 @@ const fixturesDirectory = join(__dirname, '..', 'cypress', 'fixtures'); const engines = [ { - "name": "porcupine", - "dir": "keyword_files" + name: 'porcupine', + dir: 'keyword_files', }, { - "name": "rhino", - "dir": "contexts" - } + name: 'rhino', + dir: 'contexts', + }, ]; -const sourceDirectory = join( - __dirname, - "..", - "..", - "..", - "resources", -); +const sourceDirectory = join(__dirname, '..', '..', '..', 'resources'); -const testDataSource = join( - sourceDirectory, - '.test', - 'test_data.json' -); +const testDataSource = join(sourceDirectory, '.test', 'test_data.json'); try { fs.mkdirSync(outputDirectory, { recursive: true }); @@ -37,7 +28,18 @@ try { fs.mkdirSync(join(fixturesDirectory, 'audio_samples'), { recursive: true }); fs.readdirSync(join(sourceDirectory, 'audio_samples')).forEach(file => { - fs.copyFileSync(join(sourceDirectory, 'audio_samples', file), join(fixturesDirectory, 'audio_samples', file)); + let src = join(sourceDirectory, 'audio_samples', file); + + // Bug in Cypress means we can't read utf-8 file names, so we have to hash them + let encodedAudioName = createHash('md5') + .update(file.replace('.wav', '')) + .digest('hex'); + let dst = join( + fixturesDirectory, + 'audio_samples', + `${encodedAudioName}.wav` + ); + fs.copyFileSync(src, dst); }); for (const engine of engines) { @@ -52,21 +54,41 @@ try { sourceDirectory, engine.name, 'resources' - ) + ); fs.mkdirSync(join(outputDirectory, engine.name), { recursive: true }); fs.readdirSync(paramsSourceDirectory).forEach(file => { - fs.copyFileSync(join(paramsSourceDirectory, file), join(outputDirectory, engine.name, file)); + fs.copyFileSync( + join(paramsSourceDirectory, file), + join(outputDirectory, engine.name, file) + ); }); fs.mkdirSync(join(outputDirectory, engine.dir), { recursive: true }); fs.readdirSync(engineSourceDirectory).forEach(folder => { if (folder.includes(engine.dir)) { - fs.readdirSync(join(engineSourceDirectory, folder, 'wasm')).forEach(file => { - fs.copyFileSync( - join(engineSourceDirectory, folder, 'wasm', file), - join(outputDirectory, engine.dir, file.replace("ā", "a").replace("ō", "o"))); - }); + fs.readdirSync(join(engineSourceDirectory, folder, 'wasm')).forEach( + file => { + let src = join(engineSourceDirectory, folder, 'wasm', file); + if (folder === engine.dir) { + let dst = join(outputDirectory, engine.dir, file); + fs.copyFileSync(src, dst); + } else { + let fileParts = file.split('_wasm'); + + // Bug in Cypress means we can't read utf-8 file names, so we have to hash them + let encodedFileName = createHash('md5') + .update(fileParts[0]) + .digest('hex'); + let dst = join( + outputDirectory, + engine.dir, + `${encodedFileName}_wasm${fileParts[1]}` + ); + fs.copyFileSync(src, dst); + } + } + ); } }); } diff --git a/sdk/web/test/picovoice.test.ts b/sdk/web/test/picovoice.test.ts index f1fdfce4f..746bdec6c 100644 --- a/sdk/web/test/picovoice.test.ts +++ b/sdk/web/test/picovoice.test.ts @@ -1,20 +1,22 @@ -import { Picovoice, PicovoiceWorker } from "../"; -import testData from "./test_data.json"; +import { createHash } from 'crypto'; + +import { Picovoice, PicovoiceWorker } from '../'; +import testData from './test_data.json'; // @ts-ignore -import picovoiceKeyword from "./keyword_files/picovoice_wasm"; +import picovoiceKeyword from './keyword_files/picovoice_wasm'; // @ts-ignore -import porcupineParams from "./porcupine/porcupine_params"; +import porcupineParams from './porcupine/porcupine_params'; // @ts-ignore -import coffeeMakerContext from "./contexts/coffee_maker_wasm"; +import coffeeMakerContext from './contexts/coffee_maker_wasm'; // @ts-ignore -import rhinoParams from "./rhino/rhino_params"; +import rhinoParams from './rhino/rhino_params'; import { PvModel } from '@picovoice/web-utils'; -import { PorcupineKeyword } from "@picovoice/porcupine-web"; -import { RhinoContext, RhinoInference } from "@picovoice/rhino-web"; +import { PorcupineKeyword } from '@picovoice/porcupine-web'; +import { RhinoContext, RhinoInference } from '@picovoice/rhino-web'; -const ACCESS_KEY: string = Cypress.env("ACCESS_KEY"); +const ACCESS_KEY: string = Cypress.env('ACCESS_KEY'); function delay(time: number) { return new Promise(resolve => setTimeout(resolve, time)); @@ -23,20 +25,33 @@ function delay(time: number) { const runInitTest = async ( instance: typeof Picovoice | typeof PicovoiceWorker, params: { - accessKey?: string, - keyword?: PorcupineKeyword, - context?: RhinoContext, - porcupineModel?: PvModel, - rhinoModel?: PvModel, - expectFailure?: boolean, + accessKey?: string; + keyword?: PorcupineKeyword; + context?: RhinoContext; + porcupineModel?: PvModel; + rhinoModel?: PvModel; + expectFailure?: boolean; } = {} ) => { const { accessKey = ACCESS_KEY, - keyword = { publicPath: '/test/keyword_files/picovoice_wasm.ppn', forceWrite: true, label: 'picovoice' }, - context = { publicPath: '/test/contexts/coffee_maker_wasm.rhn', forceWrite: true }, - porcupineModel = { publicPath: '/test/porcupine/porcupine_params.pv', forceWrite: true }, - rhinoModel = { publicPath: '/test/rhino/rhino_params.pv', forceWrite: true }, + keyword = { + publicPath: '/test/keyword_files/picovoice_wasm.ppn', + forceWrite: true, + label: 'picovoice', + }, + context = { + publicPath: '/test/contexts/coffee_maker_wasm.rhn', + forceWrite: true, + }, + porcupineModel = { + publicPath: '/test/porcupine/porcupine_params.pv', + forceWrite: true, + }, + rhinoModel = { + publicPath: '/test/rhino/rhino_params.pv', + forceWrite: true, + }, expectFailure = false, } = params; @@ -78,61 +93,79 @@ const runProcTest = async ( instance: typeof Picovoice | typeof PicovoiceWorker, inputPcm: Int16Array, params: { - accessKey?: string, - keyword?: PorcupineKeyword, - context?: RhinoContext, - porcupineModel?: PvModel, - rhinoModel?: PvModel, + accessKey?: string; + keyword?: PorcupineKeyword; + context?: RhinoContext; + porcupineModel?: PvModel; + rhinoModel?: PvModel; } = {}, expectedContext?: any ) => { const { accessKey = ACCESS_KEY, - keyword = { publicPath: '/test/keyword_files/picovoice_wasm.ppn', forceWrite: true, label: 'picovoice' }, - context = { publicPath: '/test/contexts/coffee_maker_wasm.rhn', forceWrite: true }, - porcupineModel = { publicPath: '/test/porcupine/porcupine_params.pv', forceWrite: true }, - rhinoModel = { publicPath: '/test/rhino/rhino_params.pv', forceWrite: true }, + keyword = { + publicPath: '/test/keyword_files/picovoice_wasm.ppn', + forceWrite: true, + label: 'picovoice', + }, + context = { + publicPath: '/test/contexts/coffee_maker_wasm.rhn', + forceWrite: true, + }, + porcupineModel = { + publicPath: '/test/porcupine/porcupine_params.pv', + forceWrite: true, + }, + rhinoModel = { + publicPath: '/test/rhino/rhino_params.pv', + forceWrite: true, + }, } = params; let keywordDetection = false; let inference: RhinoInference; - const runProcess = () => new Promise(async (resolve, reject) => { - const picovoice = await instance.create( - accessKey, - keyword, - () => { - keywordDetection = true; - }, - porcupineModel, - context, - (rhinoInference: RhinoInference) => { - if (keywordDetection && rhinoInference.isFinalized) { - inference = rhinoInference; - resolve(); - } - }, - rhinoModel, - { - processErrorCallback: (error: string) => { - reject(error); + const runProcess = () => + new Promise(async (resolve, reject) => { + const picovoice = await instance.create( + accessKey, + keyword, + () => { + keywordDetection = true; + }, + porcupineModel, + context, + (rhinoInference: RhinoInference) => { + if (keywordDetection && rhinoInference.isFinalized) { + inference = rhinoInference; + resolve(); + } + }, + rhinoModel, + { + processErrorCallback: (error: string) => { + reject(error); + }, } - } - ); + ); - for (let i = 0; i < (inputPcm.length - picovoice.frameLength! + 1); i += picovoice.frameLength!) { - await picovoice.process(inputPcm.slice(i, i + picovoice.frameLength!)); - await delay(32); - } + for ( + let i = 0; + i < inputPcm.length - picovoice.frameLength! + 1; + i += picovoice.frameLength! + ) { + await picovoice.process(inputPcm.slice(i, i + picovoice.frameLength!)); + await delay(32); + } - await delay(1000); + await delay(1000); - if (picovoice instanceof PicovoiceWorker) { - picovoice.terminate(); - } else { - await picovoice.release(); - } - }); + if (picovoice instanceof PicovoiceWorker) { + picovoice.terminate(); + } else { + await picovoice.release(); + } + }); try { await runProcess(); @@ -143,9 +176,9 @@ const runProcTest = async ( } }; -describe("Picovoice Binding", function () { +describe('Picovoice Binding', function () { for (const instance of [Picovoice, PicovoiceWorker]) { - const instanceString = (instance === PicovoiceWorker) ? 'worker' : 'main'; + const instanceString = instance === PicovoiceWorker ? 'worker' : 'main'; it(`should be able to init with public path (${instanceString})`, () => { cy.wrap(null).then(async () => { @@ -156,10 +189,14 @@ describe("Picovoice Binding", function () { it(`should be able to init with base64 (${instanceString})`, () => { cy.wrap(null).then(async () => { await runInitTest(instance, { - keyword: { base64: picovoiceKeyword, forceWrite: true, label: picovoiceKeyword }, + keyword: { + base64: picovoiceKeyword, + forceWrite: true, + label: picovoiceKeyword, + }, porcupineModel: { base64: porcupineParams, forceWrite: true }, context: { base64: coffeeMakerContext, forceWrite: true }, - rhinoModel: { base64: rhinoParams, forceWrite: true } + rhinoModel: { base64: rhinoParams, forceWrite: true }, }); }); }); @@ -167,8 +204,16 @@ describe("Picovoice Binding", function () { it(`should be able to handle UTF-8 public path (${instanceString})`, () => { cy.wrap(null).then(async () => { await runInitTest(instance, { - porcupineModel: { publicPath: '/test/porcupine/porcupine_params.pv', forceWrite: true, customWritePath: '테스트' }, - rhinoModel: { publicPath: '/test/rhino/rhino_params.pv', forceWrite: true, customWritePath: 'オレンジ' }, + porcupineModel: { + publicPath: '/test/porcupine/porcupine_params.pv', + forceWrite: true, + customWritePath: '테스트', + }, + rhinoModel: { + publicPath: '/test/rhino/rhino_params.pv', + forceWrite: true, + customWritePath: 'オレンジ', + }, }); }); }); @@ -177,7 +222,7 @@ describe("Picovoice Binding", function () { cy.wrap(null).then(async () => { await runInitTest(instance, { porcupineModel: { publicPath: 'invalid', forceWrite: true }, - expectFailure: true + expectFailure: true, }); }); }); @@ -186,7 +231,7 @@ describe("Picovoice Binding", function () { cy.wrap(null).then(async () => { await runInitTest(instance, { rhinoModel: { base64: 'invalid', forceWrite: true }, - expectFailure: true + expectFailure: true, }); }); }); @@ -195,7 +240,7 @@ describe("Picovoice Binding", function () { cy.wrap(null).then(async () => { await runInitTest(instance, { accessKey: 'invalid', - expectFailure: true + expectFailure: true, }); }); }); @@ -203,8 +248,12 @@ describe("Picovoice Binding", function () { it(`should be able to handle invalid sensitivity(${instanceString})`, () => { cy.wrap(null).then(async () => { await runInitTest(instance, { - context: { publicPath: '/test/contexts/picovoice.rhn', forceWrite: true, sensitivity: -1 }, - expectFailure: true + context: { + publicPath: '/test/contexts/picovoice.rhn', + forceWrite: true, + sensitivity: -1, + }, + expectFailure: true, }); }); }); @@ -212,26 +261,51 @@ describe("Picovoice Binding", function () { for (const testParam of testData.tests.parameters) { it(`should be able to process (${testParam.language}) (${instanceString})`, () => { try { - const suffix = (testParam.language === 'en') ? '' : `_${testParam.language}`; - cy.getFramesFromFile(`audio_samples/${testParam.audio_file}`).then( async pcm => { - await runProcTest( - instance, - pcm, - { - keyword: { - publicPath: `/test/keyword_files/${encodeURIComponent(testParam.wakeword)}_wasm.ppn`, - forceWrite: true, - label: testParam.wakeword - }, - porcupineModel: { publicPath: `/test/porcupine/porcupine_params${suffix}.pv`, forceWrite: true }, - context: { - publicPath: `/test/contexts/${encodeURIComponent(testParam.context_name.replace("ā", "a").replace("ō", "o"))}_wasm.rhn`, - forceWrite: true, + const encodedAudioName = createHash('md5') + .update(testParam.audio_file.replace('.wav', '')) + .digest('hex'); + const suffix = + testParam.language === 'en' ? '' : `_${testParam.language}`; + cy.getFramesFromFile(`audio_samples/${encodedAudioName}.wav`).then( + async pcm => { + let keywordName = testParam.wakeword; + let contextName = testParam.context_name; + if (testParam.language !== 'en') { + // Bug in Cypress means we can't read utf-8 file names, so we have to hash them + keywordName = createHash('md5') + .update(testParam.wakeword) + .digest('hex'); + contextName = createHash('md5') + .update(testParam.context_name) + .digest('hex'); + } + + await runProcTest( + instance, + pcm, + { + keyword: { + publicPath: `/test/keyword_files/${keywordName}_wasm.ppn`, + forceWrite: true, + label: testParam.wakeword, + }, + porcupineModel: { + publicPath: `/test/porcupine/porcupine_params${suffix}.pv`, + forceWrite: true, + }, + context: { + publicPath: `/test/contexts/${contextName}_wasm.rhn`, + forceWrite: true, + }, + rhinoModel: { + publicPath: `/test/rhino/rhino_params${suffix}.pv`, + forceWrite: true, + }, }, - rhinoModel: { publicPath: `/test/rhino/rhino_params${suffix}.pv`, forceWrite: true } - }, - testParam.inference); - }); + testParam.inference + ); + } + ); } catch (e) { expect(e).to.be.undefined; }