dsh-voice-mode 0.3.0 → 0.5.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.en.md +52 -47
- package/README.md +78 -63
- package/assets/architecture.svg +1 -159
- package/assets/demo.gif +0 -0
- package/lib/client.js +676 -90
- package/lib/index.js +1080 -229
- package/lib/tts-vits-worker.cjs +147 -0
- package/package.json +13 -6
- package/scripts/prefetch.mjs +158 -50
package/README.en.md
CHANGED
|
@@ -11,7 +11,18 @@
|
|
|
11
11
|
>
|
|
12
12
|
> 中文说明见 [README.md](./README.md)。
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
> **Version note (0.5.0)**: Edge cloud TTS by default (fast & natural); local TTS (VITS / Kokoro int8, privacy-first) optional + HTTP hardening + model SHA256 pinning form the merged core; `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 700 ms.
|
|
15
|
+
|
|
16
|
+
## Fork enhancements (this repo)
|
|
17
|
+
|
|
18
|
+
- **Edge cloud TTS by default; local TTS optional (privacy-first)**: local VITS (Chinese) and local Kokoro int8 (zh+en, native `sherpa-onnx-node` addon, no WASM memory limits) run in an isolated child process;
|
|
19
|
+
- **103 Kokoro voices** (F0-measured gender labels, 4 favourite male voices pinned), browsed with a ◀▶ stepper;
|
|
20
|
+
- **Delta transport**: partials upload only the new 0.9 s — long push-to-talk segments finalize in seconds;
|
|
21
|
+
- **Interaction**: a mode-switch button next to the mic (continuous ⇄ hold, persisted); hold mode records only while held;
|
|
22
|
+
- **Long segments**: hold up to 10 min (pauses don't split), continuous sentences up to 3 min, 700 ms silence split by default;
|
|
23
|
+
- **Hardening**: session-existence check, loopback + Origin guards, per-endpoint rate limits, model SHA256 pinning, download-host allowlist.
|
|
24
|
+
|
|
25
|
+

|
|
15
26
|
|
|
16
27
|
## Features
|
|
17
28
|
|
|
@@ -19,14 +30,15 @@
|
|
|
19
30
|
|
|
20
31
|
|
|
21
32
|
- **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)
|
|
22
|
-
- **Two interaction modes (switchable in settings)**:
|
|
23
|
-
- `toggle` (default) continuous listening:
|
|
24
|
-
- `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); hold `Ctrl` to record-by-keyboard, release to send
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
33
|
+
- **Two interaction modes (switchable in settings, plus a mode-switch button beside the mic)**:
|
|
34
|
+
- `toggle` (default) continuous listening: RMS VAD segmentation → streaming zipformer2 ASR (words appear as you speak, live caption preview) → automatic sentence split and send after 700 ms of silence; hold `Ctrl` to force an immediate send
|
|
35
|
+
- `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
|
|
36
|
+
- **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
|
|
37
|
+
- **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 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
|
|
38
|
+
- **Barge-in**: three sensitivity levels of voice-onset detection → local mute + host synth queue invalidation (epoch) + running turn cancellation (the half-finished part is kept and naturally flows into your new message)
|
|
39
|
+
- **Lazy model download with progress**: the zipformer2 Chinese streaming model (~160 MB, `.part` resumable) is downloaded on first use with live progress in the status bar; `npm run prefetch` can pre-download it
|
|
28
40
|
- **Resilience**: mic-denied red hint, visible model-download failure, TTS unreachable status hint (auto retry), failed submit keeps the text in the draft, SSE auto-reconnect
|
|
29
|
-
- **Settings**: Settings → Plugins → voice-mode, with voice / rate / interrupt sensitivity / silence pause / idle timeout / model mirror / auto send / interaction mode; **voices are previewable** (the "试听/Preview" button synthesizes and plays the current voice at the current rate instantly, no need to enter voice mode; custom ShortNames are previewable too)
|
|
41
|
+
- **Settings**: Settings → Plugins → voice-mode, with voice / rate / interrupt sensitivity / silence pause / idle timeout / model mirror / auto send / interaction mode / wake word; **voices are previewable** (the "试听/Preview" button synthesizes and plays the current voice at the current rate instantly, no need to enter voice mode; custom ShortNames are previewable too)
|
|
30
42
|
- **Idle exit**: auto-exit and mic release after 10 minutes of inactivity
|
|
31
43
|
|
|
32
44
|
## Interaction gestures
|
|
@@ -34,10 +46,11 @@
|
|
|
34
46
|
| Gesture | Behaviour |
|
|
35
47
|
| --- | --- |
|
|
36
48
|
| Click the mic button / `Ctrl+Shift+V` | Enter / exit voice mode |
|
|
37
|
-
| Just speak (toggle) |
|
|
49
|
+
| Just speak, pause 2 s (toggle) | Auto sentence split and send |
|
|
38
50
|
| Hold `Ctrl` (toggle, ≥250 ms speech) | Force-send the current segment immediately |
|
|
39
51
|
| **Hold the mic button (hold)** | Hold to talk, release to send; swipe up / `Esc` / blur abandons the segment; <250 ms tap exits the mode |
|
|
40
52
|
| Hold `Ctrl` (hold, ≥600 ms) | Keyboard push-to-talk, release to send |
|
|
53
|
+
| Speak the wake word first (if configured) | Activate from standby into listening (then recognition and sending begin) |
|
|
41
54
|
| Speak while AI is reading | Interrupt playback and cancel the running turn |
|
|
42
55
|
| Type in the input box | Auto-exit voice mode (draft is kept) |
|
|
43
56
|
|
|
@@ -52,7 +65,7 @@ dsh plugin --profile web add dsh-voice-mode
|
|
|
52
65
|
npx -y @deepseek-ai/dsh plugin --profile web add dsh-voice-mode
|
|
53
66
|
|
|
54
67
|
# Option 2: local tarball
|
|
55
|
-
dsh plugin --profile web add ./dsh-voice-mode-0.
|
|
68
|
+
dsh plugin --profile web add ./dsh-voice-mode-0.1.0.tgz
|
|
56
69
|
|
|
57
70
|
# Option 3: from source
|
|
58
71
|
git clone https://github.com/qishuilalala/dsh-voice-mode.git
|
|
@@ -70,34 +83,35 @@ dsh plugin --profile web add .
|
|
|
70
83
|
```sh
|
|
71
84
|
npm run prefetch # run inside the plugin dir; writes to the platform cache dir
|
|
72
85
|
# or specify the cache location: node scripts/prefetch.mjs --cache-dir /where/ever/models
|
|
73
|
-
node scripts/bench-asr.mjs --dir <test-set-dir> # P4-2 offline CER/latency/size comparison (zipformer int8 vs xlarge / small-CTC / paraformer; needs 16k mono PCM wav + same-name .txt)
|
|
74
86
|
```
|
|
75
87
|
|
|
76
88
|
## Usage
|
|
77
89
|
|
|
78
90
|
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
|
|
79
|
-
2. Choose how to speak: just talk and let the
|
|
91
|
+
2. Choose how to speak: just talk and let the 2 s pause auto-send (toggle); or hold the mic button and release to send (hold)
|
|
80
92
|
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
|
|
81
93
|
4. Click "Exit" in the status bar (or press `Ctrl+Shift+V` again) to leave voice mode
|
|
82
94
|
|
|
83
95
|
On first entry the recognition model is downloaded; the status bar shows `正在加载模型… <file> <percent>%`.
|
|
84
96
|
|
|
97
|
+
If a wake word is configured, you land in standby first (the status bar prompts `说『唤醒词』开始`), and recognizing starts after you speak the wake word.
|
|
98
|
+
|
|
85
99
|
## Settings (Settings → Plugins → Plugins config → 语音模式)
|
|
86
100
|
|
|
87
101
|
| Key | Default | Description |
|
|
88
102
|
| --- | --- | --- |
|
|
89
|
-
| `
|
|
103
|
+
| `ttsEngine` | `edge` | Read-aloud engine: `edge` Microsoft cloud (default, fast) / `vits` local Chinese / `kokoro` local zh+en (int8); **applies live** |
|
|
104
|
+
| `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 |
|
|
90
105
|
| `rate` | `1.0` | Reading speed multiplier (0.5 slow ~ 2.0 fast), **applies live** |
|
|
91
|
-
| `interruptLevel` | `0` | Barge-in sensitivity: 0 high
|
|
92
|
-
| `silenceMs` | `700` | Silence pause in ms that marks the end of a complete sentence
|
|
93
|
-
| `idleTimeoutMinutes` | `10` | Minutes of inactivity before auto-exiting voice mode |
|
|
94
|
-
| `modelHost` | default |
|
|
106
|
+
| `interruptLevel` | `0` | Barge-in sensitivity (host-side VAD frame detection + echo gate): 0 high threshold / 1 medium / 2 low |
|
|
107
|
+
| `silenceMs` | `700` | Silence pause in ms that marks the end of a complete sentence |
|
|
108
|
+
| `idleTimeoutMinutes` | `10` | Minutes of inactivity before auto-exiting voice mode (reading counts as activity) |
|
|
109
|
+
| `modelHost` | default | Model download host (use `https://hf-mirror.com` on mainland networks) |
|
|
95
110
|
| `autoSend` | `true` | Auto-send after a finalized transcript; when off, text only goes to the draft (hold `Ctrl` / release in hold mode still sends) |
|
|
96
|
-
| `mode` | `toggle` | Interaction mode: `toggle` continuous listening + silence
|
|
97
|
-
| `
|
|
98
|
-
| `senseVoice` | `true` | Re-transcribe the finalized utterance with SenseVoice (punctuation + ITN, more accurate); turning it off skips the 228 MB model and keeps streaming only |
|
|
111
|
+
| `mode` | `toggle` | Interaction mode: `toggle` continuous listening + 700 ms silence split; `hold` push-to-talk, release to send (short tap exits) |
|
|
112
|
+
| `wakeWord` | empty (off) | Wake word (e.g. `你好小D`): speak it after entering to activate, avoiding accidental triggers; empty = off |
|
|
99
113
|
|
|
100
|
-
Effect timing: `voice`/`rate
|
|
114
|
+
Effect timing: `voice`/`rate` 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.
|
|
101
115
|
|
|
102
116
|
### Common voices (full list: `node scripts/list-voices.mjs`)
|
|
103
117
|
|
|
@@ -146,9 +160,9 @@ You can also edit the `voice-mode:` section of `~/.dsh/settings.yaml` directly (
|
|
|
146
160
|
|
|
147
161
|
| Route | Description |
|
|
148
162
|
| --- | --- |
|
|
149
|
-
| `GET /voice-mode/stream` | SSE: `event: audio` (`{sessionId,
|
|
163
|
+
| `GET /voice-mode/stream` | SSE: `event: audio` (`{sessionId, seq, text, audio(base64 MP3)}`), `event: mode` (global single-active ownership), `event: tool` (beep), `event: asr-progress / asr-ready / asr-error / tts-error` |
|
|
150
164
|
| `POST /voice-mode/toggle` | `{sessionId, on}` enter/exit voice mode (globally single-active) |
|
|
151
|
-
| `POST /voice-mode/asr` | f32 LE 16k PCM payload → `{text
|
|
165
|
+
| `POST /voice-mode/asr` | Raw f32 LE 16k PCM payload → `{text}` (streaming zipformer2); returns `202 {loading}` until the model is ready; `?reset=1` discards the in-flight segment (used on wake-word hit) |
|
|
152
166
|
| `POST /voice-mode/cancel` | `{sessionId}` invalidates the TTS queue and drops the in-flight ASR segment |
|
|
153
167
|
| `POST /voice-mode/preview` | `{voice, rate?}` one-shot synthesis preview → `audio/mpeg` (400 missing voice / voice too long; 502 synthesis failure, e.g. invalid ShortName; 403 when the plugin's `enabled=false`). Does not require voice mode to be active; uses an isolated synthesis connection and does not affect the reading queue |
|
|
154
168
|
| `GET /voice-mode/config` | Client bootstrap parameters (silence threshold / sensitivity / voice and rate, etc.) |
|
|
@@ -156,7 +170,7 @@ You can also edit the `voice-mode:` section of `~/.dsh/settings.yaml` directly (
|
|
|
156
170
|
|
|
157
171
|
## Model & cache
|
|
158
172
|
|
|
159
|
-
- Recognition
|
|
173
|
+
- Recognition model: `csukuangfj/sherpa-onnx-streaming-zipformer-zh-int8-2025-06-30` (encoder ≈154 MB / decoder / joiner / tokens, ~160 MB total), running host-side via sherpa-onnx (Node WASM, Apache-2.0, natively cross-platform)
|
|
160
174
|
- Cache directory defaults by platform:
|
|
161
175
|
- **Windows**: `%LOCALAPPDATA%\dsh-voice-mode\models`
|
|
162
176
|
- **macOS / Linux**: `~/.cache/dsh-voice-mode/models`
|
|
@@ -168,31 +182,31 @@ You can also edit the `voice-mode:` section of `~/.dsh/settings.yaml` directly (
|
|
|
168
182
|

|
|
169
183
|
|
|
170
184
|
```
|
|
171
|
-
input: mic ──
|
|
185
|
+
input: mic ──RMS VAD (5s silence split)──▶ POST /voice-mode/asr (f32 PCM, 16k, incremental)
|
|
172
186
|
│ zipformer2 streaming ASR (host-side WASM)
|
|
173
187
|
▼
|
|
174
188
|
composer draft ──autoSend──▶ model stream ──llm/stream tap (active voice session only)
|
|
175
189
|
│ text-delta filter → sentence segmentation
|
|
176
190
|
▼
|
|
177
|
-
browser ◀── SSE /voice-mode/stream ◀── TtsQueue (
|
|
191
|
+
browser ◀── SSE /voice-mode/stream ◀── TtsQueue (local VITS / native Kokoro / Edge, sentence-by-sentence)
|
|
178
192
|
```
|
|
179
193
|
|
|
180
194
|
- Speech and reading only happen for the session pointed to by the global single-active pointer `activeVoiceSession`; other sessions pass through `llm/stream` with zero overhead (mode isolation)
|
|
181
195
|
- The `llm/stream` tap is lossless: every chunk passes through unchanged; segmentation/synthesis only observe and never block the model stream
|
|
182
|
-
-
|
|
196
|
+
- ASR runs host-side (sherpa-onnx WASM, zipformer2 Chinese streaming + SenseVoice finalization which adds punctuation); the browser only captures audio (`getUserMedia` 16k mono) and does endpoint detection
|
|
197
|
+
- Local TTS (VITS / native Kokoro) runs in an isolated child process (fork, auto-restart); barge-in kills the in-flight synthesis instantly to free CPU
|
|
183
198
|
- The TTS queue is per-session with an epoch version: old frames are all invalidated after a barge-in, so it is truly silent
|
|
184
199
|
|
|
185
200
|
## Known limitations
|
|
186
201
|
|
|
187
|
-
- Barge-in
|
|
202
|
+
- Barge-in relies on browser echo cancellation (`echoCancellation`); loud speaker volume may leak into the mic (no JS-level AEC)
|
|
188
203
|
- `Ctrl+Shift+V` overrides the browser's "paste as plain text" shortcut (normal `Ctrl+V` paste still works)
|
|
189
204
|
- The recognition model prioritizes Simplified Chinese; recognition quality is affected by ambient noise
|
|
190
205
|
- Browser autoplay policy: reading requires prior user interaction on the page (clicking the mic satisfies it); if the browser blocks playback and the status bar shows no hint, make sure the page is foregrounded and not muted
|
|
206
|
+
- **The wake word is a lightweight implementation** (text matching on the streaming transcript, not a dedicated KWS engine): it may lag or misfire in noisy environments; the wake word itself never enters the chat (the buffer is dropped on hit)
|
|
191
207
|
- In hold mode, switching windows/tabs while holding **abandons the segment** (prevents continuous recording); come back and hold again
|
|
192
208
|
- The hero (new-session empty state) has no voice entry: voice mode is a session-level feature; enter a session first and use the mic button in the input toolbar
|
|
193
209
|
- The preview request timeout uses `AbortSignal.timeout` (Chrome 103+ / Firefox 100+ / Safari 16+); on older browsers clicking preview immediately shows a failure hint — an expected degradation
|
|
194
|
-
- **Security**: the plugin HTTP surface (`/voice-mode/*`) follows the host security model — do not expose the dsh port to the public internet; when publishing behind a reverse proxy, add auth at the proxy layer (e.g. basic auth). `/asr` validates the active voice session (403 otherwise); `/toggle` `/cancel` `/preview` are unauthenticated availability surfaces (they can only flip voice-mode state / stop TTS; a local malicious page could trigger that, with no privilege escalation or eavesdropping — reads are CORS-restricted)
|
|
195
|
-
- **Safari / iOS**: requires HTTPS or localhost (secure context for the mic); first entry needs mic permission (iOS: Settings → Safari → Microphone); recognition/reading pause in the background on iOS and resume on return (may drop a sentence) — keep the page in the foreground during voice mode
|
|
196
210
|
|
|
197
211
|
## Troubleshooting
|
|
198
212
|
|
|
@@ -236,7 +250,7 @@ changing deps, run `npm pack --dry-run` and `pnpm test` as regression.
|
|
|
236
250
|
|
|
237
251
|
```sh
|
|
238
252
|
pnpm install && pnpm build # esbuild: lib/index.js (host) + lib/client.js (browser)
|
|
239
|
-
pnpm test # unit tests
|
|
253
|
+
pnpm test # segmenter/wakeword unit tests + pre-release self-check (no network)
|
|
240
254
|
node test/hold-e2e.js # hold-mode acceptance (standalone browser, /asr route interception)
|
|
241
255
|
systemctl restart dsh # Linux; restart the dsh process on other platforms
|
|
242
256
|
```
|
|
@@ -244,34 +258,25 @@ systemctl restart dsh # Linux; restart the dsh process on other platform
|
|
|
244
258
|
> Note: dsh installs the plugin as a pnpm `file:` link (directory copy); after `node build.mjs` you
|
|
245
259
|
> must copy `lib/client.js` to `<profile>/node_modules/dsh-voice-mode/lib/` and restart dsh before
|
|
246
260
|
> the browser picks up the new bundle.
|
|
247
|
-
>
|
|
248
|
-
> Dev-mode latency telemetry (P1-5): run `localStorage.setItem('dsh-voice-mode.telemetry', '1')` in the
|
|
249
|
-
> browser console, refresh, and enter voice mode — the status bar shows per-stage latencies of the
|
|
250
|
-
> speech-end → first-audio chain (end → endpoint → submit → 1st token → 1st sentence → 1st chunk →
|
|
251
|
-
> 1st audio, plus the total), for P1 acceptance measurement. Remove the key to disable (off by default, zero collection).
|
|
252
261
|
|
|
253
262
|
### Structure
|
|
254
263
|
|
|
255
264
|
```
|
|
256
|
-
src/index.ts host: single-active pointer, llm/stream tap, SSE, settings registration
|
|
257
|
-
src/asr-host.ts host: zipformer2 streaming ASR +
|
|
258
|
-
src/tts-queue.ts host: per-session TTS queue +
|
|
265
|
+
src/index.ts host: single-active pointer, llm/stream tap, SSE, settings registration
|
|
266
|
+
src/asr-host.ts host: zipformer2 streaming ASR + lazy model download (.part resume)
|
|
267
|
+
src/tts-queue.ts host: per-session TTS queue + epoch barge-in
|
|
259
268
|
src/segmenter.ts host: sentence segmentation (markdown stripping + terminating punctuation)
|
|
260
|
-
src/
|
|
261
|
-
src/
|
|
262
|
-
src/resample.ts client: linear resampling (capture / echo reference)
|
|
263
|
-
src/client.tsx client: mic button + status bar + overlay + playback engine (Web Audio queue) + barge-in
|
|
264
|
-
src/settings-form.tsx client: settings card (Plugins → plugin config)
|
|
265
|
-
scripts/bench-asr.mjs offline CER/latency/size comparison of streaming ASR models
|
|
269
|
+
src/client.tsx client: mic button + status bar + reading overlay + barge-in
|
|
270
|
+
src/asr.ts client: getUserMedia + RMS VAD + partial polling
|
|
266
271
|
scripts/prefetch.mjs model pre-download (cross-platform cache dir + resume)
|
|
267
272
|
test/segmenter.test.mjs sentence segmentation unit tests
|
|
268
|
-
test/
|
|
273
|
+
test/wakeword.test.mjs wake-word matching unit tests
|
|
269
274
|
test/verify-client.mjs pre-release self-check (bundle manifest/exports/shape)
|
|
270
275
|
test/hold-e2e.js hold-mode end-to-end acceptance (standalone browser)
|
|
271
276
|
scripts/list-voices.mjs print all Edge TTS voices (source of the voice table)
|
|
272
277
|
```
|
|
273
278
|
|
|
274
|
-
Integration probes (`hold-e2e.js`, `
|
|
279
|
+
Integration probes (`hold-e2e.js`, `spoken-prompt-rpc.sh`, `spoken-toggle-ui-check.js`) live in the repo root `test/`, outside this npm package.
|
|
275
280
|
|
|
276
281
|
## License
|
|
277
282
|
|
package/README.md
CHANGED
|
@@ -9,22 +9,43 @@ DeepSeek Harness 语音双工对话模式:会话内一键进入 → 边说边
|
|
|
9
9
|
|
|
10
10
|
> **Full-duplex voice mode for DeepSeek Harness** — streamed ASR to an editable draft, sentence-by-sentence read-aloud with live captions, and speaking interrupts playback and the running turn.
|
|
11
11
|
|
|
12
|
+

|
|
13
|
+
|
|
12
14
|

|
|
13
15
|
|
|
16
|
+
> **版本说明(0.4.0)**:朗读默认 **Edge 云端**(快速自然),本地 TTS(VITS/Kokoro)可选(隐私优先)+ HTTP 安全加固 + 模型 SHA256 固定为合入核心;`wakeWord`(唤醒词)与 `toolBeep`(工具提示音)已完整接入;早期 fork 的 `asrModel`(双语 paraformer)与 `punctuate`(神经标点)已移除——SenseVoice 定稿本身已带标点,流式识别固定为 zipformer2。静音断句默认 700 毫秒。
|
|
17
|
+
|
|
18
|
+
## Fork 增强(本仓库新增)
|
|
19
|
+
|
|
20
|
+
本仓库在上游基础上加入了大量增强,核心如下(完整清单见 git 历史与迭代记录):
|
|
21
|
+
|
|
22
|
+
- **朗读默认 Edge 云端;本地 TTS 可选(隐私优先)**:选本地则回复文本不出本机——
|
|
23
|
+
- 本地 VITS(`sherpa-onnx-vits-zh-ll`,纯中文,5 说话人);
|
|
24
|
+
- 本地 Kokoro(`kokoro-int8-multi-lang-v1_1`,**中英混读**,int8 约 109MB),经 `sherpa-onnx-node` **原生 addon** 运行(无 WASM 内存上限,连续合成不崩);
|
|
25
|
+
- Edge 云端朗读保留为可选(设置 `ttsEngine: edge`);设置面板「朗读引擎」热切换。
|
|
26
|
+
- **Kokoro 音色全量 103 个**(F0 实测标定性别),四个常用男声置顶带编号;音色面板用 **下拉列表 + ◀▶ 步进**切换;
|
|
27
|
+
- **增量传输**:partial 只传新增 0.9 秒,长段按住说话松手**秒出定稿**(不再整段重传重解码);
|
|
28
|
+
- **交互增强**:输入框旁**模式切换按钮**(持续聆听 ⇄ 按住说话,保存到设置);按住说模式下**按住才录、不按住不打断**;
|
|
29
|
+
- **长段支持**:按住说整段上限 10 分钟(停顿不断句);持续聆听单句上限 3 分钟;静音断句默认 700 毫秒;
|
|
30
|
+
- **朗读稳定性**:打断即终止在途合成释放 CPU;句间不再有 3-5 秒停顿;长朗读不触发空闲下线;
|
|
31
|
+
- **安全加固**:会话存在性校验 / 回环+Origin 校验 / 全端点限流 / **ASR+TTS 全模型 SHA256 固定** / 下载域名白名单 / 重定向守卫。
|
|
32
|
+
|
|
33
|
+
> ⚠️ 上文截图与 `assets/demo.gif` 为**上游旧版界面**(单按钮时期);当前界面在语音按钮旁多一颗「模式切换」按钮。
|
|
34
|
+
|
|
14
35
|
## 功能
|
|
15
36
|
|
|
16
37
|
- **语音模式**:输入框工具排麦克风按钮或全局快捷键 `Ctrl+Shift+V` 进入/退出;全局单活(同一时刻仅一个会话处于语音模式,切换会话自动让出)
|
|
17
|
-
-
|
|
18
|
-
- `toggle
|
|
19
|
-
- `hold` 按住说话:短按进入/退出,**按住麦克风按钮说话、松手即发**(滑出取消、`Esc
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
- **设置**:设置 → Plugins → 插件配置 → 语音模式(voice-mode
|
|
25
|
-
-
|
|
38
|
+
- **两种交互模式(输入框旁按钮或设置可切换,切换即持久化)**:
|
|
39
|
+
- `toggle`(默认)持续聆听:RMS VAD 分段 → zipformer2 流式识别(边说边出字,实时字幕预览)→ 静音约 700 毫秒自动断句进草稿并自动发送;按住 `Ctrl` 强制立即发送
|
|
40
|
+
- `hold` 按住说话:短按进入/退出,**按住麦克风按钮说话、松手即发**(滑出取消、`Esc`/失焦放弃本段);按住期间停顿不断句(上限 10 分钟);`Ctrl` 按住即录、松开即发
|
|
41
|
+
- **唤醒词(可选,默认关)**:设置 `wakeWord` 后进入待机态,说出唤醒词才开始识别(如「你好小D」)
|
|
42
|
+
- **输出链路**:只朗读最终答复的 `text-delta`(reasoning/工具调用不读),按句流式朗读(默认 Edge 云端;可切本地 VITS/Kokoro,中英混读选 Kokoro)+ 右下角实时字幕浮层;工具调用触发提示音;全文照常写入聊天记录;口语化提示词(设置 `spokenFormat`,默认开)让回复为自然短句、不带 Markdown 排版符号
|
|
43
|
+
- **开口打断(barge-in)**:服务端 Silero VAD 帧级检测 + 回声门控(echoGateDb)三档灵敏度 → 本地静音 + host 合成队列作废 + 正在运行的回合取消(保留半截并自然续入新消息);朗读中自动切超灵敏档
|
|
44
|
+
- **模型懒加载与进度**:首次使用自动下载识别/合成模型(`.part` 断点续传),状态条实时显示进度;可用 `npm run prefetch` 预下载
|
|
45
|
+
- **设置**:设置 → Plugins → 插件配置 → 语音模式(voice-mode),可调朗读引擎/音色/语速/打断灵敏度/静音停顿/空闲超时/模型镜像/自动发送/交互模式/唤醒词/口语化提示词;**音色可试听**(按当前音色+语速即时合成预览,自定义 ShortName 亦可)
|
|
46
|
+
- **界面语言**:跟随浏览器语言(中文 / English;切换后刷新页面生效)
|
|
26
47
|
- **容错**:麦克风被拒红点提示、模型下载失败可见提示、TTS 连接失败状态条提示(自动退避重试)、提交失败文字留在草稿、SSE 断线自动重连
|
|
27
|
-
- **空闲退出**:10
|
|
48
|
+
- **空闲退出**:10 分钟无活动自动退出并释放麦克风(**正在朗读计为活动**,长朗读不会中途下线)
|
|
28
49
|
|
|
29
50
|
## 安装
|
|
30
51
|
|
|
@@ -36,20 +57,13 @@ bundle 插件安装后需重启 dsh 生效(Linux:`systemctl restart dsh`;
|
|
|
36
57
|
|
|
37
58
|
## 操作手势
|
|
38
59
|
|
|
39
|
-
**快速真机验收(第一步,约 3 分钟)**:
|
|
40
|
-
|
|
41
|
-
1. 进入任一会话,`Ctrl+Shift+V` 或点输入栏麦克风按钮进入语音模式(状态条显示「聆听中…」);
|
|
42
|
-
2. 说一句完整的话(如「帮我看看今天天气」)→ 状态条实时字幕应在 ≤400ms 出现、停顿后自动发送(或按住 `Ctrl` 立即定稿);
|
|
43
|
-
3. AI 回复开始朗读时开口打断 → 朗读即刻停止、你说的话进入识别;此时查看 设置 → 语音模式 →「语音模型」区块应显示三模型状态(第一次用会显示下载进度)。
|
|
44
|
-
|
|
45
|
-
完整验收清单(P1-P4 指标/回归/平台)见 `.scratch/voice-duplex-upgrade/acceptance-checklist.md`。
|
|
46
|
-
|
|
47
|
-
|
|
48
60
|
| 手势 | 作用 |
|
|
49
61
|
| --- | --- |
|
|
50
62
|
| `Ctrl+Shift+V` | 进入 / 退出语音模式 |
|
|
51
|
-
| 直接说话 | `toggle
|
|
63
|
+
| 直接说话 | `toggle`:边说边出字,停顿约 700 毫秒自动发送;按住 `Ctrl` 强制立即发送 |
|
|
52
64
|
| 按住麦克风按钮 | `hold`:松手发送;短按退出;滑出 / `Esc` / 失焦放弃本段 |
|
|
65
|
+
| 点输入框旁模式按钮 | 在「持续聆听 ⇄ 按住说」间切换(保存到设置) |
|
|
66
|
+
| 说唤醒词 | 待机态激活识别(配置后) |
|
|
53
67
|
| AI 朗读时开口说话 | 打断朗读并取消当前回合 |
|
|
54
68
|
| 点状态条「退出」 | 退出语音模式 |
|
|
55
69
|
| 点字幕浮层「跳过」 | 跳过当前句朗读 |
|
|
@@ -58,30 +72,26 @@ bundle 插件安装后需重启 dsh 生效(Linux:`systemctl restart dsh`;
|
|
|
58
72
|
|
|
59
73
|
| 键 | 默认 | 说明 |
|
|
60
74
|
| --- | --- | --- |
|
|
61
|
-
| `
|
|
75
|
+
| `ttsEngine` | `edge` | 朗读引擎:`edge` 微软云端(默认,快)/ `vits` 本地中文 / `kokoro` 本地中英;**即时生效** |
|
|
76
|
+
| `voice` | 按引擎 | 音色:VITS 五说话人;Kokoro 103 个(下拉+◀▶,62 深沉/68 浑厚/75 清亮/76 磁性置顶);Edge 进入时自动加载全量 322 个。行内「试听」可即时预览 |
|
|
62
77
|
| `rate` | `1.0` | 朗读语速倍率(0.5 慢速 ~ 2.0 快速),**即时生效** |
|
|
63
|
-
| `interruptLevel` | `0` |
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `silenceMs` | `700` | 说完整一句的静音停顿毫秒数(至少 250ms 语音才判句) |
|
|
68
|
-
| `idleTimeoutMinutes` | `10` | 无活动自动退出语音模式的分钟数 |
|
|
69
|
-
| `modelHost` | 默认源 | ASR 模型下载源(国内网络填 `https://hf-mirror.com`) |
|
|
78
|
+
| `interruptLevel` | `0` | 发声打断灵敏度(服务端 VAD 帧级检测 + 回声门控):0 高门槛 / 1 中 / 2 低 |
|
|
79
|
+
| `silenceMs` | `700` | 说完整一句的静音停顿毫秒数 |
|
|
80
|
+
| `idleTimeoutMinutes` | `10` | 无活动自动退出语音模式的分钟数(朗读计为活动) |
|
|
81
|
+
| `modelHost` | 默认源 | 模型下载源(国内网络填 `https://hf-mirror.com`) |
|
|
70
82
|
| `autoSend` | `true` | 识别定稿后自动发送;关闭则只进草稿(按住 `Ctrl` / hold 松手仍会发送) |
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
83
|
+
| `mode` | `toggle` | 交互模式:`toggle` 持续聆听 + 700ms 静音断句;`hold` 按住说话、松手发送(短按退出) |
|
|
84
|
+
| `wakeWord` | 空(关) | 唤醒词(如「你好小D」):进入后先说唤醒词激活,避免误触;空 = 关闭 |
|
|
73
85
|
| `spokenFormat` | `false` | 语音会话注入口语化提示词:开启后**仅当前语音会话**的回复被注入「口语化短句、不用 Markdown 排版符号」提示词(朗读更顺),**即时生效** |
|
|
74
|
-
| `senseVoice` | `true` | 定稿用 SenseVoice 重译(带标点 + 数字归一化,识别更准);关闭可省 228MB 模型、只走流式识别 |
|
|
75
86
|
|
|
76
|
-
生效范围:`voice`/`rate`/`spokenFormat
|
|
87
|
+
生效范围:`voice`/`rate`/`ttsEngine`/`spokenFormat` **立即生效**;其余设置下次进入语音模式时生效。设置项默认值由插件配置(`base` 层)提供。
|
|
77
88
|
|
|
78
|
-
###
|
|
89
|
+
### 本地音色(VITS / Kokoro)
|
|
79
90
|
|
|
80
|
-
- `
|
|
81
|
-
- `
|
|
82
|
-
- `node scripts/bench-asr.mjs --dir <测试集目录>`(P4-2):离线 CER/段延迟/体积对照(现役 zipformer int8 vs xlarge / small-CTC / 在线 Paraformer),测试集 = 16k 单声道 PCM `.wav` + 同名 `.txt` 参考文本
|
|
91
|
+
- **VITS(纯中文)**:`suyingxue` 素映雪·女 / `gunian` 顾念·男 / `fushiyu` 傅斯遇·女 / `bingjiao` 冰娇·男 / `bazong` 霸总·男
|
|
92
|
+
- **Kokoro(中英混读均可)**:103 个音色全量入表,面板按编号 + 实测性别标注;四个常用男声置顶:`62` 深沉 / `68` 浑厚 / `75` 清亮 / `76` 磁性;中文女声 `48` 小北 / `49` 小妮 / `50` 小小 / `51` 小艺。音色只是风格向量,**语言能力与音色无关**。
|
|
83
93
|
|
|
84
|
-
###
|
|
94
|
+
### 常用 Edge 音色(完整清单见 `node scripts/list-voices.mjs`)
|
|
85
95
|
|
|
86
96
|
| ShortName | 说明 |
|
|
87
97
|
| --- | --- |
|
|
@@ -98,68 +108,73 @@ bundle 插件安装后需重启 dsh 生效(Linux:`systemctl restart dsh`;
|
|
|
98
108
|
|
|
99
109
|
### 配置(bundle config / settings.yaml)
|
|
100
110
|
|
|
101
|
-
`voice-mode` 命名空间配置可直接写入 `~/.dsh/settings.yaml`;插件总开关 `enabled`(默认 `true
|
|
111
|
+
`voice-mode` 命名空间配置可直接写入 `~/.dsh/settings.yaml`;插件总开关 `enabled`(默认 `true`)、模型缓存目录 `cacheDir`、在安装配置中设置。
|
|
102
112
|
|
|
103
113
|
## 工作原理
|
|
104
114
|
|
|
105
115
|
```
|
|
106
|
-
麦克风(16kHz, AEC) ─▶
|
|
107
|
-
|
|
108
|
-
|
|
116
|
+
麦克风(16kHz, AEC) ─▶ 浏览器 VAD 分段 ─▶ HOST zipformer2 流式识别(本地 WASM, 增量传输)
|
|
117
|
+
│
|
|
118
|
+
▼
|
|
119
|
+
用户说话 ◀── 打断 ◀── 音箱 ◀── 逐句合成 ◀── 分句(text-delta 过滤) ◀── SenseVoice 标点定稿
|
|
120
|
+
│
|
|
121
|
+
本地 VITS / 本地 Kokoro(原生 addon, 子进程) / Edge 云端(可选)
|
|
109
122
|
```
|
|
110
123
|
|
|
111
|
-
- 识别在 **host 端本地运行**(zipformer2 int8 WASM
|
|
112
|
-
-
|
|
124
|
+
- 识别在 **host 端本地运行**(zipformer2 中文 int8 WASM + SenseVoice 定稿,模型懒下载),音频不上传第三方;识别定稿由 SenseVoice 补标点;
|
|
125
|
+
- 朗读默认 **Edge 云端**;本地 VITS 纯中文 / Kokoro 原生中英(跑在独立子进程、崩溃自愈)可选(隐私优先);
|
|
113
126
|
- 同一时间仅一个会话处于语音模式(全局单活);LLM 流被无损观察(不阻塞)。
|
|
114
127
|
|
|
115
128
|
## 已知限制
|
|
116
129
|
|
|
117
|
-
-
|
|
130
|
+
- 发声打断依赖浏览器回声消除(`echoCancellation`);扬声器音量过大时可能漏声到麦克风
|
|
118
131
|
- `Ctrl+Shift+V` 会覆盖浏览器「粘贴纯文本」快捷键(普通粘贴仍可用 `Ctrl+V`)
|
|
119
|
-
-
|
|
132
|
+
- 识别质量受环境噪声影响;zipformer2 中文流式 + SenseVoice 多语(中英日韩粤)定稿
|
|
120
133
|
- 浏览器自动播放策略:朗读需要页面已有用户交互(点击麦克风即满足);「试听」依赖 `AbortSignal.timeout`(Safari 16+ / Chrome 103+ / Firefox 100+;老浏览器点击试听会立即提示失败,属预期降级)
|
|
134
|
+
- **唤醒词为轻量实现**(流式文本匹配,非专用 KWS 引擎):嘈杂环境可能延迟或误激活;唤醒词本身不会进入聊天
|
|
121
135
|
- hold 模式按住时切换窗口/标签页会**放弃本段**(防持续收音)
|
|
122
136
|
- hero(新会话空态)无语音入口:请先进入会话使用麦克风按钮
|
|
123
137
|
- `spokenFormat` 提示词经官方 `system-prompt/assemble` 瀑布注入;若当前会话使用**完整提示词**配置(persona `complete: true` 的 agent preset),提示词不注入(官方 complete 契约优先)
|
|
138
|
+
- 本地 Kokoro 每次打断后下一句朗读前约有 1 秒引擎重建时间(打断即终止在途合成的代价)
|
|
124
139
|
- **苹果 Safari / iOS**:
|
|
125
140
|
- 需 **HTTPS 或 localhost**(iOS/macOS Safari 强制安全上下文;`http://` 局域网 IP 下麦克风不可用)
|
|
126
141
|
- 首次进入需授权麦克风;被拒后到「设置 → Safari → 麦克风」开启(iOS)
|
|
127
142
|
- iOS 后台/锁屏时识别与朗读暂停,回前台自动恢复(可能丢句);建议语音模式期间保持前台
|
|
128
|
-
- **安全说明**:插件 HTTP 面(`/voice-mode/*`)遵循宿主安全模型——请勿将 dsh 端口直接暴露公网;经反向代理发布时由代理层(如 basic auth
|
|
143
|
+
- **安全说明**:插件 HTTP 面(`/voice-mode/*`)遵循宿主安全模型——请勿将 dsh 端口直接暴露公网;经反向代理发布时由代理层(如 basic auth)鉴权;插件侧对敏感操作保留会话归属校验
|
|
129
144
|
|
|
130
145
|
## 故障排查
|
|
131
146
|
|
|
132
147
|
| 现象 | 处理 |
|
|
133
148
|
| --- | --- |
|
|
134
149
|
| 点麦克风无反应,状态条红字 | 浏览器拒绝麦克风:地址栏(iOS 为 设置 → Safari → 麦克风)开启后重试 |
|
|
135
|
-
| 状态条「正在加载模型… x%」卡住 |
|
|
136
|
-
| 朗读无声音/无字幕 |
|
|
150
|
+
| 状态条「正在加载模型… x%」卡住 | 检查网络;模型较大可先 `npm run prefetch`;国内网络 `modelHost` 配 `https://hf-mirror.com` |
|
|
151
|
+
| 朗读无声音/无字幕 | 本地引擎首次合成需加载模型;若持续失败查看状态条提示(自动退避重试);确认页面前台且未静音 |
|
|
137
152
|
| 语音模式进不去 | 检查插件 `enabled`;多标签页时确认当前会话为活动会话 |
|
|
138
|
-
| 识别到但不是我要说的 |
|
|
153
|
+
| 识别到但不是我要说的 | 环境噪声或唤醒词误判:降低音量、提高 `interruptLevel`(高门槛)或启用 `wakeWord` |
|
|
154
|
+
| 按住说话松手后没反应 | 确认交互模式为「按住说」且按住期间按钮高亮;松手后识别定稿约 1 秒内进入草稿 |
|
|
139
155
|
|
|
140
156
|
## 开发
|
|
141
157
|
|
|
142
158
|
```sh
|
|
143
159
|
pnpm install && pnpm build # esbuild:lib/index.js(host)+ lib/client.js(browser)
|
|
144
|
-
pnpm test #
|
|
160
|
+
pnpm test # segmenter/wakeword 单测 + 发布前自检(无需网络)
|
|
145
161
|
systemctl restart dsh # 本机加载新 host 代码;其他平台重启 dsh 进程
|
|
146
162
|
```
|
|
147
163
|
|
|
148
|
-
> 注意:dsh 安装的是 pnpm `file:` 链接(目录拷贝),改完 `node build.mjs` 后需把 `lib/client.js` 同步到 `<profile>/node_modules/dsh-voice-mode/lib/` 再刷新页面(`lib/index.js` 与工作区为同一文件自动同步)。集成探测脚本(`test/hold-e2e.js`、`test/
|
|
149
|
-
|
|
150
|
-
> 开发模式延迟埋点(P1-5):浏览器控制台执行 `localStorage.setItem('dsh-voice-mode.telemetry', '1')` 后刷新页面,进入语音模式时状态条会实时显示「说完 → 端点 → 定稿 → 首Token → 首句 → 首chunk → 首音」各段耗时与合计(说完→首音),供 P1 延迟验收测量;`localStorage.removeItem('dsh-voice-mode.telemetry')` 关闭(默认关闭,零采集)。
|
|
164
|
+
> 注意:dsh 安装的是 pnpm `file:` 链接(目录拷贝),改完 `node build.mjs` 后需把 `lib/client.js` 同步到 `<profile>/node_modules/dsh-voice-mode/lib/` 再刷新页面(`lib/index.js` 与工作区为同一文件自动同步)。集成探测脚本(`test/hold-e2e.js`、`test/spoken-prompt-rpc.sh`、`test/spoken-toggle-ui-check.js`)位于仓库根 `test/`,不在 npm 包内。
|
|
151
165
|
|
|
152
166
|
```
|
|
153
|
-
src/index.ts
|
|
154
|
-
src/asr-host.ts
|
|
155
|
-
src/
|
|
156
|
-
src/
|
|
157
|
-
src/
|
|
158
|
-
src/
|
|
159
|
-
src/
|
|
160
|
-
src/
|
|
161
|
-
src/
|
|
162
|
-
src/
|
|
167
|
+
src/index.ts host:单活指针、llm/stream tap、SSE、settings 注册、口语化提示词注入
|
|
168
|
+
src/asr-host.ts host:zipformer2 流式识别 + SenseVoice 定稿 + 模型懒下载(.part 断点续传)+ 增量喂料
|
|
169
|
+
src/models.ts host:模型下载/校验(SHA256 固定 + 域名白名单)
|
|
170
|
+
src/security.ts host:限流器与安全守卫
|
|
171
|
+
src/tts-local.ts host:本地 TTS 引擎(VITS WASM / Kokoro 原生 addon,子进程管理)
|
|
172
|
+
src/tts-vits-worker.ts 子进程:合成执行(base64 IPC,空文本静音守卫)
|
|
173
|
+
src/tts-queue.ts host:逐会话 TTS 队列 + epoch 打断机制
|
|
174
|
+
src/segmenter.ts host:句子切分 + 文本消毒(markdown 剥离 + 噪声字符剔除)
|
|
175
|
+
src/asr.ts client:音频采集、VAD 分段、增量识别、唤醒词、按住说门控
|
|
176
|
+
src/client.tsx client:麦克风按钮 + 模式切换按钮 + 状态条 + 字幕浮层 + 打断
|
|
177
|
+
src/strings.ts client:中英文案字典(navigator.language)
|
|
163
178
|
```
|
|
164
179
|
|
|
165
180
|
## License
|