pi-voicekit 0.1.2 → 0.1.4

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
@@ -6,32 +6,30 @@
6
6
  > Not affiliated with the original author. Old name: `pi-listen`.
7
7
 
8
8
  <p align="center">
9
- <img src="https://raw.githubusercontent.com/CyFeng16/pi-voicekit/main/assets/banner.png" alt="pi-listen — Voice input for the Pi coding agent" width="100%" />
9
+ <img src="https://raw.githubusercontent.com/CyFeng16/pi-voicekit/main/assets/brand/banner-en.png" alt="pi-voicekit — Voice input and output for the Pi coding agent" width="100%" />
10
10
  </p>
11
11
 
12
- **Hold-to-talk voice input for [Pi](https://github.com/earendil-works/pi-coding-agent).** Cloud streaming via Deepgram or fully offline with local models.
12
+ **Voice in and voice out for [Pi](https://github.com/earendil-works/pi-coding-agent).**
13
+ Hold-to-talk STT — Deepgram streaming (cloud) or 21 offline models — plus TTS that
14
+ speaks the agent's replies (Kitten, Kokoro, Piper, or Deepgram Aura).
13
15
 
14
16
  [![npm version](https://img.shields.io/npm/v/pi-voicekit.svg)](https://www.npmjs.com/package/pi-voicekit)
15
17
  [![license](https://img.shields.io/npm/l/pi-voicekit.svg)](https://github.com/CyFeng16/pi-voicekit/blob/main/LICENSE)
16
18
  [![original author](https://img.shields.io/badge/original_author-@baanditeagle-1DA1F2?logo=x&logoColor=white)](https://x.com/baanditeagle)
17
19
 
18
- > **v7.0.0 — World-class TTS UX** — pick models from `/voice-settings` Speak
19
- > tab (no more JSON editing), auto-download on selection with progress, voice
20
- > picker for every backend, first-run onboarding with smart-default
21
- > recommendation by your system locale, and `ttsAutoSpeak: true` finally
22
- > works — auto-speaks the agent's responses with code-block stripping and
23
- > rate limiting. Diagnostic command `/voice-speak-info` shows everything.
24
- > Resume-on-interrupt downloads. Plus all v6 features (14 local models from
25
- > 25 MB Kitten Nano up, Deepgram Aura cloud, region-strict language matching,
26
- > sentence-aware chunking). [Full changelog →](CHANGELOG.md)
20
+ > **v0.1.3 — current release** — audio capture prefers `ffmpeg` when
21
+ > `PULSE_SERVER` is set (SSH audio tunnel / remote PulseAudio), so remote
22
+ > microphones record reliably. Voice in **and** voice out: 21 offline STT models,
23
+ > 20 local TTS voices plus Deepgram Aura, driven by one `/voice-settings` panel
24
+ > with 5 tabs. The 0.1.x line is documented in the [changelog](CHANGELOG.md).
27
25
 
28
26
  ---
29
27
 
30
28
  ## See How It Works
31
29
 
32
30
  <p align="center">
33
- <a href="https://github.com/CyFeng16/pi-voicekit/blob/main/assets/pi-listen.mp4">
34
- <img src="https://raw.githubusercontent.com/CyFeng16/pi-voicekit/main/assets/banner.png" alt="Watch demo video" width="600" />
31
+ <a href="https://github.com/CyFeng16/pi-voicekit/blob/main/assets/demo/pi-voicekit-demo.mp4">
32
+ <img src="https://raw.githubusercontent.com/CyFeng16/pi-voicekit/main/assets/brand/banner-en.png" alt="Watch demo video" width="600" />
35
33
  </a>
36
34
  <br>
37
35
  <em>Click to watch the demo video</em>
@@ -50,7 +48,7 @@ pi install npm:pi-voicekit
50
48
 
51
49
  ### 2. Choose your backend
52
50
 
53
- pi-listen supports two transcription backends:
51
+ pi-voicekit supports two transcription backends:
54
52
 
55
53
  | | Deepgram (cloud) | Local models (offline) |
56
54
  | ---------------- | -------------------------------------------------------- | --------------------------------------------------- |
@@ -79,7 +77,7 @@ No setup needed — run `/voice-settings`, switch backend to Local, and select a
79
77
 
80
78
  ### 3. Open Pi
81
79
 
82
- On first launch, pi-listen checks your setup and tells you what's ready:
80
+ On first launch, pi-voicekit checks your setup and tells you what's ready:
83
81
 
84
82
  - Backend configured (Deepgram key or local model)
85
83
  - Audio capture tool detected (sox, ffmpeg, or arecord)
@@ -87,7 +85,7 @@ On first launch, pi-listen checks your setup and tells you what's ready:
87
85
 
88
86
  ### Audio capture
89
87
 
90
- pi-listen auto-detects your audio tool. No manual install needed if you already have sox or ffmpeg.
88
+ pi-voicekit auto-detects your audio tool. No manual install needed if you already have sox or ffmpeg.
91
89
 
92
90
  | Priority | Tool | Platforms | Install |
93
91
  | -------- | --------------- | --------------------- | ------------------------------------------------------------ |
@@ -95,33 +93,42 @@ pi-listen auto-detects your audio tool. No manual install needed if you already
95
93
  | 2 | **ffmpeg** | macOS, Linux, Windows | `brew install ffmpeg` / `apt install ffmpeg` |
96
94
  | 3 | **arecord** | Linux only | Pre-installed (ALSA) |
97
95
 
96
+ > When `PULSE_SERVER` is set (SSH audio tunnel or remote PulseAudio) the order
97
+ > becomes **ffmpeg → sox → arecord** — network Pulse sources need ffmpeg.
98
+
98
99
  ---
99
100
 
100
101
  ## Settings Panel
101
102
 
102
- All configuration lives in one place: `/voice-settings`. Four tabs cover everything you need.
103
+ All configuration lives in one place: `/voice-settings`. Five tabs cover everything you need.
103
104
 
104
105
  ### General — backend, language, scope
105
106
 
106
- <img src="https://raw.githubusercontent.com/CyFeng16/pi-voicekit/main/assets/settings-general.png" alt="General settings — backend, model, language, scope, voice toggle" width="600" />
107
+ <img src="https://raw.githubusercontent.com/CyFeng16/pi-voicekit/main/assets/screenshots/settings-general.png" alt="General settings — backend, model, language, scope, voice toggle" width="600" />
107
108
 
108
109
  Toggle between Deepgram (cloud, live streaming) and Local (offline, batch mode). Change language, scope, and enable/disable voice — all with keyboard shortcuts.
109
110
 
110
111
  ### Models — browse, search, install
111
112
 
112
- <img src="https://raw.githubusercontent.com/CyFeng16/pi-voicekit/main/assets/settings-models.png" alt="Models tab — browse 21 models with accuracy/speed ratings" width="600" />
113
+ <img src="https://raw.githubusercontent.com/CyFeng16/pi-voicekit/main/assets/screenshots/settings-models.png" alt="Models tab — browse 21 models with accuracy/speed ratings" width="600" />
113
114
 
114
115
  Browse 21 models from Parakeet, Whisper, Moonshine, SenseVoice, GigaAM, Paraformer, and Qwen3. Each model shows accuracy and speed ratings (●●●●○/●●●●○), fitness badges, and download status. Fuzzy search to find models fast. Press Enter to activate and download.
115
116
 
116
117
  ### Downloaded — manage installed models
117
118
 
118
- <img src="https://raw.githubusercontent.com/CyFeng16/pi-voicekit/main/assets/settings-downloaded.png" alt="Downloaded tab — manage installed models, activate or delete" width="600" />
119
+ <img src="https://raw.githubusercontent.com/CyFeng16/pi-voicekit/main/assets/screenshots/settings-downloaded.png" alt="Downloaded tab — manage installed models, activate or delete" width="600" />
119
120
 
120
121
  See what's installed, total disk usage, and which model is active. Press Enter to activate, `x` to delete. Models from [Handy](https://github.com/cjpais/handy) are auto-detected and can be imported without re-downloading.
121
122
 
123
+ ### Speak — TTS models and voices
124
+
125
+ Pick a TTS backend (local sherpa-onnx or Deepgram Aura), browse 20 local voices
126
+ from ~13 MB, download on selection, and choose a voice per backend. Auto-speak of
127
+ agent replies is toggled here.
128
+
122
129
  ### Device — hardware profile and dependencies
123
130
 
124
- <img src="https://raw.githubusercontent.com/CyFeng16/pi-voicekit/main/assets/settings-device.png" alt="Device tab — hardware profile, dependencies, disk space" width="600" />
131
+ <img src="https://raw.githubusercontent.com/CyFeng16/pi-voicekit/main/assets/screenshots/settings-device.png" alt="Device tab — hardware profile, dependencies, disk space" width="600" />
125
132
 
126
133
  See your hardware profile (RAM, CPU, GPU), dependency status (sherpa-onnx runtime), available disk space, and total downloaded models. Model recommendations are based on this profile.
127
134
 
@@ -133,7 +140,7 @@ See your hardware profile (RAM, CPU, GPU), dependency status (sherpa-onnx runtim
133
140
 
134
141
  | Action | Key | Notes |
135
142
  | -------------------- | -------------------- | ----------------------------------------------------------------------- |
136
- | **Record to editor** | Hold `SPACE` (≥1.2s) | Release to finalize. Pre-records during warmup so you don't miss words. |
143
+ | **Record to editor** | Hold `SPACE` (≥0.7s) | Release to finalize. Pre-records during warmup so you don't miss words. |
137
144
  | **Toggle recording** | `Ctrl+Shift+V` | Works in all terminals — press to start, press again to stop. |
138
145
  | **Clear editor** | `Escape` × 2 | Double-tap within 500ms to clear all text. |
139
146
 
@@ -150,10 +157,15 @@ See your hardware profile (RAM, CPU, GPU), dependency status (sherpa-onnx runtim
150
157
  | ------------------------ | --------------------------------------------------------- |
151
158
  | `/voice-settings` | Settings panel — backend, models, language, scope, device |
152
159
  | `/voice-models` | Settings panel (Models tab) |
160
+ | `/voice-setup` | Run the first-run setup wizard |
161
+ | `/voice-language` | Open the settings panel to change language |
153
162
  | `/voice-speak <text>` | Speak text out loud (TTS) |
154
163
  | `/voice-speak-test` | Speak a sample sentence |
155
164
  | `/voice-speak-toggle` | Enable / disable TTS |
156
- | `/voice-autosubmit` `[on | off]` | Toggle: STT text auto-sent to the agent |
165
+ | `/voice-stream` | Toggle Deepgram streaming TTS (cloud) |
166
+ | `/voice-speak-stop` | Stop in-flight TTS playback |
167
+ | `/voice-autosubmit` | Toggle: STT text auto-sent to the agent (`on`/`off`) |
168
+ | `/voice-hold-delay` | Set hold-to-talk delay (200-3000 ms, default 700) |
157
169
  | `/voice-speak-models` | Browse / install TTS voice models |
158
170
  | `/voice-speak-info` | Diagnose TTS state |
159
171
  | `/voice-help` | Keyboard + command reference (or press `F1`) |
@@ -245,10 +257,10 @@ Models from [Handy](https://github.com/cjpais/handy) (`~/Library/Application Sup
245
257
  | **Device-aware recommendations** | Scores models against your hardware. Only best-in-class models get [recommended]. |
246
258
  | **Enterprise download pipeline** | Pre-checks (disk, network, permissions), live progress with speed/ETA, post-verification |
247
259
  | **Handy integration** | Auto-detects models from Handy app, imports via symlink |
248
- | **Audio fallback chain** | Tries sox, ffmpeg, arecord in order |
260
+ | **Audio fallback chain** | Tries sox → ffmpeg → arecord in order — ffmpeg first when `PULSE_SERVER` is set |
249
261
  | **Pre-recording** | Audio capture starts during warmup — you never miss the first word |
250
262
  | **Tail recording** | Keeps recording 1.5s after release so your last word isn't clipped |
251
- | **Live streaming** | Deepgram Nova 3 WebSocket — interim transcripts as you speak |
263
+ | **Live streaming** | Deepgram Nova 3 WebSocket (Nova 2 for Chinese locales) — live interim transcripts |
252
264
  | **56+ languages** | Deepgram: 56+ with live streaming. Local: up to 57 depending on model. |
253
265
  | **Continuous dictation** | `/voice dictate` for long-form input without holding keys |
254
266
  | **Typing cooldown** | Space holds within 400ms of typing are ignored |
@@ -260,15 +272,47 @@ Models from [Handy](https://github.com/cjpais/handy) (`~/Library/Application Sup
260
272
  ## Architecture
261
273
 
262
274
  ```
263
- extensions/voice.ts Main extension — state machine, recording, UI, settings panel
264
- extensions/voice/config.ts Config loading, saving, migration
265
- extensions/voice/onboarding.ts First-run wizard, language picker
266
- extensions/voice/deepgram.ts Deepgram URL builder, API key resolver
267
- extensions/voice/local.ts Model catalog (21 models), in-process transcription
268
- extensions/voice/device.ts Device profiling — RAM, GPU, CPU, container detection
269
- extensions/voice/model-download.ts Download manager — resume, progress, verification, Handy import
270
- extensions/voice/sherpa-engine.ts sherpa-onnx bindings — recognizer lifecycle, inference
271
- extensions/voice/settings-panel.ts Settings panel — Component interface, overlay, 4 tabs
275
+ # core
276
+ extensions/voice.ts Main extension — state machine, recording, UI, command surface
277
+ extensions/voice/config.ts Config loading, saving, migration
278
+ extensions/voice/onboarding.ts First-run wizard, language picker
279
+ extensions/voice/audio-tool.ts Capture tool detection (sox / ffmpeg / arecord)
280
+ extensions/voice/hold-to-talk.ts Hold detection, Kitty and non-Kitty terminals
281
+ extensions/voice/release-controller.ts Recording lifecycle, release handling
282
+
283
+ # speech-to-text
284
+ extensions/voice/deepgram.ts Deepgram URL builder, API key resolver
285
+ extensions/voice/local.ts Model catalog (21 models), in-process transcription
286
+ extensions/voice/sherpa-engine.ts sherpa-onnx bindings — recognizer lifecycle, inference
287
+ extensions/voice/sherpa-loader.ts Lazy native module loading
288
+ extensions/voice/model-download.ts Download manager — resume, progress, verification, Handy import
289
+ extensions/voice/device.ts Device profiling — RAM, GPU, CPU, container detection
290
+
291
+ # text-to-speech
292
+ extensions/voice/speak.ts Speak entry point, auto-speak wiring
293
+ extensions/voice/tts-engine.ts sherpa-onnx TTS synthesis
294
+ extensions/voice/tts-deepgram.ts Deepgram Aura voices (cloud)
295
+ extensions/voice/tts-local-models.ts Local TTS catalog — 20 voices (Kitten, Kokoro, Piper)
296
+ extensions/voice/tts-playback.ts Playback, buffering, player detection
297
+ extensions/voice/tts-text-filter.ts Code-block stripping, sentence prep
298
+ extensions/voice/tts-onboarding.ts TTS onboarding flow
299
+ extensions/voice/tts-onboarding-overlay.ts TTS onboarding overlay
300
+ extensions/voice/tts-install-progress.ts Model install progress widget
301
+ extensions/voice/tts-playback-indicator.ts Speaking indicator widget
302
+
303
+ # settings and UI
304
+ extensions/voice/settings-panel.ts Settings panel — overlay, 5 tabs
305
+ extensions/voice/ui-picker.ts Generic list picker
306
+ extensions/voice/ui-help-overlay.ts Keyboard and command reference
307
+ extensions/voice/ui-aura.ts Visual primitives (Liquid Braille, Aurora)
308
+ extensions/voice/ui-widget-base.ts Widget registry and base class
309
+ extensions/voice/ui-render-ticker.ts Shared render ticker
310
+ extensions/voice/ui-icons.ts Glyph and icon set
311
+ extensions/voice/ui-width.ts CJK-aware visual width helpers
312
+ extensions/voice/ui-locale-labels.ts Native language and voice labels
313
+
314
+ # types
315
+ extensions/voice/sherpa-onnx-node.d.ts Type declarations for the optional native module
272
316
  ```
273
317
 
274
318
  ---
@@ -300,6 +344,8 @@ Settings stored in Pi's settings files under the `voice` key:
300
344
  into `~/.pi/agent/settings.json`. If you paste a key during onboarding, that is
301
345
  an explicit save and it still goes to `~/.env.secrets` or `~/.zshrc`.
302
346
 
347
+ Hold-to-talk delay defaults to **700 ms** (`/voice-hold-delay` accepts 200–3000 ms).
348
+
303
349
  ---
304
350
 
305
351
  ## Troubleshooting
@@ -310,6 +356,7 @@ Run `/voice test` inside Pi for full diagnostics.
310
356
  | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
311
357
  | "DEEPGRAM_API_KEY not set" | [Get a key](https://dpgr.am/pi-voice) → `export DEEPGRAM_API_KEY="..."` in `~/.zshrc` |
312
358
  | "No audio capture tool found" | `brew install sox` or `brew install ffmpeg` |
359
+ | Remote microphone records silence | Audio over PulseAudio/SSH — install ffmpeg on the Pi side (capture then prefers ffmpeg) |
313
360
  | Space doesn't activate voice | Run `/voice-settings` — voice may be disabled |
314
361
  | Local model not transcribing | Check `/voice-settings` → Device tab for sherpa-onnx status |
315
362
  | Download failed | Partial downloads auto-resume on retry. Check disk space in Device tab. |
@@ -321,7 +368,7 @@ Run `/voice test` inside Pi for full diagnostics.
321
368
 
322
369
  - **Cloud STT** — audio is sent to Deepgram for transcription (Deepgram backend only)
323
370
  - **Local STT** — audio never leaves your machine (local backend)
324
- - **No telemetry** — pi-listen does not collect or transmit usage data
371
+ - **No telemetry** — pi-voicekit does not collect or transmit usage data
325
372
  - **API key** — stored in env var or Pi settings, never logged
326
373
 
327
374
  See [SECURITY.md](SECURITY.md) for vulnerability reporting.
@@ -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
+ }
@@ -150,7 +150,7 @@ export function readJsonFile(filePath: string): Record<string, unknown> {
150
150
  return JSON.parse(fs.readFileSync(filePath, "utf8"));
151
151
  } catch (err) {
152
152
  process.stderr.write(
153
- `[pi-voice] Warning: failed to read ${filePath}: ${err instanceof Error ? err.message : err}\n`
153
+ `[pi-voicekit] Warning: failed to read ${filePath}: ${err instanceof Error ? err.message : err}\n`
154
154
  );
155
155
  return {};
156
156
  }
@@ -303,7 +303,7 @@ export function loadGlobalToggleShortcut(options: ConfigPathOptions = {}): strin
303
303
  const candidate = (globalVoice as any).toggleShortcut;
304
304
  if (isValidShortcut(candidate)) return candidate;
305
305
  process.stderr.write(
306
- `[pi-voice] Warning: invalid toggleShortcut "${candidate}" in settings, using default "${fallback}"\n`
306
+ `[pi-voicekit] Warning: invalid toggleShortcut "${candidate}" in settings, using default "${fallback}"\n`
307
307
  );
308
308
  }
309
309
  } catch {
@@ -314,7 +314,7 @@ export function finalizeOnboardingConfig(
314
314
  }
315
315
 
316
316
  export async function promptFirstRunOnboarding(ctx: VoiceUiContext): Promise<FirstRunDecision> {
317
- const choice = await ctx.ui.select("Set up pi-voice now?", ["Start voice setup", "Remind me later"]);
317
+ const choice = await ctx.ui.select("Set up pi-voicekit now?", ["Start voice setup", "Remind me later"]);
318
318
 
319
319
  return { action: choice === "Start voice setup" ? "start" : "later" };
320
320
  }
@@ -665,7 +665,7 @@ export async function runVoiceOnboarding(
665
665
  }
666
666
 
667
667
  // ─── Choose scope ────────────────────────────────────────
668
- const scopeChoice = await ctx.ui.select("Where should pi-voice settings be saved?", [
668
+ const scopeChoice = await ctx.ui.select("Where should pi-voicekit settings be saved?", [
669
669
  "Global (all projects)",
670
670
  "Project only (this repo)",
671
671
  ]);
@@ -193,7 +193,7 @@ export class VoiceSettingsPanel {
193
193
  // commands remain available (panel-less).
194
194
  if (isPanelTooNarrow(width)) {
195
195
  return [
196
- ` ${this.bold("pi-listen")}`,
196
+ ` ${this.bold("pi-voicekit")}`,
197
197
  ` ${this.warning(`Terminal too narrow ${ICON.middot} resize to ${ICON.arrowRight} 60 cols`)}`,
198
198
  ` ${this.dim("Slash commands still work: /voice-speak, /voice-models, /voice-settings")}`,
199
199
  ];
@@ -212,7 +212,7 @@ export class VoiceSettingsPanel {
212
212
  // backend/model/voice + active language. Width-tier aware:
213
213
  // at "mid" (60-79) the row is trimmed; at "wide" (≥80) it
214
214
  // shows the full picture.
215
- lines.push(t(` ${this.bold("pi-listen")} ${this.dim(this.p.formatDeviceSummary(device))}`));
215
+ lines.push(t(` ${this.bold("pi-voicekit")} ${this.dim(this.p.formatDeviceSummary(device))}`));
216
216
  lines.push(t(" " + this.renderStatusRow(widthTier(width) === "mid")));
217
217
  lines.push(t(this.dim(" " + "─".repeat(Math.min(iw, 60)))));
218
218
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Local TTS model catalog for pi-listen v6.0.0+.
2
+ * Local TTS model catalog for pi-voicekit.
3
3
  *
4
4
  * Three tiers:
5
5
  * - Tier 0 (default): Kitten Nano v0.2 — 25.4 MB, 8 voices, English. The
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * Visual design (no emoji per v7.1 hard constraint):
19
19
  *
20
- * ┌─ pi-listen TTS ─────────────────────────────────────────────┐
20
+ * ┌─ pi-voicekit TTS ─────────────────────────────────────────────┐
21
21
  * │ │
22
22
  * │ Voice output ready. │
23
23
  * │ │
@@ -90,7 +90,11 @@ export class TtsOnboardingOverlay {
90
90
  // v7.2: rounded corners for modal feel. Title sits inline on the
91
91
  // top edge with thin spacing so the box reads as a "sheet"
92
92
  // rather than a hard frame (HIG modal aesthetic).
93
- const top = `${ICON.boxRoundedTL}${ICON.boxH.repeat(2)} ${bold("pi-listen TTS")} ${ICON.boxH.repeat(Math.max(0, innerW - 16))}${ICON.boxRoundedTR}`;
93
+ // Title sits inline on the top edge; the pad is derived from the title's
94
+ // *visual* width so the frame stays exactly as wide as the body/bottom
95
+ // rows (brand length is data, not a magic constant).
96
+ const title = "pi-voicekit TTS";
97
+ const top = `${ICON.boxRoundedTL}${ICON.boxH.repeat(2)} ${bold(title)} ${ICON.boxH.repeat(Math.max(0, innerW - visualWidth(title) - 4))}${ICON.boxRoundedTR}`;
94
98
  const bottom = `${ICON.boxRoundedBL}${ICON.boxH.repeat(innerW)}${ICON.boxRoundedBR}`;
95
99
  const hr = `${ICON.boxV}${" ".repeat(innerW)}${ICON.boxV}`;
96
100
  const row = (s: string): string => {
@@ -147,7 +151,7 @@ export class TtsOnboardingOverlay {
147
151
  const dim = (s: string) => (t ? t.fg("dim", s) : s);
148
152
  const accent = (s: string) => (t ? t.fg("accent", s) : s);
149
153
  return [
150
- ` ${accent("pi-listen TTS")}`,
154
+ ` ${accent("pi-voicekit TTS")}`,
151
155
  ` ${dim("Voice output ready. Resize to ≥60 cols for the full hint.")}`,
152
156
  ` ${accent("[↵]")} ${dim("test")} ${accent("[m]")} ${dim("pick")} ${accent("[esc]")} ${dim("skip")}`,
153
157
  ];
@@ -210,7 +210,7 @@ export interface OpenPlaybackStreamOpts {
210
210
  * Open a streaming playback sink. Returns `null` when no streaming-capable
211
211
  * player is found on PATH — the caller should fall back to the file-based
212
212
  * `play()` path. Player priority: `sox` (preferred — works on macOS via
213
- * homebrew, Linux via apt/yum, ships with most pi-listen STT installs) →
213
+ * homebrew, Linux via apt/yum, ships with most pi-voicekit STT installs) →
214
214
  * `paplay` (Linux PulseAudio) → null.
215
215
  *
216
216
  * Windows is intentionally unsupported here — PowerShell SoundPlayer
@@ -464,7 +464,7 @@ function pickStreamingPlayer(sampleRate: number): StreamingPlayerSpec | null {
464
464
  if (process.platform === "linux" && binaryAvailable("paplay")) {
465
465
  return {
466
466
  cmd: "paplay",
467
- args: ["--raw", `--rate=${sampleRate}`, "--format=s16le", "--channels=1", "--client-name=pi-listen"],
467
+ args: ["--raw", `--rate=${sampleRate}`, "--format=s16le", "--channels=1", "--client-name=pi-voicekit"],
468
468
  };
469
469
  }
470
470
  // sox last-resort: cross-platform but has the macOS CoreAudio
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * "Auroral" visual language — v7.2 world-class polish.
3
3
  *
4
- * Three primitives that elevate pi-listen's visual identity from
4
+ * Three primitives that elevate pi-voicekit's visual identity from
5
5
  * "minimal CLI" to "premium application":
6
6
  *
7
7
  * 1. Liquid Braille waveform — sub-cell vertical bars at 4-level
@@ -103,7 +103,7 @@ export class HelpOverlay {
103
103
  const w = Math.max(60, Math.min(width - 2, 90));
104
104
  const lines: string[] = [];
105
105
  lines.push(
106
- ` ${bold("pi-listen")} ${dim(ICON.middot)} ${bold("Help")} ${dim(`${ICON.middot} press [esc] to close`)}`
106
+ ` ${bold("pi-voicekit")} ${dim(ICON.middot)} ${bold("Help")} ${dim(`${ICON.middot} press [esc] to close`)}`
107
107
  );
108
108
  lines.push(` ${dim(ICON.boxH.repeat(Math.min(w, 60)))}`);
109
109
  for (const sec of HELP_SECTIONS) {
@@ -127,7 +127,7 @@ export class HelpOverlay {
127
127
  const dim = (s: string) => (t ? t.fg("dim", s) : s);
128
128
  const accent = (s: string) => (t ? t.fg("accent", s) : s);
129
129
  const lines: string[] = [];
130
- lines.push(` ${accent("pi-listen Help")}`);
130
+ lines.push(` ${accent("pi-voicekit Help")}`);
131
131
  for (const sec of HELP_SECTIONS) {
132
132
  lines.push("");
133
133
  lines.push(` ${accent(sec.heading)}`);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Native-script + gender labels for the Voice picker (§8 of v7.1 plan).
3
3
  *
4
- * Hand-curated for the languages pi-listen ships voices for, using the
4
+ * Hand-curated for the languages pi-voicekit ships voices for, using the
5
5
  * BCP-47 base-language tag (not script/region) as the lookup key. Three
6
6
  * intentional omissions:
7
7
  *
@@ -8,7 +8,7 @@
8
8
  * two halves of nothing.
9
9
  *
10
10
  * Hand-curated EAW Wide/Fullwidth ranges from Unicode 15.1 EastAsianWidth.txt
11
- * — only the blocks pi-listen actually ships labels for (CJK, Hangul,
11
+ * — only the blocks pi-voicekit actually ships labels for (CJK, Hangul,
12
12
  * Hiragana/Katakana, fullwidth ASCII). Hindi/Devanagari and Arabic
13
13
  * intentionally NOT covered: per the v7.1 plan their voices are rendered
14
14
  * with romanized labels, so a precise width here is unnecessary and a
@@ -1,5 +1,5 @@
1
1
  /**
2
- * pi-voice — Enterprise-grade voice STT for Pi CLI.
2
+ * pi-voicekit — Enterprise-grade voice STT for Pi CLI.
3
3
  *
4
4
  * Architecture (modeled after Claude Code's voice pipeline):
5
5
  *
@@ -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 of preference
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
- // 1. SoX rec — purpose-built for recording, best quality
254
- if (commandExists("rec")) {
255
- _cachedAudioTool = {
256
- name: "sox",
257
- cmd: "rec",
258
- args: [
259
- "-q",
260
- "--buffer",
261
- "4096",
262
- "-c",
263
- String(CHANNELS),
264
- "-b",
265
- "16",
266
- "-e",
267
- "signed-integer",
268
- "-t",
269
- "raw",
270
- "-",
271
- "rate",
272
- String(SAMPLE_RATE),
273
- ],
274
- };
275
- return _cachedAudioTool;
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
- // 2. ffmpeg — widely installed, captures from default mic
279
- if (commandExists("ffmpeg")) {
280
- const isLinux = process.platform === "linux";
281
- const isMac = process.platform === "darwin";
282
- const isWin = process.platform === "win32";
283
- // Input device varies by platform
284
- let inputArgs: string[];
285
- if (isMac) {
286
- inputArgs = ["-f", "avfoundation", "-i", ":default"];
287
- } else if (isLinux) {
288
- inputArgs = ["-f", "pulse", "-i", "default"];
289
- } else if (isWin) {
290
- // DirectShow has no "default" alias — enumerate devices and pick the first audio device
291
- const dshowDevice = detectWindowsAudioDevice();
292
- inputArgs = dshowDevice
293
- ? ["-f", "dshow", "-i", `audio=${dshowDevice}`]
294
- : ["-f", "dshow", "-i", "audio=Microphone"]; // last-resort guess
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
- // 3. arecord — built into Linux ALSA, zero install
320
- if (process.platform === "linux" && commandExists("arecord")) {
321
- _cachedAudioTool = {
322
- name: "arecord",
323
- cmd: "arecord",
324
- args: ["-q", "-f", "S16_LE", "-r", String(SAMPLE_RATE), "-c", String(CHANNELS), "-t", "raw"],
325
- };
326
- return _cachedAudioTool;
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
  }
@@ -2270,7 +2279,7 @@ export default function (pi: ExtensionAPI) {
2270
2279
  // actually loaded after a `pi install .` (path-installed
2271
2280
  // extensions cache modules across `pi install` reinstalls — only
2272
2281
  // a fresh `pi` process picks up source changes).
2273
- voiceDebug("pi-listen v7.1.3 loaded", { reason });
2282
+ voiceDebug("pi-voicekit loaded", { reason });
2274
2283
 
2275
2284
  // Migration / setup runs on EVERY session_start, regardless of reason.
2276
2285
  // Only the first-run notification is gated on isStartup.
@@ -2314,7 +2323,7 @@ export default function (pi: ExtensionAPI) {
2314
2323
  ? `Local model: ${LOCAL_MODELS.find((m) => m.id === config.localModel)?.name || config.localModel} (offline, batch mode)`
2315
2324
  : "Deepgram Nova-3 (cloud, live streaming)";
2316
2325
  const lines = [
2317
- "pi-listen ready!",
2326
+ "pi-voicekit ready!",
2318
2327
  "",
2319
2328
  " Hold SPACE to record → release to transcribe",
2320
2329
  ` ${toggleShortcutLabel} to toggle recording`,
@@ -2330,7 +2339,7 @@ export default function (pi: ExtensionAPI) {
2330
2339
  // No backend configured — show install hint only on actual startup.
2331
2340
  if (!isStartup) return;
2332
2341
  const lines = [
2333
- "pi-listen installed — voice input for Pi",
2342
+ "pi-voicekit installed — voice input for Pi",
2334
2343
  "",
2335
2344
  " Two backends available:",
2336
2345
  " • Deepgram — cloud, live streaming, $200 free credit (6–12 months of use)",
@@ -2946,27 +2955,27 @@ export default function (pi: ExtensionAPI) {
2946
2955
  // ─── /voice-setup → redirects to settings panel ─────────────────────────
2947
2956
 
2948
2957
  pi.registerCommand("voice-setup", {
2949
- description: "Open pi-listen settings panel",
2958
+ description: "Open pi-voicekit settings panel",
2950
2959
  handler: async (_args, cmdCtx) => openSettingsPanel(cmdCtx),
2951
2960
  });
2952
2961
 
2953
2962
  // ─── /voice-language → redirects to settings panel ───────────────────────
2954
2963
 
2955
2964
  pi.registerCommand("voice-language", {
2956
- description: "Open pi-listen settings to change language",
2965
+ description: "Open pi-voicekit settings to change language",
2957
2966
  handler: async (_args, cmdCtx) => openSettingsPanel(cmdCtx),
2958
2967
  });
2959
2968
 
2960
2969
  // ─── /voice-help → v7.1 §11 keyboard / command reference ────────────────
2961
2970
 
2962
2971
  pi.registerCommand("voice-help", {
2963
- description: "Show pi-listen keyboard + command reference",
2972
+ description: "Show pi-voicekit keyboard + command reference",
2964
2973
  handler: async (_args, cmdCtx) => openHelpOverlay(cmdCtx),
2965
2974
  });
2966
2975
 
2967
2976
  async function openHelpOverlay(cmdCtx: ExtensionCommandContext): Promise<void> {
2968
2977
  if (!cmdCtx.hasUI) {
2969
- cmdCtx.ui.notify("pi-listen: hold space=record · /voice-speak <text> · /voice-settings · /voice-help", "info");
2978
+ cmdCtx.ui.notify("pi-voicekit: hold space=record · /voice-speak <text> · /voice-settings · /voice-help", "info");
2970
2979
  return;
2971
2980
  }
2972
2981
  const { HelpOverlay } = await import("./voice/ui-help-overlay");
@@ -3628,10 +3637,10 @@ export default function (pi: ExtensionAPI) {
3628
3637
  handler: async (_args, cmdCtx) => openSettingsPanel(cmdCtx, 3),
3629
3638
  });
3630
3639
 
3631
- // ─── /voice-settings — unified pi-listen settings panel ─────────────
3640
+ // ─── /voice-settings — unified pi-voicekit settings panel ─────────────
3632
3641
 
3633
3642
  pi.registerCommand("voice-settings", {
3634
- description: "Open pi-listen settings — backend, models, language, device",
3643
+ description: "Open pi-voicekit settings — backend, models, language, device",
3635
3644
  handler: async (_args, cmdCtx) => openSettingsPanel(cmdCtx),
3636
3645
  });
3637
3646
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-voicekit",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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": [
@@ -46,10 +46,9 @@
46
46
  "typecheck": "bunx tsc -p tsconfig.json",
47
47
  "test": "bun test",
48
48
  "check": "bun run typecheck && bun run test",
49
- "format": "bunx prettier@3.3.3 --write \"**/*.{ts,json,yml,yaml}\"",
50
- "format:check": "bunx prettier@3.3.3 --check \"**/*.{ts,json,yml,yaml}\"",
51
- "release:dry": "bun run check && bun publish --dry-run",
52
- "release": "bun run check && bun publish --access public"
49
+ "format": "prettier --write \"**/*.{ts,json,yml,yaml}\"",
50
+ "format:check": "prettier --check \"**/*.{ts,json,yml,yaml}\"",
51
+ "release:dry": "bun run check && bun publish --dry-run"
53
52
  },
54
53
  "peerDependencies": {
55
54
  "@earendil-works/pi-coding-agent": "*",
@@ -58,7 +57,8 @@
58
57
  "devDependencies": {
59
58
  "@earendil-works/pi-coding-agent": "^0.85.0",
60
59
  "@earendil-works/pi-tui": "^0.85.0",
61
- "@types/ws": "^8"
60
+ "@types/ws": "^8",
61
+ "prettier": "3.3.3"
62
62
  },
63
63
  "pi": {
64
64
  "extensions": [