@codexstar/pi-listen 1.0.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/LICENSE +21 -0
- package/README.md +283 -0
- package/daemon.py +517 -0
- package/docs/API.md +273 -0
- package/docs/ARCHITECTURE.md +114 -0
- package/docs/backends.md +196 -0
- package/docs/plans/2026-03-12-pi-voice-master-plan.md +613 -0
- package/docs/plans/2026-03-12-pi-voice-model-aware-execution-plan.md +256 -0
- package/docs/plans/2026-03-12-pi-voice-onboarding-remediation-plan.md +391 -0
- package/docs/plans/pi-voice-model-aware-review.md +196 -0
- package/docs/plans/pi-voice-model-detection-qa-plan.md +226 -0
- package/docs/plans/pi-voice-model-detection-research.md +483 -0
- package/docs/plans/pi-voice-onboarding-ux-plan.md +388 -0
- package/docs/plans/pi-voice-release-validation-plan.md +386 -0
- package/docs/plans/pi-voice-remaining-implementation-plan.md +524 -0
- package/docs/plans/pi-voice-review-findings.md +227 -0
- package/docs/plans/pi-voice-technical-remediation-plan.md +613 -0
- package/docs/qa-matrix.md +69 -0
- package/docs/qa-results.md +357 -0
- package/docs/troubleshooting.md +265 -0
- package/extensions/voice/config.ts +206 -0
- package/extensions/voice/diagnostics.ts +212 -0
- package/extensions/voice/install.ts +62 -0
- package/extensions/voice/onboarding.ts +315 -0
- package/extensions/voice.ts +1149 -0
- package/package.json +48 -0
- package/scripts/setup-macos.sh +374 -0
- package/scripts/setup-windows.ps1 +271 -0
- package/transcribe.py +497 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import type { ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { VoiceConfig, VoiceSettingsScope } from "./config";
|
|
3
|
+
import {
|
|
4
|
+
getModelReadiness,
|
|
5
|
+
recommendVoiceSetup,
|
|
6
|
+
type BackendAvailability,
|
|
7
|
+
type DiagnosticsPreference,
|
|
8
|
+
type EnvironmentDiagnostics,
|
|
9
|
+
} from "./diagnostics";
|
|
10
|
+
import { buildProvisioningPlan } from "./install";
|
|
11
|
+
|
|
12
|
+
type VoiceUiContext = ExtensionContext | ExtensionCommandContext;
|
|
13
|
+
|
|
14
|
+
export interface OnboardingResult {
|
|
15
|
+
config: VoiceConfig;
|
|
16
|
+
selectedScope: VoiceSettingsScope;
|
|
17
|
+
summaryLines: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FirstRunDecision {
|
|
21
|
+
action: "start" | "later";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function finalizeOnboardingConfig(
|
|
25
|
+
config: VoiceConfig,
|
|
26
|
+
options: { validated: boolean; source: "first-run" | "setup-command" },
|
|
27
|
+
): VoiceConfig {
|
|
28
|
+
if (options.validated) {
|
|
29
|
+
const timestamp = new Date().toISOString();
|
|
30
|
+
return {
|
|
31
|
+
...config,
|
|
32
|
+
onboarding: {
|
|
33
|
+
...config.onboarding,
|
|
34
|
+
completed: true,
|
|
35
|
+
schemaVersion: config.version,
|
|
36
|
+
completedAt: timestamp,
|
|
37
|
+
lastValidatedAt: timestamp,
|
|
38
|
+
source: options.source,
|
|
39
|
+
skippedAt: undefined,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...config,
|
|
46
|
+
onboarding: {
|
|
47
|
+
...config.onboarding,
|
|
48
|
+
completed: false,
|
|
49
|
+
schemaVersion: config.version,
|
|
50
|
+
completedAt: undefined,
|
|
51
|
+
lastValidatedAt: undefined,
|
|
52
|
+
source: "repair",
|
|
53
|
+
skippedAt: undefined,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function promptFirstRunOnboarding(ctx: VoiceUiContext): Promise<FirstRunDecision> {
|
|
59
|
+
const choice = await ctx.ui.select("Set up pi-voice now?", [
|
|
60
|
+
"Start voice setup",
|
|
61
|
+
"Remind me later",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
return { action: choice === "Start voice setup" ? "start" : "later" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getModelOptions(backend: BackendAvailability, recommendedModel: string): string[] {
|
|
68
|
+
return backend.models.map((model) => {
|
|
69
|
+
const installed = backend.installed_models?.includes(model) ?? false;
|
|
70
|
+
if (model === recommendedModel && installed) return `${model} (recommended, installed)`;
|
|
71
|
+
if (model === recommendedModel) return `${model} (recommended)`;
|
|
72
|
+
if (installed) return `${model} (installed)`;
|
|
73
|
+
return model;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildSelectableBackends(
|
|
78
|
+
targetMode: "api" | "local",
|
|
79
|
+
diagnostics: EnvironmentDiagnostics,
|
|
80
|
+
): BackendAvailability[] {
|
|
81
|
+
if (targetMode === "api") {
|
|
82
|
+
return [
|
|
83
|
+
diagnostics.backends.find((candidate) => candidate.name === "deepgram") ?? {
|
|
84
|
+
name: "deepgram",
|
|
85
|
+
available: diagnostics.hasDeepgramKey,
|
|
86
|
+
type: "cloud",
|
|
87
|
+
default_model: "nova-3",
|
|
88
|
+
models: ["nova-3", "nova-2", "whisper-large", "whisper-medium", "whisper-small"],
|
|
89
|
+
install: "Set DEEPGRAM_API_KEY",
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const discoveredLocalBackends = diagnostics.backends.filter((candidate) => candidate.type === "local");
|
|
95
|
+
const fallbackLocalBackends: BackendAvailability[] = [
|
|
96
|
+
{
|
|
97
|
+
name: "faster-whisper",
|
|
98
|
+
available: false,
|
|
99
|
+
type: "local",
|
|
100
|
+
default_model: "small",
|
|
101
|
+
models: ["tiny", "base", "small", "medium", "large-v3-turbo"],
|
|
102
|
+
install: "python3 -m pip install faster-whisper",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "moonshine",
|
|
106
|
+
available: false,
|
|
107
|
+
type: "local",
|
|
108
|
+
default_model: "moonshine/base",
|
|
109
|
+
models: ["moonshine/tiny", "moonshine/base"],
|
|
110
|
+
install: "python3 -m pip install 'useful-moonshine[onnx]'",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "whisper-cpp",
|
|
114
|
+
available: false,
|
|
115
|
+
type: "local",
|
|
116
|
+
default_model: "small",
|
|
117
|
+
models: ["tiny", "base", "small", "medium", "large"],
|
|
118
|
+
install: "brew install whisper-cpp",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "parakeet",
|
|
122
|
+
available: false,
|
|
123
|
+
type: "local",
|
|
124
|
+
default_model: "nvidia/parakeet-tdt-0.6b-v2",
|
|
125
|
+
models: ["nvidia/parakeet-tdt-0.6b-v2", "nvidia/parakeet-ctc-0.6b", "nvidia/parakeet-tdt-1.1b"],
|
|
126
|
+
install: "python3 -m pip install 'nemo_toolkit[asr]'",
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
const merged = new Map<string, BackendAvailability>();
|
|
130
|
+
for (const backend of [...discoveredLocalBackends, ...fallbackLocalBackends]) {
|
|
131
|
+
if (!merged.has(backend.name)) merged.set(backend.name, backend);
|
|
132
|
+
}
|
|
133
|
+
return [...merged.values()];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function runVoiceOnboarding(
|
|
137
|
+
ctx: VoiceUiContext,
|
|
138
|
+
currentConfig: VoiceConfig,
|
|
139
|
+
diagnostics: EnvironmentDiagnostics,
|
|
140
|
+
): Promise<OnboardingResult | undefined> {
|
|
141
|
+
const modeChoice = await ctx.ui.select("How do you want to use speech-to-text?", [
|
|
142
|
+
"Recommended for me",
|
|
143
|
+
"Cloud API (fastest setup)",
|
|
144
|
+
"Local download (offline / private)",
|
|
145
|
+
]);
|
|
146
|
+
if (!modeChoice) return undefined;
|
|
147
|
+
|
|
148
|
+
const preference = await askPreference(ctx, modeChoice);
|
|
149
|
+
if (!preference) return undefined;
|
|
150
|
+
|
|
151
|
+
const recommendation = recommendVoiceSetup(diagnostics, preference);
|
|
152
|
+
const targetMode =
|
|
153
|
+
modeChoice === "Recommended for me"
|
|
154
|
+
? recommendation.mode
|
|
155
|
+
: modeChoice.startsWith("Cloud API")
|
|
156
|
+
? "api"
|
|
157
|
+
: "local";
|
|
158
|
+
|
|
159
|
+
let backend = recommendation.backend;
|
|
160
|
+
let model = recommendation.model;
|
|
161
|
+
|
|
162
|
+
if (targetMode === "api") {
|
|
163
|
+
const cloudSelection = await chooseBackendAndModel(
|
|
164
|
+
ctx,
|
|
165
|
+
buildSelectableBackends("api", diagnostics),
|
|
166
|
+
"Choose API provider/model",
|
|
167
|
+
);
|
|
168
|
+
if (!cloudSelection) return undefined;
|
|
169
|
+
backend = cloudSelection.backend;
|
|
170
|
+
model = cloudSelection.model;
|
|
171
|
+
} else {
|
|
172
|
+
const localSelection = await chooseBackendAndModel(
|
|
173
|
+
ctx,
|
|
174
|
+
buildSelectableBackends("local", diagnostics),
|
|
175
|
+
"Choose local backend/model",
|
|
176
|
+
);
|
|
177
|
+
if (!localSelection) return undefined;
|
|
178
|
+
backend = localSelection.backend;
|
|
179
|
+
model = localSelection.model;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const scopeChoice = await ctx.ui.select("Where should pi-voice settings be saved?", [
|
|
183
|
+
"Global (all projects)",
|
|
184
|
+
"Project only (this repo)",
|
|
185
|
+
]);
|
|
186
|
+
if (!scopeChoice) return undefined;
|
|
187
|
+
const selectedScope: VoiceSettingsScope = scopeChoice.startsWith("Project") ? "project" : "global";
|
|
188
|
+
|
|
189
|
+
const draftConfig: VoiceConfig = {
|
|
190
|
+
...currentConfig,
|
|
191
|
+
mode: targetMode,
|
|
192
|
+
backend,
|
|
193
|
+
model,
|
|
194
|
+
scope: selectedScope,
|
|
195
|
+
};
|
|
196
|
+
const selectedBackend = buildSelectableBackends(targetMode, diagnostics).find((candidate) => candidate.name === backend);
|
|
197
|
+
const modelReadiness = getModelReadiness(selectedBackend, model);
|
|
198
|
+
const provisioningPlan = buildProvisioningPlan(draftConfig, diagnostics);
|
|
199
|
+
const fixableIssues = recommendation.fixableIssues.length > 0
|
|
200
|
+
? recommendation.fixableIssues.map((issue) => `- ${issue}`)
|
|
201
|
+
: ["- No immediate setup issues detected"];
|
|
202
|
+
const recommendationLine =
|
|
203
|
+
modeChoice === "Recommended for me"
|
|
204
|
+
? `Recommendation: ${recommendation.reason}`
|
|
205
|
+
: targetMode === "api"
|
|
206
|
+
? "Selected path: Cloud API. We'll validate credentials and recording prerequisites."
|
|
207
|
+
: modelReadiness === "installed"
|
|
208
|
+
? "Selected path: Local model already installed and ready to configure."
|
|
209
|
+
: "Selected path: Local setup. We'll validate dependencies and any required downloads.";
|
|
210
|
+
const summaryLines = [
|
|
211
|
+
`Mode: ${targetMode === "api" ? "Cloud API" : "Local download"}`,
|
|
212
|
+
`Backend: ${backend}`,
|
|
213
|
+
`Model: ${model}`,
|
|
214
|
+
`Model status: ${modelReadiness}`,
|
|
215
|
+
`Scope: ${selectedScope}`,
|
|
216
|
+
"",
|
|
217
|
+
recommendationLine,
|
|
218
|
+
"",
|
|
219
|
+
"Checks before voice is fully ready:",
|
|
220
|
+
...fixableIssues,
|
|
221
|
+
"",
|
|
222
|
+
"Suggested commands:",
|
|
223
|
+
...(provisioningPlan.commands.length > 0
|
|
224
|
+
? provisioningPlan.commands.map((command) => `- ${command}`)
|
|
225
|
+
: ["- No install commands required"]),
|
|
226
|
+
...(provisioningPlan.manualSteps.length > 0
|
|
227
|
+
? ["", "Manual steps:", ...provisioningPlan.manualSteps.map((step) => `- ${step}`)]
|
|
228
|
+
: []),
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const confirm = await ctx.ui.confirm("Confirm voice setup", summaryLines.join("\n"));
|
|
232
|
+
if (!confirm) return undefined;
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
selectedScope,
|
|
236
|
+
summaryLines,
|
|
237
|
+
config: {
|
|
238
|
+
...currentConfig,
|
|
239
|
+
mode: targetMode,
|
|
240
|
+
backend,
|
|
241
|
+
model,
|
|
242
|
+
scope: selectedScope,
|
|
243
|
+
onboarding: {
|
|
244
|
+
...currentConfig.onboarding,
|
|
245
|
+
completed: false,
|
|
246
|
+
schemaVersion: currentConfig.version,
|
|
247
|
+
source: "first-run",
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function askPreference(
|
|
254
|
+
ctx: VoiceUiContext,
|
|
255
|
+
modeChoice: string,
|
|
256
|
+
): Promise<DiagnosticsPreference | undefined> {
|
|
257
|
+
if (modeChoice.startsWith("Cloud API")) return "speed";
|
|
258
|
+
if (modeChoice.startsWith("Local download")) return "privacy";
|
|
259
|
+
|
|
260
|
+
const choice = await ctx.ui.select("What matters most to you?", [
|
|
261
|
+
"Balanced default",
|
|
262
|
+
"Fastest setup",
|
|
263
|
+
"Best privacy / offline use",
|
|
264
|
+
"Best accuracy",
|
|
265
|
+
"Lowest resource usage",
|
|
266
|
+
]);
|
|
267
|
+
if (!choice) return undefined;
|
|
268
|
+
|
|
269
|
+
if (choice.startsWith("Fastest")) return "speed";
|
|
270
|
+
if (choice.startsWith("Best privacy")) return "privacy";
|
|
271
|
+
if (choice.startsWith("Best accuracy")) return "accuracy";
|
|
272
|
+
if (choice.startsWith("Lowest resource")) return "low-resource";
|
|
273
|
+
return "balanced";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function chooseBackendAndModel(
|
|
277
|
+
ctx: VoiceUiContext,
|
|
278
|
+
backends: BackendAvailability[],
|
|
279
|
+
title: string,
|
|
280
|
+
): Promise<{ backend: BackendAvailability["name"]; model: string } | undefined> {
|
|
281
|
+
if (backends.length === 0) return undefined;
|
|
282
|
+
|
|
283
|
+
const backendOptions = backends.map((backend) => {
|
|
284
|
+
const installedCount = backend.installed_models?.length ?? 0;
|
|
285
|
+
if (backend.type === "cloud") {
|
|
286
|
+
return `${backend.name} — ${backend.available ? "API ready" : backend.install ?? "needs API key"}`;
|
|
287
|
+
}
|
|
288
|
+
if (installedCount > 0) {
|
|
289
|
+
return `${backend.name} — ${installedCount} installed model${installedCount === 1 ? "" : "s"}`;
|
|
290
|
+
}
|
|
291
|
+
const suffix = backend.available ? "backend ready" : backend.install ?? "needs setup";
|
|
292
|
+
return `${backend.name} — ${suffix}`;
|
|
293
|
+
});
|
|
294
|
+
const backendChoice = await ctx.ui.select(title, backendOptions);
|
|
295
|
+
if (!backendChoice) return undefined;
|
|
296
|
+
const backendIndex = backendOptions.indexOf(backendChoice);
|
|
297
|
+
const selectedBackend = backends[Math.max(0, backendIndex)];
|
|
298
|
+
|
|
299
|
+
const recommendedModel = selectedBackend.installed_models?.includes(selectedBackend.default_model)
|
|
300
|
+
? selectedBackend.default_model
|
|
301
|
+
: selectedBackend.installed_models?.[0] ?? selectedBackend.default_model;
|
|
302
|
+
const modelChoice = await ctx.ui.select(
|
|
303
|
+
`Choose model for ${selectedBackend.name}`,
|
|
304
|
+
getModelOptions(selectedBackend, recommendedModel),
|
|
305
|
+
);
|
|
306
|
+
if (!modelChoice) return undefined;
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
backend: selectedBackend.name,
|
|
310
|
+
model: modelChoice
|
|
311
|
+
.replace(" (recommended, installed)", "")
|
|
312
|
+
.replace(" (recommended)", "")
|
|
313
|
+
.replace(" (installed)", ""),
|
|
314
|
+
};
|
|
315
|
+
}
|