pi-voice-input 0.2.14 → 0.2.16
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/extensions/voice-input.ts +46 -13
- package/package.json +1 -1
|
@@ -23,6 +23,10 @@ import WebSocket from "ws";
|
|
|
23
23
|
|
|
24
24
|
const CONFIG_PATH = path.join(homedir(), ".pi", "agent", "voice-input.config.json");
|
|
25
25
|
const VOLC_API_KEY_URL = "https://console.volcengine.com/speech/new/setting/apikeys?projectName=default";
|
|
26
|
+
// pi-bridge redirects child_process calls whose cwd is inside the bridged fake
|
|
27
|
+
// project directory. Audio capture and system volume control must always use
|
|
28
|
+
// the local machine, so force those subprocesses to a known-local cwd.
|
|
29
|
+
const LOCAL_AUDIO_PROCESS_CWD = homedir();
|
|
26
30
|
const DEFAULT_SHORTCUT = Key.ctrlShift("r");
|
|
27
31
|
const DEFAULT_POSTPROCESS_MODEL = "";
|
|
28
32
|
const DEFAULT_POSTPROCESS_CONTEXT_TOKENS = 20000;
|
|
@@ -267,17 +271,17 @@ function timestampForFilename(): string {
|
|
|
267
271
|
}
|
|
268
272
|
|
|
269
273
|
function commandExists(command: string): boolean {
|
|
270
|
-
return spawnSync("sh", ["-lc", `command -v ${command}`], { stdio: "ignore" }).status === 0;
|
|
274
|
+
return spawnSync("sh", ["-lc", `command -v ${command}`], { cwd: LOCAL_AUDIO_PROCESS_CWD, stdio: "ignore" }).status === 0;
|
|
271
275
|
}
|
|
272
276
|
|
|
273
277
|
function commandOutput(command: string, args: string[], timeoutMs = 1500): string {
|
|
274
|
-
const result = spawnSync(command, args, { encoding: "utf8", timeout: timeoutMs });
|
|
278
|
+
const result = spawnSync(command, args, { cwd: LOCAL_AUDIO_PROCESS_CWD, encoding: "utf8", timeout: timeoutMs });
|
|
275
279
|
if (result.status !== 0) return "";
|
|
276
280
|
return (result.stdout || "").trim();
|
|
277
281
|
}
|
|
278
282
|
|
|
279
283
|
function runCommand(command: string, args: string[], timeoutMs = 1500): boolean {
|
|
280
|
-
return spawnSync(command, args, { stdio: "ignore", timeout: timeoutMs }).status === 0;
|
|
284
|
+
return spawnSync(command, args, { cwd: LOCAL_AUDIO_PROCESS_CWD, stdio: "ignore", timeout: timeoutMs }).status === 0;
|
|
281
285
|
}
|
|
282
286
|
|
|
283
287
|
function formatPercent(value: number): string {
|
|
@@ -1308,22 +1312,46 @@ function cleanPostprocessOutput(output: string): string {
|
|
|
1308
1312
|
return text;
|
|
1309
1313
|
}
|
|
1310
1314
|
|
|
1315
|
+
const EXPLICIT_ENGLISH_MULTILINE_PATTERN =
|
|
1316
|
+
/\b(?:new\s*line|newline|line break|next line|new paragraph|paragraph break|carriage return|press enter|separate lines?|multi[- ]line|multiple lines)\b/i;
|
|
1317
|
+
const EXPLICIT_CHINESE_MULTILINE_PATTERN = /(?:换行|新的一行|另起一行|下一行|回车|分行|多行|逐行|每行|空一行|新段落|另起一段|分段)/u;
|
|
1318
|
+
const CJK_LIKE_PATTERN = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
|
|
1319
|
+
const CJK_PUNCTUATION_PATTERN = /[,。!?、;:()《》「」『』“”‘’]/u;
|
|
1320
|
+
const CLOSING_PUNCTUATION_PATTERN = /^[,.;:!?,。!?、;:))\]}》」』”’]/u;
|
|
1321
|
+
const OPENING_PUNCTUATION_PATTERN = /[(([{\[《「『“‘]$/u;
|
|
1322
|
+
|
|
1311
1323
|
function rawTextRequestsMultiline(rawText: string): boolean {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1324
|
+
// Existing newlines in raw ASR are not reliable user intent: providers can
|
|
1325
|
+
// insert segment or sentence breaks on their own. Treat only spoken layout
|
|
1326
|
+
// commands as intentional multiline input.
|
|
1327
|
+
return EXPLICIT_ENGLISH_MULTILINE_PATTERN.test(rawText) || EXPLICIT_CHINESE_MULTILINE_PATTERN.test(rawText);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function lineBreakJoiner(left: string, right: string): string {
|
|
1331
|
+
if (!left || !right) return "";
|
|
1332
|
+
if (CLOSING_PUNCTUATION_PATTERN.test(right) || OPENING_PUNCTUATION_PATTERN.test(left)) return "";
|
|
1333
|
+
if (CJK_PUNCTUATION_PATTERN.test(left) || CJK_PUNCTUATION_PATTERN.test(right)) return "";
|
|
1334
|
+
if (CJK_LIKE_PATTERN.test(left) && CJK_LIKE_PATTERN.test(right)) return "";
|
|
1335
|
+
return " ";
|
|
1317
1336
|
}
|
|
1318
1337
|
|
|
1319
1338
|
function collapseUnexpectedLineBreaks(text: string): string {
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
.replace(/[ \t\f\v]*\n+[ \t\f\v]*/g,
|
|
1339
|
+
const normalized = text.replace(/\r\n?/g, "\n");
|
|
1340
|
+
return normalized
|
|
1341
|
+
.replace(/[ \t\f\v]*\n+[ \t\f\v]*/g, (match, offset: number, source: string) => {
|
|
1342
|
+
const left = source.slice(0, offset).replace(/[ \t\f\v]+$/g, "").at(-1) ?? "";
|
|
1343
|
+
const right = source.slice(offset + match.length).replace(/^[ \t\f\v]+/g, "").at(0) ?? "";
|
|
1344
|
+
return lineBreakJoiner(left, right);
|
|
1345
|
+
})
|
|
1323
1346
|
.replace(/[ \t\f\v]{2,}/g, " ")
|
|
1324
1347
|
.trim();
|
|
1325
1348
|
}
|
|
1326
1349
|
|
|
1350
|
+
function normalizeRawTextForPostprocess(rawText: string): string {
|
|
1351
|
+
const raw = rawText.trim();
|
|
1352
|
+
return rawTextRequestsMultiline(raw) ? raw : collapseUnexpectedLineBreaks(raw);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1327
1355
|
function preserveExpectedPostprocessLayout(rawText: string, output: string): string {
|
|
1328
1356
|
if (rawTextRequestsMultiline(rawText)) return output.trim();
|
|
1329
1357
|
return collapseUnexpectedLineBreaks(output);
|
|
@@ -1418,7 +1446,7 @@ async function postprocessTranscript(ctx: ExtensionContext, rawText: string, con
|
|
|
1418
1446
|
messages: [
|
|
1419
1447
|
{
|
|
1420
1448
|
role: "user",
|
|
1421
|
-
content: buildPostprocessPrompt(ctx, raw, config),
|
|
1449
|
+
content: buildPostprocessPrompt(ctx, normalizeRawTextForPostprocess(raw), config),
|
|
1422
1450
|
timestamp: Date.now(),
|
|
1423
1451
|
},
|
|
1424
1452
|
],
|
|
@@ -1503,6 +1531,7 @@ async function startRecording(ctx: ExtensionContext) {
|
|
|
1503
1531
|
let child: ReturnType<typeof spawn>;
|
|
1504
1532
|
try {
|
|
1505
1533
|
child = spawn(cmd[0], cmd.slice(1), {
|
|
1534
|
+
cwd: LOCAL_AUDIO_PROCESS_CWD,
|
|
1506
1535
|
detached: true,
|
|
1507
1536
|
stdio: ["ignore", "ignore", "ignore"],
|
|
1508
1537
|
});
|
|
@@ -1595,6 +1624,7 @@ async function stopRecording(ctx: ExtensionContext, transcribe = true) {
|
|
|
1595
1624
|
|
|
1596
1625
|
let finalText = result.text;
|
|
1597
1626
|
let postprocessMs = 0;
|
|
1627
|
+
let postprocessSucceeded = false;
|
|
1598
1628
|
let postprocessUsed = false;
|
|
1599
1629
|
if (config.postprocessEnabled) {
|
|
1600
1630
|
ctx.ui.setStatus("voice-input", ctx.ui.theme.fg("warning", "● polishing"));
|
|
@@ -1602,7 +1632,7 @@ async function stopRecording(ctx: ExtensionContext, transcribe = true) {
|
|
|
1602
1632
|
try {
|
|
1603
1633
|
finalText = await postprocessTranscript(ctx, result.text, config);
|
|
1604
1634
|
postprocessMs = Date.now() - postprocessStart;
|
|
1605
|
-
|
|
1635
|
+
postprocessSucceeded = true;
|
|
1606
1636
|
} catch (error) {
|
|
1607
1637
|
postprocessMs = Date.now() - postprocessStart;
|
|
1608
1638
|
ctx.ui.notify(
|
|
@@ -1612,6 +1642,9 @@ async function stopRecording(ctx: ExtensionContext, transcribe = true) {
|
|
|
1612
1642
|
}
|
|
1613
1643
|
}
|
|
1614
1644
|
|
|
1645
|
+
finalText = preserveExpectedPostprocessLayout(result.text, finalText);
|
|
1646
|
+
postprocessUsed = postprocessSucceeded && finalText.trim() !== result.text.trim();
|
|
1647
|
+
|
|
1615
1648
|
ctx.ui.setStatus("voice-input", undefined);
|
|
1616
1649
|
insertIntoEditor(ctx, finalText);
|
|
1617
1650
|
ctx.ui.notify(
|