dsh-voice-mode 0.7.2 → 0.7.3
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.en.md +9 -9
- package/README.md +6 -6
- package/lib/client.js +73 -38
- package/lib/index.js +55 -43
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
>
|
|
12
12
|
> 中文说明见 [README.md](./README.md)。
|
|
13
13
|
|
|
14
|
-
> **Version note (0.6.0)**: Edge cloud TTS by default (fast & natural); local TTS (VITS / Kokoro, privacy-first) optional + HTTP hardening + model SHA256 pinning form the merged core; Kokoro adds a model-precision choice (`int8` default 109 MB / `fp32` better quality 311 MB); `wakeWord` (wake word) and `toolBeep` (tool-call beep) are fully wired; the early fork's `asrModel` (bilingual paraformer) and `punctuate` (neural punctuation) were removed — SenseVoice finalization already adds punctuation, and streaming ASR is fixed to zipformer2. Silence split defaults to
|
|
14
|
+
> **Version note (0.6.0)**: Edge cloud TTS by default (fast & natural); local TTS (VITS / Kokoro, privacy-first) optional + HTTP hardening + model SHA256 pinning form the merged core; Kokoro adds a model-precision choice (`int8` default 109 MB / `fp32` better quality 311 MB); `wakeWord` (wake word) and `toolBeep` (tool-call beep) are fully wired; the early fork's `asrModel` (bilingual paraformer) and `punctuate` (neural punctuation) were removed — SenseVoice finalization already adds punctuation, and streaming ASR is fixed to zipformer2. Silence split defaults to 1500 ms.
|
|
15
15
|
|
|
16
16
|
## Fork enhancements (this repo)
|
|
17
17
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
- **103 Kokoro voices** (F0-measured gender labels, 4 favourite male voices pinned), browsed with a ◀▶ stepper;
|
|
20
20
|
- **Delta transport**: partials upload only the new 0.9 s — long push-to-talk segments finalize in seconds;
|
|
21
21
|
- **Interaction**: a mode-switch button next to the mic (continuous ⇄ hold, persisted); hold mode records only while held;
|
|
22
|
-
- **Long segments**:
|
|
22
|
+
- **Long segments**: continuous listening stitches consecutive segments into one message (internally chunked at 30 s and concatenated across chunks); 1500 ms silence split by default; hold keeps pauses from splitting;
|
|
23
23
|
- **Hardening**: session-existence check, loopback + Origin guards, per-endpoint rate limits, model SHA256 pinning, download-host allowlist.
|
|
24
24
|
|
|
25
25
|
> ⚠️ The screenshots below (and `assets/demo.gif`) show the **upstream legacy single-button UI**; the current UI adds a mode-switch button next to the mic.
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
- **Voice mode**: toggle with the microphone button in the input toolbar or the global shortcut `Ctrl+Shift+V`; globally single-active (only one session is in voice mode at a time; switching sessions yields automatically)
|
|
31
31
|
- **Two interaction modes (switchable in settings, plus a mode-switch button beside the mic)**:
|
|
32
|
-
- `toggle` (default) continuous listening: RMS VAD segmentation → streaming zipformer2 ASR (words appear as you speak, live caption preview) → automatic sentence split
|
|
32
|
+
- `toggle` (default) continuous listening: RMS VAD segmentation → streaming zipformer2 ASR (words appear as you speak, live caption preview) → automatic sentence split after 1500 ms of silence into the draft, consecutive segments joined into one message, then auto-sent after ~1500 ms more of silence (≈3 s total); hold `Ctrl` to force an immediate send
|
|
33
33
|
- `hold` push-to-talk: short tap to enter/exit, **hold the mic button to talk, release to send** (swipe up to cancel, `Esc`/blur abandons the segment; pauses do not split while held, up to 10 min); hold `Ctrl` to record-by-keyboard, release to send
|
|
34
34
|
- **Wake word (optional, off by default)**: after setting `wakeWord`, entering voice mode starts in standby, and recognition only begins once the wake word is spoken (e.g. `你好小D`), preventing accidental triggers
|
|
35
35
|
- **Output pipeline**: only the final answer's `text-delta` is read (reasoning/tool calls are skipped), streamed sentence-by-sentence (Edge cloud by default; local VITS / Kokoro, int8/fp32, optional) with a live caption overlay at the bottom-right; tool calls trigger a beep; the full text is still written to the chat; in voice mode a spoken-format system prompt is injected (short natural sentences, no Markdown decoration), and the reader side strips markers as well for a smoother listening experience
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
| Gesture | Behaviour |
|
|
45
45
|
| --- | --- |
|
|
46
46
|
| Click the mic button / `Ctrl+Shift+V` | Enter / exit voice mode |
|
|
47
|
-
| Just speak, pause
|
|
47
|
+
| Just speak, pause ~1500 ms (toggle) | Accumulate into draft, auto-send after ~3 s of quiet |
|
|
48
48
|
| Hold `Ctrl` (toggle, ≥250 ms speech) | Force-send the current segment immediately |
|
|
49
49
|
| **Hold the mic button (hold)** | Hold to talk, release to send; swipe up / `Esc` / blur abandons the segment; <250 ms tap exits the mode |
|
|
50
50
|
| Hold `Ctrl` (hold, ≥600 ms) | Keyboard push-to-talk, release to send |
|
|
@@ -86,7 +86,7 @@ npm run prefetch # run inside the plugin dir; writes to the platform ca
|
|
|
86
86
|
## Usage
|
|
87
87
|
|
|
88
88
|
1. Click the mic button in the input toolbar (or press `Ctrl+Shift+V`) to enter voice mode; a status bar appears above the input box
|
|
89
|
-
2. Choose how to speak: just talk and let the
|
|
89
|
+
2. Choose how to speak: just talk and let the ~1500 ms pause split and ~3 s of quiet auto-send (toggle); or hold the mic button and release to send (hold)
|
|
90
90
|
3. The AI answer is read sentence-by-sentence with a caption overlay at the bottom-right; click "Skip" or just start speaking to interrupt
|
|
91
91
|
4. Click "Exit" in the status bar (or press `Ctrl+Shift+V` again) to leave voice mode
|
|
92
92
|
|
|
@@ -103,11 +103,11 @@ If a wake word is configured, you land in standby first (the status bar prompts
|
|
|
103
103
|
| `voice` | per engine | Voice: 5 VITS speakers; 103 Kokoro voices (◀▶ stepper; 62/68/75/76 favourite males pinned); Edge ShortNames below. The inline "试听" button previews it at the current rate |
|
|
104
104
|
| `rate` | `1.0` | Reading speed multiplier (0.5 slow ~ 2.0 fast), **applies live** |
|
|
105
105
|
| `interruptLevel` | `0` | Barge-in sensitivity (host-side VAD frame detection + echo gate): 0 high threshold / 1 medium / 2 low |
|
|
106
|
-
| `silenceMs` | `
|
|
106
|
+
| `silenceMs` | `1500` | Silence pause in ms that marks the end of a complete sentence |
|
|
107
107
|
| `idleTimeoutMinutes` | `10` | Minutes of inactivity before auto-exiting voice mode (reading counts as activity) |
|
|
108
108
|
| `modelHost` | default | Model download host (use `https://hf-mirror.com` on mainland networks) |
|
|
109
109
|
| `autoSend` | `true` | Auto-send after a finalized transcript; when off, text only goes to the draft (hold `Ctrl` / release in hold mode still sends) |
|
|
110
|
-
| `mode` | `toggle` | Interaction mode: `toggle` continuous listening +
|
|
110
|
+
| `mode` | `toggle` | Interaction mode: `toggle` continuous listening + 1500 ms silence split; `hold` push-to-talk, release to send (short tap exits) |
|
|
111
111
|
| `wakeWord` | empty (off) | Wake word (e.g. `你好小D`): speak it after entering to activate, avoiding accidental triggers; empty = off |
|
|
112
112
|
|
|
113
113
|
Effect timing: `voice`/`rate`/`ttsEngine`/`kokoroModel`/`spokenFormat` take effect **immediately** (TTS hot-swap); the rest apply on the next voice-mode entry. Defaults come from the plugin config (`base` layer) — they follow the config unless explicitly changed.
|
|
@@ -145,7 +145,7 @@ You can also edit the `voice-mode:` section of `~/.dsh/settings.yaml` directly (
|
|
|
145
145
|
voice: zh-CN-XiaoxiaoNeural
|
|
146
146
|
rate: 1.0
|
|
147
147
|
interruptLevel: 0
|
|
148
|
-
silenceMs:
|
|
148
|
+
silenceMs: 1500
|
|
149
149
|
idleTimeoutMinutes: 10
|
|
150
150
|
modelHost: https://huggingface.co
|
|
151
151
|
```
|
|
@@ -181,7 +181,7 @@ You can also edit the `voice-mode:` section of `~/.dsh/settings.yaml` directly (
|
|
|
181
181
|

|
|
182
182
|
|
|
183
183
|
```
|
|
184
|
-
input: mic ──RMS VAD (
|
|
184
|
+
input: mic ──RMS VAD (1500 ms silence split)──▶ POST /voice-mode/asr (f32 PCM, 16k, incremental)
|
|
185
185
|
│ zipformer2 streaming ASR (host-side WASM)
|
|
186
186
|
▼
|
|
187
187
|
composer draft ──autoSend──▶ model stream ──llm/stream tap (active voice session only)
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ DeepSeek Harness 语音双工对话模式:会话内一键进入 → 边说边
|
|
|
13
13
|
|
|
14
14
|

|
|
15
15
|
|
|
16
|
-
> **版本说明(0.6.0)**:朗读默认 **Edge 云端**(快速自然),本地 TTS(VITS / Kokoro)可选(隐私优先)+ HTTP 安全加固 + 模型 SHA256 固定为合入核心;Kokoro 新增**模型精度可选**(`int8` 默认 109MB / `fp32` 音质更好 311MB);`wakeWord`(唤醒词)与 `toolBeep`(工具提示音)已完整接入;早期 fork 的 `asrModel`(双语 paraformer)与 `punctuate`(神经标点)已移除——SenseVoice 定稿本身已带标点,流式识别固定为 zipformer2。静音断句默认
|
|
16
|
+
> **版本说明(0.6.0)**:朗读默认 **Edge 云端**(快速自然),本地 TTS(VITS / Kokoro)可选(隐私优先)+ HTTP 安全加固 + 模型 SHA256 固定为合入核心;Kokoro 新增**模型精度可选**(`int8` 默认 109MB / `fp32` 音质更好 311MB);`wakeWord`(唤醒词)与 `toolBeep`(工具提示音)已完整接入;早期 fork 的 `asrModel`(双语 paraformer)与 `punctuate`(神经标点)已移除——SenseVoice 定稿本身已带标点,流式识别固定为 zipformer2。静音断句默认 1500 毫秒。
|
|
17
17
|
|
|
18
18
|
## Fork 增强(本仓库新增)
|
|
19
19
|
|
|
@@ -26,7 +26,7 @@ DeepSeek Harness 语音双工对话模式:会话内一键进入 → 边说边
|
|
|
26
26
|
- **Kokoro 音色全量 103 个**(F0 实测标定性别),四个常用男声置顶带编号;音色面板用 **下拉列表 + ◀▶ 步进**切换;
|
|
27
27
|
- **增量传输**:partial 只传新增 0.9 秒,长段按住说话松手**秒出定稿**(不再整段重传重解码);
|
|
28
28
|
- **交互增强**:输入框旁**模式切换按钮**(持续聆听 ⇄ 按住说话,保存到设置);按住说模式下**按住才录、不按住不打断**;
|
|
29
|
-
-
|
|
29
|
+
- **长段支持**:持续聆听连续多段自动拼成一条消息(内部按 30s 分块识别,跨块累积拼接),静音断句默认 1500 毫秒;按住说停顿不断句;
|
|
30
30
|
- **朗读稳定性**:打断即终止在途合成释放 CPU;句间不再有 3-5 秒停顿;长朗读不触发空闲下线;
|
|
31
31
|
- **安全加固**:会话存在性校验 / 回环+Origin 校验 / 全端点限流 / **ASR+TTS 全模型 SHA256 固定** / 下载域名白名单 / 重定向守卫。
|
|
32
32
|
|
|
@@ -36,7 +36,7 @@ DeepSeek Harness 语音双工对话模式:会话内一键进入 → 边说边
|
|
|
36
36
|
|
|
37
37
|
- **语音模式**:输入框工具排麦克风按钮或全局快捷键 `Ctrl+Shift+V` 进入/退出;全局单活(同一时刻仅一个会话处于语音模式,切换会话自动让出)
|
|
38
38
|
- **两种交互模式(输入框旁按钮或设置可切换,切换即持久化)**:
|
|
39
|
-
- `toggle`(默认)持续聆听:RMS VAD 分段 → zipformer2 流式识别(边说边出字,实时字幕预览)→ 静音约
|
|
39
|
+
- `toggle`(默认)持续聆听:RMS VAD 分段 → zipformer2 流式识别(边说边出字,实时字幕预览)→ 静音约 1500 毫秒断句进草稿,连续多段拼成一条消息,再静音约 1500 毫秒(合计约 3 秒)自动发送;按住 `Ctrl` 强制立即发送
|
|
40
40
|
- `hold` 按住说话:短按进入/退出,**按住麦克风按钮说话、松手即发**(滑出取消、`Esc`/失焦放弃本段);按住期间停顿不断句(上限 10 分钟);`Ctrl` 按住即录、松开即发
|
|
41
41
|
- **唤醒词(可选,默认关)**:设置 `wakeWord` 后进入待机态,说出唤醒词才开始识别(如「你好小D」)
|
|
42
42
|
- **输出链路**:只朗读最终答复的 `text-delta`(reasoning/工具调用不读),按句流式朗读(默认 Edge 云端;可切本地 VITS/Kokoro,中英混读选 Kokoro)+ 右下角实时字幕浮层;工具调用触发提示音;全文照常写入聊天记录;口语化提示词(设置 `spokenFormat`,默认开)让回复为自然短句、不带 Markdown 排版符号
|
|
@@ -60,7 +60,7 @@ bundle 插件安装后需重启 dsh 生效(Linux:`systemctl restart dsh`;
|
|
|
60
60
|
| 手势 | 作用 |
|
|
61
61
|
| --- | --- |
|
|
62
62
|
| `Ctrl+Shift+V` | 进入 / 退出语音模式 |
|
|
63
|
-
| 直接说话 | `toggle`:边说边出字,停顿约
|
|
63
|
+
| 直接说话 | `toggle`:边说边出字,停顿约 1500 毫秒断句进草稿、再静音约 1500 毫秒(合计约 3 秒)自动发送;按住 `Ctrl` 强制立即发送 |
|
|
64
64
|
| 按住麦克风按钮 | `hold`:松手发送;短按退出;滑出 / `Esc` / 失焦放弃本段 |
|
|
65
65
|
| 点输入框旁模式按钮 | 在「持续聆听 ⇄ 按住说」间切换(保存到设置) |
|
|
66
66
|
| 说唤醒词 | 待机态激活识别(配置后) |
|
|
@@ -77,11 +77,11 @@ bundle 插件安装后需重启 dsh 生效(Linux:`systemctl restart dsh`;
|
|
|
77
77
|
| `voice` | 按引擎 | 音色:VITS 五说话人;Kokoro 103 个(下拉+◀▶,62 深沉/68 浑厚/75 清亮/76 磁性置顶);Edge 进入时自动加载全量 322 个。行内「试听」可即时预览 |
|
|
78
78
|
| `rate` | `1.0` | 朗读语速倍率(0.5 慢速 ~ 2.0 快速),**即时生效** |
|
|
79
79
|
| `interruptLevel` | `0` | 发声打断灵敏度(服务端 VAD 帧级检测 + 回声门控):0 高门槛 / 1 中 / 2 低 |
|
|
80
|
-
| `silenceMs` | `
|
|
80
|
+
| `silenceMs` | `1500` | 说完整一句的静音停顿毫秒数 |
|
|
81
81
|
| `idleTimeoutMinutes` | `10` | 无活动自动退出语音模式的分钟数(朗读计为活动) |
|
|
82
82
|
| `modelHost` | 默认源 | 模型下载源(国内网络填 `https://hf-mirror.com`) |
|
|
83
83
|
| `autoSend` | `true` | 识别定稿后自动发送;关闭则只进草稿(按住 `Ctrl` / hold 松手仍会发送) |
|
|
84
|
-
| `mode` | `toggle` | 交互模式:`toggle` 持续聆听 +
|
|
84
|
+
| `mode` | `toggle` | 交互模式:`toggle` 持续聆听 + 1500ms 静音断句;`hold` 按住说话、松手发送(短按退出) |
|
|
85
85
|
| `wakeWord` | 空(关) | 唤醒词(如「你好小D」):进入后先说唤醒词激活,避免误触;空 = 关闭 |
|
|
86
86
|
| `spokenFormat` | `true` | 语音会话注入口语化提示词:开启后**仅当前语音会话**的回复被注入「口语化短句、不用 Markdown 排版符号」提示词(朗读更顺),**即时生效** |
|
|
87
87
|
|
package/lib/client.js
CHANGED
|
@@ -992,7 +992,6 @@ function createAsrEngine(config, sessionId) {
|
|
|
992
992
|
beginHeld() {
|
|
993
993
|
if (!active || holdActive) return;
|
|
994
994
|
holdActive = true;
|
|
995
|
-
forcePending = true;
|
|
996
995
|
segmentEpoch++;
|
|
997
996
|
utteranceEndAt = null;
|
|
998
997
|
segment = [];
|
|
@@ -1307,10 +1306,10 @@ var zh = {
|
|
|
1307
1306
|
sev0: "0 \u9AD8\u95E8\u69DB",
|
|
1308
1307
|
sev1: "1 \u4E2D",
|
|
1309
1308
|
sev2: "2 \u4F4E",
|
|
1310
|
-
descSilence: "\u8BF4\u5B8C\u6574\u4E00\u53E5\u7684\u9759\u97F3\u505C\u987F\u6BEB\u79D2\u6570\uFF08\u9ED8\u8BA4
|
|
1309
|
+
descSilence: "\u8BF4\u5B8C\u6574\u4E00\u53E5\u7684\u9759\u97F3\u505C\u987F\u6BEB\u79D2\u6570\uFF08\u9ED8\u8BA4 1500 \u6BEB\u79D2\uFF0C\u7ED9\u601D\u8003\u505C\u987F\u7559\u7A7A\u95F4\uFF1B\u81F3\u5C11 250ms \u8BED\u97F3\u624D\u5224\u53E5\uFF0C\u9632\u77ED\u4FC3\u566A\u58F0\u8BEF\u89E6\u53D1\uFF09",
|
|
1311
1310
|
descIdle: "\u65E0\u6D3B\u52A8\u81EA\u52A8\u9000\u51FA\u8BED\u97F3\u6A21\u5F0F\u7684\u5206\u949F\u6570\uFF08\u9ED8\u8BA4 10\uFF09",
|
|
1312
1311
|
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",
|
|
1313
|
-
descAutoSend: "\
|
|
1312
|
+
descAutoSend: "\u9759\u97F3\u5230\u70B9\u81EA\u52A8\u53D1\u9001\uFF08\u8FDE\u7EED\u591A\u6BB5\u62FC\u6210\u4E00\u6761\uFF1B\u5173=\u53EA\u8FDB\u8349\u7A3F\uFF1B\u6309\u4F4F Ctrl / hold \u677E\u624B\u4ECD\u53D1\u9001\uFF09",
|
|
1314
1313
|
descAutoResume: "\u5207\u6362\u56DE\u4E0A\u6B21\u8BED\u97F3\u4F1A\u8BDD\u65F6\u81EA\u52A8\u6062\u590D\u8BED\u97F3\u6A21\u5F0F\uFF08\u9ED8\u8BA4\u5173\uFF0C\u9700\u9EA6\u514B\u98CE\u6743\u9650\u5DF2\u6388\u4E88\uFF1B\u7701\u53BB\u6BCF\u6B21\u5207\u6362\u4F1A\u8BDD\u540E\u91CD\u65B0\u70B9\u9EA6\u514B\u98CE\uFF09",
|
|
1315
1314
|
descSpokenFormat: "\u8BED\u97F3\u4F1A\u8BDD\u6CE8\u5165\u53E3\u8BED\u5316\u63D0\u793A\u8BCD\uFF08\u56DE\u590D\u53E3\u8BED\u5316\u3001\u77ED\u53E5\u3001\u4E0D\u7528 Markdown \u6392\u7248\u7B26\u53F7\uFF0C\u6717\u8BFB\u66F4\u987A\u66F4\u5FEB\uFF1B\u9ED8\u8BA4\u5F00\uFF0C\u6539\u52A8\u5373\u65F6\u751F\u6548\uFF09",
|
|
1316
1315
|
descSenseVoice: "\u5B9A\u7A3F\u7528 SenseVoice \u91CD\u8BD1\uFF08\u5E26\u6807\u70B9 + \u6570\u5B57\u5F52\u4E00\u5316\u3001\u8BC6\u522B\u66F4\u51C6\uFF1B\u9ED8\u8BA4\u5F00\u3002\u5173\u95ED\u53EF\u7701 228MB \u6A21\u578B\uFF0C\u53EA\u8D70\u6D41\u5F0F\u8BC6\u522B\uFF09",
|
|
@@ -1448,10 +1447,10 @@ var en = {
|
|
|
1448
1447
|
sev0: "0 high",
|
|
1449
1448
|
sev1: "1 medium",
|
|
1450
1449
|
sev2: "2 low",
|
|
1451
|
-
descSilence: "Silence pause before a sentence is committed (default
|
|
1450
|
+
descSilence: "Silence pause before a sentence is committed (default 1500 ms; at least 250 ms of speech required, guards against noise triggers)",
|
|
1452
1451
|
descIdle: "Auto-exit voice mode after idle minutes (default 10)",
|
|
1453
1452
|
descModelHost: "ASR model download source (official source / mirror, or any custom URL)",
|
|
1454
|
-
descAutoSend: "Auto-send
|
|
1453
|
+
descAutoSend: "Auto-send once quiet (consecutive segments join into one message; off = draft only; Ctrl / hold still sends)",
|
|
1455
1454
|
descAutoResume: "Auto-resume voice mode when switching back to the last voice session (default off, requires granted mic permission)",
|
|
1456
1455
|
descSpokenFormat: "Inject spoken-format prompt into voice replies (colloquial, short sentences, no Markdown; default on, live)",
|
|
1457
1456
|
descSenseVoice: "Re-transcribe the finalized utterance with SenseVoice (punctuation + ITN, more accurate; default on \u2014 turn off to skip the 228 MB model and keep streaming only)",
|
|
@@ -2458,7 +2457,7 @@ function VoiceSettingsCard({ scope }) {
|
|
|
2458
2457
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Section, { title: t("secRecognition"), children: [
|
|
2459
2458
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "senseVoice", desc: t("descSenseVoice"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", checked: Boolean(value.senseVoice), onChange: (e) => void scope.set("senseVoice", e.target.checked) }) }),
|
|
2460
2459
|
/* @__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) }) }),
|
|
2461
|
-
/* @__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 ??
|
|
2460
|
+
/* @__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 ?? 1500, min: 500, max: 3e4, step: 100 }) }),
|
|
2462
2461
|
/* @__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 }) })
|
|
2463
2462
|
] }),
|
|
2464
2463
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Section, { title: t("secModel"), children: /* @__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://..." }) }) }),
|
|
@@ -2485,7 +2484,7 @@ var TELEMETRY_VIEW = [
|
|
|
2485
2484
|
{ stage: "first-tts-chunk", key: "telFirstChunk" },
|
|
2486
2485
|
{ stage: "first-audio-played", key: "telFirstPlayed" }
|
|
2487
2486
|
];
|
|
2488
|
-
var BUILD_TAG = "
|
|
2487
|
+
var BUILD_TAG = "4ab9c0d";
|
|
2489
2488
|
var TELEMETRY_FLAG = "dsh-voice-mode.telemetry";
|
|
2490
2489
|
var telemetryEnabled = typeof localStorage !== "undefined" && localStorage.getItem(TELEMETRY_FLAG) === "1";
|
|
2491
2490
|
console.log("[dsh-voice] build=" + BUILD_TAG);
|
|
@@ -2778,7 +2777,7 @@ function createVoiceBus(basePath = BASE_PATH2, ctx) {
|
|
|
2778
2777
|
let activeSessionId = null;
|
|
2779
2778
|
const DEFAULT_BOOT = {
|
|
2780
2779
|
basePath: BASE_PATH2,
|
|
2781
|
-
silenceMs:
|
|
2780
|
+
silenceMs: 1500,
|
|
2782
2781
|
interruptLevel: 0,
|
|
2783
2782
|
idleTimeoutMinutes: 10,
|
|
2784
2783
|
autoSend: true,
|
|
@@ -3241,6 +3240,7 @@ function MicButton({
|
|
|
3241
3240
|
const engineRef = (0, import_react2.useRef)(null);
|
|
3242
3241
|
const actionsRef = (0, import_react2.useRef)(inputActions);
|
|
3243
3242
|
const submitTimerRef = (0, import_react2.useRef)(null);
|
|
3243
|
+
const autoSendTimerRef = (0, import_react2.useRef)(null);
|
|
3244
3244
|
const idleTimerRef = (0, import_react2.useRef)(null);
|
|
3245
3245
|
const runningRef = (0, import_react2.useRef)(false);
|
|
3246
3246
|
const mountedRef = (0, import_react2.useRef)(true);
|
|
@@ -3248,7 +3248,7 @@ function MicButton({
|
|
|
3248
3248
|
const manualHoldRef = (0, import_react2.useRef)(false);
|
|
3249
3249
|
const breakRef = (0, import_react2.useRef)(null);
|
|
3250
3250
|
const pausedForHiddenRef = (0, import_react2.useRef)(false);
|
|
3251
|
-
const bootNow = () => bus.ui.boot ?? { basePath: "/voice-mode", silenceMs:
|
|
3251
|
+
const bootNow = () => bus.ui.boot ?? { basePath: "/voice-mode", silenceMs: 1500, interruptLevel: 0, idleTimeoutMinutes: 10, autoSend: true, autoResume: false, mode: "toggle", bargeInMode: "auto", echoGateDb: 6, shortcut: "Ctrl+Shift+V", wakeWord: "", toolBeep: false };
|
|
3252
3252
|
useVoiceCss();
|
|
3253
3253
|
const [, bumpUi] = (0, import_react2.useState)(0);
|
|
3254
3254
|
(0, import_react2.useEffect)(
|
|
@@ -3309,6 +3309,7 @@ function MicButton({
|
|
|
3309
3309
|
setLocalMode("off");
|
|
3310
3310
|
clearIdle();
|
|
3311
3311
|
cancelPendingSubmit();
|
|
3312
|
+
cancelAutoSend();
|
|
3312
3313
|
clearBreakTimer();
|
|
3313
3314
|
setHolding(false);
|
|
3314
3315
|
isSpeechTrueCount = 0;
|
|
@@ -3329,11 +3330,61 @@ function MicButton({
|
|
|
3329
3330
|
submitTimerRef.current = null;
|
|
3330
3331
|
}
|
|
3331
3332
|
};
|
|
3333
|
+
const cancelAutoSend = () => {
|
|
3334
|
+
if (autoSendTimerRef.current) {
|
|
3335
|
+
clearTimeout(autoSendTimerRef.current);
|
|
3336
|
+
autoSendTimerRef.current = null;
|
|
3337
|
+
}
|
|
3338
|
+
};
|
|
3339
|
+
const submitDraftNow = (expectedText) => {
|
|
3340
|
+
cancelPendingSubmit();
|
|
3341
|
+
const actions = actionsRef.current;
|
|
3342
|
+
const submitFn = actions?.submit;
|
|
3343
|
+
if (typeof submitFn !== "function") return;
|
|
3344
|
+
const draftSnapshot = (expectedText ?? draftRef.current).trim();
|
|
3345
|
+
if (!draftSnapshot) return;
|
|
3346
|
+
const doSubmit = () => {
|
|
3347
|
+
try {
|
|
3348
|
+
const r = submitFn();
|
|
3349
|
+
if (r && typeof r.then === "function") {
|
|
3350
|
+
r.catch(() => {
|
|
3351
|
+
bus.setUi({ error: t("sendFailKept") });
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
3354
|
+
} catch {
|
|
3355
|
+
bus.setUi({ error: t("sendFailKept") });
|
|
3356
|
+
}
|
|
3357
|
+
};
|
|
3358
|
+
doSubmit();
|
|
3359
|
+
let retryCount = 0;
|
|
3360
|
+
submitTimerRef.current = setInterval(() => {
|
|
3361
|
+
retryCount++;
|
|
3362
|
+
const phase = phaseRef.current;
|
|
3363
|
+
if (retryCount > 3 || phase === "submitting" || phase === "adjudicating" || draftRef.current.trim() !== draftSnapshot) {
|
|
3364
|
+
cancelPendingSubmit();
|
|
3365
|
+
return;
|
|
3366
|
+
}
|
|
3367
|
+
doSubmit();
|
|
3368
|
+
}, 500);
|
|
3369
|
+
};
|
|
3370
|
+
const scheduleAutoSend = () => {
|
|
3371
|
+
cancelAutoSend();
|
|
3372
|
+
const delay = bootNow().silenceMs;
|
|
3373
|
+
autoSendTimerRef.current = setTimeout(() => {
|
|
3374
|
+
autoSendTimerRef.current = null;
|
|
3375
|
+
const eng = engineRef.current;
|
|
3376
|
+
if (eng && (eng.state === "speech" || eng.holding)) return;
|
|
3377
|
+
if (bus.ui.playing) return;
|
|
3378
|
+
if (bootNow().autoSend === false) return;
|
|
3379
|
+
submitDraftNow();
|
|
3380
|
+
}, delay);
|
|
3381
|
+
};
|
|
3332
3382
|
const exitMode = async (_reason) => {
|
|
3333
3383
|
if (localRef.current === "off") return;
|
|
3334
3384
|
setLocalMode("off");
|
|
3335
3385
|
clearIdle();
|
|
3336
3386
|
cancelPendingSubmit();
|
|
3387
|
+
cancelAutoSend();
|
|
3337
3388
|
isSpeechTrueCount = 0;
|
|
3338
3389
|
fixtureRecorder.save("exit");
|
|
3339
3390
|
breakRef.current = null;
|
|
@@ -3521,6 +3572,7 @@ function MicButton({
|
|
|
3521
3572
|
}
|
|
3522
3573
|
engine.onState((s) => {
|
|
3523
3574
|
bus.setUi({ state: s });
|
|
3575
|
+
if (s === "speech") cancelAutoSend();
|
|
3524
3576
|
if (s === "idle") resetIdle();
|
|
3525
3577
|
});
|
|
3526
3578
|
engine.onError((key) => {
|
|
@@ -3538,45 +3590,27 @@ function MicButton({
|
|
|
3538
3590
|
const actions = actionsRef.current;
|
|
3539
3591
|
const trimmed = text.trim();
|
|
3540
3592
|
if (!trimmed) return;
|
|
3593
|
+
let nextDraft = trimmed;
|
|
3541
3594
|
try {
|
|
3542
3595
|
const curText = draftRef.current;
|
|
3543
|
-
|
|
3596
|
+
nextDraft = curText ? `${curText} ${trimmed}` : trimmed;
|
|
3544
3597
|
if (typeof actions?.setDraft === "function") actions.setDraft(nextDraft);
|
|
3545
3598
|
else if (typeof actions?.setDraft === "function") actions.setDraft(nextDraft);
|
|
3546
|
-
else {
|
|
3547
|
-
}
|
|
3548
3599
|
} catch {
|
|
3549
3600
|
try {
|
|
3550
3601
|
actions?.setDraft?.(trimmed);
|
|
3551
3602
|
} catch {
|
|
3552
3603
|
}
|
|
3553
3604
|
}
|
|
3554
|
-
if (
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
}
|
|
3564
|
-
} catch {
|
|
3565
|
-
bus.setUi({ error: t("sendFailKept") });
|
|
3566
|
-
}
|
|
3567
|
-
};
|
|
3568
|
-
cancelPendingSubmit();
|
|
3569
|
-
doSubmit();
|
|
3570
|
-
let retryCount = 0;
|
|
3571
|
-
submitTimerRef.current = setInterval(() => {
|
|
3572
|
-
retryCount++;
|
|
3573
|
-
const phase = phaseRef.current;
|
|
3574
|
-
if (retryCount > 3 || phase === "submitting" || phase === "adjudicating" || draftRef.current.trim() !== trimmed) {
|
|
3575
|
-
cancelPendingSubmit();
|
|
3576
|
-
return;
|
|
3577
|
-
}
|
|
3578
|
-
doSubmit();
|
|
3579
|
-
}, 500);
|
|
3605
|
+
if (meta?.force) {
|
|
3606
|
+
cancelAutoSend();
|
|
3607
|
+
submitDraftNow(nextDraft);
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3610
|
+
if (bootNow().mode === "hold") return;
|
|
3611
|
+
if (bus.ui.playing) return;
|
|
3612
|
+
if (bootNow().autoSend === false) return;
|
|
3613
|
+
scheduleAutoSend();
|
|
3580
3614
|
});
|
|
3581
3615
|
bus.setUi({ state: "idle", partial: "", levels: [], error: null, model: null, ttsNotice: null });
|
|
3582
3616
|
if (!mountedRef.current) {
|
|
@@ -3650,6 +3684,7 @@ function MicButton({
|
|
|
3650
3684
|
mountedRef.current = false;
|
|
3651
3685
|
clearIdle();
|
|
3652
3686
|
cancelPendingSubmit();
|
|
3687
|
+
cancelAutoSend();
|
|
3653
3688
|
isSpeechTrueCount = 0;
|
|
3654
3689
|
const sid = sidRef.current;
|
|
3655
3690
|
if ((localRef.current === "on" || localRef.current === "pending") && sid) {
|
package/lib/index.js
CHANGED
|
@@ -392,7 +392,7 @@ function rmsOf(samples) {
|
|
|
392
392
|
return Math.sqrt(sum / samples.length);
|
|
393
393
|
}
|
|
394
394
|
function createAsrRuntime(options) {
|
|
395
|
-
const { cacheDir, modelHost, broadcast, senseVoice, allowCustomHost } = options;
|
|
395
|
+
const { cacheDir, modelHost, broadcast, senseVoice, silenceMs, allowCustomHost } = options;
|
|
396
396
|
let lastProgress = null;
|
|
397
397
|
const localBroadcast = (event, payload) => {
|
|
398
398
|
if (event === "asr-progress") lastProgress = payload;
|
|
@@ -473,11 +473,11 @@ function createAsrRuntime(options) {
|
|
|
473
473
|
}
|
|
474
474
|
return vadLoading;
|
|
475
475
|
};
|
|
476
|
-
const newVad = (vadPath, threshold = 0.5) => createVad({
|
|
476
|
+
const newVad = (vadPath, threshold = 0.5, minSilenceDuration = 0.5) => createVad({
|
|
477
477
|
sileroVad: {
|
|
478
478
|
model: vadPath,
|
|
479
479
|
threshold,
|
|
480
|
-
minSilenceDuration
|
|
480
|
+
minSilenceDuration,
|
|
481
481
|
minSpeechDuration: 0.25,
|
|
482
482
|
maxSpeechDuration: 20,
|
|
483
483
|
windowSize: 512
|
|
@@ -492,7 +492,7 @@ function createAsrRuntime(options) {
|
|
|
492
492
|
if (seg.vad) return seg.vad;
|
|
493
493
|
const vadPath = await ensureVadModel();
|
|
494
494
|
if (!vadPath) return null;
|
|
495
|
-
seg.vad = newVad(vadPath);
|
|
495
|
+
seg.vad = newVad(vadPath, 0.5, silenceMs() / 1e3);
|
|
496
496
|
return seg.vad;
|
|
497
497
|
};
|
|
498
498
|
const detectVads = /* @__PURE__ */ new Map();
|
|
@@ -1184,44 +1184,54 @@ var TtsQueue = class {
|
|
|
1184
1184
|
try {
|
|
1185
1185
|
while (q.pending.length > 0) {
|
|
1186
1186
|
const item = q.pending.shift();
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
q.
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
final: false,
|
|
1199
|
-
audio: buf.toString("base64"),
|
|
1200
|
-
mime
|
|
1201
|
-
};
|
|
1202
|
-
for (const fn of this.listeners) {
|
|
1203
|
-
try {
|
|
1204
|
-
fn(dataFrame);
|
|
1205
|
-
} catch {
|
|
1187
|
+
const MAX_SYNTH_ATTEMPTS = 3;
|
|
1188
|
+
let buf = null;
|
|
1189
|
+
for (let attempt = 0; attempt < MAX_SYNTH_ATTEMPTS; attempt++) {
|
|
1190
|
+
if (item.epoch !== q.epoch) break;
|
|
1191
|
+
try {
|
|
1192
|
+
buf = await this.engine.synthesize(item.text);
|
|
1193
|
+
break;
|
|
1194
|
+
} catch (e) {
|
|
1195
|
+
console.warn(`[dsh-voice-mode] synthesis failed (${attempt + 1}/${MAX_SYNTH_ATTEMPTS}): ${String(e)}`);
|
|
1196
|
+
if (attempt < MAX_SYNTH_ATTEMPTS - 1) {
|
|
1197
|
+
await new Promise((r) => setTimeout(r, 400 * (attempt + 1)));
|
|
1206
1198
|
}
|
|
1207
1199
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1200
|
+
}
|
|
1201
|
+
if (item.epoch !== q.epoch) continue;
|
|
1202
|
+
if (buf === null) continue;
|
|
1203
|
+
q.errorNotified = false;
|
|
1204
|
+
q.backoff = 0;
|
|
1205
|
+
const sentenceId = q.seq++;
|
|
1206
|
+
const mime = this.engine.mime;
|
|
1207
|
+
const dataFrame = {
|
|
1208
|
+
sessionId,
|
|
1209
|
+
sentenceId,
|
|
1210
|
+
chunkId: 0,
|
|
1211
|
+
final: false,
|
|
1212
|
+
audio: buf.toString("base64"),
|
|
1213
|
+
mime
|
|
1214
|
+
};
|
|
1215
|
+
for (const fn of this.listeners) {
|
|
1216
|
+
try {
|
|
1217
|
+
fn(dataFrame);
|
|
1218
|
+
} catch {
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
const finalFrame = {
|
|
1222
|
+
sessionId,
|
|
1223
|
+
sentenceId,
|
|
1224
|
+
chunkId: 1,
|
|
1225
|
+
final: true,
|
|
1226
|
+
text: item.text,
|
|
1227
|
+
audio: "",
|
|
1228
|
+
mime
|
|
1229
|
+
};
|
|
1230
|
+
for (const fn of this.listeners) {
|
|
1231
|
+
try {
|
|
1232
|
+
fn(finalFrame);
|
|
1233
|
+
} catch {
|
|
1222
1234
|
}
|
|
1223
|
-
} catch (e) {
|
|
1224
|
-
console.warn(`[dsh-voice-mode] synthesis failed: ${String(e)}`);
|
|
1225
1235
|
}
|
|
1226
1236
|
}
|
|
1227
1237
|
} catch (e) {
|
|
@@ -1773,7 +1783,7 @@ var VOICE_SETTINGS_DEFAULTS = {
|
|
|
1773
1783
|
voice: "zh-CN-XiaoxiaoNeural",
|
|
1774
1784
|
rate: 1,
|
|
1775
1785
|
interruptLevel: 0,
|
|
1776
|
-
silenceMs:
|
|
1786
|
+
silenceMs: 1500,
|
|
1777
1787
|
idleTimeoutMinutes: 10,
|
|
1778
1788
|
modelHost: "",
|
|
1779
1789
|
autoSend: true,
|
|
@@ -1801,10 +1811,10 @@ function createVoiceSettingsSchema(defs) {
|
|
|
1801
1811
|
),
|
|
1802
1812
|
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"),
|
|
1803
1813
|
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"),
|
|
1804
|
-
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
|
|
1814
|
+
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 1500 \u6BEB\u79D2\uFF0C\u7ED9\u601D\u8003\u505C\u987F\u7559\u7A7A\u95F4\uFF1B\u81F3\u5C11 250ms \u8BED\u97F3\u624D\u5224\u53E5\uFF0C\u9632\u77ED\u4FC3\u566A\u58F0\u8BEF\u89E6\u53D1\uFF09"),
|
|
1805
1815
|
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"),
|
|
1806
1816
|
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"),
|
|
1807
|
-
autoSend: z.boolean().default(d.autoSend).description("\
|
|
1817
|
+
autoSend: z.boolean().default(d.autoSend).description("\u9759\u97F3\u5230\u70B9\u81EA\u52A8\u53D1\u9001\uFF08\u8FDE\u7EED\u591A\u6BB5\u62FC\u6210\u4E00\u6761\u6D88\u606F\uFF1B\u5173\u95ED\u5219\u53EA\u8FDB\u8349\u7A3F\u4F9B\u7F16\u8F91\uFF1B\u6309\u4F4F Ctrl / hold \u677E\u624B\u4ECD\u4F1A\u53D1\u9001\uFF09"),
|
|
1808
1818
|
autoResume: z.boolean().default(d.autoResume).description("\u5207\u6362\u56DE\u4E0A\u6B21\u8BED\u97F3\u4F1A\u8BDD\u65F6\u81EA\u52A8\u6062\u590D\u8BED\u97F3\u6A21\u5F0F\uFF08\u9ED8\u8BA4\u5173\uFF0C\u9700\u9EA6\u514B\u98CE\u6743\u9650\u5DF2\u6388\u4E88\uFF1B\u5173\u95ED\u5219\u6BCF\u6B21\u5207\u6362\u4F1A\u8BDD\u540E\u9700\u91CD\u65B0\u70B9\u9EA6\u514B\u98CE\uFF09"),
|
|
1809
1819
|
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"),
|
|
1810
1820
|
bargeInMode: z.union([z.const("auto"), z.const("manual")]).default(d.bargeInMode).description("\u6253\u65AD\u65B9\u5F0F\uFF1Aauto \u81EA\u52A8\u6253\u65AD\uFF08\u5F00\u53E3\u5373\u6253\u65AD\uFF0C\u8033\u673A/\u5B89\u9759\u73AF\u5883\u63A8\u8350\uFF09\uFF1Bmanual \u624B\u52A8\u6253\u65AD\uFF08\u5916\u653E\u63A8\u8350\u2014\u2014\u5916\u653E\u56DE\u58F0\u4F1A\u8BEF\u89E6\u53D1\u81EA\u52A8\u6253\u65AD\uFF0C\u6539\u6309\u4F4F\u9EA6\u514B\u98CE/Ctrl \u663E\u5F0F\u6253\u65AD\uFF0C\u6C38\u4E0D\u81EA\u6253\u65AD\uFF09"),
|
|
@@ -1828,7 +1838,7 @@ var Config = z.object({
|
|
|
1828
1838
|
voice: z.string().default("zh-CN-XiaoxiaoNeural"),
|
|
1829
1839
|
rate: z.number().default(1),
|
|
1830
1840
|
interruptLevel: z.union([z.const(0), z.const(1), z.const(2)]).default(0),
|
|
1831
|
-
silenceMs: z.number().default(
|
|
1841
|
+
silenceMs: z.number().default(1500),
|
|
1832
1842
|
idleTimeoutMinutes: z.number().default(10)
|
|
1833
1843
|
});
|
|
1834
1844
|
function apply(ctx, config) {
|
|
@@ -1896,6 +1906,8 @@ function apply(ctx, config) {
|
|
|
1896
1906
|
modelHost: () => vset.modelHost,
|
|
1897
1907
|
// P4:SenseVoice 定稿重译开关(实时读取,关闭则不下载/不创建模型)。
|
|
1898
1908
|
senseVoice: () => vset.senseVoice,
|
|
1909
|
+
// 断句静音阈值(实时读取):端点 VAD minSilenceDuration 跟随设置。
|
|
1910
|
+
silenceMs: () => vset.silenceMs,
|
|
1899
1911
|
allowCustomHost: config.allowCustomModelHost,
|
|
1900
1912
|
broadcast
|
|
1901
1913
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-voice-mode",
|
|
3
3
|
"description": "Full-duplex voice plugin for DeepSeek Harness: local zipformer2 streaming ASR (no API key) → editable draft; Edge TTS or local VITS / Kokoro read-aloud with live captions; true barge-in; hardened HTTP surface + model SHA256 pinning; compatible with dsh 0.1.1 → 0.1.2. · DSH 语音双工插件:流式识别入草稿、按句朗读+实时字幕、开口即打断;免 API Key、安全加固、新旧版本兼容。",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"repository": {
|