@lumiastream/wakeword 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md CHANGED
@@ -1,3 +1,156 @@
1
- Get Sox Binaries:
1
+ # LumiaWakeWord
2
2
 
3
- // https://github.com/zackees/static-sox/tree/main/bin sox static binary
3
+ A wake word detection library using Vosk and SoX for real-time voice recognition.
4
+
5
+ ## Features
6
+ - Real-time wake word detection
7
+ - Multi-platform support (Windows, macOS, Linux)
8
+ - Audio device selection (especially useful for Windows)
9
+ - Confidence threshold filtering
10
+ - Dynamic grammar updates
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @lumiastream/wakeword
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```javascript
21
+ import { startWakeWord, listAudioDevices } from "@lumiastream/wakeword";
22
+
23
+ // List available audio devices
24
+ const devices = await listAudioDevices();
25
+ console.log("Available devices:", devices);
26
+
27
+ // Start wake word detection
28
+ const wakeWord = startWakeWord({
29
+ grammar: ["hello", "lumia", "computer"],
30
+ confidence: 0.7,
31
+ device: "0" // Optional: specify audio device
32
+ });
33
+
34
+ // Handle detections
35
+ wakeWord.stdout.on("data", (data) => {
36
+ const lines = data.toString().split("\n");
37
+ for (const line of lines) {
38
+ if (line.startsWith("voice|")) {
39
+ const word = line.split("|")[1];
40
+ console.log(`Wake word detected: ${word}`);
41
+ }
42
+ }
43
+ });
44
+ ```
45
+
46
+ ## Audio Device Selection
47
+
48
+ ### Windows Users
49
+ Windows users often need to select the correct audio input device. Use these commands to find and test devices:
50
+
51
+ ```bash
52
+ # List all available audio devices
53
+ npm run list-devices
54
+
55
+ # Interactive device testing
56
+ npm run test-devices
57
+
58
+ # Use a specific device
59
+ AUDIO_DEVICE=1 npm start
60
+ ```
61
+
62
+ ### Setting the Audio Device
63
+
64
+ **Method 1: Environment Variable**
65
+ ```bash
66
+ set AUDIO_DEVICE=1
67
+ npm start
68
+ ```
69
+
70
+ **Method 2: Programmatically**
71
+ ```javascript
72
+ startWakeWord({
73
+ device: "1",
74
+ grammar: ["hello", "lumia"]
75
+ });
76
+ ```
77
+
78
+ See [AUDIO_DEVICES.md](AUDIO_DEVICES.md) for detailed device configuration guide.
79
+
80
+ ## API Reference
81
+
82
+ ### `listAudioDevices()`
83
+ Returns a Promise that resolves to an array of available audio devices.
84
+
85
+ ```javascript
86
+ const devices = await listAudioDevices();
87
+ // Returns: [{ id: "0", name: "Microphone (Realtek)" }, ...]
88
+ ```
89
+
90
+ ### `startWakeWord(options)`
91
+ Starts the wake word detection process.
92
+
93
+ **Options:**
94
+ - `device` (string): Audio device ID to use
95
+ - `soxPath` (string): Path to SoX binary (optional)
96
+ - `sampleRate` (number): Sample rate, default 16000
97
+ - `grammar` (string[]): Array of wake words to detect
98
+ - `confidence` (number): Confidence threshold (0-1), default 0.7
99
+
100
+ **Returns:** ChildProcess instance
101
+
102
+ ## Scripts
103
+
104
+ ```bash
105
+ npm run list-devices # List available audio devices
106
+ npm run test-devices # Interactive device testing
107
+ npm start # Start wake word detection
108
+ ```
109
+
110
+ ## Example
111
+
112
+ Run the included example:
113
+ ```bash
114
+ node example.js
115
+ ```
116
+
117
+ ## Dependencies
118
+
119
+ - [Vosk](https://alphacephei.com/vosk/) - Speech recognition toolkit
120
+ - [SoX](http://sox.sourceforge.net/) - Sound processing tool
121
+ - Vosk model included: vosk-model-small-en-us-0.15
122
+
123
+ ## Platform Notes
124
+
125
+ ### Windows
126
+ - Default uses device "0" if not specified
127
+ - Use `npm run test-devices` to find the correct device
128
+ - USB microphones may appear as separate devices
129
+
130
+ ### macOS/Linux
131
+ - Uses system default audio input automatically
132
+ - Device selection usually not needed
133
+
134
+ ## Troubleshooting
135
+
136
+ 1. **No audio detected on Windows:**
137
+ - Run `npm run test-devices` to find the correct device
138
+ - Set `AUDIO_DEVICE` environment variable
139
+
140
+ 2. **Poor recognition quality:**
141
+ - Adjust confidence threshold (lower = more sensitive)
142
+ - Try different audio devices
143
+ - Ensure microphone is not muted
144
+
145
+ 3. **Device not found:**
146
+ - Ensure microphone is connected before starting
147
+ - Check system audio settings
148
+
149
+ ## SoX Binaries
150
+
151
+ Pre-compiled SoX binaries are included for all platforms.
152
+ Source: https://github.com/zackees/static-sox/tree/main/bin
153
+
154
+ ## License
155
+
156
+ See LICENSE file for details.
package/lib/index.js ADDED
@@ -0,0 +1,75 @@
1
+ import { listAudioDevices } from "./list-devices.js";
2
+ import { spawn } from "child_process";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const here = dirname(fileURLToPath(import.meta.url));
7
+
8
+ /**
9
+ * List available audio input devices
10
+ * @returns {Promise<Array<{id: string, name: string}>>} Array of available devices
11
+ */
12
+ export { listAudioDevices };
13
+
14
+ /**
15
+ * Start the wake word detection with optional device selection
16
+ * @param {Object} options - Configuration options
17
+ * @param {string} [options.device] - Audio device ID to use
18
+ * @param {string} [options.soxPath] - Path to SoX binary
19
+ * @param {number} [options.sampleRate] - Sample rate (default: 16000)
20
+ * @param {Array<string>} [options.grammar] - Wake words to detect
21
+ * @param {number} [options.confidence] - Confidence threshold (0-1)
22
+ * @returns {ChildProcess} The spawned voice detection process
23
+ */
24
+ export function startWakeWord(options = {}) {
25
+ const {
26
+ device = null,
27
+ soxPath = null,
28
+ sampleRate = 16000,
29
+ grammar = [],
30
+ confidence = 0.7,
31
+ } = options;
32
+
33
+ const voiceScript = join(here, "voice.js");
34
+ const args = [voiceScript];
35
+
36
+ if (soxPath) {
37
+ args.push(soxPath);
38
+ } else {
39
+ args.push(""); // placeholder for default sox path
40
+ }
41
+
42
+ if (device) {
43
+ args.push(device);
44
+ }
45
+
46
+ const env = { ...process.env };
47
+ if (sampleRate) {
48
+ env.SAMPLE_RATE = sampleRate.toString();
49
+ }
50
+ if (device && !args[2]) {
51
+ env.AUDIO_DEVICE = device;
52
+ }
53
+
54
+ const proc = spawn("node", args, {
55
+ env,
56
+ stdio: ["pipe", "pipe", "inherit"],
57
+ });
58
+
59
+ // Send initial grammar if provided
60
+ if (grammar.length > 0) {
61
+ proc.stdin.write(`update,${grammar.join(",")}\n`);
62
+ }
63
+
64
+ // Send confidence threshold if provided
65
+ if (confidence !== 0.7) {
66
+ proc.stdin.write(`confidence,${confidence}\n`);
67
+ }
68
+
69
+ return proc;
70
+ }
71
+
72
+ export default {
73
+ listAudioDevices,
74
+ startWakeWord,
75
+ };
@@ -0,0 +1,128 @@
1
+ import { spawn } from "child_process";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { existsSync } from "node:fs";
5
+
6
+ const here = dirname(fileURLToPath(import.meta.url));
7
+
8
+ function unpacked(p) {
9
+ return p.includes("app.asar")
10
+ ? p.replace("app.asar", "app.asar.unpacked")
11
+ : p;
12
+ }
13
+
14
+ const exeName = { win32: "sox.exe", darwin: "soxmac", linux: "soxlinux" }[
15
+ process.platform
16
+ ];
17
+
18
+ let soxPath = process.argv[2] || join(here, "..", "binaries", exeName);
19
+ soxPath = unpacked(soxPath);
20
+
21
+ if (!existsSync(soxPath)) {
22
+ console.error(`SoX not found: ${soxPath}`);
23
+ process.exit(1);
24
+ }
25
+
26
+ export function listAudioDevices() {
27
+ return new Promise((resolve, reject) => {
28
+ if (process.platform !== "win32") {
29
+ // On macOS and Linux, SoX uses default device
30
+ resolve([{ id: "default", name: "Default Audio Input" }]);
31
+ return;
32
+ }
33
+
34
+ // Windows: Parse available devices from SoX help output
35
+ const proc = spawn(soxPath, ["-t", "waveaudio", "-h"], {
36
+ encoding: "utf8",
37
+ windowsHide: true,
38
+ });
39
+
40
+ let output = "";
41
+ let errorOutput = "";
42
+
43
+ proc.stdout?.on("data", (data) => {
44
+ output += data.toString();
45
+ });
46
+
47
+ proc.stderr?.on("data", (data) => {
48
+ errorOutput += data.toString();
49
+ });
50
+
51
+ proc.on("close", (code) => {
52
+ const devices = [];
53
+
54
+ // Parse output for device list
55
+ const lines = (output + errorOutput).split("\n");
56
+ let inDeviceList = false;
57
+
58
+ for (const line of lines) {
59
+ // Look for input device section
60
+ if (line.includes("Input:") || line.includes("input")) {
61
+ inDeviceList = true;
62
+ continue;
63
+ }
64
+
65
+ // Stop at output section
66
+ if (line.includes("Output:") || line.includes("output")) {
67
+ inDeviceList = false;
68
+ }
69
+
70
+ // Parse device entries (typically in format "0: Device Name")
71
+ if (inDeviceList) {
72
+ const match = line.match(/^\s*(\d+):\s*(.+)$/);
73
+ if (match) {
74
+ devices.push({
75
+ id: match[1],
76
+ name: match[2].trim(),
77
+ });
78
+ }
79
+ }
80
+ }
81
+
82
+ if (devices.length === 0) {
83
+ // Fallback: try to extract any numbered devices
84
+ const allMatches = (output + errorOutput).matchAll(/(\d+):\s*([^\n]+)/g);
85
+ for (const match of allMatches) {
86
+ const name = match[2].trim();
87
+ // Filter out obvious non-device entries
88
+ if (!name.includes("SoX") && !name.includes("Usage") && name.length > 0) {
89
+ devices.push({
90
+ id: match[1],
91
+ name: name,
92
+ });
93
+ }
94
+ }
95
+ }
96
+
97
+ // If still no devices found, provide default options
98
+ if (devices.length === 0) {
99
+ devices.push(
100
+ { id: "0", name: "Device 0 (Default)" },
101
+ { id: "1", name: "Device 1" },
102
+ { id: "2", name: "Device 2" }
103
+ );
104
+ }
105
+
106
+ resolve(devices);
107
+ });
108
+
109
+ proc.on("error", (err) => {
110
+ reject(err);
111
+ });
112
+ });
113
+ }
114
+
115
+ // If run directly, list devices
116
+ if (import.meta.url === `file://${process.argv[1]}`) {
117
+ listAudioDevices()
118
+ .then((devices) => {
119
+ console.log("Available audio input devices:");
120
+ devices.forEach((device) => {
121
+ console.log(` ${device.id}: ${device.name}`);
122
+ });
123
+ })
124
+ .catch((err) => {
125
+ console.error("Error listing devices:", err);
126
+ process.exit(1);
127
+ });
128
+ }
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { listAudioDevices } from "./list-devices.js";
4
+ import { spawn } from "child_process";
5
+ import readline from "node:readline";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const here = dirname(fileURLToPath(import.meta.url));
10
+
11
+ async function testDevice(deviceId) {
12
+ console.log(`\nTesting device ${deviceId}...`);
13
+ console.log("Speak into your microphone. Press Ctrl+C to stop.\n");
14
+
15
+ const voiceScript = join(here, "voice.js");
16
+ const proc = spawn("node", [voiceScript, undefined, deviceId], {
17
+ stdio: ["pipe", "inherit", "inherit"],
18
+ });
19
+
20
+ // Send test grammar
21
+ proc.stdin.write("update,hello,test,lumia\n");
22
+
23
+ return new Promise((resolve) => {
24
+ proc.on("close", () => {
25
+ resolve();
26
+ });
27
+
28
+ // Allow user to stop with Ctrl+C
29
+ process.on("SIGINT", () => {
30
+ proc.kill();
31
+ resolve();
32
+ });
33
+ });
34
+ }
35
+
36
+ async function main() {
37
+ console.log("LumiaWakeWord Audio Device Tester");
38
+ console.log("==================================\n");
39
+
40
+ try {
41
+ const devices = await listAudioDevices();
42
+
43
+ if (process.platform !== "win32") {
44
+ console.log("Note: On macOS/Linux, the default audio input is used automatically.");
45
+ console.log("Device selection is primarily for Windows users.\n");
46
+ }
47
+
48
+ console.log("Available audio input devices:");
49
+ devices.forEach((device) => {
50
+ console.log(` ${device.id}: ${device.name}`);
51
+ });
52
+
53
+ if (process.platform === "win32") {
54
+ const rl = readline.createInterface({
55
+ input: process.stdin,
56
+ output: process.stdout,
57
+ });
58
+
59
+ const deviceId = await new Promise((resolve) => {
60
+ rl.question(
61
+ "\nEnter device ID to test (or press Enter for default): ",
62
+ (answer) => {
63
+ rl.close();
64
+ resolve(answer.trim() || "0");
65
+ }
66
+ );
67
+ });
68
+
69
+ await testDevice(deviceId);
70
+
71
+ console.log("\n\nTo use this device permanently, you can:");
72
+ console.log(`1. Set environment variable: AUDIO_DEVICE=${deviceId}`);
73
+ console.log(`2. Pass as 3rd argument when running voice.js`);
74
+ console.log(`3. Update your application to pass the device ID\n`);
75
+ } else {
76
+ console.log("\nTesting default audio input...");
77
+ await testDevice(null);
78
+ }
79
+ } catch (err) {
80
+ console.error("Error:", err);
81
+ process.exit(1);
82
+ }
83
+ }
84
+
85
+ main();
package/lib/voice.js CHANGED
@@ -1,4 +1,3 @@
1
- // voice.js (ESM)
2
1
  import { Model, Recognizer, setLogLevel } from "vosk-koffi";
3
2
  import record from "./record.js";
4
3
  import { dirname, join } from "node:path";
@@ -18,16 +17,20 @@ function unpacked(p) {
18
17
  }
19
18
 
20
19
  /* ------------------------------------------------------------------ */
21
- /* 1. Resolve SoX binary */
20
+ /* 1. Resolve SoX binary and audio device */
22
21
  /* ------------------------------------------------------------------ */
23
22
  const exeName = { win32: "sox.exe", darwin: "soxmac", linux: "soxlinux" }[
24
23
  process.platform
25
24
  ];
26
25
 
27
- /* Priority: argv[2] → fallback to sibling binaries/<exe> */
26
+ /* Priority for sox path: argv[2] → fallback to sibling binaries/<exe> */
27
+ /* Priority for device: argv[3] → env var → default */
28
28
  let soxPath = process.argv[2] || join(here, "..", "binaries", exeName);
29
29
  soxPath = unpacked(soxPath);
30
30
 
31
+ // Parse device from argv[3] or environment variable
32
+ let audioDevice = process.argv[3] || process.env.AUDIO_DEVICE || null;
33
+
31
34
  if (!existsSync(soxPath)) throw new Error(`SoX not found: ${soxPath}`);
32
35
  try {
33
36
  chmodSync(soxPath, 0o755);
@@ -60,8 +63,17 @@ rec.setWords(true);
60
63
  /* 4. Start the microphone */
61
64
  /* ------------------------------------------------------------------ */
62
65
  const recArgs = { sampleRate: SAMPLE_RATE, threshold: 0, binPath: soxPath };
63
- if (process.platform === "win32") {
66
+
67
+ // Set device based on platform and configuration
68
+ if (audioDevice !== null) {
69
+ // User specified a device explicitly
70
+ recArgs.device = audioDevice;
71
+ console.error(`Using audio device: ${audioDevice}`);
72
+ } else if (process.platform === "win32") {
73
+ // Windows: default to device 0 if not specified
64
74
  recArgs.device = "0";
75
+ console.error("Using default Windows audio device: 0");
76
+ console.error("To specify a different device, use: AUDIO_DEVICE=<device_id> or pass as 3rd argument");
65
77
  }
66
78
 
67
79
  const mic = record.record(recArgs).stream();
@@ -123,7 +135,7 @@ const rl = readline.createInterface({ input: process.stdin, terminal: false });
123
135
 
124
136
  rl.on("line", (line) => {
125
137
  const trimmed = line.trim();
126
- if (!trimmed.startsWith("update,") || !trimmed.startsWith("confidence,"))
138
+ if (!trimmed.startsWith("update,") && !trimmed.startsWith("confidence,"))
127
139
  return;
128
140
 
129
141
  if (trimmed.startsWith("confidence,")) {
package/package.json CHANGED
@@ -1,15 +1,18 @@
1
1
  {
2
2
  "name": "@lumiastream/wakeword",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
- "main": "lib/voice.js",
5
+ "main": "lib/index.js",
6
6
  "files": [
7
7
  "lib/",
8
8
  "models/",
9
9
  "binaries/"
10
10
  ],
11
11
  "scripts": {
12
- "postinstall": "chmod +x binaries/soxmac binaries/soxlinux binaries/sox.exe || true"
12
+ "postinstall": "chmod +x binaries/soxmac binaries/soxlinux binaries/sox.exe || true",
13
+ "list-devices": "node lib/list-devices.js",
14
+ "test-devices": "node lib/test-devices.js",
15
+ "start": "node lib/voice.js"
13
16
  },
14
17
  "dependencies": {
15
18
  "vosk-koffi": "^1.1.1"