Skip to content

Commit c3b27e1

Browse files
author
David Bartle
authored
NodeJS binding (#294)
1 parent c60f69e commit c3b27e1

18 files changed

+5408
-77
lines changed

README.md

+122-77
Large diffs are not rendered by default.

binding/nodejs/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
lib
3+
resources

binding/nodejs/README.md

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Porcupine Binding for NodeJS
2+
3+
## Porcupine
4+
5+
Porcupine is is a highly accurate and lightweight wake word engine. It enables building always-listening voice-enabled applications using cutting edge voice AI.
6+
7+
Porcupine is:
8+
9+
- private and offline
10+
- [accurate](https://github.com/Picovoice/wake-word-benchmark)
11+
- [resource efficient](https://www.youtube.com/watch?v=T0tAnh8tUQg) (runs even on microcontrollers)
12+
- data efficient (wake words can be easily generated by simply typing them, without needing thousands of hours of bespoke audio training data and manual effort)
13+
- scalable to many simultaneous wake-words / always-on voice commands
14+
- cross-platform
15+
16+
To learn more about Porcupine, see the [product](https://picovoice.ai/products/porcupine/), [documentation](https://picovoice.ai/docs/), and [GitHub](https://github.com/Picovoice/porcupine/) pages.
17+
18+
### Custom wake words
19+
20+
Porcupine includes several built-in keywords, which are stored as `.ppn` files. To train custom PPN files, see the [Picovoice Console](https://picovoice.ai/console/).
21+
22+
Unlike the built-in keywords, custom PPN files generated with the Picovoice Console carry restrictions including (but not limited to): training allowance, time limits, available platforms, and commercial usage.
23+
24+
## Compatibility
25+
26+
This binding is for running Porcupine on **NodeJS 12+** on the following platforms:
27+
28+
- Linux (x86_64)
29+
- macOS (x86_64)
30+
- Raspberry Pi (2,3,4)
31+
- Windows (x86_64)
32+
33+
### Web Browsers
34+
35+
This binding is for NodeJS and **does not work in a browser**. Looking to run Porcupine in-browser? Use the [JavaScript WebAssembly](https://github.com/Picovoice/porcupine/tree/master/binding/javascript) binding instead.
36+
37+
## Usage
38+
39+
The binding provides the Porcupine class. Create instances of the Porcupine class to detect specific keywords.
40+
41+
### Quick Start: Built-in keywords
42+
43+
The built-in keywords give a quick way to get started. Here we can specify that we want to listen for the wake words "grasshopper" and "bumblebee" with [sensitivities](https://picovoice.ai/faq/porcupine/#what-should-i-set-the-sensitivity-value-to) of 0.5 and 0.65, respectively. Since Porcupine can listen to many keywords, they are provided as an array argument.
44+
45+
```javascript
46+
const Porcupine = require("@picovoice/porcupine-node");
47+
48+
const {
49+
GRASSHOPPER,
50+
BUMBLEBEE,
51+
} = require("@picovoice/porcupine-node/builtin_keywords");
52+
53+
let engineInstance = new Porcupine([GRASSHOPPER, BUMBLEBEE], [0.5, 0.65]);
54+
55+
// process a single frame of audio
56+
// the keywordIndex provies the index of the keyword detected, or -1 if no keyword was detected
57+
let keywordIndex = engineInstance.process(frame);
58+
```
59+
60+
#### List of built-in keywords
61+
62+
- AMERICANO
63+
- BLUEBERRY
64+
- BUMBLEBEE
65+
- GRAPEFRUIT
66+
- GRASSHOPPER
67+
- PICOVOICE
68+
- PORCUPINE
69+
- TERMINATOR
70+
71+
### Custom keywords
72+
73+
Providing an array of strings instead of the built-in enums allows you to specify an aboslute path to a keyword PPN file:
74+
75+
```javascript
76+
let engineInstance = new Porcupine(
77+
["/absolute/path/to/your/keyword.ppn"],
78+
[0.5]
79+
);
80+
```
81+
82+
### Override model and library paths
83+
84+
The Porcupine constructor accepts two optional positional parameters for the absolute paths to the model and dynamic library, should you need to override them (typically, you will not).
85+
86+
```javascript
87+
let engineInstance = new Porcupine(
88+
keywordPaths,
89+
sensitivities,
90+
modelFilePath,
91+
libraryFilePath
92+
);
93+
```
94+
95+
## Using the bindings from source
96+
97+
## Unit Tests
98+
99+
Run `yarn` (or`npm install`) from the [binding/nodejs](https://github.com/Picovoice/porcupine/tree/master/binding/nodejs) directory to install project dependencies. This will also run a script to copy all of the necessary shared resources from the Porcupine repository into the package directory.
100+
101+
Run `yarn test` (or `npm run test`) from the [binding/nodejs](https://github.com/Picovoice/porcupine/tree/master/binding/nodejs) directory to execute the test suite.

binding/nodejs/builtin_keywords.js

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//
2+
// Copyright 2020 Picovoice Inc.
3+
//
4+
// You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE"
5+
// file accompanying this source.
6+
//
7+
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
8+
// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
9+
// specific language governing permissions and limitations under the License.
10+
//
11+
"use strict";
12+
const path = require("path");
13+
14+
const { PvArgumentError } = require("./errors");
15+
const { getPlatform } = require("./platforms");
16+
17+
const AMERICANO = 0;
18+
const BLUEBERRY = 1;
19+
const BUMBLEBEE = 2;
20+
const GRAPEFRUIT = 3;
21+
const GRASSHOPPER = 4;
22+
const PICOVOICE = 5;
23+
const PORCUPINE = 6;
24+
const TERMINATOR = 7;
25+
26+
const AMERICANO_STRING = "americano";
27+
const BLUEBERRY_STRING = "blueberry";
28+
const BUMBLEBEE_STRING = "bumblebee";
29+
const GRAPEFRUIT_STRING = "grapefruit";
30+
const GRASSHOPPER_STRING = "grasshopper";
31+
const PICOVOICE_STRING = "picovoice";
32+
const PORCUPINE_STRING = "porcupine";
33+
const TERMINATOR_STRING = "terminator";
34+
35+
const BUILTIN_KEYWORDS_ENUMS = new Set();
36+
BUILTIN_KEYWORDS_ENUMS.add(AMERICANO);
37+
BUILTIN_KEYWORDS_ENUMS.add(BLUEBERRY);
38+
BUILTIN_KEYWORDS_ENUMS.add(BUMBLEBEE);
39+
BUILTIN_KEYWORDS_ENUMS.add(GRAPEFRUIT);
40+
BUILTIN_KEYWORDS_ENUMS.add(GRASSHOPPER);
41+
BUILTIN_KEYWORDS_ENUMS.add(PICOVOICE);
42+
BUILTIN_KEYWORDS_ENUMS.add(PORCUPINE);
43+
BUILTIN_KEYWORDS_ENUMS.add(TERMINATOR);
44+
45+
const BUILTIN_KEYWORDS_STRINGS = new Set();
46+
BUILTIN_KEYWORDS_STRINGS.add(AMERICANO_STRING);
47+
BUILTIN_KEYWORDS_STRINGS.add(BLUEBERRY_STRING);
48+
BUILTIN_KEYWORDS_STRINGS.add(BUMBLEBEE_STRING);
49+
BUILTIN_KEYWORDS_STRINGS.add(GRAPEFRUIT_STRING);
50+
BUILTIN_KEYWORDS_STRINGS.add(GRASSHOPPER_STRING);
51+
BUILTIN_KEYWORDS_STRINGS.add(PICOVOICE_STRING);
52+
BUILTIN_KEYWORDS_STRINGS.add(PORCUPINE_STRING);
53+
BUILTIN_KEYWORDS_STRINGS.add(TERMINATOR_STRING);
54+
55+
const BUILTIN_KEYWORDS_ENUM_TO_STRING = new Map();
56+
BUILTIN_KEYWORDS_ENUM_TO_STRING.set(AMERICANO, AMERICANO_STRING);
57+
BUILTIN_KEYWORDS_ENUM_TO_STRING.set(BLUEBERRY, BLUEBERRY_STRING);
58+
BUILTIN_KEYWORDS_ENUM_TO_STRING.set(BUMBLEBEE, BUMBLEBEE_STRING);
59+
BUILTIN_KEYWORDS_ENUM_TO_STRING.set(GRAPEFRUIT, GRAPEFRUIT_STRING);
60+
BUILTIN_KEYWORDS_ENUM_TO_STRING.set(GRASSHOPPER, GRASSHOPPER_STRING);
61+
BUILTIN_KEYWORDS_ENUM_TO_STRING.set(PICOVOICE, PICOVOICE_STRING);
62+
BUILTIN_KEYWORDS_ENUM_TO_STRING.set(PORCUPINE, PORCUPINE_STRING);
63+
BUILTIN_KEYWORDS_ENUM_TO_STRING.set(TERMINATOR, TERMINATOR_STRING);
64+
65+
const BUILTIN_KEYWORDS_STRING_TO_ENUM = new Map();
66+
BUILTIN_KEYWORDS_STRING_TO_ENUM.set(AMERICANO_STRING, AMERICANO);
67+
BUILTIN_KEYWORDS_STRING_TO_ENUM.set(BLUEBERRY_STRING, BLUEBERRY);
68+
BUILTIN_KEYWORDS_STRING_TO_ENUM.set(BUMBLEBEE_STRING, BUMBLEBEE);
69+
BUILTIN_KEYWORDS_STRING_TO_ENUM.set(GRAPEFRUIT_STRING, GRAPEFRUIT);
70+
BUILTIN_KEYWORDS_STRING_TO_ENUM.set(GRASSHOPPER_STRING, GRASSHOPPER);
71+
BUILTIN_KEYWORDS_STRING_TO_ENUM.set(PICOVOICE_STRING, PICOVOICE);
72+
BUILTIN_KEYWORDS_STRING_TO_ENUM.set(PORCUPINE_STRING, PORCUPINE);
73+
BUILTIN_KEYWORDS_STRING_TO_ENUM.set(TERMINATOR_STRING, TERMINATOR);
74+
75+
function getBuiltinKeywordPath(builtinKeyword) {
76+
if (!Number.isInteger(builtinKeyword)) {
77+
throw new PvArgumentError(
78+
`getBuiltinKeywordPath argument '${builtinKeyword}' is not an integer (enum) value`
79+
);
80+
}
81+
82+
let platform = getPlatform();
83+
let keywordString;
84+
if (!BUILTIN_KEYWORDS_ENUMS.has(builtinKeyword)) {
85+
throw new PvArgumentError(
86+
`Keyword argument ${builtinKeyword} does not map to one of the built-in keywords: [${Array.from(
87+
BUILTIN_KEYWORDS_STRINGS
88+
)}]`
89+
);
90+
} else {
91+
keywordString = BUILTIN_KEYWORDS_ENUM_TO_STRING.get(builtinKeyword);
92+
}
93+
94+
return path.resolve(
95+
__dirname,
96+
`resources/keyword_files/${platform}/${keywordString}_${platform}.ppn`
97+
);
98+
}
99+
100+
module.exports = {
101+
AMERICANO: AMERICANO,
102+
BLUEBERRY: BLUEBERRY,
103+
BUMBLEBEE: BUMBLEBEE,
104+
GRAPEFRUIT: GRAPEFRUIT,
105+
GRASSHOPPER: GRASSHOPPER,
106+
PICOVOICE: PICOVOICE,
107+
PORCUPINE: PORCUPINE,
108+
TERMINATOR: TERMINATOR,
109+
BUILTIN_KEYWORDS_ENUMS: BUILTIN_KEYWORDS_ENUMS,
110+
BUILTIN_KEYWORDS_STRINGS: BUILTIN_KEYWORDS_STRINGS,
111+
BUILTIN_KEYWORDS_ENUM_TO_STRING: BUILTIN_KEYWORDS_ENUM_TO_STRING,
112+
BUILTIN_KEYWORDS_STRING_TO_ENUM: BUILTIN_KEYWORDS_STRING_TO_ENUM,
113+
getBuiltinKeywordPath: getBuiltinKeywordPath,
114+
};

binding/nodejs/copy.js

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//
2+
// Copyright 2020 Picovoice Inc.
3+
//
4+
// You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE"
5+
// file accompanying this source.
6+
//
7+
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
8+
// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
9+
// specific language governing permissions and limitations under the License.
10+
//
11+
"use strict";
12+
13+
const mkdirp = require("mkdirp");
14+
const ncp = require("ncp").ncp;
15+
16+
console.log("Copying library files...");
17+
18+
// Library & Model
19+
mkdirp.sync("./lib/common");
20+
ncp(
21+
"../../lib/common/porcupine_params.pv",
22+
"./lib/common/porcupine_params.pv",
23+
function (err) {
24+
if (err) {
25+
return console.error(err);
26+
}
27+
console.log("../../lib/common copied.");
28+
}
29+
);
30+
31+
let platforms = ["mac", "raspberry-pi", "windows", "linux"];
32+
33+
function copyLibraryFilter(path) {
34+
return !path.includes("arm11");
35+
}
36+
37+
for (let platform of platforms) {
38+
ncp(
39+
`../../lib/${platform}`,
40+
`./lib/${platform}`,
41+
{ filter: copyLibraryFilter },
42+
function (err) {
43+
if (err) {
44+
return console.error(err);
45+
}
46+
console.log(`../../lib/${platform}.`);
47+
}
48+
);
49+
}
50+
51+
// Keywords (resources)
52+
// Only ship keywords that work on every platform (LCD)
53+
let lcdKeywords = [
54+
"americano",
55+
"blueberry",
56+
"bumblebee",
57+
"grapefruit",
58+
"grasshopper",
59+
"picovoice",
60+
"porcupine",
61+
"terminator",
62+
];
63+
64+
console.log("Copying keyword files...");
65+
66+
mkdirp.sync("./resources/keyword_files");
67+
68+
for (let platform of platforms) {
69+
mkdirp.sync(`./resources/keyword_files/${platform}`);
70+
71+
for (let keyword of lcdKeywords) {
72+
ncp(
73+
`../../resources/keyword_files/${platform}/${keyword}_${platform}.ppn`,
74+
`./resources/keyword_files/${platform}/${keyword}_${platform}.ppn`,
75+
function (err) {
76+
if (err) {
77+
return console.error(err);
78+
}
79+
console.log(
80+
`../../resources/keyword_files/${platform}/${keyword}_${platform}.ppn.`
81+
);
82+
}
83+
);
84+
}
85+
}

binding/nodejs/errors.js

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//
2+
// Copyright 2020 Picovoice Inc.
3+
//
4+
// You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE"
5+
// file accompanying this source.
6+
//
7+
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
8+
// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
9+
// specific language governing permissions and limitations under the License.
10+
//
11+
"use strict";
12+
13+
class PvArgumentError extends Error {}
14+
class PvStateError extends Error {}
15+
class PvUnsupportedPlatformError extends Error {}
16+
17+
// pv_status_t error codes
18+
class PvStatusOutOfMemoryError extends Error {}
19+
class PvStatusIoError extends Error {}
20+
class PvStatusInvalidArgumentError extends Error {}
21+
class PvStatusStopIterationError extends Error {}
22+
class PvStatusKeyError extends Error {}
23+
class PvStatusInvalidStateError extends Error {}
24+
25+
const PV_STATUS_T = require("./pv_status_t");
26+
27+
function pvStatusToException(pvStatusInt, errorMessage) {
28+
switch (pvStatusInt) {
29+
case PV_STATUS_T.OUT_OF_MEMORY:
30+
throw new PvStatusOutOfMemoryError(errorMessage);
31+
case PV_STATUS_T.IO_ERROR:
32+
throw new PvStatusIoError(errorMessage);
33+
case PV_STATUS_T.INVALID_ARGUMENT:
34+
throw new PvStatusInvalidArgumentError(errorMessage);
35+
case PV_STATUS_T.STOP_ITERATION:
36+
throw new PvStatusStopIterationError(errorMessage);
37+
case PV_STATUS_T.KEY_ERROR:
38+
throw new PvStatusKeyError(errorMessage);
39+
case PV_STATUS_T.INVALID_STATE:
40+
throw new PvStatusInvalidStateError(errorMessage);
41+
default:
42+
console.warn(`Unmapped error code: ${pvStatusInt}`);
43+
throw new Error(errorMessage);
44+
}
45+
}
46+
47+
module.exports = {
48+
PvArgumentError,
49+
PvStateError,
50+
PvStatusInvalidArgumentError,
51+
PvStatusInvalidStateError,
52+
PvStatusIoError,
53+
PvStatusKeyError,
54+
PvStatusOutOfMemoryError,
55+
PvStatusStopIterationError,
56+
pvStatusToException,
57+
PvUnsupportedPlatformError,
58+
};

0 commit comments

Comments
 (0)