Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3.0 node #588

Merged
merged 4 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/nodejs-demos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [14.x, 16.x, 18.x, 20.x]
node-version: [16.x, 18.x, 20.x]
include:
- os: ubuntu-latest
platform: linux
Expand All @@ -45,6 +45,14 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}

- name: Pre-build binding dependencies
run: npm install yarn
working-directory: binding/nodejs

- name: Build Node.js SDK
run: yarn && yarn build
working-directory: binding/nodejs

- name: Pre-build dependencies
run: npm install yarn
Expand Down Expand Up @@ -83,6 +91,10 @@ jobs:
- name: Pre-build dependencies
run: npm install --global yarn

- name: Build Node.js SDK
run: yarn && yarn build
working-directory: binding/nodejs

- name: Install dependencies
run: yarn install

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [14.x, 16.x, 18.x, 20.x]
node-version: [16.x, 18.x, 20.x]

steps:
- uses: actions/checkout@v3
Expand Down
4 changes: 2 additions & 2 deletions binding/nodejs/copy.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2020-2021 Picovoice Inc.
// Copyright 2020-2023 Picovoice Inc.
//
// You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE"
// file accompanying this source.
Expand All @@ -10,7 +10,7 @@
//
"use strict";

const mkdirp = require("mkdirp");
const { mkdirp } = require("mkdirp");
const ncp = require("ncp").ncp;

