dsh-voice-mode 0.1.5 → 0.2.0

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/README.md CHANGED
@@ -198,6 +198,12 @@ input: mic ──RMS VAD(2s 静音切句)──▶ POST /voice-mode/asr(f
198
198
  - hero(新会话空态)没有语音入口:语音模式是会话级功能,请先进入会话使用输入框麦克风按钮
199
199
  - 「试听」的请求超时兜底使用 `AbortSignal.timeout`(Chrome 103+ / Firefox 100+ / Safari 16+);更老的浏览器点击试听会立即显示失败提示,属预期降级
200
200
  - `spokenFormat` 提示词经官方 `system-prompt/assemble` 瀑布注入;若当前会话使用**完整提示词**配置(persona `complete: true` 的 agent preset),其提示词会整体替换系统提示词(官方 complete 契约),此时口语化提示词不注入
201
+ - **苹果 Safari / iOS**:
202
+ - 需 **HTTPS 或 localhost**(iOS/macOS Safari 强制安全上下文;`http://` 局域网 IP 下麦克风不可用)
203
+ - 首次进入需授权麦克风;被拒后到「设置 → Safari → 麦克风」开启(iOS)
204
+ - iOS 后台/锁屏时识别与朗读暂停,回前台自动恢复(可能丢句);建议语音模式期间保持前台
205
+ - 桌面 macOS Safari 若提示音暂无声音,请先点一次麦克风按钮再触发(浏览器音频策略)
206
+ - **安全说明**:插件 HTTP 面(`/voice-mode/*`)遵循宿主安全模型——请勿将 dsh 端口直接暴露公网;经反向代理发布时由代理层(如 basic auth)鉴权;插件侧对敏感操作保留会话归属校验(sessionId 门控)
201
207
 
202
208
  ## 故障排查
203
209
 
package/lib/client.js CHANGED
@@ -73,6 +73,15 @@ var INTERRUPT_LEVELS = {
73
73
  function createAsrEngine(config, sessionId) {
74
74
  let state = "idle";
75
75
  const stateListeners = /* @__PURE__ */ new Set();
76
+ const errorListeners = /* @__PURE__ */ new Set();
77
+ const emitError = (msg) => {
78
+ for (const fn of errorListeners) {
79
+ try {
80
+ fn(msg);
81
+ } catch {
82
+ }
83
+ }
84
+ };
76
85
  const transcriptListeners = /* @__PURE__ */ new Set();
77
86
  const partialListeners = /* @__PURE__ */ new Set();
78
87
  const speechStartListeners = /* @__PURE__ */ new Set();
@@ -108,11 +117,11 @@ function createAsrEngine(config, sessionId) {
108
117
  }
109
118
  };
110
119
  const emit = (listeners, text, meta) => {
111
- const t2 = text.trim();
112
- if (!t2) return;
120
+ const t3 = text.trim();
121
+ if (!t3) return;
113
122
  for (const fn of listeners) {
114
123
  try {
115
- fn(t2, meta);
124
+ fn(t3, meta);
116
125
  } catch {
117
126
  }
118
127
  }
@@ -138,7 +147,7 @@ function createAsrEngine(config, sessionId) {
138
147
  let res = await fetch(asrUrl(false), {
139
148
  method: "POST",
140
149
  headers: { "content-type": "application/octet-stream" },
141
- body: samples.slice().buffer
150
+ body: samples.buffer
142
151
  });
143
152
  if (res.status === 202) {
144
153
  setState("loading-model");
@@ -148,7 +157,7 @@ function createAsrEngine(config, sessionId) {
148
157
  const r2 = await fetch(asrUrl(false), {
149
158
  method: "POST",
150
159
  headers: { "content-type": "application/octet-stream" },
151
- body: samples.slice().buffer
160
+ body: samples.buffer
152
161
  });
153
162
  resolve(r2);
154
163
  } catch {
@@ -205,7 +214,7 @@ function createAsrEngine(config, sessionId) {
205
214
  let res = await fetch(asrUrl(true), {
206
215
  method: "POST",
207
216
  headers: { "content-type": "application/octet-stream" },
208
- body: samples.slice().buffer
217
+ body: samples.buffer
209
218
  });
210
219
  if (res.status === 202) {
211
220
  setState("loading-model");
@@ -216,7 +225,7 @@ function createAsrEngine(config, sessionId) {
216
225
  await fetch(asrUrl(true), {
217
226
  method: "POST",
218
227
  headers: { "content-type": "application/octet-stream" },
219
- body: samples.slice().buffer
228
+ body: samples.buffer
220
229
  })
221
230
  );
222
231
  } catch {
@@ -233,6 +242,7 @@ function createAsrEngine(config, sessionId) {
233
242
  if (out.text) emit(transcriptListeners, out.text, meta);
234
243
  } catch {
235
244
  setState(active ? speechActive ? "speech" : "listening" : "idle");
245
+ emitError("recognitionFail");
236
246
  }
237
247
  })();
238
248
  };
@@ -391,7 +401,7 @@ function createAsrEngine(config, sessionId) {
391
401
  }
392
402
  processor = null;
393
403
  try {
394
- stream?.getTracks().forEach((t2) => t2.stop());
404
+ stream?.getTracks().forEach((t3) => t3.stop());
395
405
  } catch {
396
406
  }
397
407
  stream = null;
@@ -474,6 +484,12 @@ function createAsrEngine(config, sessionId) {
474
484
  transcriptListeners.delete(fn);
475
485
  };
476
486
  },
487
+ onError(fn) {
488
+ errorListeners.add(fn);
489
+ return () => {
490
+ errorListeners.delete(fn);
491
+ };
492
+ },
477
493
  onPartial(fn) {
478
494
  partialListeners.add(fn);
479
495
  return () => {
@@ -502,10 +518,138 @@ function createAsrEngine(config, sessionId) {
502
518
  };
503
519
  }
504
520
 
521
+ // src/strings.ts
522
+ var zh = {
523
+ stateVoiceMode: "\u8BED\u97F3\u6A21\u5F0F",
524
+ ttsNoticeFail: "\u6717\u8BFB\u8FDE\u63A5\u5931\u8D25\uFF1A\u6B63\u5728\u91CD\u8BD5\u2026",
525
+ enterFail: "\u8FDB\u5165\u8BED\u97F3\u6A21\u5F0F\u5931\u8D25",
526
+ disabled: "\u8BED\u97F3\u6A21\u5F0F\u5DF2\u7981\u7528\uFF08\u63D2\u4EF6 enabled=false\uFF09",
527
+ sendFailKept: "\u53D1\u9001\u5931\u8D25\uFF0C\u5DF2\u4FDD\u7559\u5728\u8349\u7A3F",
528
+ micDenied: "\u9EA6\u514B\u98CE\u88AB\u62D2\u7EDD\uFF1A\u8BF7\u5728\u6D4F\u89C8\u5668\u5730\u5740\u680F\u5141\u8BB8\u9EA6\u514B\u98CE\u6743\u9650",
529
+ micUnavailable: "\u9EA6\u514B\u98CE\u4E0D\u53EF\u7528",
530
+ hold: "\u6309\u4F4F",
531
+ recognizing: "\u8BC6\u522B\u4E2D\u2026",
532
+ holdToTalk: "\u6309\u4F4F\u8BF4\u8BDD",
533
+ voiceDetected: "\u8BED\u97F3\u4E2D",
534
+ entering: "\u8FDB\u5165\u4E2D\u2026",
535
+ voiceBtn: "\u8BED\u97F3",
536
+ ariaActive: "\u8BED\u97F3\u6A21\u5F0F\u8FDB\u884C\u4E2D",
537
+ ariaEnter: "\u8FDB\u5165\u8BED\u97F3\u5BF9\u8BDD\u6A21\u5F0F",
538
+ titleHold: "\u8BED\u97F3\u6A21\u5F0F\u8FDB\u884C\u4E2D \xB7 \u6309\u4F4F\u8BF4\u8BDD\u3001\u677E\u624B\u53D1\u9001\uFF1B\u77ED\u6309\u9000\u51FA\uFF1BEsc/\u5931\u53BB\u7126\u70B9\u653E\u5F03\uFF1BCtrl+Shift+V \u9000\u51FA",
539
+ titleToggle: "\u8BED\u97F3\u6A21\u5F0F\u8FDB\u884C\u4E2D \xB7 \u70B9\u51FB\u9000\u51FA\uFF08Ctrl+Shift+V\uFF09\xB7 \u6309\u4F4F Ctrl \u7ACB\u5373\u53D1\u9001",
540
+ titleEnter: "\u8FDB\u5165\u8BED\u97F3\u5BF9\u8BDD\u6A21\u5F0F\uFF08Ctrl+Shift+V\uFF09",
541
+ loadingModel: "\u6B63\u5728\u52A0\u8F7D\u6A21\u578B\u2026",
542
+ listening: "\u8046\u542C\u4E2D\u2026",
543
+ wakeWord: "\u5524\u9192\u8BCD",
544
+ barHold: "\u8BED\u97F3\u6A21\u5F0F \xB7 \u6309\u4F4F\u8BF4\u8BDD\uFF08\u77ED\u6309\u9000\u51FA\uFF09",
545
+ barListening: "\u8BED\u97F3\u6A21\u5F0F \xB7 \u8046\u542C\u4E2D\u2026",
546
+ reading: "\u6717\u8BFB\u4E2D\u2026",
547
+ recognitionFail: "\u8BC6\u522B\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5",
548
+ modelDownloadFail: "\u8BED\u97F3\u6A21\u578B\u4E0B\u8F7D\u5931\u8D25\uFF08{file}\uFF09\uFF1A\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u65B0\u8FDB\u5165\u8BED\u97F3\u6A21\u5F0F\u91CD\u8BD5",
549
+ startFail: "\u8BED\u97F3\u6A21\u5F0F\u542F\u52A8\u5931\u8D25\uFF1A{err}",
550
+ holdDots: "\u6309\u4F4F\u8BF4\u8BDD\u2026",
551
+ sayWake: "\u8BF4\u300C{wake}\u300D\u5F00\u59CB",
552
+ exit: "\u9000\u51FA",
553
+ skip: "\u8DF3\u8FC7",
554
+ configUnavailableNote: "\uFF08\u8BBE\u7F6E\u6587\u6863\u672A\u5C31\u7EEA\uFF0C\u9762\u677F\u5C31\u7EEA\u540E\u4F1A\u81EA\u52A8\u51FA\u73B0\uFF09\u3002",
555
+ // settings-form
556
+ previewNameFirst: "\u8BF7\u5148\u586B\u5199\u97F3\u8272\u540D\uFF08ShortName\uFF09",
557
+ previewDisabled: "\u8BED\u97F3\u6A21\u5F0F\u5DF2\u7981\u7528\uFF08\u63D2\u4EF6 enabled=false\uFF09\uFF0C\u65E0\u6CD5\u8BD5\u542C",
558
+ previewPlayFail: "\u8BD5\u542C\u5931\u8D25\uFF1A\u65E0\u6CD5\u64AD\u653E\u8BE5\u97F3\u8272",
559
+ previewAutoplay: "\u6D4F\u89C8\u5668\u62E6\u622A\u4E86\u81EA\u52A8\u64AD\u653E\uFF0C\u8BF7\u518D\u70B9\u4E00\u6B21\u8BD5\u542C",
560
+ previewCheck: "\u8BD5\u542C\u5931\u8D25\uFF1A\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216\u97F3\u8272\u540D\uFF08ShortName\uFF09\u662F\u5426\u6B63\u786E",
561
+ previewBtnTitle: "\u8BD5\u542C\u5F53\u524D\u97F3\u8272\uFF08\u5F53\u524D\u8BED\u901F\uFF09",
562
+ synthesizing: "\u5408\u6210\u4E2D\u2026",
563
+ preview: "\u8BD5\u542C",
564
+ custom: "\u81EA\u5B9A\u4E49",
565
+ // settings rows
566
+ descVoice: "Edge TTS \u97F3\u8272\uFF08\u4E0B\u62C9\u5E38\u7528\uFF0C\u5176\u4F59\u9009\u300C\u81EA\u5B9A\u4E49\u300D\u624B\u52A8\u586B ShortName\uFF09",
567
+ descRate: "\u6717\u8BFB\u8BED\u901F\u500D\u7387\uFF080.5 \u6162\u901F \uFF5E 2.0 \u5FEB\u901F\uFF0C1.0 \u6B63\u5E38\uFF09",
568
+ descInterrupt: "\u53D1\u58F0\u6253\u65AD\u7075\u654F\u5EA6\uFF080 \u9AD8\u95E8\u69DB / 1 \u4E2D / 2 \u4F4E\uFF09",
569
+ sev0: "0 \u9AD8\u95E8\u69DB",
570
+ sev1: "1 \u4E2D",
571
+ sev2: "2 \u4F4E",
572
+ descSilence: "\u8BF4\u5B8C\u6574\u4E00\u53E5\u7684\u9759\u97F3\u505C\u987F\u6BEB\u79D2\u6570\uFF08\u9ED8\u8BA4 2000 = 2 \u79D2\uFF09",
573
+ descIdle: "\u65E0\u6D3B\u52A8\u81EA\u52A8\u9000\u51FA\u8BED\u97F3\u6A21\u5F0F\u7684\u5206\u949F\u6570\uFF08\u9ED8\u8BA4 10\uFF09",
574
+ descModelHost: "ASR \u6A21\u578B\u4E0B\u8F7D\u6E90\uFF08\u5B98\u65B9\u6E90 / \u56FD\u5185\u955C\u50CF\uFF0C\u6216\u9009\u300C\u81EA\u5B9A\u4E49\u300D\u586B\u4EFB\u610F\u955C\u50CF\uFF09",
575
+ descAutoSend: "\u8BC6\u522B\u5B9A\u7A3F\u540E\u81EA\u52A8\u53D1\u9001\uFF08\u5173=\u53EA\u8FDB\u8349\u7A3F\uFF1B\u6309\u4F4F Ctrl / hold \u677E\u624B\u4ECD\u53D1\u9001\uFF09",
576
+ descSpokenFormat: "\u8BED\u97F3\u4F1A\u8BDD\u6CE8\u5165\u53E3\u8BED\u5316\u63D0\u793A\u8BCD\uFF08\u56DE\u590D\u53E3\u8BED\u5316\u3001\u4E0D\u7528 Markdown \u6392\u7248\u7B26\u53F7\uFF0C\u6717\u8BFB\u66F4\u987A\uFF1B\u9ED8\u8BA4\u5173\uFF0C\u6539\u52A8\u5373\u65F6\u751F\u6548\uFF09",
577
+ descMode: "\u4EA4\u4E92\u6A21\u5F0F\uFF08toggle \u6301\u7EED\u8046\u542C+\u9759\u97F3\u65AD\u53E5 / hold \u6309\u4F4F\u8BF4\u8BDD\uFF09",
578
+ modeToggle: "\u6301\u7EED\u8046\u542C",
579
+ modeHold: "\u6309\u4F4F\u8BF4\u8BDD",
580
+ descWakeWord: "\u5524\u9192\u8BCD\uFF08\u9ED8\u8BA4\u5173\uFF1B\u5982\u300C\u4F60\u597D\u5C0FD\u300D\uFF0C\u8BF4\u51FA\u540E\u5F00\u59CB\u8BC6\u522B\uFF09",
581
+ wakePlaceholder: "\u5982\uFF1A\u4F60\u597D\u5C0FD",
582
+ settingsCardDesc: "\u97F3\u8272 / \u8BED\u901F / \u6253\u65AD\u7075\u654F\u5EA6 / \u9759\u97F3\u505C\u987F / \u7A7A\u95F2\u8D85\u65F6 / \u6A21\u578B\u955C\u50CF / \u81EA\u52A8\u53D1\u9001 / \u4EA4\u4E92\u6A21\u5F0F / \u5524\u9192\u8BCD / \u53E3\u8BED\u5316\u63D0\u793A\u8BCD",
583
+ configUnavailable: "\u914D\u7F6E\u6682\u4E0D\u53EF\u7528"
584
+ };
585
+ var en = {
586
+ stateVoiceMode: "Voice Mode",
587
+ ttsNoticeFail: "Read-aloud connection lost: retrying\u2026",
588
+ enterFail: "Failed to enter voice mode",
589
+ disabled: "Voice mode disabled (plugin enabled=false)",
590
+ sendFailKept: "Send failed; text kept in draft",
591
+ micDenied: "Microphone denied: allow mic access for this site",
592
+ micUnavailable: "Microphone unavailable",
593
+ hold: "Hold",
594
+ recognizing: "Recognizing\u2026",
595
+ holdToTalk: "Hold to talk",
596
+ voiceDetected: "Voice active",
597
+ entering: "Entering\u2026",
598
+ voiceBtn: "Voice",
599
+ ariaActive: "Voice mode active",
600
+ ariaEnter: "Enter voice mode",
601
+ titleHold: "Voice mode \xB7 hold to talk, release to send; tap to exit; Esc/blur cancels; Ctrl+Shift+V exits",
602
+ titleToggle: "Voice mode \xB7 click to exit (Ctrl+Shift+V) \xB7 hold Ctrl to send now",
603
+ titleEnter: "Enter voice mode (Ctrl+Shift+V)",
604
+ loadingModel: "Loading model\u2026",
605
+ listening: "Listening\u2026",
606
+ wakeWord: "Wake word",
607
+ barHold: "Voice mode \xB7 hold to talk (tap to exit)",
608
+ barListening: "Voice mode \xB7 listening\u2026",
609
+ reading: "Reading\u2026",
610
+ recognitionFail: "Recognition failed, try again",
611
+ modelDownloadFail: "Model download failed ({file}): check network and re-enter voice mode",
612
+ startFail: "Voice mode failed to start: {err}",
613
+ holdDots: "Hold to talk\u2026",
614
+ sayWake: 'Say "{wake}" to start',
615
+ exit: "Exit",
616
+ skip: "Skip",
617
+ configUnavailableNote: " (settings document not ready; the panel will appear when it is).",
618
+ previewNameFirst: "Enter a voice ShortName first",
619
+ previewDisabled: "Voice mode disabled; preview unavailable",
620
+ previewPlayFail: "Preview failed: cannot play this voice",
621
+ previewAutoplay: "Autoplay blocked \u2014 click preview again",
622
+ previewCheck: "Preview failed: check network or ShortName",
623
+ previewBtnTitle: "Preview voice (current rate)",
624
+ synthesizing: "Synthesizing\u2026",
625
+ preview: "Preview",
626
+ custom: "Custom",
627
+ descVoice: "Edge TTS voice (presets, or a custom ShortName)",
628
+ descRate: "Speech rate (0.5 slow \u2013 2.0 fast, 1.0 normal)",
629
+ descInterrupt: "Interrupt sensitivity (0 high barrier / 2 low)",
630
+ sev0: "0 high",
631
+ sev1: "1 medium",
632
+ sev2: "2 low",
633
+ descSilence: "Silence pause before a sentence is committed (default 2000 ms)",
634
+ descIdle: "Auto-exit voice mode after idle minutes (default 10)",
635
+ descModelHost: "ASR model download source (official source / mirror, or any custom URL)",
636
+ descAutoSend: "Auto-send after finalized recognition (off = draft only; Ctrl / hold still sends)",
637
+ descSpokenFormat: "Inject spoken-format prompt into voice replies (colloquial, no Markdown; default off, live)",
638
+ descMode: "Interaction mode (toggle: continuous listen + auto-send / hold: press to talk)",
639
+ modeToggle: "Continue listen",
640
+ modeHold: "Hold to talk",
641
+ descWakeWord: "Wake word (default off; e.g. Hey D)",
642
+ wakePlaceholder: "e.g. Hey D",
643
+ settingsCardDesc: "Voice / rate / interrupt / silence / idle / model host / auto-send / mode / wake word / spoken format",
644
+ configUnavailable: "Configuration unavailable"
645
+ };
646
+ var lang = typeof navigator !== "undefined" && /^zh\b/i.test(navigator.language ?? "") ? "zh" : "en";
647
+ var t = (key) => lang === "zh" ? zh[key] : en[key] ?? zh[key];
648
+
505
649
  // src/settings-form.tsx
506
650
  var import_react = require("react");
507
651
  var import_jsx_runtime = require("react/jsx-runtime");
508
- var t = {
652
+ var t2 = {
509
653
  bg: "var(--dsw-alias-bg-layer-3)",
510
654
  bgOpen: "var(--dsw-alias-bg-layer-2)",
511
655
  border: "var(--dsw-alias-border-l2)",
@@ -515,8 +659,8 @@ var t = {
515
659
  };
516
660
  var BASE_PATH = "/voice-mode";
517
661
  var cardStyle = {
518
- border: `1px solid ${t.border}`,
519
- background: t.bg,
662
+ border: `1px solid ${t2.border}`,
663
+ background: t2.bg,
520
664
  borderRadius: 12,
521
665
  overflow: "hidden"
522
666
  };
@@ -536,18 +680,18 @@ var setHeader = {
536
680
  display: "flex"
537
681
  };
538
682
  var setHeadText = { flexDirection: "column", flex: 1, gap: 4, minWidth: 0, display: "flex" };
539
- var setName = { color: t.label, fontSize: 15, fontWeight: 600, lineHeight: 1.4 };
540
- var setDesc = { color: t.term, fontSize: 13, lineHeight: 1.5 };
541
- var setChevron = { color: t.term, flex: "none", transition: "transform .16s", display: "inline-flex" };
542
- var setBody = { borderTop: `1px solid ${t.border}`, margin: "0 16px", paddingBottom: 8 };
683
+ var setName = { color: t2.label, fontSize: 15, fontWeight: 600, lineHeight: 1.4 };
684
+ var setDesc = { color: t2.term, fontSize: 13, lineHeight: 1.5 };
685
+ var setChevron = { color: t2.term, flex: "none", transition: "transform .16s", display: "inline-flex" };
686
+ var setBody = { borderTop: `1px solid ${t2.border}`, margin: "0 16px", paddingBottom: 8 };
543
687
  var setRow = { alignItems: "center", gap: 12, padding: "12px 0", display: "flex" };
544
688
  var setLabelBox = { flexDirection: "column", flex: 1, gap: 3, minWidth: 0, display: "flex" };
545
689
  var setLabel = { fontSize: 13, lineHeight: "20px" };
546
- var setHint = { color: t.term, fontSize: 12, lineHeight: "18px" };
547
- var setSeg = { border: `1px solid ${t.border}`, borderRadius: 8, flexShrink: 0, gap: 2, padding: 2, display: "inline-flex" };
690
+ var setHint = { color: t2.term, fontSize: 12, lineHeight: "18px" };
691
+ var setSeg = { border: `1px solid ${t2.border}`, borderRadius: 8, flexShrink: 0, gap: 2, padding: 2, display: "inline-flex" };
548
692
  var setSegBtn = (on) => ({
549
693
  font: "inherit",
550
- color: on ? t.label : "var(--dsw-alias-label-secondary)",
694
+ color: on ? t2.label : "var(--dsw-alias-label-secondary)",
551
695
  cursor: "pointer",
552
696
  background: on ? "var(--dsw-alias-bg-layer-2)" : "transparent",
553
697
  border: "none",
@@ -563,9 +707,9 @@ var inputStyle = {
563
707
  maxWidth: "100%",
564
708
  padding: "7px 10px",
565
709
  borderRadius: 8,
566
- border: `1px solid ${t.border}`,
710
+ border: `1px solid ${t2.border}`,
567
711
  background: "var(--dsw-alias-bg-layer-2)",
568
- color: t.label,
712
+ color: t2.label,
569
713
  fontSize: 13,
570
714
  fontFamily: "inherit",
571
715
  outline: "none"
@@ -700,7 +844,10 @@ function SelectField({
700
844
  },
701
845
  children: [
702
846
  options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: o.v, children: o.label }, o.v)),
703
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "__custom__", children: "\u81EA\u5B9A\u4E49\u2026" })
847
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("option", { value: "__custom__", children: [
848
+ t("custom"),
849
+ "\u2026"
850
+ ] })
704
851
  ]
705
852
  }
706
853
  ),
@@ -728,7 +875,7 @@ function VoicePreviewButton({ voice, rate }) {
728
875
  if (busy) return;
729
876
  const v = voice.trim();
730
877
  if (!v) {
731
- setNote("\u8BF7\u5148\u586B\u5199\u97F3\u8272\u540D\uFF08ShortName\uFF09");
878
+ setNote(t("previewNameFirst"));
732
879
  return;
733
880
  }
734
881
  setBusy(true);
@@ -749,7 +896,7 @@ function VoicePreviewButton({ voice, rate }) {
749
896
  signal: AbortSignal.timeout(15e3)
750
897
  });
751
898
  if (res.status === 403) {
752
- setNote("\u8BED\u97F3\u6A21\u5F0F\u5DF2\u7981\u7528\uFF08\u63D2\u4EF6 enabled=false\uFF09\uFF0C\u65E0\u6CD5\u8BD5\u542C");
899
+ setNote(t("previewDisabled"));
753
900
  return;
754
901
  }
755
902
  if (!res.ok) throw new Error(`preview http ${res.status}`);
@@ -759,18 +906,18 @@ function VoicePreviewButton({ voice, rate }) {
759
906
  audio.onended = () => URL.revokeObjectURL(url);
760
907
  audio.onerror = () => {
761
908
  URL.revokeObjectURL(url);
762
- setNote("\u8BD5\u542C\u5931\u8D25\uFF1A\u65E0\u6CD5\u64AD\u653E\u8BE5\u97F3\u8272");
909
+ setNote(t("previewPlayFail"));
763
910
  };
764
911
  try {
765
912
  await audio.play();
766
913
  } catch (e) {
767
914
  URL.revokeObjectURL(url);
768
915
  setNote(
769
- e instanceof DOMException && e.name === "NotAllowedError" ? "\u6D4F\u89C8\u5668\u62E6\u622A\u4E86\u81EA\u52A8\u64AD\u653E\uFF0C\u8BF7\u518D\u70B9\u4E00\u6B21\u8BD5\u542C" : "\u8BD5\u542C\u5931\u8D25\uFF1A\u65E0\u6CD5\u64AD\u653E\u8BE5\u97F3\u8272"
916
+ e instanceof DOMException && e.name === "NotAllowedError" ? t("previewAutoplay") : t("previewPlayFail")
770
917
  );
771
918
  }
772
919
  } catch {
773
- setNote("\u8BD5\u542C\u5931\u8D25\uFF1A\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216\u97F3\u8272\u540D\uFF08ShortName\uFF09\u662F\u5426\u6B63\u786E");
920
+ setNote(t("previewCheck"));
774
921
  } finally {
775
922
  setBusy(false);
776
923
  }
@@ -783,18 +930,18 @@ function VoicePreviewButton({ voice, rate }) {
783
930
  gap: 5,
784
931
  alignSelf: "flex-start",
785
932
  cursor: busy ? "default" : "pointer",
786
- color: t.label,
933
+ color: t2.label,
787
934
  background: "var(--dsw-alias-bg-layer-2)",
788
- border: `1px solid ${t.border}`,
935
+ border: `1px solid ${t2.border}`,
789
936
  borderRadius: 6,
790
937
  padding: "4px 10px",
791
938
  fontSize: 12,
792
939
  lineHeight: "18px"
793
940
  };
794
941
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { display: "flex", flexDirection: "column", gap: 4, alignItems: "flex-start" }, children: [
795
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", onClick: play, disabled: busy, style: btnStyle, title: "\u8BD5\u542C\u5F53\u524D\u97F3\u8272\uFF08\u5F53\u524D\u8BED\u901F\uFF09", children: [
942
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", onClick: play, disabled: busy, style: btnStyle, title: t("previewBtnTitle"), children: [
796
943
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 16 16", width: 11, height: 11, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { fill: "currentColor", d: "M4 3l9 5-9 5z" }) }),
797
- busy ? "\u5408\u6210\u4E2D\u2026" : "\u8BD5\u542C"
944
+ busy ? t("synthesizing") : t("preview")
798
945
  ] }),
799
946
  note && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-state-error-primary)", fontSize: 12, lineHeight: "18px" }, children: note })
800
947
  ] });
