@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 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
+ ![the same sentence transcribed without keyterms (sub agent, Worktree T's config) and with them (subagent, worktree tsconfig)](https://raw.githubusercontent.com/sfgartland/better-pi-voice/main/media/screenshot.png)
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 already retains the whole
254
- utterance, but it is not implemented yet.
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 30) is a **target, not a limit**, and only its lower
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 **released after every recording by default**, because a warm reload is only
299
- about 0.4 s and holding ~700 MB between recordings is not a good trade on a laptop. Set
300
- `parakeet.idleUnloadMs` to keep it resident instead — useful for larger models, where a
301
- reload costs real time. When residency is on, the model is released by whichever of these
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": 30, // cut at a pause near this length; 0 = only at the end
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": 0, // 0 = release after each recording (see above)
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.1.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",