@codexstar/pi-listen 1.0.10 → 1.0.12
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.
|
@@ -18,7 +18,7 @@ export function buildProvisioningPlan(config: VoiceConfig, diagnostics: Environm
|
|
|
18
18
|
|
|
19
19
|
if (config.mode === "api") {
|
|
20
20
|
if (config.backend === "deepgram" && !diagnostics.hasDeepgramKey) {
|
|
21
|
-
manualSteps.push("Set DEEPGRAM_API_KEY
|
|
21
|
+
manualSteps.push("Set DEEPGRAM_API_KEY — get a free key at https://dpgr.am/pi-voice ($200 free credits)");
|
|
22
22
|
}
|
|
23
23
|
} else {
|
|
24
24
|
if (!diagnostics.hasPython) {
|
|
@@ -140,7 +140,7 @@ export async function runVoiceOnboarding(
|
|
|
140
140
|
): Promise<OnboardingResult | undefined> {
|
|
141
141
|
const modeChoice = await ctx.ui.select("How do you want to use speech-to-text?", [
|
|
142
142
|
"Recommended for me",
|
|
143
|
-
"Cloud API (fastest setup)",
|
|
143
|
+
"Cloud API (fastest setup — free $200 credits from Deepgram)",
|
|
144
144
|
"Local download (offline / private)",
|
|
145
145
|
]);
|
|
146
146
|
if (!modeChoice) return undefined;
|
|
@@ -168,6 +168,77 @@ export async function runVoiceOnboarding(
|
|
|
168
168
|
if (!cloudSelection) return undefined;
|
|
169
169
|
backend = cloudSelection.backend;
|
|
170
170
|
model = cloudSelection.model;
|
|
171
|
+
|
|
172
|
+
// ─── Deepgram API key setup ──────────────────────────────
|
|
173
|
+
if (backend === "deepgram" && !diagnostics.hasDeepgramKey) {
|
|
174
|
+
const keyAction = await ctx.ui.select(
|
|
175
|
+
"Deepgram API key not found. What would you like to do?",
|
|
176
|
+
[
|
|
177
|
+
"Paste API key now",
|
|
178
|
+
"I'll set it up later (ask pi to help or export DEEPGRAM_API_KEY=...)",
|
|
179
|
+
],
|
|
180
|
+
);
|
|
181
|
+
if (!keyAction) return undefined;
|
|
182
|
+
|
|
183
|
+
if (keyAction.startsWith("Paste")) {
|
|
184
|
+
ctx.ui.notify(
|
|
185
|
+
[
|
|
186
|
+
"Get your free Deepgram API key:",
|
|
187
|
+
" → https://dpgr.am/pi-voice",
|
|
188
|
+
" (Sign up → $200 free credits, no card needed)",
|
|
189
|
+
"",
|
|
190
|
+
"Paste your key below:",
|
|
191
|
+
].join("\n"),
|
|
192
|
+
"info",
|
|
193
|
+
);
|
|
194
|
+
const apiKey = await ctx.ui.input("DEEPGRAM_API_KEY");
|
|
195
|
+
if (apiKey && apiKey.trim().length > 10) {
|
|
196
|
+
const trimmedKey = apiKey.trim();
|
|
197
|
+
// Write to ~/.env.secrets if it exists, else ~/.zshrc
|
|
198
|
+
const fs = await import("node:fs");
|
|
199
|
+
const os = await import("node:os");
|
|
200
|
+
const home = os.homedir();
|
|
201
|
+
const envSecretsPath = `${home}/.env.secrets`;
|
|
202
|
+
const zshrcPath = `${home}/.zshrc`;
|
|
203
|
+
const exportLine = `export DEEPGRAM_API_KEY="${trimmedKey}"`;
|
|
204
|
+
|
|
205
|
+
const targetFile = fs.existsSync(envSecretsPath) ? envSecretsPath : zshrcPath;
|
|
206
|
+
const existing = fs.existsSync(targetFile) ? fs.readFileSync(targetFile, "utf-8") : "";
|
|
207
|
+
|
|
208
|
+
if (existing.includes("DEEPGRAM_API_KEY")) {
|
|
209
|
+
// Replace existing line
|
|
210
|
+
const updated = existing.replace(/^export DEEPGRAM_API_KEY=.*$/m, exportLine);
|
|
211
|
+
fs.writeFileSync(targetFile, updated);
|
|
212
|
+
} else {
|
|
213
|
+
fs.appendFileSync(targetFile, `\n${exportLine}\n`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Also set for current process so daemon can use it immediately
|
|
217
|
+
process.env.DEEPGRAM_API_KEY = trimmedKey;
|
|
218
|
+
diagnostics.hasDeepgramKey = true;
|
|
219
|
+
|
|
220
|
+
ctx.ui.notify(
|
|
221
|
+
`API key saved to ${targetFile}\nActive in this session. New terminals will pick it up automatically.`,
|
|
222
|
+
"info",
|
|
223
|
+
);
|
|
224
|
+
} else if (apiKey !== undefined && apiKey !== null) {
|
|
225
|
+
ctx.ui.notify(
|
|
226
|
+
"Key looks too short — skipped. You can set it later:\n export DEEPGRAM_API_KEY=\"your-key\"",
|
|
227
|
+
"warning",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
ctx.ui.notify(
|
|
232
|
+
[
|
|
233
|
+
"No problem! When you're ready:",
|
|
234
|
+
" 1. Get a key → https://dpgr.am/pi-voice ($200 free credits)",
|
|
235
|
+
" 2. Run: export DEEPGRAM_API_KEY=\"your-key\"",
|
|
236
|
+
" 3. Or ask pi: \"help me set up my Deepgram API key\"",
|
|
237
|
+
].join("\n"),
|
|
238
|
+
"info",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
171
242
|
} else {
|
|
172
243
|
const localSelection = await chooseBackendAndModel(
|
|
173
244
|
ctx,
|
package/extensions/voice.ts
CHANGED
|
@@ -351,6 +351,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
351
351
|
|
|
352
352
|
// ─── Voice: Start / Stop / Transcribe ────────────────────────────────────
|
|
353
353
|
|
|
354
|
+
const MAX_RECORDING_SECS = 30; // Safety cap: auto-stop after 30s
|
|
355
|
+
|
|
354
356
|
async function startVoiceRecording(target: "editor" | "btw" = "editor"): Promise<boolean> {
|
|
355
357
|
if (voiceState !== "idle" || !ctx) return false;
|
|
356
358
|
|
|
@@ -363,14 +365,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
363
365
|
recordingStart = Date.now();
|
|
364
366
|
setVoiceState("recording");
|
|
365
367
|
statusTimer = setInterval(() => {
|
|
366
|
-
if (voiceState === "recording")
|
|
368
|
+
if (voiceState === "recording") {
|
|
369
|
+
updateVoiceStatus();
|
|
370
|
+
// Safety: auto-stop after MAX_RECORDING_SECS
|
|
371
|
+
const elapsed = (Date.now() - recordingStart) / 1000;
|
|
372
|
+
if (elapsed >= MAX_RECORDING_SECS) {
|
|
373
|
+
isHolding = false;
|
|
374
|
+
stopVoiceRecording(target);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
367
377
|
}, 1000);
|
|
368
378
|
|
|
369
379
|
if (ctx.hasUI) {
|
|
370
380
|
ctx.ui.setWidget("voice-recording", [
|
|
371
381
|
target === "btw"
|
|
372
|
-
? " BTW Recording...
|
|
373
|
-
: " Recording...
|
|
382
|
+
? " 🎙 BTW Recording... Ctrl+Shift+V to stop"
|
|
383
|
+
: " 🎙 Recording... Ctrl+Shift+V to stop (or release SPACE)",
|
|
374
384
|
], { placement: "aboveEditor" });
|
|
375
385
|
}
|
|
376
386
|
return true;
|
|
@@ -679,6 +689,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
679
689
|
if (voiceState === "idle") {
|
|
680
690
|
await startVoiceRecording("editor");
|
|
681
691
|
} else if (voiceState === "recording") {
|
|
692
|
+
isHolding = false;
|
|
682
693
|
await stopVoiceRecording("editor");
|
|
683
694
|
}
|
|
684
695
|
},
|
|
@@ -744,7 +755,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
744
755
|
// ─── /voice command ──────────────────────────────────────────────────────
|
|
745
756
|
|
|
746
757
|
pi.registerCommand("voice", {
|
|
747
|
-
description: "Voice input: /voice [on|off|test|info|setup|reconfigure|doctor|backends|daemon]",
|
|
758
|
+
description: "Voice input: /voice [on|off|stop|test|info|setup|reconfigure|doctor|backends|daemon]",
|
|
748
759
|
handler: async (args, cmdCtx) => {
|
|
749
760
|
ctx = cmdCtx;
|
|
750
761
|
const sub = (args || "").trim().toLowerCase();
|
|
@@ -754,7 +765,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
754
765
|
updateVoiceStatus();
|
|
755
766
|
setupHoldToTalk();
|
|
756
767
|
ensureDaemon(config).catch(() => {});
|
|
757
|
-
cmdCtx.ui.notify("Voice enabled
|
|
768
|
+
cmdCtx.ui.notify("Voice enabled.\n Hold SPACE (empty editor) → release to transcribe\n Ctrl+Shift+V → toggle recording on/off\n Auto-stops after 30s", "info");
|
|
758
769
|
return;
|
|
759
770
|
}
|
|
760
771
|
|
|
@@ -767,6 +778,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
767
778
|
return;
|
|
768
779
|
}
|
|
769
780
|
|
|
781
|
+
if (sub === "stop") {
|
|
782
|
+
// Emergency stop — cancel any active recording
|
|
783
|
+
if (voiceState === "recording") {
|
|
784
|
+
isHolding = false;
|
|
785
|
+
await stopVoiceRecording("editor");
|
|
786
|
+
cmdCtx.ui.notify("Recording stopped and transcribed.", "info");
|
|
787
|
+
} else {
|
|
788
|
+
cmdCtx.ui.notify("No recording in progress.", "info");
|
|
789
|
+
}
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
770
793
|
if (sub === "test") {
|
|
771
794
|
cmdCtx.ui.notify("Testing voice setup...", "info");
|
|
772
795
|
const diagnostics = scanEnvironment(TRANSCRIBE_SCRIPT);
|