@@ -828,22 +975,22 @@ function VoiceSettingsCard({ scope }) {
828
975
  const value = snap?.value ?? {};
829
976
  const unavailable = snap?.status === "unavailable" || snap?.status === "error";
830
977
  if (unavailable) {
831
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-dshvm-settings": "card", style: { color: t.term, fontSize: 12, padding: "14px 16px", ...cardStyle }, children: [
832
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-state-error-primary)" }, children: "\u914D\u7F6E\u6682\u4E0D\u53EF\u7528" }),
833
- "\uFF08\u8BBE\u7F6E\u6587\u6863\u672A\u5C31\u7EEA\uFF0C\u9762\u677F\u5C31\u7EEA\u540E\u4F1A\u81EA\u52A8\u51FA\u73B0\uFF09\u3002"
978
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-dshvm-settings": "card", style: { color: t2.term, fontSize: 12, padding: "14px 16px", ...cardStyle }, children: [
979
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-state-error-primary)" }, children: t("configUnavailable") }),
980
+ t("configUnavailableNote")
834
981
  ] });
835
982
  }
836
983
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-dshvm-settings": "card", style: cardStyle, children: [
837
984
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: focusVisibleCss }),
838
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", "aria-expanded": !collapsed, onClick: () => setCollapsed((c) => !c), style: { ...setHeader, background: collapsed ? "transparent" : t.bgOpen }, children: [
985
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", "aria-expanded": !collapsed, onClick: () => setCollapsed((c) => !c), style: { ...setHeader, background: collapsed ? "transparent" : t2.bgOpen }, children: [
839
986
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: setHeadText, children: [
840
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: setName, children: "\u8BED\u97F3\u6A21\u5F0F" }),
841
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: setDesc, children: "\u97F3\u8272 / \u8BED\u901F / \u6253\u65AD\u7075\u654F\u5EA6 / \u9759\u97F3\u505C\u987F / \u7A7A\u95F2\u8D85\u65F6 / \u6A21\u578B\u955C\u50CF / \u81EA\u52A8\u53D1\u9001 / \u4EA4\u4E92\u6A21\u5F0F / \u5524\u9192\u8BCD / \u53E3\u8BED\u5316\u63D0\u793A\u8BCD" })
987
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: setName, children: t("stateVoiceMode") }),
988
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: setDesc, children: t("settingsCardDesc") })
842
989
  ] }),
