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 +82 -35
- package/extensions/voice/audio-tool.ts +16 -0
- package/extensions/voice/config.ts +2 -2
- package/extensions/voice/onboarding.ts +2 -2
- package/extensions/voice/settings-panel.ts +2 -2
- package/extensions/voice/tts-local-models.ts +1 -1
- package/extensions/voice/tts-onboarding-overlay.ts +7 -3
- package/extensions/voice/tts-playback.ts +2 -2
- package/extensions/voice/ui-aura.ts +1 -1
- package/extensions/voice/ui-help-overlay.ts +2 -2
- package/extensions/voice/ui-locale-labels.ts +1 -1
- package/extensions/voice/ui-width.ts +1 -1
- package/extensions/voice.ts +94 -85
- package/package.json +6 -6
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-
|
|
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
|
-
**
|
|
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
|
[](https://www.npmjs.com/package/pi-voicekit)
|
|
15
17
|
[](https://github.com/CyFeng16/pi-voicekit/blob/main/LICENSE)
|
|
16
18
|
[](https://x.com/baanditeagle)
|
|
17
19
|
|
|
18
|
-
> **
|
|
19
|
-
>
|
|
20
|
-
>
|
|
21
|
-
>
|
|
22
|
-
>
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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`.
|
|
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` (≥
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
264
|
-
extensions/voice
|
|
265
|
-
extensions/voice/
|
|
266
|
-
extensions/voice/
|
|
267
|
-
extensions/voice/
|
|
268
|
-
extensions/voice/
|
|
269
|
-
extensions/voice/
|
|
270
|
-
|
|
271
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Visual design (no emoji per v7.1 hard constraint):
|
|
19
19
|
*
|
|
20
|
-
* ┌─ pi-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
package/extensions/voice.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pi-
|
|
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
|
|
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
|
}
|
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
3640
|
+
// ─── /voice-settings — unified pi-voicekit settings panel ─────────────
|
|
3632
3641
|
|
|
3633
3642
|
pi.registerCommand("voice-settings", {
|
|
3634
|
-
description: "Open pi-
|
|
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.
|
|
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": "
|
|
50
|
-
"format:check": "
|
|
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": [
|