UNPKG

@picovoice/porcupine-node

Version:

Picovoice Porcupine Node.js binding

225 lines 10.7 kB
// // Copyright 2020-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. // "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const fs = require("fs"); const path = require("path"); const pv_status_t_1 = require("./pv_status_t"); const errors_1 = require("./errors"); const platforms_1 = require("./platforms"); const builtin_keywords_1 = require("./builtin_keywords"); const MODEL_PATH_DEFAULT = "../lib/common/porcupine_params.pv"; class Porcupine { _pvPorcupine; _handle; _version; _sampleRate; _frameLength; /** * Creates an instance of Porcupine. * @param {string} accessKey AccessKey obtained from Picovoice Console (https://console.picovoice.ai/). * @param {Array} keywords Absolute paths to keyword model files (`.ppn`). * @param {Array} sensitivities Sensitivity values for detecting keywords. * Each value should be a number within [0, 1]. A higher sensitivity results in fewer misses * at the cost of increasing the false alarm rate. * @param options Optional configuration arguments. * @param {string} options.modelPath Path to the Porcupine model (.pv extension) * @param {string} options.device String representation of the device (e.g., CPU or GPU) to use for inference. * 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 {string} options.libraryPath Path to the Porcupine library (.node extension) */ constructor(accessKey, keywords, sensitivities, options = {}) { if (accessKey === null || accessKey === undefined || accessKey.length === 0) { throw new errors_1.PorcupineInvalidArgumentError(`No AccessKey provided to Porcupine`); } const { modelPath = path.resolve(__dirname, MODEL_PATH_DEFAULT), device = 'best', libraryPath = (0, platforms_1.getSystemLibraryPath)(), } = options; if (keywords === null || keywords === undefined || keywords.length === 0) { throw new errors_1.PorcupineInvalidArgumentError(`keywordPaths are null/undefined/empty (${keywords})`); } if (sensitivities === null || sensitivities === undefined || sensitivities.length === 0) { throw new errors_1.PorcupineInvalidArgumentError(`sensitivities are null/undefined/empty (${sensitivities})`); } for (const sensitivity of sensitivities) { if (sensitivity < 0 || sensitivity > 1 || isNaN(sensitivity)) { throw new RangeError(`Sensitivity value in 'sensitivities' not in range [0,1]: ${sensitivity}`); } } if (!Array.isArray(keywords)) { throw new errors_1.PorcupineInvalidArgumentError(`Keywords is not an array: ${keywords}`); } if (keywords.length !== sensitivities.length) { throw new errors_1.PorcupineInvalidArgumentError(`Number of keywords (${keywords.length}) does not match number of sensitivities (${sensitivities.length})`); } if (!fs.existsSync(libraryPath)) { throw new errors_1.PorcupineInvalidArgumentError(`File not found at 'libraryPath': ${libraryPath}`); } if (!fs.existsSync(modelPath)) { throw new errors_1.PorcupineInvalidArgumentError(`File not found at 'modelPath': ${modelPath}`); } const keywordPaths = []; for (let i = 0; i < keywords.length; i++) { const keyword = keywords[i]; if (Object.values(builtin_keywords_1.BuiltinKeyword).includes(keyword)) { keywordPaths[i] = (0, builtin_keywords_1.getBuiltinKeywordPath)(keyword); } else { if (!fs.existsSync(keyword)) { throw new errors_1.PorcupineInvalidArgumentError(`File not found in 'keywords': ${keyword}`); } else { keywordPaths[i] = keyword; } } } const pvPorcupine = require(libraryPath); // eslint-disable-line this._pvPorcupine = pvPorcupine; let porcupineHandleAndStatus = null; try { pvPorcupine.set_sdk("nodejs"); porcupineHandleAndStatus = pvPorcupine.init(accessKey, modelPath, device, keywordPaths.length, keywordPaths, sensitivities); } catch (err) { (0, errors_1.pvStatusToException)(pv_status_t_1.default[err.code], err); } const status = porcupineHandleAndStatus.status; if (status !== pv_status_t_1.default.SUCCESS) { this.handlePvStatus(status, "Porcupine failed to initialize"); } this._handle = porcupineHandleAndStatus.handle; this._frameLength = pvPorcupine.frame_length(); this._sampleRate = pvPorcupine.sample_rate(); this._version = pvPorcupine.version(); } /** * @returns number of audio samples per frame (i.e. the length of the array provided to the process function) * @see {@link process} */ get frameLength() { return this._frameLength; } /** * @returns the audio sampling rate accepted by Porcupine */ get sampleRate() { return this._sampleRate; } /** * @returns the version of the Porcupine engine */ get version() { return this._version; } /** * Process a frame of pcm audio. * * @param {Array} frame of mono, 16-bit, linear-encoded PCM audio. * The specific array length can be attained by calling `.frameLength`. * The incoming audio needs to have a sample rate equal to `.sampleRate` and be 16-bit linearly-encoded. * Porcupine operates on single-channel audio. * @returns {number} Index of observed keyword at the end of the current frame. * Indexing is 0-based and matches the ordering of keyword models provided to the constructor. * If no keyword is detected then it returns -1. */ process(frame) { if (this._handle === 0 || this._handle === null || this._handle === undefined) { throw new errors_1.PorcupineInvalidStateError("Porcupine is not initialized"); } if (frame === undefined || frame === null) { throw new errors_1.PorcupineInvalidArgumentError(`Frame array provided to process() is undefined or null`); } else if (frame.length !== this.frameLength) { throw new errors_1.PorcupineInvalidArgumentError(`Size of frame array provided to 'process' (${frame.length}) does not match the engine 'frameLength' (${this.frameLength})`); } // sample the first frame to check for non-integer values if (!Number.isInteger(frame[0])) { throw new errors_1.PorcupineInvalidArgumentError(`Non-integer frame values provided to process(): ${frame[0]}. Porcupine requires 16-bit integers`); } const frameBuffer = new Int16Array(frame); let keywordAndStatus = null; try { keywordAndStatus = this._pvPorcupine.process(this._handle, frameBuffer); } catch (err) { (0, errors_1.pvStatusToException)(pv_status_t_1.default[err.code], err); } const status = keywordAndStatus.status; if (status !== pv_status_t_1.default.SUCCESS) { this.handlePvStatus(status, "Porcupine failed to process"); } const keywordIndex = keywordAndStatus.keyword_index; return keywordIndex; } /** * Releases the resources acquired by Porcupine. * * Be sure to call this when finished with the instance * to reclaim the memory that was allocated by the C library. */ release() { if (this._handle > 0) { this._pvPorcupine.delete(this._handle); this._handle = 0; } else { // eslint-disable-next-line no-console console.warn("Porcupine is not initialized; nothing to destroy"); } } /** * Lists all available devices that Porcupine can use for inference. Each entry in the list can be the `device` argument * of the constructor. * * @returns List of all available devices that Porcupine can use for inference. */ static listAvailableDevices(options = {}) { const { libraryPath = (0, platforms_1.getSystemLibraryPath)(), } = options; const pvPorcupine = require(libraryPath); // eslint-disable-line let porcupineHardwareDevicesResult = null; try { porcupineHardwareDevicesResult = pvPorcupine.list_hardware_devices(); } catch (err) { (0, errors_1.pvStatusToException)(err.code, err); } const status = porcupineHardwareDevicesResult.status; if (status !== pv_status_t_1.default.SUCCESS) { const errorObject = pvPorcupine.get_error_stack(); if (errorObject.status === pv_status_t_1.default.SUCCESS) { (0, errors_1.pvStatusToException)(status, 'Porcupine failed to get available devices', errorObject.message_stack); } else { (0, errors_1.pvStatusToException)(status, 'Unable to get Porcupine error state'); } } return porcupineHardwareDevicesResult.hardware_devices; } handlePvStatus(status, message) { const errorObject = this._pvPorcupine.get_error_stack(); if (errorObject.status === pv_status_t_1.default.SUCCESS) { (0, errors_1.pvStatusToException)(status, message, errorObject.message_stack); } else { (0, errors_1.pvStatusToException)(status, "Unable to get Porcupine error state"); } } } exports.default = Porcupine; //# sourceMappingURL=porcupine.js.map