843
990
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...setChevron, transform: collapsed ? "rotate(0deg)" : "rotate(180deg)" }, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 16 16", width: 14, height: 14, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { fill: "currentColor", d: "M4 6l4 4 4-4z" }) }) })
844
991
  ] }),
845
992
  !collapsed && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: setBody, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: 4 }, children: [
846
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "voice", desc: "Edge TTS \u97F3\u8272\uFF08\u4E0B\u62C9\u5E38\u7528\uFF0C\u5176\u4F59\u9009\u300C\u81EA\u5B9A\u4E49\u300D\u624B\u52A8\u586B ShortName\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
993
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "voice", desc: t("descVoice"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
847
994
  SelectField,
848
995
  {
849
996
  score: scope,
@@ -854,44 +1001,45 @@ function VoiceSettingsCard({ scope }) {
854
1001
  footer: (v) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VoicePreviewButton, { voice: v, rate: Number(value.rate ?? 1) })
855
1002
  }
856
1003
  ) }),
857
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "rate", desc: "\u6717\u8BFB\u8BED\u901F\u500D\u7387\uFF080.5 \u6162\u901F \uFF5E 2.0 \u5FEB\u901F\uFF0C1.0 \u6B63\u5E38\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NumberField, { score: scope, field: "rate", value: value.rate ?? 1, min: 0.5, max: 2, step: 0.1 }) }),
858
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "interruptLevel", desc: "\u53D1\u58F0\u6253\u65AD\u7075\u654F\u5EA6\uFF080 \u9AD8\u95E8\u69DB / 1 \u4E2D / 2 \u4F4E\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1004
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "rate", desc: t("descRate"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NumberField, { score: scope, field: "rate", value: value.rate ?? 1, min: 0.5, max: 2, step: 0.1 }) }),
1005
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "interruptLevel", desc: t("descInterrupt"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
859
1006
  SegGroup,
860
1007
  {
861
1008
  score: scope,
862
1009
  field: "interruptLevel",
863
1010
  value: value.interruptLevel,
864
1011
  options: [
865
- { v: 0, label: "0 \u9AD8\u95E8\u69DB" },
866
- { v: 1, label: "1 \u4E2D" },
867
- { v: 2, label: "2 \u4F4E" }
1012
+ { v: 0, label: t("sev0") },
1013
+ { v: 1, label: t("sev1") },
1014
+ { v: 2, label: t("sev2") }
868
1015
  ]
869
1016
  }
870
1017
  ) }),
