Skip to content

Commit

Permalink
Nodejs demo pvspeaker (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
albho authored Aug 16, 2024
1 parent dea2440 commit bd7a0f7
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 165 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/nodejs-demos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:

strategy:
matrix:
machine: [ rpi3-32, rpi3-64, rpi4-32, rpi4-64, rpi5-64, jetson ]
machine: [ rpi3-32, rpi3-64, rpi4-32, rpi4-64, rpi5-64 ]

steps:
- uses: actions/checkout@v3
Expand Down
13 changes: 4 additions & 9 deletions demo/nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ voice assistants. Orca is:
- Linux (x86_64), macOS (x86_64, arm64), Windows (x86_64)
- Android and iOS
- Chrome, Safari, Firefox, and Edge
- Raspberry Pi (3, 4, 5) and NVIDIA Jetson Nano
- Raspberry Pi (3, 4, 5)

## Compatibility

- Node.js 16+
- Runs on Linux (x86_64), macOS (x86_64, arm64), Windows (x86_64), Raspberry Pi (3, 4, 5), and NVIDIA Jetson Nano.
- Runs on Linux (x86_64), macOS (x86_64, arm64), Windows (x86_64), and Raspberry Pi (3, 4, 5).

## Installation

Expand Down Expand Up @@ -45,13 +45,8 @@ In the single synthesis mode, the text is synthesized in a single call to the Or
In this demo, we simulate a response from a language model by creating a text stream from a user-defined text.
We stream that text to Orca and play the synthesized audio as soon as it gets generated.

[node-speaker](https://github.com/TooTallNate/node-speaker) is used to play pcm audio generated by Orca to your
device's speakers. On Debian/Ubuntu, `node-speaker` uses the [ALSA](http://www.alsa-project.org/) backend, so you'll
need to install `libasound2-dev` before starting the demo:

```console
sudo apt-get install libasound2-dev
```
[@picovoice/pvspeaker-node](https://www.npmjs.com/package/@picovoice/pvspeaker-node) is used to play pcm audio generated
by Orca to your device's speakers.

Execute the following:

Expand Down
8 changes: 2 additions & 6 deletions demo/nodejs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@picovoice/orca-node-demo",
"version": "0.2.0",
"version": "0.2.1",
"description": "Picovoice Orca Node.js file-based and streaming demos",
"scripts": {
"file": "node file.js",
Expand All @@ -22,18 +22,14 @@
"license": "Apache-2.0",
"dependencies": {
"@picovoice/orca-node": "=0.2.0",
"@picovoice/pvspeaker-node": "^1.0.0",
"commander": "^6.1.0",
"pcm-convert": "^1.6.5",
"prettier": "^2.6.2",
"readline": "^1.3.0",
"systeminformation": "^5.22.11",
"tiktoken": "^1.0.15",
"wavefile": "^11.0.0"
},
"devDependencies": {},
"optionalDependencies": {
"speaker": "^0.5.5"
},
"homepage": "https://picovoice.ai/platform/orca/",
"repository": {
"type": "git",
Expand Down
98 changes: 54 additions & 44 deletions demo/nodejs/streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@
'use strict';

const os = require('os');
const si = require('systeminformation');
const { program } = require('commander');
const { performance } = require('perf_hooks');
const { execSync } = require('child_process');
const tiktoken = require('tiktoken');
const convert = require('pcm-convert');

const { PvSpeaker } = require('@picovoice/pvspeaker-node');
const { Orca, OrcaActivationLimitReachedError } = require('@picovoice/orca-node');

program
Expand Down Expand Up @@ -46,7 +45,20 @@ program
.option(
'--audio_wait_chunks <number>',
'Number of PCM chunks to wait before starting to play audio',
'0',
)
.option(
'--buffer_size_secs <number>',
'The size in seconds of the internal buffer used by PvSpeaker to play audio',
'20',
)
.option(
'--audio_device_index <number>',
'Index of input audio device',
'-1',
)
.option(
'--show_audio_devices',
'Only list available audio output devices and exit',
);

if (process.argv.length < 2) {
Expand Down Expand Up @@ -142,6 +154,17 @@ async function streamingDemo() {
let text = program['text_to_stream'];
let tokensPerSeconds = program['tokens_per_second'];
let audioWaitChunks = program['audio_wait_chunks'];
let bufferSizeSecs = Number(program['buffer_size_secs']);
let deviceIndex = Number(program['audio_device_index']);
let showAudioDevices = program['show_audio_devices'];

if (showAudioDevices) {
const devices = PvSpeaker.getAvailableDevices();
for (let i = 0; i < devices.length; i++) {
console.log(`index: ${i}, device name: ${devices[i]}`);
}
return;
}

if (audioWaitChunks === undefined || audioWaitChunks === null) {
audioWaitChunks = 0;
Expand All @@ -154,7 +177,7 @@ async function streamingDemo() {
}

try {
let orca = new Orca(
const orca = new Orca(
accessKey,
{
'modelPath': modelFilePath,
Expand All @@ -167,42 +190,17 @@ async function streamingDemo() {
let speaker = null;

try {
require.resolve('speaker');
await si.audio((devices) => {
if (devices.length > 0 && devices[0].driver !== null) {
console.log(`Playing from device: ${devices[0].name}`);
const Speaker = require('speaker');
speaker = new Speaker({
channels: 1,
bitDepth: 8,
sampleRate: orca.sampleRate,
});
} else {
console.error('Note: No sound card(s) detected. Orca will generate the pcm, but no audio will be played.');
}
});
const bitsPerSample = 16;
speaker = new PvSpeaker(orca.sampleRate, bitsPerSample, { bufferSizeSecs, deviceIndex });
speaker.start();
} catch (e) {
console.error('\nNote: External package \'node-speaker\' was not installed successfully. This package may not be compatible with your machine. ' +
'Orca will generate the pcm, but it will not be played to your speakers.');
console.error('\nNote: External package \'@picovoice/pvspeaker-node\' failed to initialize.' +
' Orca will generate the pcm, but it will not be played to your speakers.');
}

const pcmBuffer = [];

function playStream() {
if (pcmBuffer.length === 0) return;

const pcmInt16 = pcmBuffer.shift();

// for some reason, "speaker" does not accept Int16Array
const pcmUint8 = convert(pcmInt16, 'int16', 'uint8');
try {
speaker?.write(pcmUint8);
} catch (e) {
console.log(`'node-speaker' unable to play audio: ${e}`);
}

playStream();
}
let pcmBuffer = [];
let numAudioChunks = 0;
let isStartedPlaying = false;

process.stdout.write('\nSimulated text stream: ');

Expand All @@ -217,11 +215,19 @@ async function streamingDemo() {
if (timeFirstAudioAvailable === null) {
timeFirstAudioAvailable = ((performance.now() - startTime) / 1000).toFixed(2);
}
pcmBuffer.push(pcm);
if (pcmBuffer.length >= audioWaitChunks) {
playStream();
pcmBuffer.push(...pcm);
numAudioChunks++;
}

if (pcmBuffer.length > 0 && speaker !== null && (isStartedPlaying || numAudioChunks >= audioWaitChunks)) {
const arrayBuffer = new Int16Array(pcmBuffer).buffer;
const written = speaker.write(arrayBuffer);
if (written < arrayBuffer.byteLength) {
pcmBuffer = pcmBuffer.slice(written);
}
isStartedPlaying = true;
}

await sleepSecs(1 / tokensPerSeconds);
}

Expand All @@ -230,18 +236,22 @@ async function streamingDemo() {
if (timeFirstAudioAvailable === null) {
timeFirstAudioAvailable = ((performance.now() - startTime) / 1000).toFixed(2);
}
pcmBuffer.push(flushedPcm);
playStream();
pcmBuffer.push(...flushedPcm);
}
const elapsedTime = ((performance.now() - startTime) / 1000).toFixed(2);

console.log(`\n\nTime to finish text stream: ${elapsedTime} seconds`);
console.log(`Time to receive first audio: ${timeFirstAudioAvailable} seconds after text stream started`);
console.log('\nWaiting for audio to finish...');

speaker?.end();
if (speaker !== null) {
const arrayBuffer = new Int16Array(pcmBuffer).buffer;
speaker.flush(arrayBuffer);
speaker.stop();
speaker.release();
}
stream.close();
orca?.release();
orca.release();
} catch (err) {
if (err instanceof OrcaActivationLimitReachedError) {
console.error(`AccessKey '${accessKey}' has reached it's processing limit.`);
Expand Down
108 changes: 3 additions & 105 deletions demo/nodejs/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,99 +7,16 @@
resolved "https://registry.yarnpkg.com/@picovoice/orca-node/-/orca-node-0.2.0.tgz#928eab700374581acbacfe9b54c368a0a5714b27"
integrity sha512-eOdWXCKUp1YOI7w8ExAfzeqyyr9GheA0x3adzpFw2vb/5xqVExct6wklESOzJ2dQ0EkSSqzWWnXzLqMd02lfRg==

audio-format@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/audio-format/-/audio-format-2.3.2.tgz#f23fc73b308c6dfba1904da6d4cd9c7621731abd"
integrity sha512-5IA2grZhaVhpGxX6lbJm8VVh/SKQULMXXrFxuiodi0zhzDPRB8BJfieo89AclEQv4bDxZRH4lv06qNnxqkFhKQ==
dependencies:
is-audio-buffer "^1.0.11"
is-buffer "^1.1.5"
is-plain-obj "^1.1.0"
pick-by-alias "^1.2.0"
sample-rate "^2.0.0"

bindings@^1.3.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
dependencies:
file-uri-to-path "1.0.0"

buffer-alloc-unsafe@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==

buffer-alloc@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
dependencies:
buffer-alloc-unsafe "^1.1.0"
buffer-fill "^1.0.0"

buffer-fill@^1.0.0:
"@picovoice/pvspeaker-node@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==
resolved "https://registry.yarnpkg.com/@picovoice/pvspeaker-node/-/pvspeaker-node-1.0.0.tgz#6b87b86138a8bb256e46ea41cb7d258ff8534337"
integrity sha512-qhH4ktObGj/Dd7iiptPXqf89U3l7FbCiaB1xY439YE3EFLsfsvZvjAzEwsF5A01h47AbXJTCdAjvkXrcsdt0/w==

commander@^6.1.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==

debug@^4.0.0:
version "4.3.5"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e"
integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==
dependencies:
ms "2.1.2"

[email protected]:
version "1.0.0"
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==

is-audio-buffer@^1.0.11:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-audio-buffer/-/is-audio-buffer-1.1.0.tgz#f4c7f274d20180f993f9b1650768cb77bedd4470"
integrity sha512-fmPC/dizJmP4ITCsW5oTQGMJ9wZVE+A/zAe6FQo3XwgERxmXHmm3ON5XkWDAxmyxvsrDmWx3NArpSgamp/59AA==

is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==

is-plain-obj@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==

[email protected]:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==

object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==

pcm-convert@^1.6.5:
version "1.6.5"
resolved "https://registry.yarnpkg.com/pcm-convert/-/pcm-convert-1.6.5.tgz#81b5f2d0c2fb386edb8b9f69ce2374e093aeb7cc"
integrity sha512-5CEspU4j8aEQ80AhNbcLfpT0apc93E6endFxahWd4sV70I6PN7LPdz8GoYm/1qr400K9bUVsVA+KxNgbFROZPw==
dependencies:
audio-format "^2.3.2"
is-audio-buffer "^1.0.11"
is-buffer "^1.1.5"
object-assign "^4.1.1"

pick-by-alias@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pick-by-alias/-/pick-by-alias-1.2.0.tgz#5f7cb2b1f21a6e1e884a0c87855aa4a37361107b"
integrity sha512-ESj2+eBxhGrcA1azgHs7lARG5+5iLakc/6nlfbpjcLl00HuuUOIuORhYXN4D1HfvMSKuVtFQjAlnwi1JHEeDIw==

prettier@^2.6.2:
version "2.8.8"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
Expand All @@ -110,25 +27,6 @@ readline@^1.3.0:
resolved "https://registry.yarnpkg.com/readline/-/readline-1.3.0.tgz#c580d77ef2cfc8752b132498060dc9793a7ac01c"
integrity sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==

sample-rate@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/sample-rate/-/sample-rate-2.0.1.tgz#3f508969262b2c729a95d4d42b795a5b33610bd0"
integrity sha512-AIK0vVBiAEObmpJOxQu/WCyklnWGqzTSDII4O7nBo+SJHmfgBUiYhgV/Y3Ohz76gfSlU6R5CIAKggj+nAOLSvg==

speaker@^0.5.5:
version "0.5.5"
resolved "https://registry.yarnpkg.com/speaker/-/speaker-0.5.5.tgz#8c5fa2aaee2f272889a8778dcad1fc30a13d42a3"
integrity sha512-IBeMZUITigYBO139h0+1MAgBHNZF55GFJN4U/Box35Sg49cfqYkbCO92TXoCUy22Ast08zfqKuXLvPxq9CWwLw==
dependencies:
bindings "^1.3.0"
buffer-alloc "^1.1.0"
debug "^4.0.0"

systeminformation@^5.22.11:
version "5.22.11"
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.22.11.tgz#42be7b650ce0a8b940c06219a6647f6ab3f7a349"
integrity sha512-aLws5yi4KCHTb0BVvbodQY5bY8eW4asMRDTxTW46hqw9lGjACX6TlLdJrkdoHYRB0qs+MekqEq1zG7WDnWE8Ug==

tiktoken@^1.0.15:
version "1.0.15"
resolved "https://registry.yarnpkg.com/tiktoken/-/tiktoken-1.0.15.tgz#a1e11681fa51b50c81bb7eaaee53b7a66e844a23"
Expand Down
1 change: 1 addition & 0 deletions resources/.lint/spell-check/dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ frombuffer
drwav
wchars
pvrecorder
pvspeaker
pvcheetah
itok
numpy
Expand Down

0 comments on commit bd7a0f7

Please sign in to comment.