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.
@@ -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
- return (
1313
- /\r|\n/.test(rawText) ||
1314
- /\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.test(rawText) ||
1315
- /(?:换行|新的一行|另起一行|下一行|回车|分行|多行|逐行|每行|空一行|新段落|另起一段|分段)/u.test(rawText)
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
- return text
1321
- .replace(/\r\n?/g, "\n")
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
- postprocessUsed = finalText.trim() !== result.text.trim();
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(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-voice-input",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "Press Ctrl+Shift+R to dictate prompts into Pi using VolcEngine ASR",
5
5
  "type": "module",
6
6
  "keywords": [