871
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "silenceMs", desc: "\u8BF4\u5B8C\u6574\u4E00\u53E5\u7684\u9759\u97F3\u505C\u987F\u6BEB\u79D2\u6570\uFF08\u9ED8\u8BA4 2000 = 2 \u79D2\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NumberField, { score: scope, field: "silenceMs", value: value.silenceMs ?? 2e3, min: 500, max: 3e4, step: 100 }) }),
872
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "idleTimeoutMinutes", desc: "\u65E0\u6D3B\u52A8\u81EA\u52A8\u9000\u51FA\u8BED\u97F3\u6A21\u5F0F\u7684\u5206\u949F\u6570\uFF08\u9ED8\u8BA4 10\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NumberField, { score: scope, field: "idleTimeoutMinutes", value: value.idleTimeoutMinutes ?? 10, min: 1, max: 120, step: 1 }) }),
873
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "modelHost", desc: "ASR \u6A21\u578B\u4E0B\u8F7D\u6E90\uFF08\u5B98\u65B9\u6E90 / \u56FD\u5185\u955C\u50CF\uFF0C\u6216\u9009\u300C\u81EA\u5B9A\u4E49\u300D\u586B\u4EFB\u610F\u955C\u50CF\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectField, { score: scope, field: "modelHost", value: value.modelHost ?? "", options: HOST_OPTIONS, placeholder: "https://..." }) }),
874
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "autoSend", desc: "\u8BC6\u522B\u5B9A\u7A3F\u540E\u81EA\u52A8\u53D1\u9001\uFF08\u5173=\u53EA\u8FDB\u8349\u7A3F\uFF1B\u6309\u4F4F Ctrl / hold \u677E\u624B\u4ECD\u53D1\u9001\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", checked: Boolean(value.autoSend), onChange: (e) => void scope.set("autoSend", e.target.checked) }) }),
875
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "spokenFormat", desc: "\u8BED\u97F3\u4F1A\u8BDD\u6CE8\u5165\u53E3\u8BED\u5316\u63D0\u793A\u8BCD\uFF08\u56DE\u590D\u53E3\u8BED\u5316\u3001\u4E0D\u7528 Markdown \u6392\u7248\u7B26\u53F7\uFF0C\u6717\u8BFB\u66F4\u987A\uFF1B\u9ED8\u8BA4\u5173\uFF0C\u6539\u52A8\u5373\u65F6\u751F\u6548\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", checked: Boolean(value.spokenFormat), onChange: (e) => void scope.set("spokenFormat", e.target.checked) }) }),
876
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "mode", desc: "\u4EA4\u4E92\u6A21\u5F0F\uFF08toggle \u6301\u7EED\u8046\u542C+\u9759\u97F3\u65AD\u53E5 / hold \u6309\u4F4F\u8BF4\u8BDD\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1018
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "silenceMs", desc: t("descSilence"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NumberField, { score: scope, field: "silenceMs", value: value.silenceMs ?? 2e3, min: 500, max: 3e4, step: 100 }) }),
1019
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "idleTimeoutMinutes", desc: t("descIdle"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NumberField, { score: scope, field: "idleTimeoutMinutes", value: value.idleTimeoutMinutes ?? 10, min: 1, max: 120, step: 1 }) }),
1020
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "modelHost", desc: t("descModelHost"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectField, { score: scope, field: "modelHost", value: value.modelHost ?? "", options: HOST_OPTIONS, placeholder: "https://..." }) }),
1021
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "autoSend", desc: t("descAutoSend"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", checked: Boolean(value.autoSend), onChange: (e) => void scope.set("autoSend", e.target.checked) }) }),
1022
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "spokenFormat", desc: t("descSpokenFormat"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", checked: Boolean(value.spokenFormat), onChange: (e) => void scope.set("spokenFormat", e.target.checked) }) }),
1023
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "mode", desc: t("descMode"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
877
1024
  SegGroup,
878
1025
  {
879
1026
  score: scope,
880
1027
  field: "mode",
881
1028
  value: value.mode,
882
1029
  options: [
883
- { v: "toggle", label: "\u6301\u7EED\u8046\u542C" },
884
- { v: "hold", label: "\u6309\u4F4F\u8BF4\u8BDD" }
1030
+ { v: "toggle", label: t("modeToggle") },
1031
+ { v: "hold", label: t("modeHold") }
885
1032
  ]
886
1033
  }
887
1034
  ) }),
