/* 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. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import { loadModel } from '@picovoice/web-utils'; import PvWorker from 'web-worker:./rhino_worker_handler.ts'; import { InferenceCallback, RhinoContext, RhinoModel, RhinoOptions, RhinoWorkerInitResponse, RhinoWorkerProcessResponse, RhinoWorkerReleaseResponse, RhinoWorkerResetResponse, PvStatus, } from './types'; import { pvStatusToException } from './rhino_errors'; export class RhinoWorker { private readonly _worker: Worker; private readonly _version: string; private readonly _frameLength: number; private readonly _sampleRate: number; private readonly _contextInfo: string; private static _wasmSimd: string; private static _wasmSimdLib: string; private static _wasmPThread: string; private static _wasmPThreadLib: string; private static _sdk: string = "web"; private constructor( worker: Worker, version: string, frameLength: number, sampleRate: number, contextInfo: string ) { this._worker = worker; this._version = version; this._frameLength = frameLength; this._sampleRate = sampleRate; this._contextInfo = contextInfo; } /** * Get Rhino engine version. */ get version(): string { return this._version; } /** * Get Rhino frame length. */ get frameLength(): number { return this._frameLength; } /** * Get sample rate. */ get sampleRate(): number { return this._sampleRate; } /** * Get Rhino worker instance. */ get worker(): Worker { return this._worker; } /** * Get context info. */ get contextInfo(): string { 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 with SIMD feature in text format. * @param wasmSimdLib Base64'd wasm SIMD file in text format. */ public static setWasmSimdLib(wasmSimdLib: string): void { if (this._wasmSimdLib === undefined) { this._wasmSimdLib = wasmSimdLib; } } /** * Set base64 wasm file with SIMD and pthread feature. * @param wasmPThread Base64'd wasm file to use to initialize wasm. */ 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; } } public static setSdk(sdk: string): void { RhinoWorker._sdk = sdk; } /** * Creates a worker instance of the Rhino Speech-to-Intent engine. * The model size is large, hence it will try to use the existing one if it exists, * otherwise saves the model in storage. * Behind the scenes, it requires the WebAssembly code to load and initialize before * it can create an instance. * * @param accessKey AccessKey generated by Picovoice Console. * @param context RhinoContext object containing a base64 * representation of or path to public binary of a Rhino context model . * @param inferenceCallback User-defined callback invoked upon processing a frame of audio. * The only input argument is an object of type RhinoInference. * @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]. * A lower endpoint duration reduces delay and improves responsiveness. A higher endpoint duration * assures Rhino doesn't return inference preemptively in case the user pauses before finishing the request. * @param options.requireEndpoint If set to `true`, Rhino requires an endpoint (a chunk of silence) * after the spoken command. If set to `false`, Rhino tries to detect silence, but if it cannot, * it still will provide inference regardless. Set to `false` only if operating in an * environment with overlapping speech (e.g. people talking in the background). * @param options.processErrorCallback User-defined callback invoked if any error happens * while processing the audio stream. Its only input argument is the error message. * * @returns An instance of RhinoWorker. */ public static async create( accessKey: string, context: RhinoContext, inferenceCallback: InferenceCallback, model: RhinoModel, options: RhinoOptions = {} ): Promise { const { processErrorCallback, ...rest } = options; const worker = new PvWorker(); const returnPromise: Promise = new Promise( (resolve, reject) => { // @ts-ignore - block from GC this.worker = worker; worker.onmessage = ( event: MessageEvent ): void => { switch (event.data.command) { case 'ok': worker.onmessage = ( ev: MessageEvent ): void => { switch (ev.data.command) { case 'ok-process': inferenceCallback(ev.data.inference); break; case 'ok': break; case 'failed': case 'error': const error = pvStatusToException(ev.data.status, ev.data.shortMessage, ev.data.messageStack); if (processErrorCallback) { processErrorCallback(error); } else { // eslint-disable-next-line no-console console.error(error); } break; default: // @ts-ignore processErrorCallback( pvStatusToException(PvStatus.RUNTIME_ERROR, `Unrecognized command: ${event.data.command}`) ); } }; resolve( new RhinoWorker( worker, event.data.version, event.data.frameLength, event.data.sampleRate, event.data.contextInfo ) ); break; case 'failed': case 'error': const error = pvStatusToException(event.data.status, event.data.shortMessage, event.data.messageStack); reject(error); break; default: // @ts-ignore reject(pvStatusToException(PvStatus.RUNTIME_ERROR, `Unrecognized command: ${event.data.command}`)); } }; } ); let customWritePath = context.customWritePath ? context.customWritePath : 'rhino_context'; const contextPath = await loadModel({ ...context, customWritePath }); const { sensitivity = 0.5 } = context; customWritePath = model.customWritePath ? model.customWritePath : 'rhino_model'; const modelPath = await loadModel({ ...model, customWritePath }); worker.postMessage({ command: 'init', accessKey: accessKey, contextPath: contextPath, sensitivity: sensitivity, modelPath: modelPath, options: rest, wasmSimd: this._wasmSimd, wasmSimdLib: this._wasmSimdLib, wasmPThread: this._wasmPThread, wasmPThreadLib: this._wasmPThreadLib, sdk: this._sdk, }); return returnPromise; } /** * Processes a frame of audio in a worker. * The transcript result will be supplied with the callback provided when initializing the worker. * Can also send a message directly using 'this.worker.postMessage({command: "process", pcm: [...]})'. * * @param pcm A frame of audio sample. */ public process(pcm: Int16Array): void { this._worker.postMessage({ command: 'process', inputFrame: pcm, }); } /** * Resets the internal Rhino state. */ public reset(): void { this._worker.postMessage({ command: 'reset', }); } /** * Releases resources acquired by WebAssembly module. */ public release(): Promise { const returnPromise: Promise = new Promise((resolve, reject) => { this._worker.onmessage = ( event: MessageEvent ): void => { switch (event.data.command) { case 'ok': resolve(); break; case 'failed': case 'error': const error = pvStatusToException(event.data.status, event.data.shortMessage, event.data.messageStack); reject(error); break; default: // @ts-ignore reject(pvStatusToException(PvStatus.RUNTIME_ERROR, `Unrecognized command: ${event.data.command}`)); } }; }); this._worker.postMessage({ command: 'release', }); return returnPromise; } /** * Terminates the active worker. Stops all requests being handled by worker. */ public terminate(): void { this._worker.terminate(); } }