pi-voicekit 0.1.2 → 0.1.3
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/extensions/voice/audio-tool.ts +16 -0
- package/extensions/voice.ts +84 -75
- package/package.json +1 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture-tool preference.
|
|
3
|
+
*
|
|
4
|
+
* SoX' `rec` is the best default for local capture, but it is known to stall on
|
|
5
|
+
* network PulseAudio servers: over an SSH audio tunnel (`PULSE_SERVER` pointing at
|
|
6
|
+
* a forwarded TCP port, e.g. `tcp:127.0.0.1:4713`) the same `rec` invocation
|
|
7
|
+
* returned zero bytes in 40% of runs on this stack, while ffmpeg was reliable in
|
|
8
|
+
* every run. So when a remote Pulse server is configured, probe ffmpeg first and
|
|
9
|
+
* keep SoX as the fallback; local capture keeps SoX first.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type AudioToolName = "ffmpeg" | "sox" | "arecord";
|
|
13
|
+
|
|
14
|
+
export function audioToolOrder(env: { PULSE_SERVER?: string | undefined } = process.env): AudioToolName[] {
|
|
15
|
+
return env.PULSE_SERVER ? ["ffmpeg", "sox", "arecord"] : ["sox", "ffmpeg", "arecord"];
|
|
16
|
+
}
|
package/extensions/voice.ts
CHANGED
|
@@ -100,6 +100,7 @@ import {
|
|
|
100
100
|
} from "./voice/local";
|
|
101
101
|
import { shouldArmReleaseDetectOnRepeat, decideRecordingStartTimer } from "./voice/hold-to-talk";
|
|
102
102
|
import { GapTimer, type TimerPort } from "./voice/release-controller";
|
|
103
|
+
import { audioToolOrder, type AudioToolName } from "./voice/audio-tool";
|
|
103
104
|
|
|
104
105
|
/** Adapter for the real event loop — lets GapTimer run under the real setTimeout. */
|
|
105
106
|
const realTimerPort: TimerPort = {
|
|
@@ -245,87 +246,95 @@ interface AudioCaptureTool {
|
|
|
245
246
|
args: string[];
|
|
246
247
|
}
|
|
247
248
|
|
|
248
|
-
// Try available audio capture tools in order
|
|
249
|
+
// Try available audio capture tools in the order given by audioToolOrder()
|
|
249
250
|
let _cachedAudioTool: AudioCaptureTool | null | undefined;
|
|
250
|
-
function detectAudioCaptureTool(): AudioCaptureTool | null {
|
|
251
|
-
if (_cachedAudioTool !== undefined) return _cachedAudioTool;
|
|
252
251
|
|
|
253
|
-
|
|
254
|
-
if (commandExists("rec"))
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
252
|
+
function probeSox(): AudioCaptureTool | null {
|
|
253
|
+
if (!commandExists("rec")) return null;
|
|
254
|
+
return {
|
|
255
|
+
name: "sox",
|
|
256
|
+
cmd: "rec",
|
|
257
|
+
args: [
|
|
258
|
+
"-q",
|
|
259
|
+
"--buffer",
|
|
260
|
+
"4096",
|
|
261
|
+
"-c",
|
|
262
|
+
String(CHANNELS),
|
|
263
|
+
"-b",
|
|
264
|
+
"16",
|
|
265
|
+
"-e",
|
|
266
|
+
"signed-integer",
|
|
267
|
+
"-t",
|
|
268
|
+
"raw",
|
|
269
|
+
"-",
|
|
270
|
+
"rate",
|
|
271
|
+
String(SAMPLE_RATE),
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
277
275
|
|
|
278
|
-
|
|
279
|
-
if (commandExists("ffmpeg"))
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
} else {
|
|
296
|
-
inputArgs = ["-f", "pulse", "-i", "default"]; // fallback for other platforms
|
|
297
|
-
}
|
|
298
|
-
_cachedAudioTool = {
|
|
299
|
-
name: "ffmpeg",
|
|
300
|
-
cmd: "ffmpeg",
|
|
301
|
-
args: [
|
|
302
|
-
...inputArgs,
|
|
303
|
-
"-ac",
|
|
304
|
-
String(CHANNELS),
|
|
305
|
-
"-ar",
|
|
306
|
-
String(SAMPLE_RATE),
|
|
307
|
-
"-sample_fmt",
|
|
308
|
-
"s16",
|
|
309
|
-
"-f",
|
|
310
|
-
"s16le",
|
|
311
|
-
"-loglevel",
|
|
312
|
-
"error",
|
|
313
|
-
"pipe:1",
|
|
314
|
-
],
|
|
315
|
-
};
|
|
316
|
-
return _cachedAudioTool;
|
|
276
|
+
function probeFfmpeg(): AudioCaptureTool | null {
|
|
277
|
+
if (!commandExists("ffmpeg")) return null;
|
|
278
|
+
const isLinux = process.platform === "linux";
|
|
279
|
+
const isMac = process.platform === "darwin";
|
|
280
|
+
const isWin = process.platform === "win32";
|
|
281
|
+
// Input device varies by platform
|
|
282
|
+
let inputArgs: string[];
|
|
283
|
+
if (isMac) {
|
|
284
|
+
inputArgs = ["-f", "avfoundation", "-i", ":default"];
|
|
285
|
+
} else if (isLinux) {
|
|
286
|
+
inputArgs = ["-f", "pulse", "-i", "default"];
|
|
287
|
+
} else if (isWin) {
|
|
288
|
+
// DirectShow has no "default" alias — enumerate devices and pick the first audio device
|
|
289
|
+
const dshowDevice = detectWindowsAudioDevice();
|
|
290
|
+
inputArgs = dshowDevice ? ["-f", "dshow", "-i", `audio=${dshowDevice}`] : ["-f", "dshow", "-i", "audio=Microphone"]; // last-resort guess
|
|
291
|
+
} else {
|
|
292
|
+
inputArgs = ["-f", "pulse", "-i", "default"]; // fallback for other platforms
|
|
317
293
|
}
|
|
294
|
+
return {
|
|
295
|
+
name: "ffmpeg",
|
|
296
|
+
cmd: "ffmpeg",
|
|
297
|
+
args: [
|
|
298
|
+
...inputArgs,
|
|
299
|
+
"-ac",
|
|
300
|
+
String(CHANNELS),
|
|
301
|
+
"-ar",
|
|
302
|
+
String(SAMPLE_RATE),
|
|
303
|
+
"-sample_fmt",
|
|
304
|
+
"s16",
|
|
305
|
+
"-f",
|
|
306
|
+
"s16le",
|
|
307
|
+
"-loglevel",
|
|
308
|
+
"error",
|
|
309
|
+
"pipe:1",
|
|
310
|
+
],
|
|
311
|
+
};
|
|
312
|
+
}
|
|
318
313
|
|
|
319
|
-
|
|
320
|
-
if (process.platform
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
314
|
+
function probeArecord(): AudioCaptureTool | null {
|
|
315
|
+
if (process.platform !== "linux" || !commandExists("arecord")) return null;
|
|
316
|
+
return {
|
|
317
|
+
name: "arecord",
|
|
318
|
+
cmd: "arecord",
|
|
319
|
+
args: ["-q", "-f", "S16_LE", "-r", String(SAMPLE_RATE), "-c", String(CHANNELS), "-t", "raw"],
|
|
320
|
+
};
|
|
321
|
+
}
|
|
328
322
|
|
|
323
|
+
const audioProbes: Record<AudioToolName, () => AudioCaptureTool | null> = {
|
|
324
|
+
sox: probeSox,
|
|
325
|
+
ffmpeg: probeFfmpeg,
|
|
326
|
+
arecord: probeArecord,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
function detectAudioCaptureTool(): AudioCaptureTool | null {
|
|
330
|
+
if (_cachedAudioTool !== undefined) return _cachedAudioTool;
|
|
331
|
+
for (const name of audioToolOrder()) {
|
|
332
|
+
const tool = audioProbes[name]();
|
|
333
|
+
if (tool) {
|
|
334
|
+
_cachedAudioTool = tool;
|
|
335
|
+
return tool;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
329
338
|
_cachedAudioTool = null;
|
|
330
339
|
return null;
|
|
331
340
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-voicekit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Voice in + voice out for Pi CLI — hold-to-talk STT (Deepgram streaming or 21 offline models) plus TTS (Kitten Nano, Piper, Kokoro, or Deepgram Aura)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|