888
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "wakeWord", desc: "\u5524\u9192\u8BCD\uFF08\u9ED8\u8BA4\u5173\uFF1B\u5982\u300C\u4F60\u597D\u5C0FD\u300D\uFF0C\u8BF4\u51FA\u540E\u5F00\u59CB\u8BC6\u522B\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextField, { score: scope, field: "wakeWord", value: value.wakeWord ?? "", placeholder: "\u5982\uFF1A\u4F60\u597D\u5C0FD" }) })
1035
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "wakeWord", desc: t("descWakeWord"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextField, { score: scope, field: "wakeWord", value: value.wakeWord ?? "", placeholder: t("wakePlaceholder") }) })
889
1036
  ] }) })
890
1037
  ] });
891
1038
  }
892
1039
 
893
1040
  // src/client.tsx
894
1041
  var import_jsx_runtime2 = require("react/jsx-runtime");
1042
+ var beepCtx = null;
895
1043
  var inject = ["slots", "sessions", "settingsScope"];
896
1044
  var WAVE_BARS = 14;
897
1045
  var BASE_PATH2 = "/voice-mode";
@@ -941,7 +1089,7 @@ function apply(ctx) {
941
1089
  name: "settings.plugin.item",
942
1090
  key: "voice-mode",
943
1091
  order: 100,
944
- label: "\u8BED\u97F3\u6A21\u5F0F"
1092
+ label: t("stateVoiceMode")
945
1093
  },
