@sfgartland/better-pi-voice 0.1.0 → 0.2.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/CHANGELOG.md +108 -0
- package/README.md +35 -14
- package/package.json +5 -3
- package/src/backends/parakeet.ts +192 -85
- package/src/backends/pause-detector.ts +192 -0
- package/src/backends/vad.ts +162 -0
- package/src/controller.ts +501 -319
- package/src/keyterms/index.ts +9 -1
- package/src/keyterms/mine.ts +41 -3
- package/src/model-download.ts +19 -0
- package/src/runtime.ts +13 -8
- package/src/session.ts +103 -0
- package/src/settings.ts +49 -10
- package/src/setup.ts +2 -1
- package/src/try-it.ts +1 -1
- package/src/types.ts +5 -0
- package/src/ui.ts +88 -22
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.0] - 2026-10-18
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Non-destructive recording queue.** Starting a recording while earlier ones are still
|
|
14
|
+
transcribing no longer drops the new utterance: each recording is queued, transcribed
|
|
15
|
+
exactly once, and its text is inserted in **recording-start order**, even when calls
|
|
16
|
+
finish out of order. When the queue is full (4 jobs, or 8 MiB of buffered audio) a new
|
|
17
|
+
capture is refused with a notice instead of silently discarding audio, and the status
|
|
18
|
+
line reports the queue depth.
|
|
19
|
+
- **Silero VAD** for batch cuts, behind a new `PauseDetector` interface, with the existing
|
|
20
|
+
RMS level heuristic as the fallback when the VAD model is unavailable.
|
|
21
|
+
- **Eager batch cuts:** beginning a new recording asks the previous batch tail to transcribe
|
|
22
|
+
immediately instead of waiting for the chunk target.
|
|
23
|
+
- A `pi-package` contract guard (`src/packaging.test.ts`) plus install/discoverability notes,
|
|
24
|
+
so the manifest cannot silently drop out of the pi package gallery.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **Sequence-aware transcription plumbing.** `EditorSink` rebuilds `prefix + finals in
|
|
29
|
+
order + live interim`, suppresses interims while jobs are pending, and submits once per
|
|
30
|
+
drained queue; `submitOnStop` no longer fires once per final.
|
|
31
|
+
- **Resident local model window** raised to 60 s (`idleUnloadMs` 0 -> 60000), so
|
|
32
|
+
back-to-back recordings stop reloading the model.
|
|
33
|
+
- **Batch chunk length** settled at 15 s (`chunkSeconds` 30 -> 15), matching
|
|
34
|
+
`DEFAULT_CHUNK_SECONDS`.
|
|
35
|
+
- **Keyterm budget is per backend:** Deepgram 100 (its cap), local correction 60.
|
|
36
|
+
- `dispose()` is now async and drains stored text on shutdown.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- A recording started during transcription was **silently dropped** (the microphone was
|
|
41
|
+
never opened and the utterance was lost with no notification). This was the bug the
|
|
42
|
+
queue replaces.
|
|
43
|
+
- Parakeet sessions are refcounted and `model.transcribe` is serialized, so an
|
|
44
|
+
unload/dispose can never free the model under a queued job.
|
|
45
|
+
- Failed, empty or hung jobs always free their queue slot via a reap timeout, so one bad
|
|
46
|
+
job cannot deadlock the queue.
|
|
47
|
+
- Stale Parakeet header comments and the README "not implemented yet" line are corrected.
|
|
48
|
+
- The fresh-install test now polls for the setup file instead of racing a fixed delay.
|
|
49
|
+
|
|
50
|
+
## [0.1.1] - 2026-10-16
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- A `media/demo.mp4` storyboard of the before/after transcription, wired into the
|
|
55
|
+
package manifest as `pi.video` so the [pi package gallery](https://pi.dev/packages)
|
|
56
|
+
can preview it.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **A skill file no longer becomes the keyterm `SKILL`.** Keyterms are mined from the
|
|
61
|
+
basenames of files the session touches, so reading `…/skills/<name>/SKILL.md` mined
|
|
62
|
+
the generic document stem `SKILL`. The local backend's correction pass then rewrote
|
|
63
|
+
ordinary words one edit away from it — `still`, `spill`, `skull` and `swill` all came
|
|
64
|
+
back as `SKILL`. Generic document names (`SKILL`, `README`, `INDEX`, `LICENSE`,
|
|
65
|
+
`CHANGELOG`, …) now mine their parent directory instead, which carries the name that
|
|
66
|
+
actually matters, and a mined term that collides with the seed jargon is no longer
|
|
67
|
+
eligible for fuzzy repair.
|
|
68
|
+
|
|
69
|
+
## [0.1.0] - 2026-10-16
|
|
70
|
+
|
|
71
|
+
First release.
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
75
|
+
- Streaming voice input for pi, one shortcut away (`ctrl+alt+z` by default), with
|
|
76
|
+
hold-to-talk and tap modes and a hard cap so a stuck key cannot leave the microphone
|
|
77
|
+
open.
|
|
78
|
+
- **Two interchangeable backends**: Deepgram `nova-3` streaming, or local Parakeet via
|
|
79
|
+
`transcribe-cpp`.
|
|
80
|
+
- **Repository-aware vocabulary biasing.** Keyterms are mined from the project name,
|
|
81
|
+
the git repository, file paths touched by the session, paths mentioned in messages,
|
|
82
|
+
a developer-jargon seed list and your own list, then sent to Deepgram as repeated
|
|
83
|
+
`keyterm` parameters (native biasing) and updatable live with a `Configure` message.
|
|
84
|
+
The local backend, which cannot be vocabulary-biased (`supports("vocabulary")` is
|
|
85
|
+
`false` for the Parakeet GGUFs), gets a deterministic correction pass instead:
|
|
86
|
+
re-joining identifiers the recogniser split, and repairing one misheard token within
|
|
87
|
+
a small edit distance of a keyterm.
|
|
88
|
+
- **A recovery layer that never loses a sentence**: one early retry before the first
|
|
89
|
+
transcript, replay of the retained audio when a stream dies unconfirmed, salvage of a
|
|
90
|
+
partial transcript when the socket dies late, promotion of an unreported interim, and
|
|
91
|
+
a circuit breaker after repeated fast failures. Audio is buffered in memory only and
|
|
92
|
+
never written to disk.
|
|
93
|
+
- **Local batch mode with pause-aware chunking**: audio is cut at real (silence-checked)
|
|
94
|
+
pauses while you speak and transcribed in the background, so only the tail is left at
|
|
95
|
+
finalize. The pause threshold is derived per recording, and a chunk is only cut where
|
|
96
|
+
the signal is genuinely quiet, so a chunk never starts mid-word.
|
|
97
|
+
- **Guided setup** (`/voice setup`, or the first shortcut press with no settings):
|
|
98
|
+
backend, key or model (with checksum-verified, resumable model downloads), run mode,
|
|
99
|
+
and shortcut, ending in an overview of the result.
|
|
100
|
+
- Commands: `/voice`, `/voice setup`, `/voice backend`, `/voice model`, `/voice key`,
|
|
101
|
+
`/voice set`, and `/voice-keyterms add|remove|list`.
|
|
102
|
+
- Diagnostics that name the cause rather than the symptom — a failed connection
|
|
103
|
+
distinguishes a rejected key, an unreachable host, and an HTTP error.
|
|
104
|
+
- A `media/screenshot.png` before/after card, exposed to the gallery as `pi.image`.
|
|
105
|
+
|
|
106
|
+
[Unreleased]: https://github.com/sfgartland/better-pi-voice/compare/v0.1.1...HEAD
|
|
107
|
+
[0.1.1]: https://github.com/sfgartland/better-pi-voice/releases/tag/v0.1.1
|
|
108
|
+
[0.1.0]: https://github.com/sfgartland/better-pi-voice/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# @sfgartland/better-pi-voice
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
Streaming voice input for pi. Hold a key, talk, and the words land in the editor —
|
|
4
6
|
in the right spelling, including the jargon and filenames generic speech
|
|
5
|
-
recognition gets wrong.
|
|
7
|
+
recognition gets wrong. ([8-second demo](https://github.com/sfgartland/better-pi-voice/raw/main/media/demo.mp4))
|
|
6
8
|
|
|
7
9
|
Two interchangeable backends behind one pipeline:
|
|
8
10
|
|
|
@@ -85,6 +87,10 @@ pi install /path/to/better-pi-voice # from a local checkout
|
|
|
85
87
|
pi install npm:@sfgartland/better-pi-voice # once published
|
|
86
88
|
```
|
|
87
89
|
|
|
90
|
+
The npm package is gallery-discoverable: it carries the `pi-package` keyword and
|
|
91
|
+
declares its extension, so `pi install npm:@sfgartland/better-pi-voice` also works from
|
|
92
|
+
the pi package gallery.
|
|
93
|
+
|
|
88
94
|
Then add a Deepgram key. The easiest way is `/voice key`:
|
|
89
95
|
|
|
90
96
|
- `/voice key <path-to-dotenv>` — use a key from an existing dotenv file (it checks one is
|
|
@@ -250,8 +256,8 @@ There is **no streaming model lighter than parakeet-0.6b**. The genuinely small
|
|
|
250
256
|
`medasr` (128 MB), `parakeet-tdt_ctc-110m` (135 MB), `canary-180m-flash` (218 MB),
|
|
251
257
|
`SenseVoiceSmall` (253 MB), `whisper-small` (270 MB) — are all batch-only, so using one
|
|
252
258
|
means giving up live text and transcribing after you release the key. With batch running
|
|
253
|
-
at 3.5× real-time that is a reasonable trade, and the pipeline
|
|
254
|
-
utterance,
|
|
259
|
+
at 3.5× real-time that is a reasonable trade, and the pipeline retains the whole
|
|
260
|
+
utterance, chunk-transcribes it while you speak, and finishes the tail when you release.
|
|
255
261
|
|
|
256
262
|
For non-English dictation, `nemotron-3.5-asr-streaming-0.6b` is the only streaming option
|
|
257
263
|
that is not parakeet, at the same size.
|
|
@@ -265,7 +271,7 @@ option for the small models, none of which can stream.
|
|
|
265
271
|
In batch mode the audio is transcribed in pieces while you are still speaking, so only
|
|
266
272
|
the tail is left when you release the key:
|
|
267
273
|
|
|
268
|
-
- `parakeet.chunkSeconds` (default
|
|
274
|
+
- `parakeet.chunkSeconds` (default 15) is a **target, not a limit**, and only its lower
|
|
269
275
|
70% matters: the audio is cut at the **first real pause** at or after that point and
|
|
270
276
|
transcribed in the background. A pause arriving later than the target still triggers a
|
|
271
277
|
cut, and unbroken speech simply grows a longer chunk
|
|
@@ -293,21 +299,36 @@ returns an **empty transcript** for any clip that begins mid-sentence, so a chun
|
|
|
293
299
|
merely the *quietest* moment silently discarded everything in it. Requiring real silence
|
|
294
300
|
costs tail latency on unbroken speech instead of costing speech.
|
|
295
301
|
|
|
302
|
+
### Recording queue
|
|
303
|
+
|
|
304
|
+
Recordings never block each other. Press the shortcut again while an earlier recording
|
|
305
|
+
is still being transcribed and a new one starts immediately; results are always emitted
|
|
306
|
+
in the order the recordings *started*, even when a later one finishes first.
|
|
307
|
+
|
|
308
|
+
- one capture at a time; each recording carries a sequence number and its own audio
|
|
309
|
+
- at most **4** recordings may be waiting, and at most **8 MiB** of buffered audio. When
|
|
310
|
+
either is reached the new press is **refused with a notification** rather than
|
|
311
|
+
silently dropped
|
|
312
|
+
- a job that never settles is abandoned after `jobReapMs` (default 30 s) so the queue
|
|
313
|
+
always advances; a recording that produced nothing stores `""` and is skipped, which
|
|
314
|
+
keeps the order intact
|
|
315
|
+
- the status line shows the depth (`1 recording`, `3 recordings`), so a backlog is
|
|
316
|
+
visible without opening anything
|
|
317
|
+
- with `submitOnStop`, a drained queue is submitted **once**, not once per recording
|
|
318
|
+
|
|
296
319
|
### Keeping the model loaded
|
|
297
320
|
|
|
298
|
-
The model is **
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
happens first:
|
|
321
|
+
The model is **held for a minute after the last recording by default**
|
|
322
|
+
(`parakeet.idleUnloadMs` is `60000`). Queued recordings arrive in bursts, and a warm
|
|
323
|
+
reload of a 135 MB–700 MB GGUF is felt between each one, so keeping it resident across a
|
|
324
|
+
burst is the better trade. Residency is released by whichever of these happens first:
|
|
303
325
|
|
|
304
|
-
- the idle window expires (`parakeet.idleUnloadMs`
|
|
326
|
+
- the idle window expires (`parakeet.idleUnloadMs`; set it to `0` to release as soon as
|
|
327
|
+
the queue empties)
|
|
305
328
|
- the machine gets busy — one-minute load average per CPU reaching **0.8**
|
|
306
329
|
(`parakeet.loadShedRatio`), checked once a minute
|
|
307
330
|
|
|
308
331
|
A new recording cancels a pending release, and session shutdown releases immediately.
|
|
309
|
-
Note that the load-shedding half only matters while residency is on: at the default of 0
|
|
310
|
-
the memory is already back after each recording, so there is nothing to shed.
|
|
311
332
|
|
|
312
333
|
## Settings
|
|
313
334
|
|
|
@@ -332,9 +353,9 @@ malformed falls back to a default and warns rather than breaking the microphone.
|
|
|
332
353
|
"parakeet": {
|
|
333
354
|
"modelPath": null, // null = read pi-voice's config if present
|
|
334
355
|
"mode": "batch", // "batch" (faster, chunked) | "streaming"
|
|
335
|
-
"chunkSeconds":
|
|
356
|
+
"chunkSeconds": 15, // cut at a pause near this length; 0 = only at the end
|
|
336
357
|
"silenceLevel": "auto", // "auto" derives the pause threshold per recording; a number pins it
|
|
337
|
-
"idleUnloadMs":
|
|
358
|
+
"idleUnloadMs": 60000, // hold the model this long after the queue empties; 0 = release at once
|
|
338
359
|
"loadShedRatio": 0.8 // with residency on, release early once load per CPU reaches this
|
|
339
360
|
},
|
|
340
361
|
"keyterms": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sfgartland/better-pi-voice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Streaming voice input for pi: Deepgram (nova-3, repo-aware keyterms) or local Parakeet, with Claude-Code-style recovery so speech is never lost.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Severin Gartland <sfgartland@hotmail.com>",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "https://github.com/sfgartland/better-pi-voice"
|
|
13
|
+
"url": "git+https://github.com/sfgartland/better-pi-voice.git"
|
|
14
14
|
},
|
|
15
15
|
"homepage": "https://github.com/sfgartland/better-pi-voice#readme",
|
|
16
16
|
"bugs": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"!src/**/*.test.ts",
|
|
37
37
|
"!src/testing/",
|
|
38
38
|
"README.md",
|
|
39
|
+
"CHANGELOG.md",
|
|
39
40
|
"LICENSE"
|
|
40
41
|
],
|
|
41
42
|
"peerDependencies": {
|
|
@@ -57,7 +58,8 @@
|
|
|
57
58
|
"extensions": [
|
|
58
59
|
"./index.ts"
|
|
59
60
|
],
|
|
60
|
-
"image": "https://raw.githubusercontent.com/sfgartland/better-pi-voice/main/media/screenshot.png"
|
|
61
|
+
"image": "https://raw.githubusercontent.com/sfgartland/better-pi-voice/main/media/screenshot.png",
|
|
62
|
+
"video": "https://raw.githubusercontent.com/sfgartland/better-pi-voice/main/media/demo.mp4"
|
|
61
63
|
},
|
|
62
64
|
"scripts": {
|
|
63
65
|
"typecheck": "tsc --noEmit",
|