From c3cc4682fbf347dbfd6931fca624de14794f447f Mon Sep 17 00:00:00 2001 From: Ian Date: Fri, 17 Nov 2023 16:58:50 -0800 Subject: [PATCH 1/4] binding --- .github/workflows/nodejs-demos.yml | 22 ++-- .github/workflows/nodejs-perf.yml | 6 -- .github/workflows/nodejs.yml | 8 +- binding/nodejs/README.md | 4 +- binding/nodejs/package.json | 6 +- binding/nodejs/src/errors.ts | 68 +++++++++--- binding/nodejs/src/index.ts | 16 +-- binding/nodejs/src/leopard.ts | 26 ++++- binding/nodejs/src/types.ts | 5 + binding/nodejs/test/index.test.ts | 167 ++++++++++++++++------------- binding/nodejs/test/test_utils.ts | 38 ++++++- binding/nodejs/yarn.lock | 15 ++- 12 files changed, 237 insertions(+), 144 deletions(-) diff --git a/.github/workflows/nodejs-demos.yml b/.github/workflows/nodejs-demos.yml index bc7fd348..c62fc2bb 100644 --- a/.github/workflows/nodejs-demos.yml +++ b/.github/workflows/nodejs-demos.yml @@ -33,7 +33,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 @@ -43,8 +43,9 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: Pre-build dependencies - run: npm install yarn + - name: Build Node.js SDK + run: yarn && yarn build + working-directory: binding/nodejs - name: Install dependencies run: yarn install @@ -62,8 +63,9 @@ jobs: steps: - uses: actions/checkout@v3 - - 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 @@ -77,18 +79,18 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - node-version: [lts/*] steps: - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} + - name: Use Node.js LTS uses: actions/setup-node@v3 with: - node-version: ${{ matrix.node-version }} + node-version: lts/* - - name: Pre-build dependencies - run: npm install yarn + - name: Build Node.js SDK + run: yarn && yarn build + working-directory: binding/nodejs - name: Install dependencies run: yarn install diff --git a/.github/workflows/nodejs-perf.yml b/.github/workflows/nodejs-perf.yml index 9cff05bd..093ed212 100644 --- a/.github/workflows/nodejs-perf.yml +++ b/.github/workflows/nodejs-perf.yml @@ -50,9 +50,6 @@ jobs: with: node-version: 18.x - - name: Pre-build dependencies - run: npm install yarn - - name: Install dependencies run: yarn install @@ -86,9 +83,6 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Pre-build dependencies - run: npm install --global yarn - - name: Install dependencies run: yarn install diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index a43e88e0..aa0f8c65 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -35,7 +35,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 @@ -45,9 +45,6 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: Pre-build dependencies - run: npm install yarn - - name: Install dependencies run: yarn install @@ -64,9 +61,6 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Pre-build dependencies - run: npm install --global yarn - - name: Install dependencies run: yarn install diff --git a/binding/nodejs/README.md b/binding/nodejs/README.md index 7c53b8fa..cfc26688 100644 --- a/binding/nodejs/README.md +++ b/binding/nodejs/README.md @@ -15,7 +15,7 @@ Leopard is an on-device speech-to-text engine. Leopard is: ## Compatibility -- Node.js 12+ +- Node.js 16+ - Runs on Linux (x86_64), macOS (x86_64, arm64), Windows (x86_64), Raspberry Pi (4, 3), and NVIDIA Jetson Nano. ## Installation @@ -37,7 +37,7 @@ Create an instance of the engine and transcribe an audio file: ```javascript const {Leopard} = require("@picovoice/leopard-node"); -const accessKey = "${ACCESS_KEY}" // Obtained from the Picovoice Console (https://console.picovoice.ai/) +const accessKey = "${ACCESS_KEY}"; // Obtained from the Picovoice Console (https://console.picovoice.ai/) const handle = new Leopard(accessKey); const result = handle.processFile('${AUDIO_PATH}'); diff --git a/binding/nodejs/package.json b/binding/nodejs/package.json index 1a2418ff..b48b3fb3 100644 --- a/binding/nodejs/package.json +++ b/binding/nodejs/package.json @@ -1,6 +1,6 @@ { "name": "@picovoice/leopard-node", - "version": "1.2.0", + "version": "2.0.0", "description": "Picovoice Leopard Node.js binding", "main": "dist/index.js", "types": "dist/types/index.d.ts", @@ -38,7 +38,7 @@ "homepage": "https://picovoice.ai/products/leopard/", "devDependencies": { "@types/jest": "^27.4.1", - "@types/node": "^17.0.21", + "@types/node": "^18.11.9", "@typescript-eslint/eslint-plugin": "^5.19.0", "@typescript-eslint/parser": "^5.19.0", "eslint": "^8.13.0", @@ -53,7 +53,7 @@ "wavefile": "^11.0.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=16.0.0" }, "cpu": [ "!ia32", diff --git a/binding/nodejs/src/errors.ts b/binding/nodejs/src/errors.ts index d5542dd9..7f0730f4 100644 --- a/binding/nodejs/src/errors.ts +++ b/binding/nodejs/src/errors.ts @@ -12,47 +12,81 @@ import PvStatus from './pv_status_t'; -export class LeopardError extends Error {} +export class LeopardError extends Error { + private readonly _message: string; + private readonly _messageStack: string[]; + + constructor(message: string, messageStack: string[] = []) { + super(LeopardError.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 LeopardOutOfMemoryError extends LeopardError {} -export class LeopardIoError extends LeopardError {} +export class LeopardIOError extends LeopardError {} export class LeopardInvalidArgumentError extends LeopardError {} export class LeopardStopIterationError extends LeopardError {} export class LeopardKeyError extends LeopardError {} export class LeopardInvalidStateError extends LeopardError {} export class LeopardRuntimeError extends LeopardError {} export class LeopardActivationError extends LeopardError {} -export class LeopardActivationLimitReached extends LeopardError {} -export class LeopardActivationThrottled extends LeopardError {} -export class LeopardActivationRefused extends LeopardError {} +export class LeopardActivationLimitReachedError extends LeopardError {} +export class LeopardActivationThrottledError extends LeopardError {} +export class LeopardActivationRefusedError extends LeopardError {} export function pvStatusToException( pvStatus: PvStatus, - errorMessage: string + errorMessage: string, + messageStack: string[] = [] ): void { switch (pvStatus) { case PvStatus.OUT_OF_MEMORY: - throw new LeopardOutOfMemoryError(errorMessage); + throw new LeopardOutOfMemoryError(errorMessage, messageStack); case PvStatus.IO_ERROR: - throw new LeopardIoError(errorMessage); + throw new LeopardIOError(errorMessage, messageStack); case PvStatus.INVALID_ARGUMENT: - throw new LeopardInvalidArgumentError(errorMessage); + throw new LeopardInvalidArgumentError(errorMessage, messageStack); case PvStatus.STOP_ITERATION: - throw new LeopardStopIterationError(errorMessage); + throw new LeopardStopIterationError(errorMessage, messageStack); case PvStatus.KEY_ERROR: - throw new LeopardKeyError(errorMessage); + throw new LeopardKeyError(errorMessage, messageStack); case PvStatus.INVALID_STATE: - throw new LeopardInvalidStateError(errorMessage); + throw new LeopardInvalidStateError(errorMessage, messageStack); case PvStatus.RUNTIME_ERROR: - throw new LeopardRuntimeError(errorMessage); + throw new LeopardRuntimeError(errorMessage, messageStack); case PvStatus.ACTIVATION_ERROR: - throw new LeopardActivationError(errorMessage); + throw new LeopardActivationError(errorMessage, messageStack); case PvStatus.ACTIVATION_LIMIT_REACHED: - throw new LeopardActivationLimitReached(errorMessage); + throw new LeopardActivationLimitReachedError(errorMessage, messageStack); case PvStatus.ACTIVATION_THROTTLED: - throw new LeopardActivationThrottled(errorMessage); + throw new LeopardActivationThrottledError(errorMessage, messageStack); case PvStatus.ACTIVATION_REFUSED: - throw new LeopardActivationRefused(errorMessage); + throw new LeopardActivationRefusedError(errorMessage, messageStack); default: // eslint-disable-next-line no-console console.warn(`Unmapped error code: ${pvStatus}`); diff --git a/binding/nodejs/src/index.ts b/binding/nodejs/src/index.ts index a38c8329..72f81548 100644 --- a/binding/nodejs/src/index.ts +++ b/binding/nodejs/src/index.ts @@ -14,13 +14,13 @@ import { Leopard } from './leopard'; import { LeopardActivationError, - LeopardActivationLimitReached, - LeopardActivationRefused, - LeopardActivationThrottled, + LeopardActivationLimitReachedError, + LeopardActivationRefusedError, + LeopardActivationThrottledError, LeopardError, LeopardInvalidArgumentError, LeopardInvalidStateError, - LeopardIoError, + LeopardIOError, LeopardKeyError, LeopardOutOfMemoryError, LeopardRuntimeError, @@ -38,16 +38,16 @@ import { export { Leopard, LeopardActivationError, - LeopardActivationLimitReached, - LeopardActivationRefused, - LeopardActivationThrottled, + LeopardActivationLimitReachedError, + LeopardActivationRefusedError, + LeopardActivationThrottledError, LeopardOptions, LeopardError, LeopardInitOptions, LeopardInputOptions, LeopardInvalidArgumentError, LeopardInvalidStateError, - LeopardIoError, + LeopardIOError, LeopardKeyError, LeopardOutOfMemoryError, LeopardRuntimeError, diff --git a/binding/nodejs/src/leopard.ts b/binding/nodejs/src/leopard.ts index d4564f34..eb40874d 100644 --- a/binding/nodejs/src/leopard.ts +++ b/binding/nodejs/src/leopard.ts @@ -66,6 +66,9 @@ export class Leopard { * @param {string} options.modelPath The path to save and use the model from (.pv extension) * @param {string} options.libraryPath the path to the Leopard dynamic library (.node extension) * @param {boolean} options.enableAutomaticPunctuation Flag to enable automatic punctuation insertion. + * @param {boolean} options.enableDiarization Flag to enable speaker diarization, which allows Leopard to + * differentiate speakers as part of the transcription process. Word metadata will include a `speakerTag` + * to identify unique speakers. */ constructor(accessKey: string, options: LeopardOptions = {}) { if ( @@ -80,6 +83,7 @@ export class Leopard { modelPath = path.resolve(__dirname, DEFAULT_MODEL_PATH), libraryPath = getSystemLibraryPath(), enableAutomaticPunctuation = false, + enableDiarization = false, } = options; if (!fs.existsSync(libraryPath)) { @@ -95,13 +99,17 @@ export class Leopard { } const pvLeopard = require(libraryPath); // eslint-disable-line + this._pvLeopard = pvLeopard; let leopardHandleAndStatus: LeopardHandleAndStatus | null = null; try { + pvLeopard.set_sdk('nodejs'); + leopardHandleAndStatus = pvLeopard.init( accessKey, modelPath, - enableAutomaticPunctuation + enableAutomaticPunctuation, + enableDiarization ); } catch (err: any) { pvStatusToException(err.code, err); @@ -109,11 +117,10 @@ export class Leopard { const status = leopardHandleAndStatus!.status; if (status !== PvStatus.SUCCESS) { - pvStatusToException(status, 'Leopard failed to initialize'); + this.handlePvStatus(status, 'Leopard failed to initialize'); } this._handle = leopardHandleAndStatus!.handle; - this._pvLeopard = pvLeopard; this._sampleRate = pvLeopard.sample_rate(); this._version = pvLeopard.version(); } @@ -169,7 +176,7 @@ export class Leopard { const status = leopardResult!.status; if (status !== PvStatus.SUCCESS) { - pvStatusToException(status, 'Leopard failed to process the audio frame'); + this.handlePvStatus(status, 'Leopard failed to process the audio data'); } return { @@ -221,7 +228,7 @@ export class Leopard { )}' is not supported` ); } - pvStatusToException(status, 'Leopard failed to process the audio file'); + this.handlePvStatus(status, 'Leopard failed to process the audio file'); } return { transcript: leopardResult!.transcript, @@ -248,4 +255,13 @@ export class Leopard { console.warn('Leopard is not initialized'); } } + + private handlePvStatus(status: PvStatus, message: string): void { + const errorObject = this._pvLeopard.get_error_stack(); + if (errorObject.status === PvStatus.SUCCESS) { + pvStatusToException(status, message, errorObject.message_stack); + } else { + pvStatusToException(status, 'Unable to get Leopard error state'); + } + } } diff --git a/binding/nodejs/src/types.ts b/binding/nodejs/src/types.ts index 0251ad04..84d1d0ad 100644 --- a/binding/nodejs/src/types.ts +++ b/binding/nodejs/src/types.ts @@ -16,6 +16,10 @@ export type LeopardWord = { endSec: number; /** Transcription confidence. It is a number within [0, 1]. */ confidence: number; + /** The speaker tag is `-1` if diarization is not enabled during initialization + * otherwise, it's a non-negative integer identifying unique speakers, with `0` reserved + * for unknown speakers */ + speakerTag: number; }; export type LeopardTranscript = { @@ -27,6 +31,7 @@ export type LeopardTranscript = { export type LeopardInitOptions = { enableAutomaticPunctuation?: boolean; + enableDiarization?: boolean; }; export type LeopardInputOptions = { diff --git a/binding/nodejs/test/index.test.ts b/binding/nodejs/test/index.test.ts index 3473f3db..b2d1113c 100644 --- a/binding/nodejs/test/index.test.ts +++ b/binding/nodejs/test/index.test.ts @@ -20,10 +20,12 @@ import { getSystemLibraryPath } from '../src/platforms'; import { getModelPathByLanguage, getAudioFile, - getTestParameters, + getLanguageTestParameters, + getDiarizationTestParameters, } from './test_utils'; -const TEST_PARAMETERS = getTestParameters(); +const LANGUAGE_TEST_PARAMETERS = getLanguageTestParameters(); +const DIARIZATION_TEST_PARAMETERS = getDiarizationTestParameters(); const ACCESS_KEY = process.argv .filter(x => x.startsWith('--access_key='))[0] @@ -66,21 +68,20 @@ const characterErrorRate = ( const validateMetadata = ( words: LeopardWord[], - transcript: string, - audioLength: number + referenceWords: LeopardWord[], + enableDiarization: boolean ) => { - const normTranscript = transcript.toUpperCase(); + expect(words.length).toEqual(referenceWords.length); for (let i = 0; i < words.length; i += 1) { - const word = words[i]; - expect(normTranscript.includes(word.word.toUpperCase())).toBeTruthy(); - expect(word.startSec).toBeGreaterThan(0); - expect(word.startSec).toBeLessThanOrEqual(word.endSec); - if (i < (words.length - 1)) { - const nextWord = words[i + 1]; - expect(word.endSec).toBeLessThanOrEqual(nextWord.startSec); + expect(words[i].word).toEqual(referenceWords[i].word); + expect(words[i].startSec).toBeCloseTo(referenceWords[i].startSec, 1); + expect(words[i].endSec).toBeCloseTo(referenceWords[i].endSec, 1); + expect(words[i].confidence).toBeCloseTo(referenceWords[i].confidence, 1); + if (enableDiarization) { + expect(words[i].speakerTag).toEqual(referenceWords[i].speakerTag); + } else { + expect(words[i].speakerTag).toEqual(-1); } - expect(word.startSec).toBeLessThan(audioLength); - expect(word.confidence >= 0 && word.confidence <= 1).toBeTruthy(); } }; @@ -89,43 +90,33 @@ const loadPcm = (audioFile: string): any => { const waveBuffer = fs.readFileSync(waveFilePath); const waveAudioFile = new WaveFile(waveBuffer); - const pcm: any = waveAudioFile.getSamples(false, Int16Array); - return pcm; + return waveAudioFile.getSamples(false, Int16Array); }; const testLeopardProcess = ( language: string, transcript: string, - punctuations: string[], - testPunctuation: boolean, + enableAutomaticPunctuation: boolean, + enableDiarization: boolean, errorRate: number, - audioFile: string + audioFile: string, + words: LeopardWord[] ) => { const modelPath = getModelPathByLanguage(language); const pcm = loadPcm(audioFile); - let normTranscript = transcript; - if (!testPunctuation) { - for (const punctuation of punctuations) { - normTranscript = normTranscript.replace(new RegExp(`[${punctuation}]`, "g"), ''); - } - } - let leopardEngine = new Leopard(ACCESS_KEY, { modelPath, - enableAutomaticPunctuation: testPunctuation, + enableAutomaticPunctuation, + enableDiarization, }); let res = leopardEngine.process(pcm); expect( - characterErrorRate(res.transcript, normTranscript) < errorRate + characterErrorRate(res.transcript, transcript) < errorRate ).toBeTruthy(); - validateMetadata( - res.words, - res.transcript, - pcm.length / leopardEngine.sampleRate - ); + validateMetadata(res.words, words, enableDiarization); leopardEngine.release(); }; @@ -133,123 +124,145 @@ const testLeopardProcess = ( const testLeopardProcessFile = ( language: string, transcript: string, - punctuations: string[], - testPunctuation: boolean, + enableAutomaticPunctuation: boolean, + enableDiarization: boolean, errorRate: number, - audioFile: string + audioFile: string, + words: LeopardWord[] ) => { const modelPath = getModelPathByLanguage(language); - const pcm = loadPcm(audioFile); - - let normTranscript = transcript; - if (!testPunctuation) { - for (const punctuation of punctuations) { - normTranscript = normTranscript.replace(new RegExp(`[${punctuation}]`, "g"), ''); - } - } let leopardEngine = new Leopard(ACCESS_KEY, { modelPath, - enableAutomaticPunctuation: testPunctuation, + enableAutomaticPunctuation, + enableDiarization, }); const waveFilePath = getAudioFile(audioFile); let res = leopardEngine.processFile(waveFilePath); expect( - characterErrorRate(res.transcript, normTranscript) < errorRate + characterErrorRate(res.transcript, transcript) < errorRate ).toBeTruthy(); - validateMetadata( - res.words, - res.transcript, - pcm.length / leopardEngine.sampleRate - ); + validateMetadata(res.words, words, enableDiarization); leopardEngine.release(); }; describe('successful processes', () => { - it.each(TEST_PARAMETERS)( + it.each(LANGUAGE_TEST_PARAMETERS)( 'testing process `%p`', ( language: string, transcript: string, - punctuations: string[], + _: string, errorRate: number, - audioFile: string + audioFile: string, + words: LeopardWord[] ) => { testLeopardProcess( language, transcript, - punctuations, + false, false, errorRate, - audioFile + audioFile, + words ); } ); - it.each(TEST_PARAMETERS)( - 'testing process `%p` with punctuation', + it.each(LANGUAGE_TEST_PARAMETERS)( + 'testing process file `%p`', ( language: string, transcript: string, - punctuations: string[], + _: string, errorRate: number, - audioFile: string + audioFile: string, + words: LeopardWord[] ) => { - testLeopardProcess( + testLeopardProcessFile( language, transcript, - punctuations, - true, + false, + false, errorRate, - audioFile + audioFile, + words ); } ); - it.each(TEST_PARAMETERS)( - 'testing process file `%p`', + it.each(LANGUAGE_TEST_PARAMETERS)( + 'testing process file `%p` with punctuation', ( language: string, + _: string, transcript: string, - punctuations: string[], errorRate: number, - audioFile: string + audioFile: string, + words: LeopardWord[] ) => { testLeopardProcessFile( language, transcript, - punctuations, + true, false, errorRate, - audioFile + audioFile, + words ); } ); - - it.each(TEST_PARAMETERS)( - 'testing process file `%p` with punctuation', + it.each(LANGUAGE_TEST_PARAMETERS)( + 'testing process file `%p` with diarization', ( language: string, transcript: string, - punctuations: string[], + _: string, errorRate: number, - audioFile: string + audioFile: string, + words: LeopardWord[] ) => { testLeopardProcessFile( language, transcript, - punctuations, + false, true, errorRate, - audioFile + audioFile, + words ); } ); }); +describe('successful diarization', () => { + it.each(DIARIZATION_TEST_PARAMETERS)( + 'testing diarization `%p`', + (language: string, audioFile: string, referenceWords: LeopardWord[]) => { + const modelPath = getModelPathByLanguage(language); + + let leopardEngine = new Leopard(ACCESS_KEY, { + modelPath, + enableDiarization: true, + }); + + const waveFilePath = getAudioFile(audioFile); + let words = leopardEngine.processFile(waveFilePath).words; + + expect(words.length).toEqual(referenceWords.length); + for (let i = 0; i < words.length; i += 1) { + expect(words[i].word).toEqual(referenceWords[i].word); + expect(words[i].speakerTag).toEqual(referenceWords[i].speakerTag); + } + + leopardEngine.release(); + } + ); +}); + describe('Defaults', () => { test('Empty AccessKey', () => { expect(() => { diff --git a/binding/nodejs/test/test_utils.ts b/binding/nodejs/test/test_utils.ts index 50ced970..c26cd7af 100644 --- a/binding/nodejs/test/test_utils.ts +++ b/binding/nodejs/test/test_utils.ts @@ -9,6 +9,7 @@ // specific language governing permissions and limitations under the License. // import * as path from 'path'; +import { LeopardWord } from '../src'; const ROOT_DIR = path.join(__dirname, '../../..'); const TEST_DATA_JSON = require(path.join( @@ -34,19 +35,46 @@ export function getAudioFile(audioFile: string): string { return path.join(ROOT_DIR, 'resources/audio_samples', audioFile); } -export function getTestParameters(): [ +export function getLanguageTestParameters(): [ + string, string, string, - string[], number, - string + string, + LeopardWord[] ][] { - let parametersJson = TEST_DATA_JSON.tests.parameters; + let parametersJson = TEST_DATA_JSON.tests.language_tests; return parametersJson.map((x: any) => [ x.language, x.transcript, - x.punctuations, + x.transcript_with_punctuation, x.error_rate, x.audio_file, + x.words.map((w: any) => ({ + word: w.word, + startSec: w.start_sec, + endSec: w.end_sec, + confidence: w.confidence, + speakerTag: w.speaker_tag, + })), + ]); +} + +export function getDiarizationTestParameters(): [ + string, + string, + LeopardWord[] +][] { + let parametersJson = TEST_DATA_JSON.tests.diarization_tests; + return parametersJson.map((x: any) => [ + x.language, + x.audio_file, + x.words.map((w: any) => ({ + word: w.word, + startSec: w.start_sec, + endSec: w.end_sec, + confidence: w.confidence, + speakerTag: w.speaker_tag, + })), ]); } diff --git a/binding/nodejs/yarn.lock b/binding/nodejs/yarn.lock index de020110..2dd5d0cb 100644 --- a/binding/nodejs/yarn.lock +++ b/binding/nodejs/yarn.lock @@ -685,10 +685,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.5.tgz#3af577099a99c61479149b716183e70b5239324a" integrity sha512-Ark2WDjjZO7GmvsyFFf81MXuGTA/d6oP38anyxWOL6EREyBKAxKoFHwBhaZxCfLRLpO8JgVXwqOwSwa7jRcjew== -"@types/node@^17.0.21": - version "17.0.45" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" - integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== +"@types/node@^18.11.9": + version "18.18.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.9.tgz#5527ea1832db3bba8eb8023ce8497b7d3f299592" + integrity sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ== + dependencies: + undici-types "~5.26.4" "@types/prettier@^2.1.5": version "2.7.2" @@ -3580,6 +3582,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== + universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" From 15f9a818ccb00a1357ebc1716f54a0b7e71f8af7 Mon Sep 17 00:00:00 2001 From: Ian Date: Mon, 20 Nov 2023 10:26:43 -0800 Subject: [PATCH 2/4] test --- binding/nodejs/test/index.test.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/binding/nodejs/test/index.test.ts b/binding/nodejs/test/index.test.ts index b2d1113c..efdff6a8 100644 --- a/binding/nodejs/test/index.test.ts +++ b/binding/nodejs/test/index.test.ts @@ -288,3 +288,25 @@ describe('manual paths', () => { leopardEngine.release(); }); }); + +describe('error message stack', () => { + test('message stack cleared after read', () => { + let error: string[] = []; + try { + new Leopard('invalid'); + } catch (e: any) { + error = e.messageStack; + } + + expect(error.length).toBeGreaterThan(0); + expect(error.length).toBeLessThanOrEqual(8); + + try { + new Leopard('invalid'); + } catch (e: any) { + for (let i = 0; i < error.length; i++) { + expect(error[i]).toEqual(e.messageStack[i]); + } + } + }); +}); From eb7858364dbcc8e784cdbc578c31ef9921ceabf4 Mon Sep 17 00:00:00 2001 From: Ian Date: Mon, 20 Nov 2023 12:02:26 -0800 Subject: [PATCH 3/4] demos --- demo/nodejs/file.js | 18 +++++++++++------- demo/nodejs/mic.js | 19 ++++++++++++++++--- demo/nodejs/package.json | 9 +++++---- demo/nodejs/yarn.lock | 11 +++++++---- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/demo/nodejs/file.js b/demo/nodejs/file.js index ab317f19..a643498b 100755 --- a/demo/nodejs/file.js +++ b/demo/nodejs/file.js @@ -1,6 +1,6 @@ #! /usr/bin/env node // -// Copyright 2020 Picovoice Inc. +// Copyright 2022-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. @@ -31,7 +31,8 @@ program "absolute path to leopard dynamic library" ) .option("-m, --model_file_path ", "absolute path to leopard model") - .option("-d, --disable_automatic_punctuation", "disable automatic punctuation") + .option("-p, --disable_automatic_punctuation", "disable automatic punctuation") + .option("-d, --disable_speaker_diarization", "disable speaker diarization") .option("-v, --verbose", "verbose mode, prints metadata"); @@ -46,6 +47,7 @@ function fileDemo() { let libraryFilePath = program["library_file_path"]; let modelFilePath = program["model_file_path"]; let disableAutomaticPunctuation = program["disable_automatic_punctuation"]; + let disableSpeakerDiarization = program["disable_speaker_diarization"]; let verbose = program["verbose"]; let engineInstance = new Leopard( @@ -53,7 +55,8 @@ function fileDemo() { { 'modelPath': modelFilePath, 'libraryPath': libraryFilePath, - 'enableAutomaticPunctuation': !disableAutomaticPunctuation + 'enableAutomaticPunctuation': !disableAutomaticPunctuation, + 'enableDiarization': !disableSpeakerDiarization } ); @@ -69,10 +72,11 @@ function fileDemo() { console.table( res.words.map(word => { return { - "word": word.word, - "Start time in Sec": word.startSec.toFixed(2), - "End time in Sec": word.endSec.toFixed(2), - "Confidence": word.confidence.toFixed(2) + "Word": word.word, + "Start time (s)": word.startSec.toFixed(2), + "End time (s)": word.endSec.toFixed(2), + "Confidence": word.confidence.toFixed(2), + "Speaker Tag": word.speakerTag }; }) ); diff --git a/demo/nodejs/mic.js b/demo/nodejs/mic.js index 6a0206e0..e8ed9656 100755 --- a/demo/nodejs/mic.js +++ b/demo/nodejs/mic.js @@ -35,7 +35,8 @@ program -1 ) .option("-s, --show_audio_devices", "show the list of available devices") - .option("-d, --disable_automatic_punctuation", "disable automatic punctuation") + .option("-p, --disable_automatic_punctuation", "disable automatic punctuation") + .option("-d, --disable_speaker_diarization", "disable speaker diarization") .option("-v, --verbose", "verbose mode, prints metadata"); if (process.argv.length < 1) { @@ -52,6 +53,7 @@ async function micDemo() { let audioDeviceIndex = program["audio_device_index"]; let showAudioDevices = program["show_audio_devices"]; let disableAutomaticPunctuation = program["disable_automatic_punctuation"]; + let disableSpeakerDiarization = program["disable_speaker_diarization"]; let verbose = program["verbose"]; let showAudioDevicesDefined = showAudioDevices !== undefined; @@ -74,7 +76,8 @@ async function micDemo() { { 'modelPath': modelFilePath, 'libraryPath': libraryFilePath, - 'enableAutomaticPunctuation': !disableAutomaticPunctuation + 'enableAutomaticPunctuation': !disableAutomaticPunctuation, + 'enableDiarization': !disableSpeakerDiarization }); const recorder = new PvRecorder(PV_RECORDER_FRAME_LENGTH, audioDeviceIndex); @@ -112,7 +115,17 @@ async function micDemo() { const res = engineInstance.process(audioFrameInt16) console.log(res.transcript); if (verbose) { - console.table(res.words); + console.table( + res.words.map(word => { + return { + "Word": word.word, + "Start time (s)": word.startSec.toFixed(2), + "End time (s)": word.endSec.toFixed(2), + "Confidence": word.confidence.toFixed(2), + "Speaker Tag": word.speakerTag + }; + }) + ); } } catch (err) { if (err instanceof LeopardActivationLimitReached) { diff --git a/demo/nodejs/package.json b/demo/nodejs/package.json index 174199de..3fb61e34 100644 --- a/demo/nodejs/package.json +++ b/demo/nodejs/package.json @@ -1,6 +1,6 @@ { "name": "@picovoice/leopard-node-demo", - "version": "1.2.2", + "version": "2.0.0", "description": "Picovoice Leopard Node.js file-based and microphone demos", "scripts": { "file": "node file.js", @@ -16,11 +16,12 @@ "author": "Picovoice Inc.", "license": "Apache-2.0", "dependencies": { - "@picovoice/leopard-node": "=1.2.0", + "@picovoice/leopard-node": "file:../../binding/nodejs", "@picovoice/pvrecorder-node": "=1.2.1", "commander": "^6.1.0", "readline": "^1.3.0", - "wavefile": "^11.0.0" + "wavefile": "^11.0.0", + "prettier": "^2.6.2" }, "devDependencies": {}, "homepage": "https://picovoice.ai/products/leopard/", @@ -30,7 +31,7 @@ "directory": "demo/nodejs" }, "engines": { - "node": ">=12.0.0" + "node": ">=16.0.0" }, "cpu": [ "!ia32", diff --git a/demo/nodejs/yarn.lock b/demo/nodejs/yarn.lock index 74bbc218..ded57fd1 100644 --- a/demo/nodejs/yarn.lock +++ b/demo/nodejs/yarn.lock @@ -2,10 +2,8 @@ # yarn lockfile v1 -"@picovoice/leopard-node@=1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@picovoice/leopard-node/-/leopard-node-1.2.0.tgz#2e210926ede28f61ba1322a1e5ab2a0e794d53e4" - integrity sha512-h86O/mrnv3WvgR1zwF4hoEdLXqpysm1PBnoNm6w0Q5u42AXdJ9KHN8bfClZA+fp8J6Yt/RE+nqNOVqeEIY1kjQ== +"@picovoice/leopard-node@file:../../binding/nodejs": + version "2.0.0" "@picovoice/pvrecorder-node@=1.2.1": version "1.2.1" @@ -17,6 +15,11 @@ commander@^6.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== +prettier@^2.6.2: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + readline@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/readline/-/readline-1.3.0.tgz#c580d77ef2cfc8752b132498060dc9793a7ac01c" From ad79a1e65787558ce7636ad0b634a502f5c3d330 Mon Sep 17 00:00:00 2001 From: Ian Date: Mon, 20 Nov 2023 13:24:54 -0800 Subject: [PATCH 4/4] performance tweak --- .github/workflows/nodejs-perf.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/nodejs-perf.yml b/.github/workflows/nodejs-perf.yml index 093ed212..4e61b7f3 100644 --- a/.github/workflows/nodejs-perf.yml +++ b/.github/workflows/nodejs-perf.yml @@ -32,14 +32,14 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] include: - os: ubuntu-latest - init_performance_threshold_sec: 2.0 - proc_performance_threshold_sec: 0.7 - - os: windows-latest init_performance_threshold_sec: 2.5 proc_performance_threshold_sec: 0.9 + - os: windows-latest + init_performance_threshold_sec: 2.5 + proc_performance_threshold_sec: 1.1 - os: macos-latest - init_performance_threshold_sec: 3.5 - proc_performance_threshold_sec: 0.9 + init_performance_threshold_sec: 4.0 + proc_performance_threshold_sec: 2.0 steps: @@ -66,19 +66,19 @@ jobs: include: - machine: rpi3-32 init_performance_threshold_sec: 7.6 - proc_performance_threshold_sec: 3.3 + proc_performance_threshold_sec: 4.5 - machine: rpi3-64 init_performance_threshold_sec: 8.4 - proc_performance_threshold_sec: 3.3 + proc_performance_threshold_sec: 4.5 - machine: rpi4-32 init_performance_threshold_sec: 5.7 - proc_performance_threshold_sec: 2.1 + proc_performance_threshold_sec: 3.3 - machine: rpi4-64 init_performance_threshold_sec: 5.1 - proc_performance_threshold_sec: 2.0 + proc_performance_threshold_sec: 3.2 - machine: jetson init_performance_threshold_sec: 5.1 - proc_performance_threshold_sec: 2.0 + proc_performance_threshold_sec: 3.2 steps: - uses: actions/checkout@v3