946
1094
  () => React.createElement(VoiceSettingsCard, { scope: ctx.settingsScope.bind({ namespace: "voice-mode" }) })
947
1095
  )
@@ -973,10 +1121,12 @@ function createAudioEngine(setUi) {
973
1121
  setUi({ playing: true, playingCaption: frame.text, ttsNotice: null });
974
1122
  void audio.play().catch(() => playNext());
975
1123
  };
976
- let beepCtx = null;
977
1124
  const toolBeep = () => {
978
1125
  try {
979
- if (!beepCtx) beepCtx = new AudioContext();
1126
+ if (!beepCtx) {
1127
+ beepCtx = new AudioContext();
1128
+ void beepCtx.resume?.();
1129
+ }
980
1130
  const osc = beepCtx.createOscillator();
981
1131
  const gain = beepCtx.createGain();
982
1132
  osc.frequency.value = 880;
@@ -1100,7 +1250,7 @@ function createVoiceBus(basePath = BASE_PATH2, ctx) {
1100
1250
  source.addEventListener("asr-error", (e) => {
1101
1251
  try {
1102
1252
  const p = JSON.parse(e.data);
1103
- ui.error = `\u8BED\u97F3\u6A21\u578B\u4E0B\u8F7D\u5931\u8D25\uFF08${p.file ?? ""}\uFF09\uFF1A\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u65B0\u8FDB\u5165\u8BED\u97F3\u6A21\u5F0F\u91CD\u8BD5`;
1253
+ ui.error = t("modelDownloadFail").replace("{file}", p.file ?? "");
1104
1254
  ui.model = null;
1105
1255
  notify();
1106
1256
  } catch {
@@ -1110,7 +1260,7 @@ function createVoiceBus(basePath = BASE_PATH2, ctx) {
1110
1260
  try {
1111
1261
  const p = JSON.parse(e.data);
1112
1262
  if (p.sessionId === activeSessionId) {
1113
- ui.ttsNotice = "\u6717\u8BFB\u8FDE\u63A5\u5931\u8D25\uFF1A\u6B63\u5728\u91CD\u8BD5\u2026";
1263
+ ui.ttsNotice = t("ttsNoticeFail");
1114
1264
  notify();
1115
1265
  }
1116
1266
  } catch {
@@ -1148,10 +1298,10 @@ function createVoiceBus(basePath = BASE_PATH2, ctx) {
1148
1298
  const out = await res.json();
1149
1299
  activeSessionId = out.active ?? null;
1150
1300
  notify();
1151
- if (!res.ok) return { ok: false, error: out.error ?? "\u8FDB\u5165\u8BED\u97F3\u6A21\u5F0F\u5931\u8D25" };
1152
- return { ok: out.active === sessionId, error: out.active === sessionId ? void 0 : "\u8FDB\u5165\u8BED\u97F3\u6A21\u5F0F\u5931\u8D25" };
1301
+ if (!res.ok) return { ok: false, error: out.error ?? t("enterFail") };
1302
+ return { ok: out.active === sessionId, error: out.active === sessionId ? void 0 : t("enterFail") };
1153
1303
  } catch {
1154
- return { ok: false, error: "\u8FDB\u5165\u8BED\u97F3\u6A21\u5F0F\u5931\u8D25" };
1304
+ return { ok: false, error: t("enterFail") };
1155
1305
  }
1156
1306
  },
1157
1307
  async exit(sessionId) {
@@ -1226,7 +1376,7 @@ function MicButton({
1226
1376
  const [, bumpUi] = (0, import_react2.useState)(0);
1227
1377
  (0, import_react2.useEffect)(
1228
1378
  () => bus.subscribe(() => {
1229
- bumpUi((t2) => t2 + 1);
1379
+ bumpUi((t3) => t3 + 1);
1230
1380
  }),
1231
1381
  [bus]
1232
1382
  );
@@ -1308,7 +1458,7 @@ function MicButton({
1308
1458
  if (!entered.ok) {
1309
1459
  setLocalMode("off");
1310
1460
  bus.setUi({
1311
- error: entered.error === "voice mode disabled" ? "\u8BED\u97F3\u6A21\u5F0F\u5DF2\u7981\u7528\uFF08\u63D2\u4EF6 enabled=false\uFF09" : entered.error ?? "\u8FDB\u5165\u8BED\u97F3\u6A21\u5F0F\u5931\u8D25"
1461
+ error: entered.error === "voice mode disabled" ? t("disabled") : entered.error ?? t("enterFail")
1312
1462
  });
1313
1463
  return;
1314
1464
  }
@@ -1319,10 +1469,18 @@ function MicButton({
1319
1469
  const engine = createAsrEngine({ silenceMs, interruptLevel, basePath, wakeWord: cfg.wakeWord }, sid);
1320
1470
  bus.setUi({ mode: cfg.mode, wakeWord: cfg.wakeWord });
1321
1471
  engineRef.current = engine;
1472
+ try {
1473
+ if (!beepCtx) beepCtx = new AudioContext();
1474
+ void beepCtx.resume?.();
1475
+ } catch {
1476
+ }
1322
1477
  engine.onState((s) => {
1323
1478
  bus.setUi({ state: s });
1324
1479
  if (s === "idle") resetIdle();
1325
1480
  });
1481
+ engine.onError((key) => {
1482
+ bus.setUi({ error: t(key) });
1483
+ });
1326
1484
  engine.onLevel((l) => {
1327
1485
  const cur = bus.ui.levels;
1328
1486
  const next = cur.length < WAVE_BARS ? [...cur, l] : [...cur.slice(1), l];
@@ -1353,11 +1511,11 @@ function MicButton({
1353
1511
  const r = actions?.submit?.();
1354
1512
  if (r && typeof r.then === "function") {
1355
1513
  r.catch(() => {
1356
- bus.setUi({ error: "\u53D1\u9001\u5931\u8D25\uFF0C\u5DF2\u4FDD\u7559\u5728\u8349\u7A3F" });
1514
+ bus.setUi({ error: t("sendFailKept") });
1357
1515
  });
1358
1516
  }
1359
1517
  } catch {
1360
- bus.setUi({ error: "\u53D1\u9001\u5931\u8D25\uFF0C\u5DF2\u4FDD\u7559\u5728\u8349\u7A3F" });
1518
+ bus.setUi({ error: t("sendFailKept") });
1361
1519
  }
1362
1520
  };
1363
1521
  cancelPendingSubmit();
@@ -1389,7 +1547,7 @@ function MicButton({
1389
1547
  resetIdle();
1390
1548
  } catch (e) {
1391
1549
  setLocalMode("off");
1392
- const msg = e instanceof DOMException ? e.name === "NotAllowedError" ? "\u9EA6\u514B\u98CE\u88AB\u62D2\u7EDD\uFF1A\u8BF7\u5728\u6D4F\u89C8\u5668\u5730\u5740\u680F\u5141\u8BB8\u9EA6\u514B\u98CE\u6743\u9650" : "\u9EA6\u514B\u98CE\u4E0D\u53EF\u7528" : `\u8BED\u97F3\u6A21\u5F0F\u542F\u52A8\u5931\u8D25\uFF1A${String(e instanceof Error ? e.message : e)}`;
1550
+ const msg = e instanceof DOMException ? e.name === "NotAllowedError" ? t("micDenied") : t("micUnavailable") : t("startFail").replace("{err}", String(e instanceof Error ? e.message : e));
1393
1551
  bus.setUi({ error: msg });
1394
1552
  const sid2 = sidRef.current;
1395
1553
  if (sid2) void bus.exit(sid2);
@@ -1480,8 +1638,8 @@ function MicButton({
1480
1638
  }, []);
1481
1639
  (0, import_react2.useEffect)(() => {
1482
1640
  const onInput = (e) => {
1483
- const t2 = e.target;
1484
- if (!(t2 instanceof HTMLTextAreaElement)) return;
1641
+ const t3 = e.target;
1642
+ if (!(t3 instanceof HTMLTextAreaElement)) return;
1485
1643
  if (localRef.current !== "on") return;
1486
1644
  void exitModeRef.current("typing");
1487
1645
  };
@@ -1530,7 +1688,7 @@ function MicButton({
1530
1688
  const livePhase = useInput ? useInput((s) => s?.phase ?? "") : "";
1531
1689
  const phaseRef = (0, import_react2.useRef)("");
1532
1690
  phaseRef.current = livePhase;
1533
- const label = on ? busy ? "\u8BC6\u522B\u4E2D\u2026" : holdMode ? "\u6309\u4F4F\u8BF4\u8BDD" : "\u8BED\u97F3\u4E2D" : local === "pending" ? "\u8FDB\u5165\u4E2D\u2026" : "\u8BED\u97F3";
1691
+ const label = on ? busy ? t("recognizing") : holdMode ? t("holdToTalk") : t("voiceDetected") : local === "pending" ? t("entering") : t("voiceBtn");
1534
1692
  const holdPtrRef = (0, import_react2.useRef)(null);
1535
1693
  const onPointerDown = (e) => {
1536
1694
  if (bootNow().mode !== "hold") return;
@@ -1581,9 +1739,9 @@ function MicButton({
1581
1739
  onPointerUp,
1582
1740
  onPointerCancel,
1583
1741
  "data-dshvm": "mic",
1584
- "aria-label": on ? "\u8BED\u97F3\u6A21\u5F0F\u8FDB\u884C\u4E2D" : "\u8FDB\u5165\u8BED\u97F3\u5BF9\u8BDD\u6A21\u5F0F",
1742
+ "aria-label": on ? t("ariaActive") : t("ariaEnter"),
1585
1743
  "aria-pressed": on,
1586
- title: on ? holdMode ? "\u8BED\u97F3\u6A21\u5F0F\u8FDB\u884C\u4E2D \xB7 \u6309\u4F4F\u8BF4\u8BDD\u3001\u677E\u624B\u53D1\u9001\uFF1B\u77ED\u6309\u9000\u51FA\uFF1BEsc/\u5931\u53BB\u7126\u70B9\u653E\u5F03\uFF1BCtrl+Shift+V \u9000\u51FA" : "\u8BED\u97F3\u6A21\u5F0F\u8FDB\u884C\u4E2D \xB7 \u70B9\u51FB\u9000\u51FA\uFF08Ctrl+Shift+V\uFF09\xB7 \u6309\u4F4F Ctrl \u7ACB\u5373\u53D1\u9001" : "\u8FDB\u5165\u8BED\u97F3\u5BF9\u8BDD\u6A21\u5F0F\uFF08Ctrl+Shift+V\uFF09",
1744
+ title: on ? holdMode ? t("titleHold") : t("titleToggle") : t("titleEnter"),
1587
1745
  style: {
1588
1746
  border: "none",
1589
1747
  background: on ? holdMode ? "rgba(88, 166, 255, 0.16)" : "rgba(63, 185, 80, 0.16)" : local === "pending" ? "rgba(88, 166, 255, 0.14)" : "transparent",
@@ -1630,7 +1788,7 @@ function VoiceStatusBar({ bus, sessionId }) {
1630
1788
  }, [bus]);
1631
1789
  const isActive = b.active === sessionId;
1632
1790
  if (!isActive) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, {});
1633
- const stateText = b.ui.state === "loading-model" ? "\u6B63\u5728\u52A0\u8F7D\u6A21\u578B\u2026" : b.ui.state === "transcribing" ? "\u8BC6\u522B\u4E2D\u2026" : b.ui.state === "speech" ? b.ui.mode === "hold" ? "\u6309\u4F4F\u8BF4\u8BDD\u2026" : "\u8046\u542C\u4E2D\u2026" : b.ui.state === "wake" ? `\u8BF4\u300C${b.ui.wakeWord || "\u5524\u9192\u8BCD"}\u300D\u5F00\u59CB` : b.ui.mode === "hold" ? "\u8BED\u97F3\u6A21\u5F0F \xB7 \u6309\u4F4F\u8BF4\u8BDD\uFF08\u77ED\u6309\u9000\u51FA\uFF09" : "\u8BED\u97F3\u6A21\u5F0F \xB7 \u8046\u542C\u4E2D\u2026";
1791
+ const stateText = b.ui.state === "loading-model" ? t("loadingModel") : b.ui.state === "transcribing" ? t("recognizing") : b.ui.state === "speech" ? b.ui.mode === "hold" ? t("holdDots") : t("listening") : b.ui.state === "wake" ? t("sayWake").replace("{wake}", b.ui.wakeWord || t("wakeWord")) : b.ui.mode === "hold" ? t("barHold") : t("barListening");
1634
1792
  const bars = Array.from({ length: WAVE_BARS }, (_, i) => b.ui.levels[i] ?? 0);
1635
1793
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1636
1794
  "div",
@@ -1661,7 +1819,7 @@ function VoiceStatusBar({ bus, sessionId }) {
1661
1819
  },
1662
1820
  i
1663
1821
  )) }),
1664
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flexGrow: 1 }, children: b.ui.error ? b.ui.error : b.ui.state === "loading-model" || b.ui.model ? b.ui.model ? `\u6B63\u5728\u52A0\u8F7D\u6A21\u578B\u2026 ${b.ui.model.file} ${b.ui.model.percent}%` : stateText : b.ui.partial ? b.ui.partial : b.ui.ttsNotice ? b.ui.ttsNotice : stateText }),
1822
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flexGrow: 1 }, children: b.ui.error ? b.ui.error : b.ui.state === "loading-model" || b.ui.model ? b.ui.model ? `${t("loadingModel")} ${b.ui.model.file} ${b.ui.model.percent}%` : stateText : b.ui.partial ? b.ui.partial : b.ui.ttsNotice ? b.ui.ttsNotice : stateText }),
1665
1823
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1666
1824
  "button",
1667
1825
  {
@@ -1676,7 +1834,7 @@ function VoiceStatusBar({ bus, sessionId }) {
1676
1834
  fontSize: 12,
1677
1835
  flexShrink: 0
1678
1836
  },
1679
- children: "\u9000\u51FA"
1837
+ children: t("exit")
1680
1838
  }
1681
1839
  )
1682
1840
  ]
@@ -1731,7 +1889,7 @@ function VoiceOverlay({ bus }) {
1731
1889
  },
1732
1890
  i
1733
1891
  )) }),
1734
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: b.ui.playingCaption ?? "\u6717\u8BFB\u4E2D\u2026" }, b.ui.playingCaption ?? "idle"),
1892
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: b.ui.playingCaption ?? t("reading") }, b.ui.playingCaption ?? "idle"),
1735
1893
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1736
1894
  "button",
1737
1895
  {
@@ -1746,7 +1904,7 @@ function VoiceOverlay({ bus }) {
1746
1904
  cursor: "pointer",
1747
1905
  flexShrink: 0
1748
1906
  },
1749
- children: "\u8DF3\u8FC7"
1907
+ children: t("skip")
1750
1908
  }
1751
1909
  )
1752
1910
  ]
package/lib/index.js CHANGED
@@ -342,7 +342,7 @@ var TtsQueue = class {
342
342
  enqueue(sessionId, text) {
343
343
  let q = this.queues.get(sessionId);
344
344
  if (!q) {
345
- q = { pending: [], busy: false, seq: 0, epoch: 0, errorNotified: false };
345
+ q = { pending: [], busy: false, seq: 0, epoch: 0, errorNotified: false, backoff: 0 };
346
346
  this.queues.set(sessionId, q);
347
347
  }
348
348
  q.pending.push({ text, epoch: q.epoch });
@@ -404,7 +404,12 @@ var TtsQueue = class {
404
404
  }
405
405
  } finally {
406
406
  q.busy = false;
407
- if (q.pending.length > 0) void this.pump(sessionId, q);
407
+ if (q.pending.length > 0) {
408
+ const delay = q.errorNotified ? q.backoff : 0;
409
+ q.backoff = Math.min(8e3, delay + 1e3);
410
+ if (delay > 0) setTimeout(() => void this.pump(sessionId, q), delay);
411
+ else void this.pump(sessionId, q);
412
+ }
408
413
  }
409
414
  }
410
415
  async close() {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-voice-mode",
3
3
  "description": "Full-duplex voice mode for DeepSeek Harness: zipformer2 streaming ASR → editable draft, Edge TTS sentence-by-sentence read-aloud with live captions, true barge-in — on-device ASR, no API key. · DSH 语音双工对话:流式识别入草稿、按句朗读+实时字幕、开口即打断,识别本地推理、无需 API Key",
4
- "version": "0.1.5",
4
+ "version": "0.2.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "repository": {