@sfgartland/better-pi-voice 0.1.0 → 0.1.1

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,67 @@
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.1.1] - 2026-10-16
10
+
11
+ ### Added
12
+
13
+ - A `media/demo.mp4` storyboard of the before/after transcription, wired into the
14
+ package manifest as `pi.video` so the [pi package gallery](https://pi.dev/packages)
15
+ can preview it.
16
+
17
+ ### Fixed
18
+
19
+ - **A skill file no longer becomes the keyterm `SKILL`.** Keyterms are mined from the
20
+ basenames of files the session touches, so reading `…/skills/<name>/SKILL.md` mined
21
+ the generic document stem `SKILL`. The local backend's correction pass then rewrote
22
+ ordinary words one edit away from it — `still`, `spill`, `skull` and `swill` all came
23
+ back as `SKILL`. Generic document names (`SKILL`, `README`, `INDEX`, `LICENSE`,
24
+ `CHANGELOG`, …) now mine their parent directory instead, which carries the name that
25
+ actually matters, and a mined term that collides with the seed jargon is no longer
26
+ eligible for fuzzy repair.
27
+
28
+ ## [0.1.0] - 2026-10-16
29
+
30
+ First release.
31
+
32
+ ### Added
33
+
34
+ - Streaming voice input for pi, one shortcut away (`ctrl+alt+z` by default), with
35
+ hold-to-talk and tap modes and a hard cap so a stuck key cannot leave the microphone
36
+ open.
37
+ - **Two interchangeable backends**: Deepgram `nova-3` streaming, or local Parakeet via
38
+ `transcribe-cpp`.
39
+ - **Repository-aware vocabulary biasing.** Keyterms are mined from the project name,
40
+ the git repository, file paths touched by the session, paths mentioned in messages,
41
+ a developer-jargon seed list and your own list, then sent to Deepgram as repeated
42
+ `keyterm` parameters (native biasing) and updatable live with a `Configure` message.
43
+ The local backend, which cannot be vocabulary-biased (`supports("vocabulary")` is
44
+ `false` for the Parakeet GGUFs), gets a deterministic correction pass instead:
45
+ re-joining identifiers the recogniser split, and repairing one misheard token within
46
+ a small edit distance of a keyterm.
47
+ - **A recovery layer that never loses a sentence**: one early retry before the first
48
+ transcript, replay of the retained audio when a stream dies unconfirmed, salvage of a
49
+ partial transcript when the socket dies late, promotion of an unreported interim, and
50
+ a circuit breaker after repeated fast failures. Audio is buffered in memory only and
51
+ never written to disk.
52
+ - **Local batch mode with pause-aware chunking**: audio is cut at real (silence-checked)
53
+ pauses while you speak and transcribed in the background, so only the tail is left at
54
+ finalize. The pause threshold is derived per recording, and a chunk is only cut where
55
+ the signal is genuinely quiet, so a chunk never starts mid-word.
56
+ - **Guided setup** (`/voice setup`, or the first shortcut press with no settings):
57
+ backend, key or model (with checksum-verified, resumable model downloads), run mode,
58
+ and shortcut, ending in an overview of the result.
59
+ - Commands: `/voice`, `/voice setup`, `/voice backend`, `/voice model`, `/voice key`,
60
+ `/voice set`, and `/voice-keyterms add|remove|list`.
61
+ - Diagnostics that name the cause rather than the symptom — a failed connection
62
+ distinguishes a rejected key, an unreachable host, and an HTTP error.
63
+ - A `media/screenshot.png` before/after card, exposed to the gallery as `pi.image`.
64
+
65
+ [Unreleased]: https://github.com/sfgartland/better-pi-voice/compare/v0.1.1...HEAD
66
+ [0.1.1]: https://github.com/sfgartland/better-pi-voice/releases/tag/v0.1.1
67
+ [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
 
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.1.1",
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",
@@ -26,6 +26,9 @@ export {
26
26
  } from "./sanitize.ts";
27
27
  export { SEED_KEYTERMS } from "./seed.ts";
28
28
 
29
+ /** Seed terms lowercased, so a mined term that collides with one can be spotted. */
30
+ const SEED_LOWER = new Set(SEED_KEYTERMS.map((term) => term.toLowerCase()));
31
+
29
32
  export type CollectOptions = MineInput & {
30
33
  /** Terms the user configured by hand. Highest priority. */
31
34
  userTerms?: readonly string[] | undefined;
@@ -61,7 +64,12 @@ export function collectKeytermSets(options: CollectOptions = {}): KeytermSet {
61
64
  const mined = mineKeyterms(options);
62
65
  const biasOrder = options.useSeed !== false ? [...user, ...SEED_KEYTERMS, ...mined] : [...user, ...mined];
63
66
  const bias = sanitizeKeyterms(biasOrder, { maxTerms: options.maxTerms });
64
- const correct = sanitizeKeyterms([...user, ...mined], { maxTerms: options.maxTerms });
67
+ // A mined term that collides with the seed jargon (a file named `settings.ts`,
68
+ // say) is still only an ordinary English word. It may bias the recogniser, but
69
+ // fuzzy repair must not aim at it — the seed is excluded from correction for
70
+ // exactly that reason. User terms are explicit, so they are left alone.
71
+ const correctable = mined.filter((term) => !SEED_LOWER.has(term.toLowerCase()));
72
+ const correct = sanitizeKeyterms([...user, ...correctable], { maxTerms: options.maxTerms });
65
73
  const cap = options.maxTerms ?? DEFAULT_MAX_TERMS;
66
74
  // Something was actually dropped, and the budget is what dropped it.
67
75
  return { bias, correct, truncated: bias.length >= cap && biasOrder.length > bias.length };
@@ -6,7 +6,7 @@
6
6
  * what makes recognition of jargon and filenames work.
7
7
  */
8
8
 
9
- import { basename } from "node:path";
9
+ import { basename, dirname } from "node:path";
10
10
  import { tokenizeLabel } from "./sanitize.ts";
11
11
 
12
12
  /** Tools whose arguments name a file the user cares about. */
@@ -103,6 +103,44 @@ function stem(path: string): string {
103
103
  return dot > 0 ? base.slice(0, dot) : base;
104
104
  }
105
105
 
106
+ /**
107
+ * Stems that name a document rather than a thing.
108
+ *
109
+ * A file called `SKILL.md` (or `README.md`, `INDEX.md`, …) is a convention. Its
110
+ * stem is a generic English word, not vocabulary, and mining it is actively
111
+ * harmful: the correction pass then rewrites real words one edit away from it
112
+ * (`still`, `spill`, `skull` and `swill` all became `SKILL`). For these names the
113
+ * parent directory is the informative one — `…/skills/brainstorming/SKILL.md` is
114
+ * about brainstorming.
115
+ */
116
+ const GENERIC_STEMS = new Set([
117
+ "skill",
118
+ "readme",
119
+ "index",
120
+ "license",
121
+ "licence",
122
+ "changelog",
123
+ "contributing",
124
+ "copying",
125
+ "notice",
126
+ "authors",
127
+ "todo",
128
+ "main",
129
+ "package",
130
+ "setup",
131
+ ]);
132
+
133
+ /** The terms one file path contributes: usually its stem, else its parent directory. */
134
+ function termsFromPath(path: string): string[] {
135
+ if (!GENERIC_STEMS.has(stem(path).toLowerCase())) return tokenizeLabel(stem(path));
136
+
137
+ const parent = basename(dirname(path));
138
+ // A generic name with no directory (`SKILL.md`) or at a filesystem root has
139
+ // nothing informative to offer; mining `.` or `/` would only add noise.
140
+ if (!parent || parent === "." || parent === "/") return [];
141
+ return tokenizeLabel(parent);
142
+ }
143
+
106
144
  /**
107
145
  * Build the candidate term list. Order matters: callers that truncate will
108
146
  * prefer the earlier entries, so the most specific signals come first.
@@ -122,12 +160,12 @@ export function mineKeyterms(input: MineInput): string[] {
122
160
  }
123
161
 
124
162
  for (const path of input.filePaths ?? []) {
125
- out.push(...tokenizeLabel(stem(path)));
163
+ out.push(...termsFromPath(path));
126
164
  }
127
165
 
128
166
  for (const text of input.texts ?? []) {
129
167
  for (const token of pathLikeTokens(text)) {
130
- out.push(...tokenizeLabel(stem(token)));
168
+ out.push(...termsFromPath(token));
131
169
  }
132
170
  }
133
171