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 +6 -0
- package/lib/client.js +232 -74
- package/lib/index.js +7 -2
- package/package.json +1 -1
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
|
|
112
|
-
if (!
|
|
120
|
+
const t3 = text.trim();
|
|
121
|
+
if (!t3) return;
|
|
113
122
|
for (const fn of listeners) {
|
|
114
123
|
try {
|
|
115
|
-
fn(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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((
|
|
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
|
|
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 ${
|
|
519
|
-
background:
|
|
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:
|
|
540
|
-
var setDesc = { color:
|
|
541
|
-
var setChevron = { color:
|
|
542
|
-
var setBody = { borderTop: `1px solid ${
|
|
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:
|
|
547
|
-
var setSeg = { border: `1px solid ${
|
|
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 ?
|
|
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 ${
|
|
710
|
+
border: `1px solid ${t2.border}`,
|
|
567
711
|
background: "var(--dsw-alias-bg-layer-2)",
|
|
568
|
-
color:
|
|
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.
|
|
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("
|
|
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("
|
|
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("
|
|
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" ? "
|
|
916
|
+
e instanceof DOMException && e.name === "NotAllowedError" ? t("previewAutoplay") : t("previewPlayFail")
|
|
770
917
|
);
|
|
771
918
|
}
|
|
772
919
|
} catch {
|
|
773
|
-
setNote("
|
|
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:
|
|
933
|
+
color: t2.label,
|
|
787
934
|
background: "var(--dsw-alias-bg-layer-2)",
|
|
788
|
-
border: `1px solid ${
|
|
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: "
|
|
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 ? "
|
|
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:
|
|
832
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-state-error-primary)" }, children: "
|
|
833
|
-
"
|
|
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" :
|
|
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: "
|
|
841
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: setDesc, children: "
|
|
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: "
|
|
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: "
|
|
858
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "interruptLevel", desc: "
|
|
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: "
|
|
866
|
-
{ v: 1, label: "
|
|
867
|
-
{ v: 2, label: "
|
|
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: "
|
|
872
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "idleTimeoutMinutes", desc: "
|
|
873
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "modelHost", desc: "
|
|
874
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "autoSend", desc: "
|
|
875
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "spokenFormat", desc: "
|
|
876
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "mode", desc: "
|
|
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: "
|
|
884
|
-
{ v: "hold", label: "
|
|
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: "
|
|
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: "
|
|
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)
|
|
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 =
|
|
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 = "
|
|
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 ?? "
|
|
1152
|
-
return { ok: out.active === sessionId, error: out.active === sessionId ? void 0 : "
|
|
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: "
|
|
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((
|
|
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" ? "
|
|
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: "
|
|
1514
|
+
bus.setUi({ error: t("sendFailKept") });
|
|
1357
1515
|
});
|
|
1358
1516
|
}
|
|
1359
1517
|
} catch {
|
|
1360
|
-
bus.setUi({ error: "
|
|
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" ? "
|
|
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
|
|
1484
|
-
if (!(
|
|
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 ? "
|
|
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 ? "
|
|
1742
|
+
"aria-label": on ? t("ariaActive") : t("ariaEnter"),
|
|
1585
1743
|
"aria-pressed": on,
|
|
1586
|
-
title: on ? holdMode ? "
|
|
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" ? "
|
|
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 ?
|
|
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: "
|
|
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 ?? "
|
|
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: "
|
|
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)
|
|
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.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"repository": {
|