console.log("Copying library files...");
Expand Down
8 changes: 4 additions & 4 deletions binding/nodejs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@picovoice/rhino-node",
"version": "2.2.1",
"version": "3.0.0",
"description": "Picovoice Rhino Node.js binding",
"main": "dist/index.js",
"types": "dist/types/index.d.ts",
Expand Down Expand Up @@ -43,8 +43,8 @@
"@typescript-eslint/parser": "^5.19.0",
"eslint": "^8.13.0",
"eslint-plugin-jest": "^27.1.6",
"jest": "^27.5.1",
"mkdirp": "^1.0.4",
"jest": "^27.5.1",
"mkdirp": "^3.0.1",
"ncp": "^2.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.6.2",
Expand All @@ -53,7 +53,7 @@
"wavefile": "^11.0.0"
},
"engines": {
"node": ">=12.0.0"
"node": ">=16.0.0"
},
"cpu": [
"!ia32",
Expand Down
58 changes: 45 additions & 13 deletions binding/nodejs/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,38 @@

import PvStatus from './pv_status_t';

export class RhinoError extends Error {}
export class RhinoError extends Error {
private readonly _message: string;
private readonly _messageStack: string[];

constructor(message: string, messageStack: string[] = []) {
super(RhinoError.errorToString(message, messageStack));
this._message = message;
this._messageStack = messageStack;
}

get message(): string {
return this._message;
}

get messageStack(): string[] {
return this._messageStack;
}

private static errorToString(
initial: string,
messageStack: string[]
): string {
let msg = initial;

if (messageStack.length > 0) {
msg += `: ${messageStack.reduce((acc, value, index) =>
acc + '\n [' + index + '] ' + value, '')}`;
}

return msg;
}
}

export class RhinoOutOfMemoryError extends RhinoError {}
export class RhinoIoError extends RhinoError {}
Expand All @@ -28,31 +59,32 @@ export class RhinoActivationRefused extends RhinoError {}

export function pvStatusToException(
pvStatus: PvStatus,
errorMessage: string
errorMessage: string,
messageStack: string[] = []
): void {
switch (pvStatus) {
case PvStatus.OUT_OF_MEMORY:
throw new RhinoOutOfMemoryError(errorMessage);
throw new RhinoOutOfMemoryError(errorMessage, messageStack);
case PvStatus.IO_ERROR:
throw new RhinoIoError(errorMessage);
throw new RhinoIoError(errorMessage, messageStack);
case PvStatus.INVALID_ARGUMENT:
throw new RhinoInvalidArgumentError(errorMessage);
throw new RhinoInvalidArgumentError(errorMessage, messageStack);
case PvStatus.STOP_ITERATION:
throw new RhinoStopIterationError(errorMessage);
throw new RhinoStopIterationError(errorMessage, messageStack);
case PvStatus.KEY_ERROR:
throw new RhinoKeyError(errorMessage);
throw new RhinoKeyError(errorMessage, messageStack);
case PvStatus.INVALID_STATE:
throw new RhinoInvalidStateError(errorMessage);
throw new RhinoInvalidStateError(errorMessage, messageStack);
case PvStatus.RUNTIME_ERROR:
throw new RhinoRuntimeError(errorMessage);
throw new RhinoRuntimeError(errorMessage, messageStack);
case PvStatus.ACTIVATION_ERROR:
throw new RhinoActivationError(errorMessage);
throw new RhinoActivationError(errorMessage, messageStack);
case PvStatus.ACTIVATION_LIMIT_REACHED:
throw new RhinoActivationLimitReached(errorMessage);
throw new RhinoActivationLimitReached(errorMessage, messageStack);
case PvStatus.ACTIVATION_THROTTLED:
throw new RhinoActivationThrottled(errorMessage);
throw new RhinoActivationThrottled(errorMessage, messageStack);
case PvStatus.ACTIVATION_REFUSED:
throw new RhinoActivationRefused(errorMessage);
throw new RhinoActivationRefused(errorMessage, messageStack);
default:
// eslint-disable-next-line no-console
console.warn(`Unmapped error code: ${pvStatus}`);
Expand Down
3 changes: 2 additions & 1 deletion binding/nodejs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@

import Rhino, { RhinoInference } from './rhino';
import { getInt16Frames, checkWaveFile } from './wave_util';
import * as RhinoErrors from "./errors";

export { Rhino, RhinoInference, getInt16Frames, checkWaveFile };
export { Rhino, RhinoInference, getInt16Frames, checkWaveFile, RhinoErrors };
49 changes: 41 additions & 8 deletions binding/nodejs/src/rhino.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,12 @@
}

const pvRhino = require(libraryPath); // eslint-disable-line
this._pvRhino = pvRhino;

let rhinoHandleAndStatus: RhinoHandleAndStatus | null = null;
try {
pvRhino.set_sdk("nodejs");

rhinoHandleAndStatus = pvRhino.init(
accessKey,
modelPath,
Expand All @@ -149,11 +152,10 @@

const status = rhinoHandleAndStatus!.status;
if (status !== PvStatus.SUCCESS) {
pvStatusToException(status, 'Rhino failed to initialize');
this.handlePvStatus(status, "Rhino failed to initialize");
}

this._handle = rhinoHandleAndStatus!.handle;
this._pvRhino = pvRhino;
this._frameLength = pvRhino.frame_length();
this._sampleRate = pvRhino.sample_rate();
this._version = pvRhino.version();
Expand Down Expand Up @@ -227,13 +229,38 @@

const status = finalizedAndStatus!.status;
if (status !== PvStatus.SUCCESS) {
pvStatusToException(status, 'Rhino failed to process the frame');
this.handlePvStatus(status, "Rhino failed to process the frame");
}

this.isFinalized = finalizedAndStatus!.is_finalized === 1;
return this.isFinalized;
}

/**

Check warning on line 239 in binding/nodejs/src/rhino.ts

View workflow job for this annotation

GitHub Actions / check-nodejs-codestyle

Trailing spaces not allowed
* Resets the internal state of Rhino. It should be called before the engine can be used to infer intent from a new
* stream of audio
*/
reset(): void {
if (
this._handle === 0 ||
this._handle === null ||
this._handle === undefined
) {
throw new RhinoInvalidStateError('Rhino is not initialized');
}

let status: number | null = null;
try {
status = this._pvRhino.reset(this._handle);
} catch (err: any) {
pvStatusToException(<PvStatus>err.code, err);
}

if (status && status !== PvStatus.SUCCESS) {
this.handlePvStatus(status, "Rhino failed to reset");
}
}

/**
* Gets inference results from Rhino. If the phrase was understood, it includes the specific intent name
* that was inferred, and (if applicable) slot keys and specific slot values.
Expand Down Expand Up @@ -283,7 +310,7 @@

const status = inferenceAndStatus!.status;
if (status !== PvStatus.SUCCESS) {
pvStatusToException(status, `Rhino failed to get inference: ${status}`);
this.handlePvStatus(status, "Rhino failed to get inference");
}

const inference: RhinoInference = {
Expand Down Expand Up @@ -319,10 +346,7 @@

const status = contextAndStatus!.status;
if (status !== PvStatus.SUCCESS) {
pvStatusToException(
status,
`Rhino failed to get context info: ${status}`
);
this.handlePvStatus(status, "Rhino failed to get context info");
}

return contextAndStatus!.context_info;
Expand All @@ -343,4 +367,13 @@
console.warn('Rhino is not initialized; nothing to destroy');
}
}

private handlePvStatus(status: PvStatus, message: string): void {
const errorObject = this._pvRhino.get_error_stack();
if (errorObject.status === PvStatus.SUCCESS) {
pvStatusToException(status, message, errorObject.message_stack);
} else {
pvStatusToException(status, "Unable to get Rhino error state");
}
}
}
97 changes: 79 additions & 18 deletions binding/nodejs/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,37 @@
.filter(x => x.startsWith('--access_key='))[0]
?.split('--access_key=')[1] ?? '';

function processFileHelper(rhino: Rhino, audioFile: string, maxProcessCount: number = -1) {
let processed = 0;

const waveBuffer = fs.readFileSync(audioFile);
const waveAudioFile = new WaveFile(waveBuffer);

if (!checkWaveFile(waveAudioFile, rhino.sampleRate)) {
fail(
'Audio file did not meet requirements. Wave file must be 16KHz, 16-bit, linear PCM (mono).'
);
}

const frames = getInt16Frames(waveAudioFile, rhino.frameLength);

let isFinalized = false;
for (let i = 0; i < frames.length; i++) {
const frame = frames[i];
isFinalized = rhino.process(frame);

if (isFinalized) {
break;
}
if (maxProcessCount !== -1 && processed >= maxProcessCount) {
break;
}
processed++;
}

return isFinalized;
}

function testRhinoDetection(
language: string,
context: string,
Expand All @@ -52,31 +83,61 @@
);

const waveFilePath = getAudioFileByLanguage(language, isWithinContext);
const waveBuffer = fs.readFileSync(waveFilePath);
const waveAudioFile = new WaveFile(waveBuffer);
const isFinalized = processFileHelper(engineInstance, waveFilePath);
if (isFinalized) {
if (groundTruth !== null) {
expect(engineInstance.getInference()).toEqual(groundTruth);
} else {
expect(engineInstance.getInference().isUnderstood).toBe(false);
}
}
}

if (!checkWaveFile(waveAudioFile, engineInstance.sampleRate)) {
fail(
'Audio file did not meet requirements. Wave file must be 16KHz, 16-bit, linear PCM (mono).'
describe("Reset", () => {
test("Rhino reset works successfully", () => {
const contextPath = getContextPathsByLanguage("en", "coffee_maker");
const waveFilePath = getAudioFileByLanguage("en", true);

Check warning on line 100 in binding/nodejs/test/index.test.ts

View workflow job for this annotation

GitHub Actions / check-nodejs-codestyle

Trailing spaces not allowed
const rhino = new Rhino(
ACCESS_KEY,
contextPath
);
}

const frames = getInt16Frames(waveAudioFile, engineInstance.frameLength);
let isFinalized = processFileHelper(rhino, waveFilePath, 15);
expect(isFinalized).toBe(false);

let isFinalized = false;
for (let i = 0; i < frames.length; i++) {
const frame = frames[i];
isFinalized = engineInstance.process(frame);
rhino.reset();
isFinalized = processFileHelper(rhino, waveFilePath);
expect(isFinalized).toBe(true);
expect(rhino.getInference().isUnderstood).toBe(true);
})

Check warning on line 113 in binding/nodejs/test/index.test.ts

View workflow job for this annotation

GitHub Actions / check-nodejs-codestyle

Missing semicolon
})

Check warning on line 114 in binding/nodejs/test/index.test.ts

View workflow job for this annotation

GitHub Actions / check-nodejs-codestyle

Missing semicolon

if (isFinalized) {
if (groundTruth !== null) {
expect(engineInstance.getInference()).toEqual(groundTruth);
} else {
expect(engineInstance.getInference().isUnderstood).toBe(false);
describe("error message stack", () => {
test("message stack cleared after read", () => {

Check warning on line 117 in binding/nodejs/test/index.test.ts

View workflow job for this annotation

GitHub Actions / check-nodejs-codestyle

Trailing spaces not allowed
let error: string[] = [];
try {
new Rhino(
"invalid",
getContextPathsByLanguage('en', 'coffee_maker'));
} catch (e: any) {
error = e.messageStack;
}

expect(error.length).toBeGreaterThan(0);
expect(error.length).toBeLessThanOrEqual(8);

try {
new Rhino(
"invalid",
getContextPathsByLanguage('en', 'coffee_maker'));
} catch (e: any) {
for (let i = 0; i < error.length; i++) {
expect(error[i]).toEqual(e.messageStack[i]);
}
}
}
}
});
});

describe('intent detection', () => {
it.each(WITHIN_CONTEXT_PARAMETERS)(
Expand Down
Loading