@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 +67 -0
- package/README.md +3 -1
- package/package.json +5 -3
- package/src/keyterms/index.ts +9 -1
- package/src/keyterms/mine.ts +41 -3
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
|
+

|
|
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.
|
|
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",
|
package/src/keyterms/index.ts
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/src/keyterms/mine.ts
CHANGED
|
@@ -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(...
|
|
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(...
|
|
168
|
+
out.push(...termsFromPath(token));
|
|
131
169
|
}
|
|
132
170
|
}
|
|
133
171
|
|