dsh-voice-mode 0.1.4 → 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 +9 -1
- package/lib/client.js +240 -76
- package/lib/index.js +10 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ DeepSeek Harness 语音双工对话模式:会话内一键进入 → 边说边
|
|
|
29
29
|
- **开口打断(barge-in)**:三档灵敏度的发声前沿检测 → 本地静音 + host 合成队列作废(epoch)+ 正在运行的回合取消(保留半截并自然续入你的新消息)
|
|
30
30
|
- **模型懒加载与进度**:首次使用自动下载 zipformer2 中文流式模型(约 160MB,`.part` 断点续传),状态条实时显示下载进度;可用 `npm run prefetch` 预下载
|
|
31
31
|
- **容错**:麦克风被拒红点提示、模型下载失败可见提示、TTS 连接失败状态条提示(自动重试)、提交失败文字留在草稿、SSE 断线自动重连
|
|
32
|
-
- **设置**:设置 → 插件配置 → voice-mode
|
|
32
|
+
- **设置**:设置 → Plugins → 插件配置 → 语音模式(voice-mode),可调音色 / 语速 / 打断灵敏度 / 静音停顿 / 空闲超时 / 模型镜像 / 自动发送 / 交互模式 / 唤醒词 / 口语化提示词;**音色可试听**(「试听」按钮按当前音色 + 当前语速即时合成预览,无需进入语音模式;自定义 ShortName 同样可试听)
|
|
33
33
|
- **空闲退出**:10 分钟无活动自动退出并释放麦克风
|
|
34
34
|
|
|
35
35
|
## 操作手势
|
|
@@ -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
|
|
|
@@ -243,6 +249,8 @@ pnpm install && pnpm build # esbuild:lib/index.js(host)+ lib/client.js
|
|
|
243
249
|
pnpm test # segmenter/wakeword 单测 + 发布前自检(均无需网络)
|
|
244
250
|
node test/hold-e2e.js # hold 模式验收(独立浏览器,/asr 路由拦截)
|
|
245
251
|
bash test/spoken-prompt-rpc.sh # 口语化提示词验证(RPC 直发,无需浏览器;需在线 TTS)
|
|
252
|
+
# 注:hold-e2e/spoken-prompt-rpc/spoken-toggle-ui-check 等集成探测脚本位于仓库根 test/
|
|
253
|
+
#(不在 npm 包内);npm 包内 test/ 仅含无需网络的离线单测。
|
|
246
254
|
systemctl restart dsh # Linux;其他平台重启 dsh 进程
|
|
247
255
|
```
|
|
248
256
|
|
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,11 +157,11 @@ 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 {
|
|
155
|
-
resolve(new Response(null, { status:
|
|
164
|
+
resolve(new Response(null, { status: 503 }));
|
|
156
165
|
}
|
|
157
166
|
}, 5e3);
|
|
158
167
|
});
|
|
@@ -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,11 +225,11 @@ 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 {
|
|
223
|
-
resolve(new Response(null, { status:
|
|
232
|
+
resolve(new Response(null, { status: 503 }));
|
|
224
233
|
}
|
|
225
234
|
}, 5e3);
|
|
226
235
|
});
|
|
@@ -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
|
};
|
|
@@ -358,6 +368,10 @@ function createAsrEngine(config, sessionId) {
|
|
|
358
368
|
});
|
|
359
369
|
const AC = window.AudioContext ?? window.webkitAudioContext;
|
|
360
370
|
audioCtx = new AC({ sampleRate: SAMPLE_RATE });
|
|
371
|
+
try {
|
|
372
|
+
await audioCtx.resume?.();
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
361
375
|
ctxRate = audioCtx.sampleRate;
|
|
362
376
|
const source = audioCtx.createMediaStreamSource(stream);
|
|
363
377
|
processor = audioCtx.createScriptProcessor(BUFFER_SIZE, 1, 1);
|
|
@@ -387,7 +401,7 @@ function createAsrEngine(config, sessionId) {
|
|
|
387
401
|
}
|
|
388
402
|
processor = null;
|
|
389
403
|
try {
|
|
390
|
-
stream?.getTracks().forEach((
|
|
404
|
+
stream?.getTracks().forEach((t3) => t3.stop());
|
|
391
405
|
} catch {
|
|
392
406
|
}
|
|
393
407
|
stream = null;
|
|
@@ -470,6 +484,12 @@ function createAsrEngine(config, sessionId) {
|
|
|
470
484
|
transcriptListeners.delete(fn);
|
|
471
485
|
};
|
|
472
486
|
},
|
|
487
|
+
onError(fn) {
|
|
488
|
+
errorListeners.add(fn);
|
|
489
|
+
return () => {
|
|
490
|
+
errorListeners.delete(fn);
|
|
491
|
+
};
|
|
492
|
+
},
|
|
473
493
|
onPartial(fn) {
|
|
474
494
|
partialListeners.add(fn);
|
|
475
495
|
return () => {
|
|
@@ -498,10 +518,138 @@ function createAsrEngine(config, sessionId) {
|
|
|
498
518
|
};
|
|
499
519
|
}
|
|
500
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
|
+
|
|
501
649
|
// src/settings-form.tsx
|
|
502
650
|
var import_react = require("react");
|
|
503
651
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
504
|
-
var
|
|
652
|
+
var t2 = {
|
|
505
653
|
bg: "var(--dsw-alias-bg-layer-3)",
|
|
506
654
|
bgOpen: "var(--dsw-alias-bg-layer-2)",
|
|
507
655
|
border: "var(--dsw-alias-border-l2)",
|
|
@@ -511,8 +659,8 @@ var t = {
|
|
|
511
659
|
};
|
|
512
660
|
var BASE_PATH = "/voice-mode";
|
|
513
661
|
var cardStyle = {
|
|
514
|
-
border: `1px solid ${
|
|
515
|
-
background:
|
|
662
|
+
border: `1px solid ${t2.border}`,
|
|
663
|
+
background: t2.bg,
|
|
516
664
|
borderRadius: 12,
|
|
517
665
|
overflow: "hidden"
|
|
518
666
|
};
|
|
@@ -532,18 +680,18 @@ var setHeader = {
|
|
|
532
680
|
display: "flex"
|
|
533
681
|
};
|
|
534
682
|
var setHeadText = { flexDirection: "column", flex: 1, gap: 4, minWidth: 0, display: "flex" };
|
|
535
|
-
var setName = { color:
|
|
536
|
-
var setDesc = { color:
|
|
537
|
-
var setChevron = { color:
|
|
538
|
-
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 };
|
|
539
687
|
var setRow = { alignItems: "center", gap: 12, padding: "12px 0", display: "flex" };
|
|
540
688
|
var setLabelBox = { flexDirection: "column", flex: 1, gap: 3, minWidth: 0, display: "flex" };
|
|
541
689
|
var setLabel = { fontSize: 13, lineHeight: "20px" };
|
|
542
|
-
var setHint = { color:
|
|
543
|
-
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" };
|
|
544
692
|
var setSegBtn = (on) => ({
|
|
545
693
|
font: "inherit",
|
|
546
|
-
color: on ?
|
|
694
|
+
color: on ? t2.label : "var(--dsw-alias-label-secondary)",
|
|
547
695
|
cursor: "pointer",
|
|
548
696
|
background: on ? "var(--dsw-alias-bg-layer-2)" : "transparent",
|
|
549
697
|
border: "none",
|
|
@@ -559,9 +707,9 @@ var inputStyle = {
|
|
|
559
707
|
maxWidth: "100%",
|
|
560
708
|
padding: "7px 10px",
|
|
561
709
|
borderRadius: 8,
|
|
562
|
-
border: `1px solid ${
|
|
710
|
+
border: `1px solid ${t2.border}`,
|
|
563
711
|
background: "var(--dsw-alias-bg-layer-2)",
|
|
564
|
-
color:
|
|
712
|
+
color: t2.label,
|
|
565
713
|
fontSize: 13,
|
|
566
714
|
fontFamily: "inherit",
|
|
567
715
|
outline: "none"
|
|
@@ -696,7 +844,10 @@ function SelectField({
|
|
|
696
844
|
},
|
|
697
845
|
children: [
|
|
698
846
|
options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: o.v, children: o.label }, o.v)),
|
|
699
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
847
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("option", { value: "__custom__", children: [
|
|
848
|
+
t("custom"),
|
|
849
|
+
"\u2026"
|
|
850
|
+
] })
|
|
700
851
|
]
|
|
701
852
|
}
|
|
702
853
|
),
|
|
@@ -724,7 +875,7 @@ function VoicePreviewButton({ voice, rate }) {
|
|
|
724
875
|
if (busy) return;
|
|
725
876
|
const v = voice.trim();
|
|
726
877
|
if (!v) {
|
|
727
|
-
setNote("
|
|
878
|
+
setNote(t("previewNameFirst"));
|
|
728
879
|
return;
|
|
729
880
|
}
|
|
730
881
|
setBusy(true);
|
|
@@ -745,7 +896,7 @@ function VoicePreviewButton({ voice, rate }) {
|
|
|
745
896
|
signal: AbortSignal.timeout(15e3)
|
|
746
897
|
});
|
|
747
898
|
if (res.status === 403) {
|
|
748
|
-
setNote("
|
|
899
|
+
setNote(t("previewDisabled"));
|
|
749
900
|
return;
|
|
750
901
|
}
|
|
751
902
|
if (!res.ok) throw new Error(`preview http ${res.status}`);
|
|
@@ -755,18 +906,18 @@ function VoicePreviewButton({ voice, rate }) {
|
|
|
755
906
|
audio.onended = () => URL.revokeObjectURL(url);
|
|
756
907
|
audio.onerror = () => {
|
|
757
908
|
URL.revokeObjectURL(url);
|
|
758
|
-
setNote("
|
|
909
|
+
setNote(t("previewPlayFail"));
|
|
759
910
|
};
|
|
760
911
|
try {
|
|
761
912
|
await audio.play();
|
|
762
913
|
} catch (e) {
|
|
763
914
|
URL.revokeObjectURL(url);
|
|
764
915
|
setNote(
|
|
765
|
-
e instanceof DOMException && e.name === "NotAllowedError" ? "
|
|
916
|
+
e instanceof DOMException && e.name === "NotAllowedError" ? t("previewAutoplay") : t("previewPlayFail")
|
|
766
917
|
);
|
|
767
918
|
}
|
|
768
919
|
} catch {
|
|
769
|
-
setNote("
|
|
920
|
+
setNote(t("previewCheck"));
|
|
770
921
|
} finally {
|
|
771
922
|
setBusy(false);
|
|
772
923
|
}
|
|
@@ -779,18 +930,18 @@ function VoicePreviewButton({ voice, rate }) {
|
|
|
779
930
|
gap: 5,
|
|
780
931
|
alignSelf: "flex-start",
|
|
781
932
|
cursor: busy ? "default" : "pointer",
|
|
782
|
-
color:
|
|
933
|
+
color: t2.label,
|
|
783
934
|
background: "var(--dsw-alias-bg-layer-2)",
|
|
784
|
-
border: `1px solid ${
|
|
935
|
+
border: `1px solid ${t2.border}`,
|
|
785
936
|
borderRadius: 6,
|
|
786
937
|
padding: "4px 10px",
|
|
787
938
|
fontSize: 12,
|
|
788
939
|
lineHeight: "18px"
|
|
789
940
|
};
|
|
790
941
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { display: "flex", flexDirection: "column", gap: 4, alignItems: "flex-start" }, children: [
|
|
791
|
-
/* @__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: [
|
|
792
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" }) }),
|
|
793
|
-
busy ? "
|
|
944
|
+
busy ? t("synthesizing") : t("preview")
|
|
794
945
|
] }),
|
|
795
946
|
note && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-state-error-primary)", fontSize: 12, lineHeight: "18px" }, children: note })
|
|
796
947
|
] });
|
|
@@ -824,22 +975,22 @@ function VoiceSettingsCard({ scope }) {
|
|
|
824
975
|
const value = snap?.value ?? {};
|
|
825
976
|
const unavailable = snap?.status === "unavailable" || snap?.status === "error";
|
|
826
977
|
if (unavailable) {
|
|
827
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-dshvm-settings": "card", style: { color:
|
|
828
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-state-error-primary)" }, children: "
|
|
829
|
-
"
|
|
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")
|
|
830
981
|
] });
|
|
831
982
|
}
|
|
832
983
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-dshvm-settings": "card", style: cardStyle, children: [
|
|
833
984
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: focusVisibleCss }),
|
|
834
|
-
/* @__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: [
|
|
835
986
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: setHeadText, children: [
|
|
836
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: setName, children: "
|
|
837
|
-
/* @__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") })
|
|
838
989
|
] }),
|
|
839
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" }) }) })
|
|
840
991
|
] }),
|
|
841
992
|
!collapsed && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: setBody, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: 4 }, children: [
|
|
842
|
-
/* @__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)(
|
|
843
994
|
SelectField,
|
|
844
995
|
{
|
|
845
996
|
score: scope,
|
|
@@ -850,44 +1001,45 @@ function VoiceSettingsCard({ scope }) {
|
|
|
850
1001
|
footer: (v) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VoicePreviewButton, { voice: v, rate: Number(value.rate ?? 1) })
|
|
851
1002
|
}
|
|
852
1003
|
) }),
|
|
853
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "rate", desc: "
|
|
854
|
-
/* @__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)(
|
|
855
1006
|
SegGroup,
|
|
856
1007
|
{
|
|
857
1008
|
score: scope,
|
|
858
1009
|
field: "interruptLevel",
|
|
859
1010
|
value: value.interruptLevel,
|
|
860
1011
|
options: [
|
|
861
|
-
{ v: 0, label: "
|
|
862
|
-
{ v: 1, label: "
|
|
863
|
-
{ v: 2, label: "
|
|
1012
|
+
{ v: 0, label: t("sev0") },
|
|
1013
|
+
{ v: 1, label: t("sev1") },
|
|
1014
|
+
{ v: 2, label: t("sev2") }
|
|
864
1015
|
]
|
|
865
1016
|
}
|
|
866
1017
|
) }),
|
|
867
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "silenceMs", desc: "
|
|
868
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "idleTimeoutMinutes", desc: "
|
|
869
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "modelHost", desc: "
|
|
870
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "autoSend", desc: "
|
|
871
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "spokenFormat", desc: "
|
|
872
|
-
/* @__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)(
|
|
873
1024
|
SegGroup,
|
|
874
1025
|
{
|
|
875
1026
|
score: scope,
|
|
876
1027
|
field: "mode",
|
|
877
1028
|
value: value.mode,
|
|
878
1029
|
options: [
|
|
879
|
-
{ v: "toggle", label: "
|
|
880
|
-
{ v: "hold", label: "
|
|
1030
|
+
{ v: "toggle", label: t("modeToggle") },
|
|
1031
|
+
{ v: "hold", label: t("modeHold") }
|
|
881
1032
|
]
|
|
882
1033
|
}
|
|
883
1034
|
) }),
|
|
884
|
-
/* @__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") }) })
|
|
885
1036
|
] }) })
|
|
886
1037
|
] });
|
|
887
1038
|
}
|
|
888
1039
|
|
|
889
1040
|
// src/client.tsx
|
|
890
1041
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
1042
|
+
var beepCtx = null;
|
|
891
1043
|
var inject = ["slots", "sessions", "settingsScope"];
|
|
892
1044
|
var WAVE_BARS = 14;
|
|
893
1045
|
var BASE_PATH2 = "/voice-mode";
|
|
@@ -937,7 +1089,7 @@ function apply(ctx) {
|
|
|
937
1089
|
name: "settings.plugin.item",
|
|
938
1090
|
key: "voice-mode",
|
|
939
1091
|
order: 100,
|
|
940
|
-
label: "
|
|
1092
|
+
label: t("stateVoiceMode")
|
|
941
1093
|
},
|
|
942
1094
|
() => React.createElement(VoiceSettingsCard, { scope: ctx.settingsScope.bind({ namespace: "voice-mode" }) })
|
|
943
1095
|
)
|
|
@@ -969,10 +1121,12 @@ function createAudioEngine(setUi) {
|
|
|
969
1121
|
setUi({ playing: true, playingCaption: frame.text, ttsNotice: null });
|
|
970
1122
|
void audio.play().catch(() => playNext());
|
|
971
1123
|
};
|
|
972
|
-
let beepCtx = null;
|
|
973
1124
|
const toolBeep = () => {
|
|
974
1125
|
try {
|
|
975
|
-
if (!beepCtx)
|
|
1126
|
+
if (!beepCtx) {
|
|
1127
|
+
beepCtx = new AudioContext();
|
|
1128
|
+
void beepCtx.resume?.();
|
|
1129
|
+
}
|
|
976
1130
|
const osc = beepCtx.createOscillator();
|
|
977
1131
|
const gain = beepCtx.createGain();
|
|
978
1132
|
osc.frequency.value = 880;
|
|
@@ -1096,7 +1250,7 @@ function createVoiceBus(basePath = BASE_PATH2, ctx) {
|
|
|
1096
1250
|
source.addEventListener("asr-error", (e) => {
|
|
1097
1251
|
try {
|
|
1098
1252
|
const p = JSON.parse(e.data);
|
|
1099
|
-
ui.error =
|
|
1253
|
+
ui.error = t("modelDownloadFail").replace("{file}", p.file ?? "");
|
|
1100
1254
|
ui.model = null;
|
|
1101
1255
|
notify();
|
|
1102
1256
|
} catch {
|
|
@@ -1106,7 +1260,7 @@ function createVoiceBus(basePath = BASE_PATH2, ctx) {
|
|
|
1106
1260
|
try {
|
|
1107
1261
|
const p = JSON.parse(e.data);
|
|
1108
1262
|
if (p.sessionId === activeSessionId) {
|
|
1109
|
-
ui.ttsNotice = "
|
|
1263
|
+
ui.ttsNotice = t("ttsNoticeFail");
|
|
1110
1264
|
notify();
|
|
1111
1265
|
}
|
|
1112
1266
|
} catch {
|
|
@@ -1144,10 +1298,10 @@ function createVoiceBus(basePath = BASE_PATH2, ctx) {
|
|
|
1144
1298
|
const out = await res.json();
|
|
1145
1299
|
activeSessionId = out.active ?? null;
|
|
1146
1300
|
notify();
|
|
1147
|
-
if (!res.ok) return { ok: false, error: out.error ?? "
|
|
1148
|
-
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") };
|
|
1149
1303
|
} catch {
|
|
1150
|
-
return { ok: false, error: "
|
|
1304
|
+
return { ok: false, error: t("enterFail") };
|
|
1151
1305
|
}
|
|
1152
1306
|
},
|
|
1153
1307
|
async exit(sessionId) {
|
|
@@ -1222,7 +1376,7 @@ function MicButton({
|
|
|
1222
1376
|
const [, bumpUi] = (0, import_react2.useState)(0);
|
|
1223
1377
|
(0, import_react2.useEffect)(
|
|
1224
1378
|
() => bus.subscribe(() => {
|
|
1225
|
-
bumpUi((
|
|
1379
|
+
bumpUi((t3) => t3 + 1);
|
|
1226
1380
|
}),
|
|
1227
1381
|
[bus]
|
|
1228
1382
|
);
|
|
@@ -1304,7 +1458,7 @@ function MicButton({
|
|
|
1304
1458
|
if (!entered.ok) {
|
|
1305
1459
|
setLocalMode("off");
|
|
1306
1460
|
bus.setUi({
|
|
1307
|
-
error: entered.error === "voice mode disabled" ? "
|
|
1461
|
+
error: entered.error === "voice mode disabled" ? t("disabled") : entered.error ?? t("enterFail")
|
|
1308
1462
|
});
|
|
1309
1463
|
return;
|
|
1310
1464
|
}
|
|
@@ -1315,10 +1469,18 @@ function MicButton({
|
|
|
1315
1469
|
const engine = createAsrEngine({ silenceMs, interruptLevel, basePath, wakeWord: cfg.wakeWord }, sid);
|
|
1316
1470
|
bus.setUi({ mode: cfg.mode, wakeWord: cfg.wakeWord });
|
|
1317
1471
|
engineRef.current = engine;
|
|
1472
|
+
try {
|
|
1473
|
+
if (!beepCtx) beepCtx = new AudioContext();
|
|
1474
|
+
void beepCtx.resume?.();
|
|
1475
|
+
} catch {
|
|
1476
|
+
}
|
|
1318
1477
|
engine.onState((s) => {
|
|
1319
1478
|
bus.setUi({ state: s });
|
|
1320
1479
|
if (s === "idle") resetIdle();
|
|
1321
1480
|
});
|
|
1481
|
+
engine.onError((key) => {
|
|
1482
|
+
bus.setUi({ error: t(key) });
|
|
1483
|
+
});
|
|
1322
1484
|
engine.onLevel((l) => {
|
|
1323
1485
|
const cur = bus.ui.levels;
|
|
1324
1486
|
const next = cur.length < WAVE_BARS ? [...cur, l] : [...cur.slice(1), l];
|
|
@@ -1349,11 +1511,11 @@ function MicButton({
|
|
|
1349
1511
|
const r = actions?.submit?.();
|
|
1350
1512
|
if (r && typeof r.then === "function") {
|
|
1351
1513
|
r.catch(() => {
|
|
1352
|
-
bus.setUi({ error: "
|
|
1514
|
+
bus.setUi({ error: t("sendFailKept") });
|
|
1353
1515
|
});
|
|
1354
1516
|
}
|
|
1355
1517
|
} catch {
|
|
1356
|
-
bus.setUi({ error: "
|
|
1518
|
+
bus.setUi({ error: t("sendFailKept") });
|
|
1357
1519
|
}
|
|
1358
1520
|
};
|
|
1359
1521
|
cancelPendingSubmit();
|
|
@@ -1385,7 +1547,7 @@ function MicButton({
|
|
|
1385
1547
|
resetIdle();
|
|
1386
1548
|
} catch (e) {
|
|
1387
1549
|
setLocalMode("off");
|
|
1388
|
-
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));
|
|
1389
1551
|
bus.setUi({ error: msg });
|
|
1390
1552
|
const sid2 = sidRef.current;
|
|
1391
1553
|
if (sid2) void bus.exit(sid2);
|
|
@@ -1476,8 +1638,8 @@ function MicButton({
|
|
|
1476
1638
|
}, []);
|
|
1477
1639
|
(0, import_react2.useEffect)(() => {
|
|
1478
1640
|
const onInput = (e) => {
|
|
1479
|
-
const
|
|
1480
|
-
if (!(
|
|
1641
|
+
const t3 = e.target;
|
|
1642
|
+
if (!(t3 instanceof HTMLTextAreaElement)) return;
|
|
1481
1643
|
if (localRef.current !== "on") return;
|
|
1482
1644
|
void exitModeRef.current("typing");
|
|
1483
1645
|
};
|
|
@@ -1526,7 +1688,7 @@ function MicButton({
|
|
|
1526
1688
|
const livePhase = useInput ? useInput((s) => s?.phase ?? "") : "";
|
|
1527
1689
|
const phaseRef = (0, import_react2.useRef)("");
|
|
1528
1690
|
phaseRef.current = livePhase;
|
|
1529
|
-
const label = on ? busy ? "
|
|
1691
|
+
const label = on ? busy ? t("recognizing") : holdMode ? t("holdToTalk") : t("voiceDetected") : local === "pending" ? t("entering") : t("voiceBtn");
|
|
1530
1692
|
const holdPtrRef = (0, import_react2.useRef)(null);
|
|
1531
1693
|
const onPointerDown = (e) => {
|
|
1532
1694
|
if (bootNow().mode !== "hold") return;
|
|
@@ -1577,9 +1739,9 @@ function MicButton({
|
|
|
1577
1739
|
onPointerUp,
|
|
1578
1740
|
onPointerCancel,
|
|
1579
1741
|
"data-dshvm": "mic",
|
|
1580
|
-
"aria-label": on ? "
|
|
1742
|
+
"aria-label": on ? t("ariaActive") : t("ariaEnter"),
|
|
1581
1743
|
"aria-pressed": on,
|
|
1582
|
-
title: on ? holdMode ? "
|
|
1744
|
+
title: on ? holdMode ? t("titleHold") : t("titleToggle") : t("titleEnter"),
|
|
1583
1745
|
style: {
|
|
1584
1746
|
border: "none",
|
|
1585
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",
|
|
@@ -1626,7 +1788,7 @@ function VoiceStatusBar({ bus, sessionId }) {
|
|
|
1626
1788
|
}, [bus]);
|
|
1627
1789
|
const isActive = b.active === sessionId;
|
|
1628
1790
|
if (!isActive) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, {});
|
|
1629
|
-
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");
|
|
1630
1792
|
const bars = Array.from({ length: WAVE_BARS }, (_, i) => b.ui.levels[i] ?? 0);
|
|
1631
1793
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1632
1794
|
"div",
|
|
@@ -1657,7 +1819,7 @@ function VoiceStatusBar({ bus, sessionId }) {
|
|
|
1657
1819
|
},
|
|
1658
1820
|
i
|
|
1659
1821
|
)) }),
|
|
1660
|
-
/* @__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 }),
|
|
1661
1823
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1662
1824
|
"button",
|
|
1663
1825
|
{
|
|
@@ -1672,7 +1834,7 @@ function VoiceStatusBar({ bus, sessionId }) {
|
|
|
1672
1834
|
fontSize: 12,
|
|
1673
1835
|
flexShrink: 0
|
|
1674
1836
|
},
|
|
1675
|
-
children: "
|
|
1837
|
+
children: t("exit")
|
|
1676
1838
|
}
|
|
1677
1839
|
)
|
|
1678
1840
|
]
|
|
@@ -1688,6 +1850,8 @@ function VoiceOverlay({ bus }) {
|
|
|
1688
1850
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1689
1851
|
"div",
|
|
1690
1852
|
{
|
|
1853
|
+
role: "status",
|
|
1854
|
+
"aria-live": "polite",
|
|
1691
1855
|
style: {
|
|
1692
1856
|
position: "fixed",
|
|
1693
1857
|
right: 16,
|
|
@@ -1725,7 +1889,7 @@ function VoiceOverlay({ bus }) {
|
|
|
1725
1889
|
},
|
|
1726
1890
|
i
|
|
1727
1891
|
)) }),
|
|
1728
|
-
/* @__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"),
|
|
1729
1893
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1730
1894
|
"button",
|
|
1731
1895
|
{
|
|
@@ -1740,7 +1904,7 @@ function VoiceOverlay({ bus }) {
|
|
|
1740
1904
|
cursor: "pointer",
|
|
1741
1905
|
flexShrink: 0
|
|
1742
1906
|
},
|
|
1743
|
-
children: "
|
|
1907
|
+
children: t("skip")
|
|
1744
1908
|
}
|
|
1745
1909
|
)
|
|
1746
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() {
|
|
@@ -439,10 +444,10 @@ function createVoiceSettingsSchema(defs) {
|
|
|
439
444
|
voice: z.string().default(d.voice).description(
|
|
440
445
|
"Edge TTS \u97F3\u8272\uFF08\u5927\u9646\u81EA\u7136\u97F3\uFF1Azh-CN-XiaoxiaoNeural \u6653\u6653\xB7\u5973 / zh-CN-XiaoyiNeural \u6653\u4F0A\xB7\u5973 / zh-CN-YunxiNeural \u4E91\u5E0C\xB7\u7537 / zh-CN-YunjianNeural \u4E91\u5065\xB7\u7537 / zh-CN-YunyangNeural \u4E91\u626C\xB7\u7537 / zh-CN-YunxiaNeural \u4E91\u590F\xB7\u7537\uFF1B\u65B9\u8A00\uFF1A\u4E1C\u5317-\u5C0F\u5317 / \u9655\u897F-\u5C0F\u59AE\uFF1B\u7CA4\u8BED\uFF1AHiuGaai/HiuMaan/WanLung\uFF1B\u53F0\u6E7E\uFF1AHsiaoChen/HsiaoYu/YunJhe\uFF1B\u5B8C\u6574\u6E05\u5355\u89C1 scripts/list-voices.mjs\uFF09"
|
|
441
446
|
),
|
|
442
|
-
rate: z.number().default(d.rate).description("\u6717\u8BFB\u8BED\u901F\u500D\u7387\uFF080.5 = \u6162\u901F\uFF0C2.0 = \u5FEB\u901F\uFF0C1.0 = \u6B63\u5E38\uFF09"),
|
|
447
|
+
rate: z.number().min(0.5).max(2).default(d.rate).description("\u6717\u8BFB\u8BED\u901F\u500D\u7387\uFF080.5 = \u6162\u901F\uFF0C2.0 = \u5FEB\u901F\uFF0C1.0 = \u6B63\u5E38\uFF09"),
|
|
443
448
|
interruptLevel: z.union([z.const(0), z.const(1), z.const(2)]).default(d.interruptLevel).description("\u53D1\u58F0\u6253\u65AD\u7075\u654F\u5EA6\uFF1A0 \u9AD8\u95E8\u69DB\uFF08\u5B89\u9759\u73AF\u5883\uFF0C\u9ED8\u8BA4\uFF09/ 1 \u4E2D / 2 \u4F4E\uFF08\u5608\u6742\u73AF\u5883\u66F4\u5BB9\u6613\u6253\u65AD\uFF09"),
|
|
444
|
-
silenceMs: z.number().default(d.silenceMs).description("\u8BF4\u5B8C\u6574\u4E00\u53E5\u7684\u9759\u97F3\u505C\u987F\u6BEB\u79D2\u6570\uFF08\u9ED8\u8BA4 2000 = 2 \u79D2\uFF09"),
|
|
445
|
-
idleTimeoutMinutes: z.number().default(d.idleTimeoutMinutes).description("\u65E0\u6D3B\u52A8\u81EA\u52A8\u9000\u51FA\u8BED\u97F3\u6A21\u5F0F\u7684\u5206\u949F\u6570\uFF08\u9ED8\u8BA4 10\uFF09"),
|
|
449
|
+
silenceMs: z.number().min(500).max(3e4).default(d.silenceMs).description("\u8BF4\u5B8C\u6574\u4E00\u53E5\u7684\u9759\u97F3\u505C\u987F\u6BEB\u79D2\u6570\uFF08\u9ED8\u8BA4 2000 = 2 \u79D2\uFF09"),
|
|
450
|
+
idleTimeoutMinutes: z.number().min(1).max(120).default(d.idleTimeoutMinutes).description("\u65E0\u6D3B\u52A8\u81EA\u52A8\u9000\u51FA\u8BED\u97F3\u6A21\u5F0F\u7684\u5206\u949F\u6570\uFF08\u9ED8\u8BA4 10\uFF09"),
|
|
446
451
|
modelHost: z.string().default(d.modelHost).description("ASR \u6A21\u578B\u4E0B\u8F7D\u6E90\uFF08\u7559\u7A7A\u7528\u9ED8\u8BA4\u6E90\uFF1B\u56FD\u5185\u7F51\u7EDC\u53EF\u586B https://hf-mirror.com\uFF09"),
|
|
447
452
|
autoSend: z.boolean().default(d.autoSend).description("\u8BC6\u522B\u5B9A\u7A3F\u540E\u81EA\u52A8\u53D1\u9001\uFF08\u5173\u95ED\u5219\u53EA\u8FDB\u8349\u7A3F\u4F9B\u7F16\u8F91\uFF1B\u6309\u4F4F Ctrl / hold \u677E\u624B\u4ECD\u4F1A\u53D1\u9001\uFF09"),
|
|
448
453
|
mode: z.union([z.const("toggle"), z.const("hold")]).default(d.mode).description("\u4EA4\u4E92\u6A21\u5F0F\uFF1Atoggle \u6301\u7EED\u8046\u542C + \u9759\u97F3\u81EA\u52A8\u65AD\u53E5\uFF08\u9ED8\u8BA4\uFF09\uFF1Bhold \u6309\u4F4F\u8BF4\u8BDD\u3001\u677E\u624B\u53D1\u9001\uFF08\u77ED\u6309\u9000\u51FA\uFF09"),
|
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": {
|