From 4b2e579b64d2e2d18417754e9a43d969f7d29a0a Mon Sep 17 00:00:00 2001 From: Eric Mikulin Date: Fri, 28 Nov 2025 16:13:27 -0800 Subject: [PATCH] v4.0 Web binding & demo --- .github/workflows/react-codestyle.yml | 3 - .github/workflows/react-demos.yml | 13 +- .github/workflows/react.yml | 12 +- .github/workflows/web-codestyle.yml | 11 +- .github/workflows/web-demos.yml | 9 +- .github/workflows/web-perf.yml | 5 +- .github/workflows/web.yml | 8 +- binding/react/package.json | 4 +- binding/react/test/use_rhino.test.ts | 19 +- binding/web/.gitignore | 2 +- binding/web/README.md | 17 + binding/web/cypress.config.ts | 10 +- binding/web/cypress/tsconfig.json | 2 +- binding/web/module.d.ts | 5 + binding/web/package.json | 12 +- binding/web/rollup.config.js | 2 +- binding/web/scripts/copy_wasm.js | 34 +- binding/web/src/index.ts | 14 +- binding/web/src/rhino.ts | 819 +++++++++++++----------- binding/web/src/rhino_worker.ts | 57 +- binding/web/src/rhino_worker_handler.ts | 6 +- binding/web/src/types.ts | 11 +- binding/web/test/rhino.test.ts | 19 +- binding/web/test/rhino_perf.test.ts | 4 +- binding/web/tsconfig.json | 7 +- binding/web/yarn.lock | 37 +- demo/react/package.json | 4 +- demo/web/index.html | 2 +- demo/web/package.json | 7 +- demo/web/scripts/run_demo.js | 2 +- demo/web/scripts/server.js | 42 ++ 31 files changed, 731 insertions(+), 468 deletions(-) create mode 100644 demo/web/scripts/server.js diff --git a/.github/workflows/react-codestyle.yml b/.github/workflows/react-codestyle.yml index 5ba0a49a6..813c97034 100644 --- a/.github/workflows/react-codestyle.yml +++ b/.github/workflows/react-codestyle.yml @@ -31,9 +31,6 @@ jobs: with: node-version: lts/* - - name: Pre-build dependencies - run: npm install yarn - - name: Run Binding Linter run: yarn && yarn lint working-directory: binding/react diff --git a/.github/workflows/react-demos.yml b/.github/workflows/react-demos.yml index 33320d41b..456481e3a 100644 --- a/.github/workflows/react-demos.yml +++ b/.github/workflows/react-demos.yml @@ -26,7 +26,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] - node-version: [ 16.x, 18.x, 20.x ] + node-version: [ 18.x, 20.x, 22.x, 24.x ] steps: - uses: actions/checkout@v3 @@ -36,8 +36,15 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: Pre-build dependencies - run: npm install yarn +# ************** REMOVE AFTER RELEASE ******************** + - name: Build Web SDK + run: yarn install && yarn copywasm && yarn copyppn && yarn build + working-directory: binding/web + + - name: Install dependencies + run: yarn install && yarn build + working-directory: binding/react +# ************** REMOVE AFTER RELEASE ******************** - name: Install dependencies run: yarn install diff --git a/.github/workflows/react.yml b/.github/workflows/react.yml index 49be2b6d6..af869c4af 100644 --- a/.github/workflows/react.yml +++ b/.github/workflows/react.yml @@ -29,7 +29,8 @@ jobs: strategy: matrix: - node-version: [16.x, 18.x, 20.x] + device: [ cpu:1, cpu ] + node-version: [ 18.x, 20.x, 22.x, 24.x ] steps: - uses: actions/checkout@v3 @@ -39,8 +40,11 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: Pre-build dependencies - run: npm install yarn +# ************** REMOVE AFTER RELEASE ******************** + - name: Build Web SDK + run: yarn install && yarn copywasm && yarn copyppn && yarn build + working-directory: binding/web +# ************** REMOVE AFTER RELEASE ******************** - name: Install dependencies run: yarn install @@ -55,4 +59,4 @@ jobs: run: yarn setup-test - name: Run test - run: yarn test --env ACCESS_KEY=${{secrets.PV_VALID_ACCESS_KEY}} + run: yarn test --env ACCESS_KEY=${{secrets.PV_VALID_ACCESS_KEY}},DEVICE=${{ matrix.device }} diff --git a/.github/workflows/web-codestyle.yml b/.github/workflows/web-codestyle.yml index 827d8df17..16fb8213a 100644 --- a/.github/workflows/web-codestyle.yml +++ b/.github/workflows/web-codestyle.yml @@ -5,14 +5,14 @@ on: push: branches: [ master ] paths: - - '**/web/*.js' - - '**/web/*.ts' + - 'binding/web/*.js' + - 'binding/web/*.ts' - '.github/workflows/web-codestyle.yml' pull_request: branches: [ master, 'v[0-9]+.[0-9]+' ] paths: - - '**/web/*.js' - - '**/web/*.ts' + - 'binding/web/*.js' + - 'binding/web/*.ts' - '.github/workflows/web-codestyle.yml' jobs: @@ -27,9 +27,6 @@ jobs: with: node-version: lts/* - - name: Pre-build dependencies - run: npm install yarn - - name: Run Binding Linter run: yarn && yarn lint working-directory: binding/web diff --git a/.github/workflows/web-demos.yml b/.github/workflows/web-demos.yml index 75b380416..2ae00236c 100644 --- a/.github/workflows/web-demos.yml +++ b/.github/workflows/web-demos.yml @@ -25,7 +25,7 @@ jobs: strategy: matrix: - node-version: [18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x, 24.x] steps: - uses: actions/checkout@v3 @@ -35,8 +35,11 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: Pre-build dependencies - run: npm install yarn +# ************** REMOVE AFTER RELEASE ******************** + - name: Build Web SDK + run: yarn install && yarn copywasm && yarn copyppn && yarn build + working-directory: binding/web +# ************** REMOVE AFTER RELEASE ******************** - name: Install dependencies run: yarn install diff --git a/.github/workflows/web-perf.yml b/.github/workflows/web-perf.yml index 7bfdbb12e..0925c147c 100644 --- a/.github/workflows/web-perf.yml +++ b/.github/workflows/web-perf.yml @@ -39,9 +39,6 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: Pre-build dependencies - run: npm install yarn - - name: Install dependencies run: yarn install @@ -55,4 +52,4 @@ jobs: run: yarn setup-test - name: Test - run: yarn test-perf --env ACCESS_KEY=${{secrets.PV_VALID_ACCESS_KEY}},NUM_TEST_ITERATIONS=20,INIT_PERFORMANCE_THRESHOLD_SEC=${{matrix.initPerformanceThresholdSec}},PROC_PERFORMANCE_THRESHOLD_SEC=${{matrix.procPerformanceThresholdSec}} + run: yarn test-perf --env ACCESS_KEY=${{secrets.PV_VALID_ACCESS_KEY}},DEVICE=cpu:1,NUM_TEST_ITERATIONS=20,INIT_PERFORMANCE_THRESHOLD_SEC=${{matrix.initPerformanceThresholdSec}},PROC_PERFORMANCE_THRESHOLD_SEC=${{matrix.procPerformanceThresholdSec}} diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 98c3a3b9a..638ee2702 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -35,7 +35,8 @@ jobs: strategy: matrix: - node-version: [16.x, 18.x, 20.x] + device: [ cpu:1, cpu ] + node-version: [18.x, 20.x, 22.x, 24.x] steps: - uses: actions/checkout@v3 @@ -45,9 +46,6 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: Pre-build dependencies - run: npm install yarn - - name: Install dependencies run: yarn install @@ -61,4 +59,4 @@ jobs: run: yarn setup-test - name: Test - run: yarn test --env ACCESS_KEY=${{secrets.PV_VALID_ACCESS_KEY}} + run: yarn test --env ACCESS_KEY=${{secrets.PV_VALID_ACCESS_KEY}},DEVICE=${{ matrix.device }} diff --git a/binding/react/package.json b/binding/react/package.json index 39f4b55f3..bcfd279d9 100644 --- a/binding/react/package.json +++ b/binding/react/package.json @@ -1,6 +1,6 @@ { "name": "@picovoice/rhino-react", - "version": "3.0.3", + "version": "4.0.0", "description": "React component for Rhino Web SDK", "entry": "src/index.ts", "module": "dist/esm/index.js", @@ -73,6 +73,6 @@ "react-dom": ">=17" }, "dependencies": { - "@picovoice/rhino-web": "=3.0.3" + "@picovoice/rhino-web": "file:../web" } } diff --git a/binding/react/test/use_rhino.test.ts b/binding/react/test/use_rhino.test.ts index 339e560e2..4301cf2ac 100644 --- a/binding/react/test/use_rhino.test.ts +++ b/binding/react/test/use_rhino.test.ts @@ -7,6 +7,7 @@ import rhinoParams from '@/rhino_params.js'; import testData from './test_data.json'; const ACCESS_KEY = Cypress.env('ACCESS_KEY'); +const DEVICE = Cypress.env('DEVICE'); describe('Rhino binding', () => { it('should be able to init via public path', () => { @@ -16,7 +17,8 @@ describe('Rhino binding', () => { () => result.current.init( ACCESS_KEY, { publicPath: "/test/contexts/coffee_maker_wasm.rhn", forceWrite: true }, - { publicPath: "/test/rhino_params.pv", forceWrite: true } + { publicPath: "/test/rhino_params.pv", forceWrite: true }, + { device: DEVICE } ) ).then(() => { expect(result.current.isLoaded).to.be.true; @@ -36,7 +38,8 @@ describe('Rhino binding', () => { () => result.current.init( ACCESS_KEY, { publicPath: "/test/contexts/coffee_maker_wasm.rhn", forceWrite: true }, - { base64: rhinoParams, forceWrite: true } + { base64: rhinoParams, forceWrite: true }, + { device: DEVICE } ) ).then(() => { expect(result.current.isLoaded).to.be.true; @@ -50,7 +53,8 @@ describe('Rhino binding', () => { () => result.current.init( ACCESS_KEY, { publicPath: "/test/contexts/coffee_maker_wasm.rhn", forceWrite: true }, - { publicPath: "/rhino_params_failed.pv", forceWrite: true } + { publicPath: "/rhino_params_failed.pv", forceWrite: true }, + { device: DEVICE } ) ).then(() => { expect(result.current.isLoaded).to.be.false; @@ -65,7 +69,8 @@ describe('Rhino binding', () => { () => result.current.init( '', { publicPath: "/test/contexts/coffee_maker_wasm.rhn", forceWrite: true }, - { publicPath: "/test/rhino_params.pv", forceWrite: true } + { publicPath: "/test/rhino_params.pv", forceWrite: true }, + { device: DEVICE } ) ).then(() => { expect(result.current.isLoaded).to.be.false; @@ -87,7 +92,8 @@ describe('Rhino binding', () => { { publicPath: testInfo.language === 'en' ? "/test/rhino_params.pv" : `/test/rhino_params_${testInfo.language}.pv`, forceWrite: true, - } + }, + { device: DEVICE } ) ).then(() => { expect(result.current.isLoaded).to.be.true; @@ -124,7 +130,8 @@ describe('Rhino binding', () => { { publicPath: testInfo.language === 'en' ? "/test/rhino_params.pv" : `/test/rhino_params_${testInfo.language}.pv`, forceWrite: true, - } + }, + { device: DEVICE } ) ).then(() => { expect(result.current.isLoaded).to.be.true; diff --git a/binding/web/.gitignore b/binding/web/.gitignore index e554d5efe..e1ff90efc 100644 --- a/binding/web/.gitignore +++ b/binding/web/.gitignore @@ -1,6 +1,6 @@ node_modules dist -lib/pv_rhino*.wasm +src/lib/* src/rhino_64.ts contexts/*.rhn test/contexts/*.rhn diff --git a/binding/web/README.md b/binding/web/README.md index 68f5cd804..c9b14939a 100644 --- a/binding/web/README.md +++ b/binding/web/README.md @@ -35,11 +35,28 @@ Rhino is: - Firefox - Safari +## Requirements + +The Eagle Web Binding uses [SharedArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer). + +Include the following headers in the response to enable the use of `SharedArrayBuffers`: + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +Refer to our [Web demo](../../demo/web) for an example on creating a server with the corresponding response headers. + +Browsers that don't support `SharedArrayBuffers` or applications that don't include the required headers will fall back to using standard `ArrayBuffers`. This will disable multithreaded processing. + ### Restrictions IndexedDB is required to use `Rhino` in a worker thread. Browsers without IndexedDB support (i.e. Firefox Incognito Mode) should use `Rhino` in the main thread. +Multi-threading is only enabled for Rhino when using on a web worker. + ## Installation ### Package diff --git a/binding/web/cypress.config.ts b/binding/web/cypress.config.ts index 71f5cce5e..b898087d5 100644 --- a/binding/web/cypress.config.ts +++ b/binding/web/cypress.config.ts @@ -7,10 +7,18 @@ export default defineConfig({ "PROC_PERFORMANCE_THRESHOLD_SEC": 1.2 }, e2e: { - defaultCommandTimeout: 30000, supportFile: "cypress/support/index.ts", specPattern: "test/*.test.{js,jsx,ts,tsx}", video: false, screenshotOnRunFailure: false, + defaultCommandTimeout: 30000, + setupNodeEvents(on, config) { + on('before:browser:launch', (browser, launchOptions) => { + if (browser.name === 'chrome') { + launchOptions.args.push('--enable-features=SharedArrayBuffer'); + } + return launchOptions; + }); + }, }, }); diff --git a/binding/web/cypress/tsconfig.json b/binding/web/cypress/tsconfig.json index df1821def..fb78f5f36 100644 --- a/binding/web/cypress/tsconfig.json +++ b/binding/web/cypress/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "types": ["cypress"] + "types": ["cypress", "node"] }, "include": [ "../test/**/*.ts", diff --git a/binding/web/module.d.ts b/binding/web/module.d.ts index 0057fb1e5..97ef90acb 100644 --- a/binding/web/module.d.ts +++ b/binding/web/module.d.ts @@ -8,6 +8,11 @@ declare module "*.rhn" { export default content; } +declare module "*.txt" { + const content: string; + export default content; +} + declare module 'web-worker:*' { const WorkerFactory: new () => Worker; export default WorkerFactory; diff --git a/binding/web/package.json b/binding/web/package.json index 2a89b97f3..e31889895 100644 --- a/binding/web/package.json +++ b/binding/web/package.json @@ -3,7 +3,7 @@ "description": "Rhino Speech-to-Intent engine for web browsers (via WebAssembly)", "author": "Picovoice Inc", "license": "Apache-2.0", - "version": "3.0.3", + "version": "4.0.0", "keywords": [ "rhino", "web", @@ -29,11 +29,11 @@ "format": "prettier --write \"**/*.{js,ts,json}\"", "copywasm": "node scripts/copy_wasm.js", "setup-test": "node scripts/setup_test.js && npx pvbase64 -i ./test/rhino_params.pv -o ./test/rhino_params.js", - "test": "cypress run --spec test/rhino.test.ts", - "test-perf": "cypress run --spec test/rhino_perf.test.ts" + "test": "cypress run --spec test/rhino.test.ts --browser chrome", + "test-perf": "cypress run --spec test/rhino_perf.test.ts --browser chrome" }, "dependencies": { - "@picovoice/web-utils": "=1.3.1" + "@picovoice/web-utils": "=1.4.3" }, "devDependencies": { "@babel/core": "^7.21.3", @@ -45,6 +45,8 @@ "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-terser": "^0.4.0", "@rollup/pluginutils": "^5.0.2", + "@types/emscripten": "1.40.0", + "@types/node": "^18.13.0", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "async-mutex": "^0.4.0", @@ -62,6 +64,6 @@ "wasm-feature-detect": "^1.5.0" }, "engines": { - "node": ">=16" + "node": ">=18" } } diff --git a/binding/web/rollup.config.js b/binding/web/rollup.config.js index 91e3e6a1c..b5c15a59d 100644 --- a/binding/web/rollup.config.js +++ b/binding/web/rollup.config.js @@ -69,7 +69,7 @@ export default { exclude: '**/node_modules/**', }), base64({ - include: ['./lib/**/*.wasm'], + include: ['./src/lib/*.wasm', './src/lib/*.txt'], }), ], }; diff --git a/binding/web/scripts/copy_wasm.js b/binding/web/scripts/copy_wasm.js index 5633068a0..ea16307c1 100644 --- a/binding/web/scripts/copy_wasm.js +++ b/binding/web/scripts/copy_wasm.js @@ -1,21 +1,37 @@ -const fs = require('fs'); -const { join } = require('path'); +const fs = require("fs"); +const { join, extname } = require("path"); -const wasmFiles = ['pv_rhino.wasm', 'pv_rhino_simd.wasm']; +const wasmFiles = [ + "pv_rhino_simd.wasm", + "pv_rhino_simd.js", + "pv_rhino_pthread.wasm", + "pv_rhino_pthread.js", +] -console.log('Copying the WASM model...'); +console.log("Copying the WASM model..."); -const sourceDirectory = join(__dirname, '..', '..', '..', 'lib', 'wasm'); +const sourceDirectory = join( + __dirname, + "..", + "..", + "..", + "lib", + "wasm" +); -const outputDirectory = join(__dirname, '..', 'lib'); +const outputDirectory = join(__dirname, "..", "src", "lib"); try { fs.mkdirSync(outputDirectory, { recursive: true }); wasmFiles.forEach(file => { - fs.copyFileSync(join(sourceDirectory, file), join(outputDirectory, file)); - }); + fs.copyFileSync(join(sourceDirectory, file), join(outputDirectory, file)) + const ext = extname(file); + if (ext === ".js") { + fs.copyFileSync(join(sourceDirectory, file), join(outputDirectory, file.replace(ext, ".txt"))); + } + }) } catch (error) { console.error(error); } -console.log('... Done!'); +console.log("... Done!"); \ No newline at end of file diff --git a/binding/web/src/index.ts b/binding/web/src/index.ts index 2538ec98d..392839cbb 100644 --- a/binding/web/src/index.ts +++ b/binding/web/src/index.ts @@ -18,15 +18,21 @@ import { RhinoWorkerResponse, } from './types'; -import rhinoWasm from '../lib/pv_rhino.wasm'; -import rhinoWasmSimd from '../lib/pv_rhino_simd.wasm'; +import rhinoWasmSimd from './lib/pv_rhino_simd.wasm'; +import rhinoWasmSimdLib from './lib/pv_rhino_simd.txt'; +import rhinoWasmPThread from './lib/pv_rhino_pthread.wasm'; +import rhinoWasmPThreadLib from './lib/pv_rhino_pthread.txt'; import * as RhinoErrors from './rhino_errors'; -Rhino.setWasm(rhinoWasm); Rhino.setWasmSimd(rhinoWasmSimd); -RhinoWorker.setWasm(rhinoWasm); +Rhino.setWasmSimdLib(rhinoWasmSimdLib); +Rhino.setWasmPThread(rhinoWasmPThread); +Rhino.setWasmPThreadLib(rhinoWasmPThreadLib); RhinoWorker.setWasmSimd(rhinoWasmSimd); +RhinoWorker.setWasmSimdLib(rhinoWasmSimdLib); +RhinoWorker.setWasmPThread(rhinoWasmPThread); +RhinoWorker.setWasmPThreadLib(rhinoWasmPThreadLib); export { InferenceCallback, diff --git a/binding/web/src/rhino.ts b/binding/web/src/rhino.ts index 8f0652d3b..0b42755b6 100644 --- a/binding/web/src/rhino.ts +++ b/binding/web/src/rhino.ts @@ -1,5 +1,5 @@ /* - Copyright 2022-2023 Picovoice Inc. + Copyright 2022-2025 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. @@ -14,15 +14,15 @@ import { Mutex } from 'async-mutex'; import { - aligned_alloc_type, arrayBufferToStringAtIndex, - buildWasm, + base64ToUint8Array, isAccessKeyValid, loadModel, - pv_free_type, - PvError } from '@picovoice/web-utils'; +import createModuleSimd from "./lib/pv_rhino_simd"; +import createModulePThread from "./lib/pv_rhino_pthread"; + import { simd } from 'wasm-feature-detect'; import { @@ -44,46 +44,55 @@ import { pvStatusToException } from './rhino_errors'; type pv_rhino_init_type = ( accessKey: number, modelPath: number, + device: number, contextPath: number, sensitivity: number, endpointDurationSec: number, requireEndpoint: number, object: number -) => Promise; +) => number; type pv_rhino_process_type = ( object: number, pcm: number, isFinalized: number -) => Promise; -type pv_rhino_reset_type = (object: number) => Promise; +) => number; +type pv_rhino_reset_type = (object: number) => number; type pv_rhino_context_info_type = ( object: number, contextInfo: number -) => Promise; -type pv_rhino_delete_type = (object: number) => Promise; -type pv_rhino_frame_length_type = () => Promise; +) => number; +type pv_rhino_delete_type = (object: number) => void; +type pv_rhino_frame_length_type = () => number; type pv_rhino_free_slots_and_values_type = ( object: number, slots: number, values: number -) => Promise; +) => number; type pv_rhino_get_intent_type = ( object: number, intent: number, numSlots: number, slots: number, values: number -) => Promise; +) => number; type pv_rhino_is_understood_type = ( object: number, isUnderstood: number -) => Promise; -type pv_rhino_version_type = () => Promise; -type pv_sample_rate_type = () => Promise; -type pv_status_to_string_type = (status: number) => Promise; -type pv_set_sdk_type = (sdk: number) => Promise; -type pv_get_error_stack_type = (messageStack: number, messageStackDepth: number) => Promise; -type pv_free_error_stack_type = (messageStack: number) => Promise; +) => number; +type pv_rhino_version_type = () => number; +type pv_sample_rate_type = () => number; +type pv_status_to_string_type = (status: number) => number; +type pv_rhino_list_hardware_devices_type = ( + hardwareDevices: number, + numHardwareDevices: number +) => number; +type pv_rhino_free_hardware_devices_type = ( + hardwareDevices: number, + numHardwareDevices: number +) => number; +type pv_set_sdk_type = (sdk: number) => void; +type pv_get_error_stack_type = (messageStack: number, messageStackDepth: number) => number; +type pv_free_error_stack_type = (messageStack: number) => void; /** @@ -93,10 +102,34 @@ type pv_free_error_stack_type = (messageStack: number) => Promise; * do some rudimentary type checking and parameter validation. */ +type RhinoModule = EmscriptenModule & { + _pv_free: (address: number) => void; + + _pv_rhino_delete: pv_rhino_delete_type; + _pv_rhino_process: pv_rhino_process_type; + _pv_rhino_reset: pv_rhino_reset_type; + _pv_rhino_context_info: pv_rhino_context_info_type; + _pv_rhino_free_slots_and_values: pv_rhino_free_slots_and_values_type; + _pv_rhino_get_intent: pv_rhino_get_intent_type; + _pv_rhino_is_understood: pv_rhino_is_understood_type; + _pv_rhino_frame_length: pv_rhino_frame_length_type + _pv_rhino_version: pv_rhino_version_type + _pv_rhino_list_hardware_devices: pv_rhino_list_hardware_devices_type; + _pv_rhino_free_hardware_devices: pv_rhino_free_hardware_devices_type; + _pv_sample_rate: pv_sample_rate_type + + _pv_set_sdk: pv_set_sdk_type; + _pv_get_error_stack: pv_get_error_stack_type; + _pv_free_error_stack: pv_free_error_stack_type; + + // em default functions + addFunction: typeof addFunction; + ccall: typeof ccall; + cwrap: typeof cwrap; +} + type RhinoWasmOutput = { - aligned_alloc: aligned_alloc_type; - memory: WebAssembly.Memory; - pvFree: pv_free_type; + module: RhinoModule; contextInfo: string; frameLength: number; @@ -114,31 +147,16 @@ type RhinoWasmOutput = { valuesAddressAddressAddress: number; messageStackAddressAddressAddress: number; messageStackDepthAddress: number; - - pvRhinoDelete: pv_rhino_delete_type; - pvRhinoFreeSlotsAndValues: pv_rhino_free_slots_and_values_type; - pvRhinoGetIntent: pv_rhino_get_intent_type; - pvRhinoIsUnderstood: pv_rhino_is_understood_type; - pvRhinoProcess: pv_rhino_process_type; - pvRhinoReset: pv_rhino_reset_type; - pvStatusToString: pv_status_to_string_type; - pvGetErrorStack: pv_get_error_stack_type; - pvFreeErrorStack: pv_free_error_stack_type; }; export class Rhino { - private readonly _pvRhinoDelete: pv_rhino_delete_type; - private readonly _pvRhinoFreeSlotsAndValues: pv_rhino_free_slots_and_values_type; - private readonly _pvRhinoGetIntent: pv_rhino_get_intent_type; - private readonly _pvRhinoIsUnderstood: pv_rhino_is_understood_type; - private readonly _pvRhinoProcess: pv_rhino_process_type; - private readonly _pvRhinoReset: pv_rhino_reset_type; - private readonly _pvStatusToString: pv_status_to_string_type; - private readonly _pvGetErrorStack: pv_get_error_stack_type; - private readonly _pvFreeErrorStack: pv_free_error_stack_type; - - private _wasmMemory: WebAssembly.Memory | undefined; - private readonly _pvFree: pv_free_type; + private _module?: RhinoModule; + + private readonly _contextInfo: string; + private readonly _frameLength: number; + private readonly _sampleRate: number; + private readonly _version: string; + private readonly _processMutex: Mutex; private readonly _contextAddress: number; @@ -153,41 +171,30 @@ export class Rhino { private readonly _messageStackAddressAddressAddress: number; private readonly _messageStackDepthAddress: number; - private static _frameLength: number; - private static _sampleRate: number; - private static _contextInfo: string; - private static _version: string; - private static _wasm: string; private static _wasmSimd: string; + private static _wasmSimdLib: string; + private static _wasmPThread: string; + private static _wasmPThreadLib: string; + private static _sdk: string = "web"; + private static _rhinoMutex = new Mutex(); + private readonly _inferenceCallback: InferenceCallback; private readonly _processErrorCallback: (error: RhinoErrors.RhinoError) => void; - private static _rhinoMutex = new Mutex(); - private constructor( handleWasm: RhinoWasmOutput, inferenceCallback: InferenceCallback, processErrorCallback: (error: RhinoErrors.RhinoError) => void ) { - Rhino._frameLength = handleWasm.frameLength; - Rhino._sampleRate = handleWasm.sampleRate; - Rhino._version = handleWasm.version; - Rhino._contextInfo = handleWasm.contextInfo; - - this._pvRhinoDelete = handleWasm.pvRhinoDelete; - this._pvRhinoFreeSlotsAndValues = handleWasm.pvRhinoFreeSlotsAndValues; - this._pvRhinoGetIntent = handleWasm.pvRhinoGetIntent; - this._pvRhinoIsUnderstood = handleWasm.pvRhinoIsUnderstood; - this._pvRhinoProcess = handleWasm.pvRhinoProcess; - this._pvRhinoReset = handleWasm.pvRhinoReset; - this._pvStatusToString = handleWasm.pvStatusToString; - this._pvGetErrorStack = handleWasm.pvGetErrorStack; - this._pvFreeErrorStack = handleWasm.pvFreeErrorStack; - - this._wasmMemory = handleWasm.memory; - this._pvFree = handleWasm.pvFree; + this._module = handleWasm.module; + + this._frameLength = handleWasm.frameLength; + this._sampleRate = handleWasm.sampleRate; + this._version = handleWasm.version; + this._contextInfo = handleWasm.contextInfo; + this._contextAddress = handleWasm.contextAddress; this._inputBufferAddress = handleWasm.inputBufferAddress; this._intentAddressAddress = handleWasm.intentAddressAddress; @@ -210,47 +217,67 @@ export class Rhino { * Get Rhino engine version. */ get version(): string { - return Rhino._version; + return this._version; } /** * Get frame length. */ get frameLength(): number { - return Rhino._frameLength; + return this._frameLength; } /** * Get sample rate. */ get sampleRate(): number { - return Rhino._sampleRate; + return this._sampleRate; } /** * Get context info. */ get contextInfo(): string { - return Rhino._contextInfo; + return this._contextInfo; + } + +/** + * Set base64 wasm file with SIMD feature. + * @param wasmSimd Base64'd wasm file to use to initialize wasm. + */ + public static setWasmSimd(wasmSimd: string): void { + if (this._wasmSimd === undefined) { + this._wasmSimd = wasmSimd; + } } /** - * Set base64 wasm file. - * @param wasm Base64'd wasm file to use to initialize wasm. + * Set base64 SIMD wasm file in text format. + * @param wasmSimdLib Base64'd wasm file in text format. */ - public static setWasm(wasm: string): void { - if (this._wasm === undefined) { - this._wasm = wasm; + public static setWasmSimdLib(wasmSimdLib: string): void { + if (this._wasmSimdLib === undefined) { + this._wasmSimdLib = wasmSimdLib; } } /** - * Set base64 wasm file with SIMD feature. - * @param wasmSimd Base64'd wasm file to use to initialize wasm. + * Set base64 wasm file with SIMD and pthread feature. + * @param wasmPThread Base64'd wasm file to use to initialize wasm. */ - public static setWasmSimd(wasmSimd: string): void { - if (this._wasmSimd === undefined) { - this._wasmSimd = wasmSimd; + public static setWasmPThread(wasmPThread: string): void { + if (this._wasmPThread === undefined) { + this._wasmPThread = wasmPThread; + } + } + + /** + * Set base64 SIMD and thread wasm file in text format. + * @param wasmPThreadLib Base64'd wasm file in text format. + */ + public static setWasmPThreadLib(wasmPThreadLib: string): void { + if (this._wasmPThreadLib === undefined) { + this._wasmPThreadLib = wasmPThreadLib; } } @@ -273,6 +300,12 @@ export class Rhino { * @param model RhinoModel object containing a base64 string * representation of or path to public binary of a Rhino parameter model used to initialize Rhino. * @param options Optional configuration arguments. + * @param options.device String representation of the device (e.g., CPU or GPU) to use. If set to `best`, the most + * suitable device is selected automatically. If set to `gpu`, the engine uses the first available GPU device. To + * select a specific GPU device, set this argument to `gpu:${GPU_INDEX}`, where `${GPU_INDEX}` is the index of the + * target GPU. If set to `cpu`, the engine will run on the CPU with the default number of threads. To specify the + * number of threads, set this argument to `cpu:${NUM_THREADS}`, where `${NUM_THREADS}` is the desired number of + * threads. * @param options.endpointDurationSec Endpoint duration in seconds. * An endpoint is a chunk of silence at the end of an utterance that marks * the end of spoken command. It should be a positive number within [0.5, 5]. @@ -327,19 +360,43 @@ export class Rhino { throw new RhinoErrors.RhinoInvalidArgumentError('Invalid AccessKey'); } + let { device = "best" } = options; + const { processErrorCallback } = options; + + const isSimd = await simd(); + if (!isSimd) { + throw new RhinoErrors.RhinoRuntimeError('Browser not supported.'); + } + + const isWorkerScope = + typeof WorkerGlobalScope !== 'undefined' && + self instanceof WorkerGlobalScope; + if ( + !isWorkerScope && + (device === 'best' || (device.startsWith('cpu') && device !== 'cpu:1')) + ) { + // eslint-disable-next-line no-console + console.warn('Multi-threading is not supported on main thread.'); + device = 'cpu:1'; + } + + const sabDefined = typeof SharedArrayBuffer !== 'undefined' + && (device !== "cpu:1"); + return new Promise((resolve, reject) => { Rhino._rhinoMutex .runExclusive(async () => { - const isSimd = await simd(); const wasmOutput = await Rhino.initWasm( accessKey.trim(), contextPath, sensitivity, - isSimd ? this._wasmSimd : this._wasm, modelPath, - options + device, + (sabDefined) ? this._wasmPThread : this._wasmSimd, + (sabDefined) ? this._wasmPThreadLib : this._wasmSimdLib, + (sabDefined) ? createModulePThread : createModuleSimd, + options, ); - const { processErrorCallback } = options; return new Rhino(wasmOutput, inferenceCallback, processErrorCallback); }) .then((result: Rhino) => { @@ -365,63 +422,54 @@ export class Rhino { this._processMutex .runExclusive(async () => { - if (this._wasmMemory === undefined) { - throw new RhinoErrors.RhinoInvalidStateError('Attempted to call Rhino process after release.'); + if (this._module === undefined) { + throw new RhinoErrors.RhinoInvalidStateError( + 'Attempted to call Rhino process after release.' + ); } - const memoryBuffer = new Int16Array(this._wasmMemory.buffer); - memoryBuffer.set( - pcm, - this._inputBufferAddress / Int16Array.BYTES_PER_ELEMENT - ); + this._module.HEAP16.set(pcm, this._inputBufferAddress / Int16Array.BYTES_PER_ELEMENT); - let status = await this._pvRhinoProcess( + let status = this._module._pv_rhino_process( this._objectAddress, this._inputBufferAddress, this._isFinalizedAddress ); - const memoryBufferUint8 = new Uint8Array(this._wasmMemory.buffer); - const memoryBufferView = new DataView(this._wasmMemory.buffer); - if (status !== PvStatus.SUCCESS) { const messageStack = await Rhino.getMessageStack( - this._pvGetErrorStack, - this._pvFreeErrorStack, + this._module._pv_get_error_stack, + this._module._pv_free_error_stack, this._messageStackAddressAddressAddress, this._messageStackDepthAddress, - memoryBufferView, - memoryBufferUint8 + this._module.HEAP32, + this._module.HEAPU8 ); throw pvStatusToException(status, "Processing failed", messageStack); } - const isFinalized = memoryBufferView.getUint8( - this._isFinalizedAddress - ); + const isFinalized = this._module.HEAPU8[this._isFinalizedAddress]; if (isFinalized === 1) { - status = await this._pvRhinoIsUnderstood( + status = this._module._pv_rhino_is_understood( this._objectAddress, this._isUnderstoodAddress ); if (status !== PvStatus.SUCCESS) { const messageStack = await Rhino.getMessageStack( - this._pvGetErrorStack, - this._pvFreeErrorStack, + this._module._pv_get_error_stack, + this._module._pv_free_error_stack, this._messageStackAddressAddressAddress, this._messageStackDepthAddress, - memoryBufferView, - memoryBufferUint8 + this._module.HEAP32, + this._module.HEAPU8 ); throw pvStatusToException(status, "Failed to get inference", messageStack); } - const isUnderstood = memoryBufferView.getUint8( - this._isUnderstoodAddress - ); + const isUnderstood = this._module.HEAPU8[this._isUnderstoodAddress]; if (isUnderstood === -1) { throw new RhinoErrors.RhinoInvalidStateError('Rhino failed to process the command'); @@ -430,7 +478,7 @@ export class Rhino { let intent = null; const slots = {}; if (isUnderstood === 1) { - status = await this._pvRhinoGetIntent( + status = this._module._pv_rhino_get_intent( this._objectAddress, this._intentAddressAddress, this._numSlotsAddress, @@ -439,30 +487,24 @@ export class Rhino { ); if (status !== PvStatus.SUCCESS) { const messageStack = await Rhino.getMessageStack( - this._pvGetErrorStack, - this._pvFreeErrorStack, + this._module._pv_get_error_stack, + this._module._pv_free_error_stack, this._messageStackAddressAddressAddress, this._messageStackDepthAddress, - memoryBufferView, - memoryBufferUint8 + this._module.HEAP32, + this._module.HEAPU8 ); throw pvStatusToException(status, "Failed to get intent", messageStack); } - const intentAddress = memoryBufferView.getInt32( - this._intentAddressAddress, - true - ); + const intentAddress = this._module.HEAP32[this._intentAddressAddress / Int32Array.BYTES_PER_ELEMENT]; intent = arrayBufferToStringAtIndex( - memoryBufferUint8, + this._module.HEAPU8, intentAddress ); - const numSlots = memoryBufferView.getInt32( - this._numSlotsAddress, - true - ); + const numSlots = this._module.HEAP32[this._numSlotsAddress / Int32Array.BYTES_PER_ELEMENT]; if (numSlots === -1) { throw new RhinoErrors.RhinoInvalidStateError('Rhino failed to get the number of slots'); } @@ -479,44 +521,37 @@ export class Rhino { slots[slot] = value; } - const slotsAddressAddress = memoryBufferView.getInt32( - this._slotsAddressAddressAddress, - true - ); - - const valuesAddressAddress = memoryBufferView.getInt32( - this._valuesAddressAddressAddress, - true - ); + const slotsAddressAddress = this._module.HEAP32[this._slotsAddressAddressAddress / Int32Array.BYTES_PER_ELEMENT]; + const valuesAddressAddress = this._module.HEAP32[this._valuesAddressAddressAddress / Int32Array.BYTES_PER_ELEMENT]; - status = await this._pvRhinoFreeSlotsAndValues( + status = this._module._pv_rhino_free_slots_and_values( this._objectAddress, slotsAddressAddress, valuesAddressAddress ); if (status !== PvStatus.SUCCESS) { const messageStack = await Rhino.getMessageStack( - this._pvGetErrorStack, - this._pvFreeErrorStack, + this._module._pv_get_error_stack, + this._module._pv_free_error_stack, this._messageStackAddressAddressAddress, this._messageStackDepthAddress, - memoryBufferView, - memoryBufferUint8 + this._module.HEAP32, + this._module.HEAPU8 ); throw pvStatusToException(status, "Failed to clean up resources", messageStack); } } - status = await this._pvRhinoReset(this._objectAddress); + status = this._module._pv_rhino_reset(this._objectAddress); if (status !== PvStatus.SUCCESS) { const messageStack = await Rhino.getMessageStack( - this._pvGetErrorStack, - this._pvFreeErrorStack, + this._module._pv_get_error_stack, + this._module._pv_free_error_stack, this._messageStackAddressAddressAddress, this._messageStackDepthAddress, - memoryBufferView, - memoryBufferUint8 + this._module.HEAP32, + this._module.HEAPU8 ); throw pvStatusToException(status, "Failed to reset", messageStack); @@ -549,54 +584,32 @@ export class Rhino { } private _getSlot(index: number): string { - const memoryBufferUint8 = new Uint8Array(this._wasmMemory.buffer); - const memoryBufferView = new DataView(this._wasmMemory.buffer); - - const slotsAddressAddress = memoryBufferView.getInt32( - this._slotsAddressAddressAddress, - true - ); + const slotsAddressAddress = this._module.HEAP32[this._slotsAddressAddressAddress / Int32Array.BYTES_PER_ELEMENT]; + const slotAddress = this._module.HEAP32[(slotsAddressAddress / Int32Array.BYTES_PER_ELEMENT) + index]; - const slotAddress = memoryBufferView.getInt32( - slotsAddressAddress + index * Int32Array.BYTES_PER_ELEMENT, - true - ); - - return arrayBufferToStringAtIndex(memoryBufferUint8, slotAddress); + return arrayBufferToStringAtIndex(this._module.HEAPU8, slotAddress); } private _getSlotValue(index: number): string { - const memoryBufferUint8 = new Uint8Array(this._wasmMemory.buffer); - const memoryBufferView = new DataView(this._wasmMemory.buffer); + const valuesAddressAddress = this._module.HEAP32[this._valuesAddressAddressAddress / Int32Array.BYTES_PER_ELEMENT]; + const valueAddress = this._module.HEAP32[(valuesAddressAddress / Int32Array.BYTES_PER_ELEMENT) + index]; - const valuesAddressAddress = memoryBufferView.getInt32( - this._valuesAddressAddressAddress, - true - ); - - const valueAddress = memoryBufferView.getInt32( - valuesAddressAddress + index * Int32Array.BYTES_PER_ELEMENT, - true - ); - - return arrayBufferToStringAtIndex(memoryBufferUint8, valueAddress); + return arrayBufferToStringAtIndex(this._module.HEAPU8, valueAddress); } /** * Resets the internal Rhino state. */ public async reset(): Promise { - const status = await this._pvRhinoReset(this._objectAddress); + const status = this._module._pv_rhino_reset(this._objectAddress); if (status !== PvStatus.SUCCESS) { - const memoryBufferView = new DataView(this._wasmMemory.buffer); - const memoryBufferUint8 = new Uint8Array(this._wasmMemory.buffer); const messageStack = await Rhino.getMessageStack( - this._pvGetErrorStack, - this._pvFreeErrorStack, + this._module._pv_get_error_stack, + this._module._pv_free_error_stack, this._messageStackAddressAddressAddress, this._messageStackDepthAddress, - memoryBufferView, - memoryBufferUint8 + this._module.HEAP32, + this._module.HEAPU8 ); throw pvStatusToException(status, "Failed to reset", messageStack); @@ -607,19 +620,21 @@ export class Rhino { * Releases resources acquired by WebAssembly module. */ public async release(): Promise { - await this._pvRhinoDelete(this._objectAddress); - await this._pvFree(this._messageStackAddressAddressAddress); - await this._pvFree(this._messageStackDepthAddress); - await this._pvFree(this._contextAddress); - await this._pvFree(this._inputBufferAddress); - await this._pvFree(this._intentAddressAddress); - await this._pvFree(this._isFinalizedAddress); - await this._pvFree(this._isUnderstoodAddress); - await this._pvFree(this._numSlotsAddress); - await this._pvFree(this._slotsAddressAddressAddress); - await this._pvFree(this._valuesAddressAddressAddress); - delete this._wasmMemory; - this._wasmMemory = undefined; + if (!this._module) { + return; + } + this._module._pv_rhino_delete(this._objectAddress); + this._module._pv_free(this._messageStackAddressAddressAddress); + this._module._pv_free(this._messageStackDepthAddress); + this._module._pv_free(this._contextAddress); + this._module._pv_free(this._inputBufferAddress); + this._module._pv_free(this._intentAddressAddress); + this._module._pv_free(this._isFinalizedAddress); + this._module._pv_free(this._isUnderstoodAddress); + this._module._pv_free(this._numSlotsAddress); + this._module._pv_free(this._slotsAddressAddressAddress); + this._module._pv_free(this._valuesAddressAddressAddress); + this._module = undefined; } public async onmessage(e: MessageEvent): Promise { @@ -637,44 +652,13 @@ export class Rhino { accessKey: string, contextPath: string, sensitivity: number, - wasmBase64: string, modelPath: string, + device: string, + wasmBase64: string, + wasmLibBase64: string, + createModuleFunc: any, initConfig: RhinoOptions ): Promise { - // A WebAssembly page has a constant size of 64KiB. -> 1MiB ~= 16 pages - // minimum memory requirements for init: 17 pages - const memory = new WebAssembly.Memory({ initial: 128 }); - - const memoryBufferUint8 = new Uint8Array(memory.buffer); - - const pvError = new PvError(); - - const exports = await buildWasm(memory, wasmBase64, pvError); - - const aligned_alloc = exports.aligned_alloc as aligned_alloc_type; - const pv_free = exports.pv_free as pv_free_type; - const pv_rhino_version = exports.pv_rhino_version as pv_rhino_version_type; - const pv_rhino_context_info = - exports.pv_rhino_context_info as pv_rhino_context_info_type; - const pv_rhino_frame_length = - exports.pv_rhino_frame_length as pv_rhino_frame_length_type; - const pv_rhino_process = exports.pv_rhino_process as pv_rhino_process_type; - const pv_rhino_is_understood = - exports.pv_rhino_is_understood as pv_rhino_is_understood_type; - const pv_rhino_get_intent = - exports.pv_rhino_get_intent as pv_rhino_get_intent_type; - const pv_rhino_delete = exports.pv_rhino_delete as pv_rhino_delete_type; - const pv_rhino_free_slots_and_values = - exports.pv_rhino_free_slots_and_values as pv_rhino_free_slots_and_values_type; - const pv_rhino_init = exports.pv_rhino_init as pv_rhino_init_type; - const pv_rhino_reset = exports.pv_rhino_reset as pv_rhino_reset_type; - const pv_status_to_string = - exports.pv_status_to_string as pv_status_to_string_type; - const pv_sample_rate = exports.pv_sample_rate as pv_sample_rate_type; - const pv_set_sdk = exports.pv_set_sdk as pv_set_sdk_type; - const pv_get_error_stack = exports.pv_get_error_stack as pv_get_error_stack_type; - const pv_free_error_stack = exports.pv_free_error_stack as pv_free_error_stack_type; - const { endpointDurationSec = 1.0, requireEndpoint = true } = initConfig; if (sensitivity && !(typeof sensitivity === 'number')) { throw new RhinoErrors.RhinoInvalidArgumentError( @@ -696,83 +680,96 @@ export class Rhino { ); } - // acquire and init memory for c_object - const objectAddressAddress = await aligned_alloc( - Int32Array.BYTES_PER_ELEMENT, - Int32Array.BYTES_PER_ELEMENT + const blob = new Blob( + [base64ToUint8Array(wasmLibBase64)], + { type: 'application/javascript' } ); + const module: RhinoModule = await createModuleFunc({ + mainScriptUrlOrBlob: blob, + wasmBinary: base64ToUint8Array(wasmBase64), + }); + + const pv_rhino_init: pv_rhino_init_type = this.wrapAsyncFunction( + module, + "pv_rhino_init", + 8); + + const objectAddressAddress = module._malloc(Int32Array.BYTES_PER_ELEMENT); if (objectAddressAddress === 0) { - throw new RhinoErrors.RhinoOutOfMemoryError('malloc failed: Cannot allocate memory'); + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory' + ); } - // acquire and init memory for c_access_key - const accessKeyAddress = await aligned_alloc( - Uint8Array.BYTES_PER_ELEMENT, - (accessKey.length + 1) * Uint8Array.BYTES_PER_ELEMENT - ); + const accessKeyEncoded = new TextEncoder().encode(accessKey); + const accessKeyAddress = module._malloc((accessKey.length + 1) * Uint8Array.BYTES_PER_ELEMENT); if (accessKeyAddress === 0) { - throw new RhinoErrors.RhinoOutOfMemoryError('malloc failed: Cannot allocate memory'); - } - for (let i = 0; i < accessKey.length; i++) { - memoryBufferUint8[accessKeyAddress + i] = accessKey.charCodeAt(i); + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory' + ); } - memoryBufferUint8[accessKeyAddress + accessKey.length] = 0; + module.HEAP8.set(accessKeyEncoded, accessKeyAddress); + module.HEAP8[accessKeyAddress + accessKeyEncoded.length] = 0; - // acquire and init memory for c_model_path - const encodedModelPath = new TextEncoder().encode(modelPath); - const modelPathAddress = await aligned_alloc( - Uint8Array.BYTES_PER_ELEMENT, - (encodedModelPath.length + 1) * Uint8Array.BYTES_PER_ELEMENT - ); + const modelPathEncoded = new TextEncoder().encode(modelPath); + const modelPathAddress = module._malloc((modelPath.length + 1) * Uint8Array.BYTES_PER_ELEMENT); if (modelPathAddress === 0) { - throw new RhinoErrors.RhinoOutOfMemoryError('malloc failed: Cannot allocate memory'); + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory' + ); } - memoryBufferUint8.set(encodedModelPath, modelPathAddress); - memoryBufferUint8[modelPathAddress + encodedModelPath.length] = 0; + module.HEAP8.set(modelPathEncoded, modelPathAddress); + module.HEAP8[modelPathAddress + modelPathEncoded.length] = 0; + + const deviceEncoded = new TextEncoder().encode(device); + const deviceAddress = module._malloc((device.length + 1) * Uint8Array.BYTES_PER_ELEMENT); + if (deviceAddress === 0) { + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory' + ); + } + module.HEAP8.set(deviceEncoded, deviceAddress); + module.HEAPU8[deviceAddress + deviceEncoded.length] = 0; - // acquire and init memory for c_context_path - const encodedContextPath = new TextEncoder().encode(contextPath); - const contextPathAddress = await aligned_alloc( - Uint8Array.BYTES_PER_ELEMENT, - (encodedContextPath.length + 1) * Uint8Array.BYTES_PER_ELEMENT - ); + const contextPathEncoded = new TextEncoder().encode(contextPath); + const contextPathAddress = module._malloc((contextPath.length + 1) * Uint8Array.BYTES_PER_ELEMENT); if (contextPathAddress === 0) { - throw new RhinoErrors.RhinoOutOfMemoryError('malloc failed: Cannot allocate memory'); + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory' + ); } - memoryBufferUint8.set(encodedContextPath, contextPathAddress); - memoryBufferUint8[contextPathAddress + encodedContextPath.length] = 0; + module.HEAP8.set(contextPathEncoded, contextPathAddress); + module.HEAP8[contextPathAddress + contextPathEncoded.length] = 0; const sdkEncoded = new TextEncoder().encode(this._sdk); - const sdkAddress = await aligned_alloc( - Uint8Array.BYTES_PER_ELEMENT, - (sdkEncoded.length + 1) * Uint8Array.BYTES_PER_ELEMENT - ); + const sdkAddress = module._malloc((sdkEncoded.length + 1) * Uint8Array.BYTES_PER_ELEMENT); if (!sdkAddress) { - throw new RhinoErrors.RhinoOutOfMemoryError('malloc failed: Cannot allocate memory'); + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory' + ); } - memoryBufferUint8.set(sdkEncoded, sdkAddress); - memoryBufferUint8[sdkAddress + sdkEncoded.length] = 0; - await pv_set_sdk(sdkAddress); + module.HEAP8.set(sdkEncoded, sdkAddress); + module.HEAP8[sdkAddress + sdkEncoded.length] = 0; + module._pv_set_sdk(sdkAddress); - const messageStackDepthAddress = await aligned_alloc( - Int32Array.BYTES_PER_ELEMENT, - Int32Array.BYTES_PER_ELEMENT - ); + const messageStackDepthAddress = module._malloc(Int32Array.BYTES_PER_ELEMENT); if (!messageStackDepthAddress) { - throw new RhinoErrors.RhinoOutOfMemoryError('malloc failed: Cannot allocate memory'); + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory' + ); } - const messageStackAddressAddressAddress = await aligned_alloc( - Int32Array.BYTES_PER_ELEMENT, - Int32Array.BYTES_PER_ELEMENT - ); + const messageStackAddressAddressAddress = module._malloc(Int32Array.BYTES_PER_ELEMENT); if (!messageStackAddressAddressAddress) { - throw new RhinoErrors.RhinoOutOfMemoryError('malloc failed: Cannot allocate memory'); + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory' + ); } let status = await pv_rhino_init( accessKeyAddress, modelPathAddress, + deviceAddress, contextPathAddress, sensitivity, endpointDurationSec, @@ -780,128 +777,112 @@ export class Rhino { objectAddressAddress ); - await pv_free(accessKeyAddress); - await pv_free(modelPathAddress); - await pv_free(contextPathAddress); - - const memoryBufferView = new DataView(memory.buffer); + module._pv_free(accessKeyAddress); + module._pv_free(modelPathAddress); + module._pv_free(deviceAddress); + module._pv_free(contextPathAddress); if (status !== PvStatus.SUCCESS) { const messageStack = await Rhino.getMessageStack( - pv_get_error_stack, - pv_free_error_stack, + module._pv_get_error_stack, + module._pv_free_error_stack, messageStackAddressAddressAddress, messageStackDepthAddress, - memoryBufferView, - memoryBufferUint8 + module.HEAP32, + module.HEAPU8 ); - throw pvStatusToException(status, "Initialization failed", messageStack, pvError); + throw pvStatusToException(status, 'Initialization failed', messageStack); } - const objectAddress = memoryBufferView.getInt32(objectAddressAddress, true); - await pv_free(objectAddressAddress); + const objectAddress = module.HEAP32[objectAddressAddress / Int32Array.BYTES_PER_ELEMENT]; + module._pv_free(objectAddressAddress); - const sampleRate = await pv_sample_rate(); - const frameLength = await pv_rhino_frame_length(); - const versionAddress = await pv_rhino_version(); + const sampleRate = module._pv_sample_rate(); + const frameLength = module._pv_rhino_frame_length(); + const versionAddress = module._pv_rhino_version(); const version = arrayBufferToStringAtIndex( - memoryBufferUint8, + module.HEAPU8, versionAddress ); - const contextInfoAddressAddress = await aligned_alloc( - Int32Array.BYTES_PER_ELEMENT, - Int32Array.BYTES_PER_ELEMENT - ); + const contextInfoAddressAddress = module._malloc(Int32Array.BYTES_PER_ELEMENT); if (contextInfoAddressAddress === 0) { throw new RhinoErrors.RhinoOutOfMemoryError('malloc failed: Cannot allocate memory'); } - status = await pv_rhino_context_info( + status = module._pv_rhino_context_info( objectAddress, contextInfoAddressAddress ); if (status !== PvStatus.SUCCESS) { const messageStack = await Rhino.getMessageStack( - pv_get_error_stack, - pv_free_error_stack, + module._pv_get_error_stack, + module._pv_free_error_stack, messageStackAddressAddressAddress, messageStackDepthAddress, - memoryBufferView, - memoryBufferUint8 + module.HEAP32, + module.HEAPU8 ); - throw pvStatusToException(status, "Failed to get context info", messageStack, pvError); + throw pvStatusToException(status, "Failed to get context info", messageStack); } - const contextInfoAddress = memoryBufferView.getInt32( - contextInfoAddressAddress, - true - ); - await pv_free(contextInfoAddressAddress); + const contextInfoAddress = module.HEAP32[contextInfoAddressAddress / Int32Array.BYTES_PER_ELEMENT]; + await module._pv_free(contextInfoAddressAddress); const contextInfo = arrayBufferToStringAtIndex( - memoryBufferUint8, + module.HEAPU8, contextInfoAddress ); - const inputBufferAddress = await aligned_alloc( - Int16Array.BYTES_PER_ELEMENT, - frameLength * Int16Array.BYTES_PER_ELEMENT - ); + const inputBufferAddress = module._malloc(frameLength * Int16Array.BYTES_PER_ELEMENT); if (inputBufferAddress === 0) { - throw new RhinoErrors.RhinoOutOfMemoryError('malloc failed: Cannot allocate memory'); + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory' + ); } - const isFinalizedAddress = await aligned_alloc( - Uint8Array.BYTES_PER_ELEMENT, - Uint8Array.BYTES_PER_ELEMENT - ); + const isFinalizedAddress = module._malloc(Uint8Array.BYTES_PER_ELEMENT); if (isFinalizedAddress === 0) { - throw new RhinoErrors.RhinoOutOfMemoryError('malloc failed: Cannot allocate memory'); + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory' + ); } - const isUnderstoodAddress = await aligned_alloc( - Uint8Array.BYTES_PER_ELEMENT, - Uint8Array.BYTES_PER_ELEMENT - ); + const isUnderstoodAddress = module._malloc(Uint8Array.BYTES_PER_ELEMENT); if (isUnderstoodAddress === 0) { - throw new RhinoErrors.RhinoOutOfMemoryError('malloc failed: Cannot allocate memory'); + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory' + ); } - const intentAddressAddress = await aligned_alloc( - Int32Array.BYTES_PER_ELEMENT, - Int32Array.BYTES_PER_ELEMENT - ); + const intentAddressAddress = module._malloc(Int32Array.BYTES_PER_ELEMENT); if (intentAddressAddress === 0) { - throw new RhinoErrors.RhinoOutOfMemoryError('malloc failed: Cannot allocate memory'); + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory' + ); } - const numSlotsAddress = await aligned_alloc( - Int32Array.BYTES_PER_ELEMENT, - Int32Array.BYTES_PER_ELEMENT - ); + const numSlotsAddress = module._malloc(Int32Array.BYTES_PER_ELEMENT); if (numSlotsAddress === 0) { - throw new RhinoErrors.RhinoOutOfMemoryError('malloc failed: Cannot allocate memory'); + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory' + ); } - const slotsAddressAddressAddress = await aligned_alloc( - Int32Array.BYTES_PER_ELEMENT, - Int32Array.BYTES_PER_ELEMENT - ); + const slotsAddressAddressAddress = module._malloc(Int32Array.BYTES_PER_ELEMENT); if (slotsAddressAddressAddress === 0) { - throw new RhinoErrors.RhinoOutOfMemoryError('malloc failed: Cannot allocate memory'); + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory' + ); } - const valuesAddressAddressAddress = await aligned_alloc( - Int32Array.BYTES_PER_ELEMENT, - Int32Array.BYTES_PER_ELEMENT - ); + const valuesAddressAddressAddress = module._malloc(Int32Array.BYTES_PER_ELEMENT); if (valuesAddressAddressAddress === 0) { - throw new RhinoErrors.RhinoOutOfMemoryError('malloc failed: Cannot allocate memory'); + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory' + ); } return { - aligned_alloc, - memory: memory, - pvFree: pv_free, + module: module, contextInfo: contextInfo, frameLength: frameLength, @@ -918,45 +899,151 @@ export class Rhino { valuesAddressAddressAddress: valuesAddressAddressAddress, messageStackAddressAddressAddress: messageStackAddressAddressAddress, messageStackDepthAddress: messageStackDepthAddress, - - pvRhinoDelete: pv_rhino_delete, - pvRhinoFreeSlotsAndValues: pv_rhino_free_slots_and_values, - pvRhinoGetIntent: pv_rhino_get_intent, - pvRhinoIsUnderstood: pv_rhino_is_understood, - pvRhinoProcess: pv_rhino_process, - pvRhinoReset: pv_rhino_reset, - pvStatusToString: pv_status_to_string, - pvGetErrorStack: pv_get_error_stack, - pvFreeErrorStack: pv_free_error_stack, }; } + /** + * Lists all available devices that Rhino can use for inference. + * Each entry in the list can be the used as the `device` argument for the `.create` method. + * + * @returns List of all available devices that Rhino can use for inference. + */ + public static async listAvailableDevices(): Promise { + return new Promise((resolve, reject) => { + Rhino._rhinoMutex + .runExclusive(async () => { + const isSimd = await simd(); + if (!isSimd) { + throw new RhinoErrors.RhinoRuntimeError('Unsupported Browser'); + } + + const blob = new Blob( + [base64ToUint8Array(this._wasmSimdLib)], + { type: 'application/javascript' } + ); + const module: RhinoModule = await createModuleSimd({ + mainScriptUrlOrBlob: blob, + wasmBinary: base64ToUint8Array(this._wasmSimd), + }); + + const hardwareDevicesAddressAddress = module._malloc(Int32Array.BYTES_PER_ELEMENT); + if (hardwareDevicesAddressAddress === 0) { + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory for hardwareDevices' + ); + } + + const numHardwareDevicesAddress = module._malloc(Int32Array.BYTES_PER_ELEMENT); + if (numHardwareDevicesAddress === 0) { + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory for numHardwareDevices' + ); + } + + const status: PvStatus = module._pv_rhino_list_hardware_devices( + hardwareDevicesAddressAddress, + numHardwareDevicesAddress + ); + + const messageStackDepthAddress = module._malloc(Int32Array.BYTES_PER_ELEMENT); + if (!messageStackDepthAddress) { + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory for messageStackDepth' + ); + } + + const messageStackAddressAddressAddress = module._malloc(Int32Array.BYTES_PER_ELEMENT); + if (!messageStackAddressAddressAddress) { + throw new RhinoErrors.RhinoOutOfMemoryError( + 'malloc failed: Cannot allocate memory messageStack' + ); + } + + if (status !== PvStatus.SUCCESS) { + const messageStack = await Rhino.getMessageStack( + module._pv_get_error_stack, + module._pv_free_error_stack, + messageStackAddressAddressAddress, + messageStackDepthAddress, + module.HEAP32, + module.HEAPU8, + ); + module._pv_free(messageStackAddressAddressAddress); + module._pv_free(messageStackDepthAddress); + + throw pvStatusToException( + status, + 'List devices failed', + messageStack + ); + } + module._pv_free(messageStackAddressAddressAddress); + module._pv_free(messageStackDepthAddress); + + const numHardwareDevices: number = module.HEAP32[numHardwareDevicesAddress / Int32Array.BYTES_PER_ELEMENT]; + module._pv_free(numHardwareDevicesAddress); + + const hardwareDevicesAddress = module.HEAP32[hardwareDevicesAddressAddress / Int32Array.BYTES_PER_ELEMENT]; + + const hardwareDevices: string[] = []; + for (let i = 0; i < numHardwareDevices; i++) { + const deviceAddress = module.HEAP32[hardwareDevicesAddress / Int32Array.BYTES_PER_ELEMENT + i]; + hardwareDevices.push(arrayBufferToStringAtIndex(module.HEAPU8, deviceAddress)); + } + module._pv_rhino_free_hardware_devices( + hardwareDevicesAddress, + numHardwareDevices + ); + module._pv_free(hardwareDevicesAddressAddress); + + return hardwareDevices; + }) + .then((result: string[]) => { + resolve(result); + }) + .catch((error: any) => { + reject(error); + }); + }); + } + private static async getMessageStack( pv_get_error_stack: pv_get_error_stack_type, pv_free_error_stack: pv_free_error_stack_type, messageStackAddressAddressAddress: number, messageStackDepthAddress: number, - memoryBufferView: DataView, + memoryBufferInt32: Int32Array, memoryBufferUint8: Uint8Array, ): Promise { - const status = await pv_get_error_stack(messageStackAddressAddressAddress, messageStackDepthAddress); + const status = pv_get_error_stack(messageStackAddressAddressAddress, messageStackDepthAddress); if (status !== PvStatus.SUCCESS) { - throw pvStatusToException(status, "Unable to get Rhino error state"); + throw pvStatusToException(status, 'Unable to get Rhino error state'); } - const messageStackAddressAddress = memoryBufferView.getInt32(messageStackAddressAddressAddress, true); + const messageStackAddressAddress = memoryBufferInt32[messageStackAddressAddressAddress / Int32Array.BYTES_PER_ELEMENT]; - const messageStackDepth = memoryBufferView.getInt32(messageStackDepthAddress, true); + const messageStackDepth = memoryBufferInt32[messageStackDepthAddress / Int32Array.BYTES_PER_ELEMENT]; const messageStack: string[] = []; for (let i = 0; i < messageStackDepth; i++) { - const messageStackAddress = memoryBufferView.getInt32( - messageStackAddressAddress + (i * Int32Array.BYTES_PER_ELEMENT), true); + const messageStackAddress = memoryBufferInt32[ + (messageStackAddressAddress / Int32Array.BYTES_PER_ELEMENT) + i + ]; const message = arrayBufferToStringAtIndex(memoryBufferUint8, messageStackAddress); messageStack.push(message); } - await pv_free_error_stack(messageStackAddressAddress); + pv_free_error_stack(messageStackAddressAddress); return messageStack; } + + protected static wrapAsyncFunction(module: RhinoModule, functionName: string, numArgs: number): (...args: any[]) => any { + // @ts-ignore + return module.cwrap( + functionName, + "number", + Array(numArgs).fill("number"), + { async: true } + ); + } } diff --git a/binding/web/src/rhino_worker.ts b/binding/web/src/rhino_worker.ts index 0714da169..15fc9ec06 100644 --- a/binding/web/src/rhino_worker.ts +++ b/binding/web/src/rhino_worker.ts @@ -1,5 +1,5 @@ /* - Copyright 2022-2023 Picovoice Inc. + Copyright 2022-2025 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. @@ -34,8 +34,11 @@ export class RhinoWorker { private readonly _sampleRate: number; private readonly _contextInfo: string; - private static _wasm: string; private static _wasmSimd: string; + private static _wasmSimdLib: string; + private static _wasmPThread: string; + private static _wasmPThreadLib: string; + private static _sdk: string = "web"; private constructor( @@ -87,23 +90,43 @@ export class RhinoWorker { return this._contextInfo; } +/** + * Set base64 wasm file with SIMD feature. + * @param wasmSimd Base64'd wasm SIMD file to use to initialize wasm. + */ + public static setWasmSimd(wasmSimd: string): void { + if (this._wasmSimd === undefined) { + this._wasmSimd = wasmSimd; + } + } + /** - * Set base64 wasm file. - * @param wasm Base64'd wasm file to use to initialize wasm. + * Set base64 wasm file with SIMD feature in text format. + * @param wasmSimdLib Base64'd wasm SIMD file in text format. */ - public static setWasm(wasm: string): void { - if (this._wasm === undefined) { - this._wasm = wasm; + public static setWasmSimdLib(wasmSimdLib: string): void { + if (this._wasmSimdLib === undefined) { + this._wasmSimdLib = wasmSimdLib; } } /** - * Set base64 wasm file with SIMD feature. - * @param wasmSimd Base64'd wasm file to use to initialize wasm. + * Set base64 wasm file with SIMD and pthread feature. + * @param wasmPThread Base64'd wasm file to use to initialize wasm. */ - public static setWasmSimd(wasmSimd: string): void { - if (this._wasmSimd === undefined) { - this._wasmSimd = wasmSimd; + public static setWasmPThread(wasmPThread: string): void { + if (this._wasmPThread === undefined) { + this._wasmPThread = wasmPThread; + } + } + + /** + * Set base64 SIMD and thread wasm file in text format. + * @param wasmPThreadLib Base64'd wasm file in text format. + */ + public static setWasmPThreadLib(wasmPThreadLib: string): void { + if (this._wasmPThreadLib === undefined) { + this._wasmPThreadLib = wasmPThreadLib; } } @@ -126,6 +149,12 @@ export class RhinoWorker { * @param model RhinoModel object containing a base64 string * representation of or path to public binary of a Rhino parameter model used to initialize Rhino. * @param options Optional configuration arguments. + * @param options.device String representation of the device (e.g., CPU or GPU) to use. If set to `best`, the most + * suitable device is selected automatically. If set to `gpu`, the engine uses the first available GPU device. To + * select a specific GPU device, set this argument to `gpu:${GPU_INDEX}`, where `${GPU_INDEX}` is the index of the + * target GPU. If set to `cpu`, the engine will run on the CPU with the default number of threads. To specify the + * number of threads, set this argument to `cpu:${NUM_THREADS}`, where `${NUM_THREADS}` is the desired number of + * threads. * @param options.endpointDurationSec Endpoint duration in seconds. * An endpoint is a chunk of silence at the end of an utterance that marks * the end of spoken command. It should be a positive number within [0.5, 5]. @@ -226,8 +255,10 @@ export class RhinoWorker { sensitivity: sensitivity, modelPath: modelPath, options: rest, - wasm: this._wasm, wasmSimd: this._wasmSimd, + wasmSimdLib: this._wasmSimdLib, + wasmPThread: this._wasmPThread, + wasmPThreadLib: this._wasmPThreadLib, sdk: this._sdk, }); diff --git a/binding/web/src/rhino_worker_handler.ts b/binding/web/src/rhino_worker_handler.ts index 7345fcdbe..621f98cbe 100644 --- a/binding/web/src/rhino_worker_handler.ts +++ b/binding/web/src/rhino_worker_handler.ts @@ -1,5 +1,5 @@ /* - Copyright 2022-2023 Picovoice Inc. + Copyright 2022-2025 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. @@ -50,8 +50,10 @@ self.onmessage = async function ( return; } try { - Rhino.setWasm(event.data.wasm); Rhino.setWasmSimd(event.data.wasmSimd); + Rhino.setWasmSimdLib(event.data.wasmSimdLib); + Rhino.setWasmPThread(event.data.wasmPThread); + Rhino.setWasmPThreadLib(event.data.wasmPThreadLib); Rhino.setSdk(event.data.sdk); rhino = await Rhino._init( event.data.accessKey, diff --git a/binding/web/src/types.ts b/binding/web/src/types.ts index d335ef237..bf1ca014f 100644 --- a/binding/web/src/types.ts +++ b/binding/web/src/types.ts @@ -1,5 +1,5 @@ /* - Copyright 2022-2023 Picovoice Inc. + Copyright 2022-2025 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. @@ -35,6 +35,8 @@ export type RhinoContext = PvModel & { export type RhinoModel = PvModel; export type RhinoOptions = { + /** @defaultValue 'best' */ + device?: string; /** @defaultValue '1.0' */ endpointDurationSec?: number; /** @defaultValue 'false' */ @@ -61,10 +63,13 @@ export type RhinoWorkerInitRequest = { accessKey: string; contextPath: string; sensitivity: number; - wasm: string; + modelPath: string; + device: string; wasmSimd: string; + wasmSimdLib: string; + wasmPThread: string; + wasmPThreadLib: string; sdk: string; - modelPath: string; options: RhinoOptions; }; diff --git a/binding/web/test/rhino.test.ts b/binding/web/test/rhino.test.ts index 2eea1e5f2..c4d280593 100644 --- a/binding/web/test/rhino.test.ts +++ b/binding/web/test/rhino.test.ts @@ -7,6 +7,7 @@ import { PvModel } from '@picovoice/web-utils'; import { RhinoError } from "../dist/types/rhino_errors"; const ACCESS_KEY: string = Cypress.env("ACCESS_KEY"); +const DEVICE = Cypress.env('DEVICE'); function delay(time: number) { return new Promise(resolve => setTimeout(resolve, time)); @@ -18,6 +19,7 @@ const runInitTest = async ( accessKey?: string, context?: RhinoContext, model?: PvModel, + device?: string, expectFailure?: boolean, } = {} ) => { @@ -25,6 +27,7 @@ const runInitTest = async ( accessKey = ACCESS_KEY, context = { publicPath: '/test/contexts/coffee_maker_wasm.rhn', forceWrite: true }, model = { publicPath: '/test/rhino_params.pv', forceWrite: true }, + device = DEVICE, expectFailure = false, } = params; @@ -35,7 +38,8 @@ const runInitTest = async ( accessKey, context, () => {}, - model + model, + { device } ); expect(rhino.sampleRate).to.be.eq(16000); expect(typeof rhino.version).to.eq('string'); @@ -66,6 +70,7 @@ const runProcTest = async ( accessKey?: string, context?: RhinoContext, model?: PvModel, + device?: string, } = {}, expectedContext?: any ) => { @@ -73,6 +78,7 @@ const runProcTest = async ( accessKey = ACCESS_KEY, context = { publicPath: '/test/contexts/coffee_maker_wasm.rhn', forceWrite: true }, model = { publicPath: '/test/rhino_params.pv', forceWrite: true }, + device = DEVICE, } = params; let inference: RhinoInference | null = null; @@ -89,6 +95,7 @@ const runProcTest = async ( }, model, { + device, processErrorCallback: (error: RhinoError) => { reject(error); } @@ -124,6 +131,7 @@ describe("Rhino Binding", function () { () => { }, { publicPath: '/test/rhino_params.pv', forceWrite: true }, { + device: DEVICE, processErrorCallback: (e: RhinoError) => { error = e; resolve(); @@ -271,7 +279,8 @@ describe("Rhino Binding", function () { numFinalized++; } }, - { publicPath: `/test/rhino_params.pv`, forceWrite: true } + { publicPath: `/test/rhino_params.pv`, forceWrite: true }, + { device: DEVICE } ); for (let i = 0; i < ((pcm.length / 2) - rhino.frameLength + 1); i += rhino.frameLength) { @@ -299,7 +308,8 @@ describe("Rhino Binding", function () { "invalidAccessKey", { publicPath: '/test/contexts/coffee_maker_wasm.rhn', forceWrite: true }, () => { }, - { publicPath: '/test/rhino_params.pv', forceWrite: true } + { publicPath: '/test/rhino_params.pv', forceWrite: true }, + { device: DEVICE } ); expect(rhino).to.be.undefined; } catch (e: any) { @@ -314,7 +324,8 @@ describe("Rhino Binding", function () { "invalidAccessKey", { publicPath: '/test/contexts/coffee_maker_wasm.rhn', forceWrite: true }, () => { }, - { publicPath: '/test/rhino_params.pv', forceWrite: true } + { publicPath: '/test/rhino_params.pv', forceWrite: true }, + { device: DEVICE } ); expect(rhino).to.be.undefined; } catch (e: any) { diff --git a/binding/web/test/rhino_perf.test.ts b/binding/web/test/rhino_perf.test.ts index 9f840fdcc..f0dd26a28 100644 --- a/binding/web/test/rhino_perf.test.ts +++ b/binding/web/test/rhino_perf.test.ts @@ -3,6 +3,7 @@ import { Rhino, RhinoContext, RhinoWorker } from "../"; import { PvModel } from '@picovoice/web-utils'; const ACCESS_KEY = Cypress.env('ACCESS_KEY'); +const DEVICE = Cypress.env('DEVICE'); const NUM_TEST_ITERATIONS = Number(Cypress.env('NUM_TEST_ITERATIONS')); const INIT_PERFORMANCE_THRESHOLD_SEC = Number(Cypress.env('INIT_PERFORMANCE_THRESHOLD_SEC')); const PROC_PERFORMANCE_THRESHOLD_SEC = Number(Cypress.env('PROC_PERFORMANCE_THRESHOLD_SEC')); @@ -36,7 +37,8 @@ async function testPerformance( isFinalized = true; } }, - model + model, + { device: DEVICE } ); let end = Date.now(); diff --git a/binding/web/tsconfig.json b/binding/web/tsconfig.json index 94eeb693b..04378d36a 100644 --- a/binding/web/tsconfig.json +++ b/binding/web/tsconfig.json @@ -4,7 +4,8 @@ "allowSyntheticDefaultImports": true, "downlevelIteration": true, "isolatedModules": false, - "lib": ["esnext", "dom"], + "noImplicitAny": false, + "lib": ["es2015", "esnext", "dom"], "module": "esnext", "moduleResolution": "node", "noEmit": false, @@ -14,8 +15,8 @@ "sourceMap": true, "strict": false, "target": "esnext", - "types": ["node"] + "types": ["node", "emscripten"] }, "include": ["src", "module.d.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "src/lib"] } diff --git a/binding/web/yarn.lock b/binding/web/yarn.lock index e9cb8ff79..0476ce1c1 100644 --- a/binding/web/yarn.lock +++ b/binding/web/yarn.lock @@ -1124,12 +1124,12 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@picovoice/web-utils@=1.3.1": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@picovoice/web-utils/-/web-utils-1.3.1.tgz#d417e98604a650b54a8e03669015ecf98c2383ec" - integrity sha512-jcDqdULtTm+yJrnHDjg64hARup+Z4wNkYuXHNx6EM8+qZkweBq9UA6XJrHAlUkPnlkso4JWjaIKhz3x8vZcd3g== +"@picovoice/web-utils@=1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@picovoice/web-utils/-/web-utils-1.4.3.tgz#1de0b20d6080c18d295c6df37c09d88bf7c4f555" + integrity sha512-7JN3YYsSD9Gtce6YKG3XqpX49dkeu7jTdbox7rHQA/X/Q3zxopXA9zlCKSq6EIjFbiX2iuzDKUx1XrFa3d8c0w== dependencies: - commander "^9.2.0" + commander "^10.0.1" "@rollup/plugin-babel@^6.0.3": version "6.0.3" @@ -1189,6 +1189,11 @@ estree-walker "^2.0.2" picomatch "^2.3.1" +"@types/emscripten@1.40.0": + version "1.40.0" + resolved "https://registry.yarnpkg.com/@types/emscripten/-/emscripten-1.40.0.tgz#765f0c77080058faafd6b24de8ccebf573c1464c" + integrity sha512-MD2JJ25S4tnjnhjWyalMS6K6p0h+zQV6+Ylm+aGbiS8tSn/aHLSGNzBgduj6FB4zH0ax2GRMGYi/8G1uOxhXWA== + "@types/estree@*", "@types/estree@^1.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.2.tgz#ff02bc3dc8317cd668dfec247b750ba1f1d62453" @@ -1209,6 +1214,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.57.tgz#1ba31c0e5c403aab90a3b7826576e6782ded779b" integrity sha512-piPoDozdPaX1hNWFJQzzgWqE40gh986VvVx/QO9RU4qYRE55ld7iepDVgZ3ccGUw0R4wge0Oy1dd+3xOQNkkUQ== +"@types/node@^18.13.0": + version "18.19.130" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.130.tgz#da4c6324793a79defb7a62cba3947ec5add00d59" + integrity sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg== + dependencies: + undici-types "~5.26.4" + "@types/resolve@1.20.2": version "1.20.2" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" @@ -1695,6 +1707,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -1705,11 +1722,6 @@ commander@^6.2.1: resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== -commander@^9.2.0: - version "9.5.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" - integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== - common-tags@^1.8.0: version "1.8.2" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" @@ -3953,6 +3965,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" diff --git a/demo/react/package.json b/demo/react/package.json index 88d8295c5..bfd380ed4 100644 --- a/demo/react/package.json +++ b/demo/react/package.json @@ -1,10 +1,10 @@ { "name": "rhino-react-demo", - "version": "3.0.0", + "version": "4.0.0", "private": true, "description": "Rhino React demo (made with Create React App)", "dependencies": { - "@picovoice/rhino-react": "~3.0.3", + "@picovoice/rhino-react": "file:../../binding/react", "@picovoice/web-voice-processor": "~4.0.8", "@types/node": "^18.11.9", "@types/react": "^19.0.0", diff --git a/demo/web/index.html b/demo/web/index.html index bca3f4fd4..ed685bce5 100644 --- a/demo/web/index.html +++ b/demo/web/index.html @@ -3,7 +3,6 @@ - +

Rhino Web Demo

diff --git a/demo/web/package.json b/demo/web/package.json index bec85b99e..421b74c69 100644 --- a/demo/web/package.json +++ b/demo/web/package.json @@ -1,7 +1,7 @@ { "name": "rhino-web-demo", "private": true, - "version": "3.0.0", + "version": "4.0.0", "description": "A basic demo to show how to use Rhino for web browsers, using the IIFE version of the library", "main": "index.js", "scripts": { @@ -22,8 +22,9 @@ "author": "Picovoice Inc", "license": "Apache-2.0", "dependencies": { - "@picovoice/rhino-web": "~3.0.3", - "@picovoice/web-voice-processor": "~4.0.8" + "@picovoice/rhino-web": "file:../../binding/web", + "@picovoice/web-voice-processor": "~4.0.8", + "mime-types": "^2.1.35" }, "devDependencies": { "@eslint/js": "^9.22.0", diff --git a/demo/web/scripts/run_demo.js b/demo/web/scripts/run_demo.js index 284358ce7..7eab7e41f 100644 --- a/demo/web/scripts/run_demo.js +++ b/demo/web/scripts/run_demo.js @@ -101,7 +101,7 @@ fs.writeFileSync( const command = process.platform === "win32" ? "npx.cmd" : "npx"; -child_process.execSync(`${command} http-server -a localhost -p 5000`, { +child_process.execSync(`node server.js -a localhost -p 5000`, { shell: true, stdio: "inherit", }); diff --git a/demo/web/scripts/server.js b/demo/web/scripts/server.js new file mode 100644 index 000000000..9c81bd87f --- /dev/null +++ b/demo/web/scripts/server.js @@ -0,0 +1,42 @@ +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const mime = require('mime-types'); + +const PORT = process.env.PORT || 5000; +const HOST = '127.0.0.1'; // Listen on localhost +const publicDir = path.join(__dirname); + +const server = http.createServer((req, res) => { + const urlPath = req.url.split('?')[0]; + const url = (urlPath === '/') ? '/index.html' : urlPath; + const filePath = path.join(publicDir, url); + const contentType = mime.lookup(filePath) || 'application/octet-stream'; + + fs.readFile(filePath, (err, content) => { + if (err) { + if (err.code === 'ENOENT') { + // File not found + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('404 Not Found'); + } else { + // Server error + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end(`500 Internal Server Error: ${err.code}`); + } + } else { + // Success + res.writeHead(200, { + 'Content-Type': contentType, + 'Content-Length': content.length, + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp' + }); + res.end(content); + } + }); +}); + +server.listen(PORT, HOST, () => { + console.log(`Server is running on http://${HOST}:${PORT}`); +}); \ No newline at end of file