pi-web-voice 0.1.2 → 0.1.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.
@@ -21,7 +21,16 @@ const hook = path.join(__dirname, "..", "hook.cjs");
21
21
  // `pi-web-voice doctor [file.wav]` checks the speech backend and exits.
22
22
  if (args[0] === "doctor") {
23
23
  const { doctor } = require("../lib/doctor.cjs");
24
- doctor(args.slice(1)).then((code) => process.exit(code));
24
+ doctor(args.slice(1)).then((code) => {
25
+ // Setting the code rather than calling process.exit lets the HTTP
26
+ // connection finish closing. Forcing an exit mid-teardown trips a libuv
27
+ // assertion on Windows: !(handle->flags & UV_HANDLE_CLOSING).
28
+ process.exitCode = code;
29
+ // Keep-alive sockets can still hold the loop open for a few seconds after
30
+ // the answer is in hand. This timer does not itself keep the process
31
+ // alive, and by the time it fires nothing is mid-close.
32
+ setTimeout(() => process.exit(code), 750).unref();
33
+ });
25
34
  return;
26
35
  }
27
36
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-voice",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Voice input for pi-web. A NODE_OPTIONS hook that injects a microphone button into the chat composer and transcribes speech with Azure AI Speech, Azure OpenAI, or any OpenAI-compatible endpoint. No fork, no patching, no rebuild.",
5
5
  "keywords": [
6
6
  "pi",
package/public/inject.js CHANGED
@@ -97,6 +97,7 @@
97
97
  denied: "麦克风权限被拒绝",
98
98
  empty: "没有识别到语音",
99
99
  failed: "转写失败",
100
+ noComposer: "找不到输入框,转写结果",
100
101
  }
101
102
  : {
102
103
  idle: "Voice input — click, or press and hold",
@@ -107,6 +108,7 @@
107
108
  denied: "Microphone permission denied",
108
109
  empty: "No speech detected",
109
110
  failed: "Transcription failed",
111
+ noComposer: "No composer found; transcript",
110
112
  };
111
113
 
112
114
  // ── audio ────────────────────────────────────────────────────────────────
@@ -224,10 +226,26 @@
224
226
 
225
227
  // ── composer ─────────────────────────────────────────────────────────────
226
228
 
229
+ // "The last visible textarea" is not good enough: pi-web's workspace
230
+ // terminal is xterm.js, which keeps a hidden IME helper textarea mounted
231
+ // after the composer. Writing there drops the transcript at best, and at
232
+ // worst hands it to the shell.
233
+ function isComposerCandidate(area) {
234
+ if (area.offsetParent === null) return false;
235
+ if (area.classList.contains("xterm-helper-textarea")) return false;
236
+ return !area.closest(".xterm");
237
+ }
238
+
227
239
  function findComposer() {
228
- const areas = Array.from(document.querySelectorAll("textarea")).filter(
229
- (area) => area.offsetParent !== null,
230
- );
240
+ // The button was mounted next to the composer's own toolbar, so walking up
241
+ // from it finds the right textarea even when the page holds several.
242
+ const button = document.getElementById(BUTTON_ID);
243
+ for (let node = button?.parentElement; node; node = node.parentElement) {
244
+ const nearby = Array.from(node.querySelectorAll("textarea")).filter(isComposerCandidate);
245
+ if (nearby.length) return nearby[nearby.length - 1];
246
+ }
247
+
248
+ const areas = Array.from(document.querySelectorAll("textarea")).filter(isComposerCandidate);
231
249
  return areas[areas.length - 1] || null;
232
250
  }
233
251
 
@@ -453,7 +471,10 @@
453
471
  // Always inserted, never sent: a wrong term is one keystroke from
454
472
  // being fixed, and Enter is right there when it is correct.
455
473
  const textarea = findComposer();
474
+ // Never fail silently: a transcript with nowhere to go is shown
475
+ // rather than dropped, so it can still be copied by hand.
456
476
  if (textarea) insertAtCaret(textarea, text);
477
+ else this.toast(`${T.noComposer}: ${text}`);
457
478
  }
458
479
  } catch (error) {
459
480
  this.toast(`${T.failed}: ${error.message}`);
@@ -516,6 +537,7 @@
516
537
  ui,
517
538
  recorder,
518
539
  config: CONFIG,
540
+ findComposer,
519
541
  get sessionId() {
520
542
  return sessionId;
521
543
  },