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 CHANGED
@@ -11,7 +11,18 @@
11
11
  >
12
12
  > 中文说明见 [README.md](./README.md)。
13
13
 
14
- ![demo](https://raw.githubusercontent.com/qishuilalala/dsh-voice-mode/HEAD/plugin/dsh-voice-mode/assets/demo.gif)
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
+ ![Voice mode: live captions and status bar](https://raw.githubusercontent.com/qishuilalala/dsh-voice-mode/HEAD/assets/screenshot-voice.png)
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: local RMS gate opens segments → streaming zipformer2 ASR (words appear as you speak, live caption preview) → automatic sentence split and send after 700 ms of silence (<250 ms utterances are treated as noise; endpoints are decided by the host-side Silero VAD, with a client-side silence timer as fallback); hold `Ctrl` to force an immediate send
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
- - **Output pipeline**: only the final answer's `text-delta` is read (reasoning/tool calls are skipped), streamed sentence-by-sentence via Edge TTS with a live caption overlay at the bottom-right; 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
26
- - **Barge-in**: **NLMS acoustic echo cancellation (P3, cross-correlation bulk-delay estimate + 64 ms short filter)** using the page's own TTS playback as the reference, then **host-side Silero VAD frame-level detection (isSpeech, sent down with partial responses) drives the interrupt** three sensitivity levels map to speech-confirmation time (0 high barrier ~0.3 s / 1 medium 0.2 s / 2 low 0.1 s); with `bargeInMode: auto` speaking interrupts automatically, with `manual` (recommended for speakers) hold the mic button / Ctrl to interrupt explicitly local mute + host synth queue invalidation (epoch) + running turn cancellation (the half-finished part is kept and naturally flows into your new message)
27
- - **Lazy model download with progress**: the zipformer2 streaming model (~160 MB), Silero VAD (~2 MB) and the SenseVoice finalization model (~228 MB, first finalize; can be disabled) are downloaded on first use all `.part` resumable with hf-mirror fallback; live progress in the status bar; `npm run prefetch` pre-downloads them
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) | Silence-based auto sentence split and send (host VAD endpoint) |
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.3.0.tgz
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 silence-based end-point auto-send (toggle); or hold the mic button and release to send (hold)
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
- | `voice` | `zh-CN-XiaoxiaoNeural` | Edge TTS voice (see the common voices table below), **applies live**; the inline "试听" button previews it at the current rate (both listed voices and custom ShortNames are previewable; failures show a visible hint) |
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 barrier / 1 medium / 2 low (~0.3/0.2/0.1 s speech confirmation) |
92
- | `silenceMs` | `700` | Silence pause in ms that marks the end of a complete sentence (at least 250 ms of speech required) |
93
- | `idleTimeoutMinutes` | `10` | Minutes of inactivity before auto-exiting voice mode |
94
- | `modelHost` | default | ASR model download host (use `https://hf-mirror.com` on mainland networks) |
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-based end-point detection; `hold` push-to-talk, release to send (short tap exits) |
97
- | `spokenFormat` | `false` | Inject a spoken-format system prompt into voice replies (colloquial short sentences, no Markdown decoration; **applies live**) |
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`/`spokenFormat`/`senseVoice` take effect **immediately**; the rest apply on the next voice-mode entry. Defaults come from the plugin config (`base` layer) — they follow the config unless explicitly changed.
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, sentenceId, chunkId, final, text?(only on the final frame), audio(base64 MP3 chunk)}` — P1-1 chunked forwarding, the client reassembles each sentence before playback), `event: latency` (`{sessionId, stage}`: first-llm-token / first-sentence-text, P1-5), `event: turn` (`{sessionId, state}`: idle / listening / finalizing / agent-speaking, P2-4), `event: mode` (global single-active ownership), `event: asr-progress / asr-ready / asr-error / tts-error` |
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, endpoint?}` (streaming zipformer2; `endpoint: true` = host-side Silero VAD end-point detection, P2-1); on `final=1` the utterance is also re-transcribed with SenseVoice (int8, punctuation + ITN, P4-1) — falls back to the streaming result when unavailable; returns `202 {loading}` until the model is ready; `?reset=1` discards the in-flight segment. `?offset=N` (P1-4): this packet is the segment slice starting at sample N — the client uploads only newly-appended samples (P1-4 incremental upload); omit for full-packets (backward compatible) |
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 models (all host-side via sherpa-onnx Node WASM, Apache-2.0): streaming `csukuangfj/sherpa-onnx-streaming-zipformer-zh-int8-2025-06-30` (~160 MB, encoder ≈154 MB / decoder / joiner / tokens); VAD `csukuangfj/vad/silero_vad.onnx` (~2 MB, P2); SenseVoice `csukuangfj/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/model.int8.onnx` (~228 MB, P4 finalize, opt-out via settings)
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
  ![architecture](https://raw.githubusercontent.com/qishuilalala/dsh-voice-mode/HEAD/plugin/dsh-voice-mode/assets/architecture.svg)
169
183
 
170
184
  ```
171
- input: mic ──local RMS gate──▶ POST /voice-mode/asr (f32 PCM, 16k, incremental; host Silero VAD endpoints + isSpeech interrupt)
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 (msedge-tts sentence-by-sentence)
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
- - zipformer2 runs host-side (sherpa-onnx Node WASM); the browser only captures audio (`getUserMedia` 16k mono) and does endpoint detection
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 now includes NLMS acoustic echo cancellation (P3, reference = this page's TTS playback, cross-correlation bulk-delay estimate + 64 ms short filter) plus host-side Silero VAD frame-level detection; browser `echoCancellation` is only a fallback. Under extreme speaker volume/distance the suppression needs on-device calibration (`echoGateDb` etc.)
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 (segmenter/aec + pre-release self-check, no network; tests import src/*.ts directly — needs Node ≥22.18, type stripping)
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, turn state machine
257
- src/asr-host.ts host: zipformer2 streaming ASR + Silero VAD endpoint + SenseVoice finalize + lazy model download (.part resume)
258
- src/tts-queue.ts host: per-session TTS queue + chunked forwarding + epoch barge-in
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/asr.ts client: getUserMedia + AEC-injected capture + VAD segmentation + incremental upload + endpoint handling
261
- src/aec.ts client: NLMS acoustic echo cancellation (pure module, unit-tested)
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/aec.test.mjs NLMS echo cancellation numeric tests (synthetic)
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`, `capture-e2e.js`, `asr-e2e.js`, `output-e2e.js`, …) live in the repo root `test/`, outside this npm package; `capture-e2e.js` self-checks the capture→partial→final loop with a fake mic.
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
+ ![dsh-voice-mode 全双工语音对话](https://raw.githubusercontent.com/qishuilalala/dsh-voice-mode/HEAD/assets/hero-banner.png)
13
+
12
14
  ![语音模式:实时字幕与状态条](https://raw.githubusercontent.com/qishuilalala/dsh-voice-mode/HEAD/assets/screenshot-voice.png)
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`(默认)持续聆听:本地 RMS 门控开段 → zipformer2 流式识别(边说边出字,实时字幕预览)→ 静音 700ms 自动断句进草稿并自动发送(不足 250ms 语音视为噪声不判句;端点判定由 host 侧 Silero VAD 神经网络完成,客户端静音计时兜底);按住 `Ctrl` 强制立即发送
19
- - `hold` 按住说话:短按进入/退出,**按住麦克风按钮说话、松手即发**(滑出取消、`Esc`/失焦放弃本段);`Ctrl` 按住即录、松开即发
20
- - **输出链路**:只朗读最终答复的 `text-delta`(reasoning/工具调用不读),按句流式 Edge TTS 朗读 + 右下角实时字幕浮层;全文照常写入聊天记录;可选口语化提示词(设置 `spokenFormat`,默认关)让回复为自然短句、不带 Markdown 排版符号,朗读侧再做一轮标记剥离
21
- - **开口打断(barge-in)**:**NLMS 回声消除(P3,互相关 bulk delay 估计 + 64ms 短滤波器)**以本页 TTS 播放为参考消掉外放回声后,**host 端 Silero VAD 帧级检测(isSpeech)随 partial 下行驱动打断**——三档灵敏度对应发声确认时长(0 高门槛约 0.3s / 1 0.2s / 2 低 0.1s);`bargeInMode` `auto` 时开口自动打断,选 `manual`(外放推荐)时改按住麦克风/Ctrl 显式打断 → 本地静音 + host 合成队列作废 + 正在运行的回合取消(保留半截并自然续入新消息)
22
- - **模型懒加载与进度**:首次使用自动下载 zipformer2 流式模型(~160MB)、Silero VAD(~2MB)与 SenseVoice 定稿模型(~228MB,首次定稿时,可关),全部 `.part` 断点续传、hf-mirror 回退;状态条实时显示进度;`npm run prefetch` 可预下载流式 ASR 模型(VAD/SenseVoice 首次使用时自动下载)
23
- - **SenseVoice 定稿解码在独立 worker 线程执行**:228MB 离线模型的载入与整段解码不占用宿主主线程事件循环(解码期间其它请求/SSE 保持响应),10s 超时兜底真实可触发,失败/超时/关闭均自动降级 zipformer 定稿,worker 崩溃后下一句自动重建
24
- - **设置**:设置 → Plugins → 插件配置 → 语音模式(voice-mode),可调音色/语速/打断灵敏度/打断方式/静音停顿/空闲超时/模型镜像/自动发送/交互模式/口语化提示词;**音色可试听**(按当前音色+语速即时合成预览,自定义 ShortName 亦可)
25
- - **界面语言**:跟随 dsh 语言设置(网页 `<html lang>`,未设置时回退浏览器语言;切换后刷新页面生效)
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`:边说边出字,停顿 700ms 自动发送;按住 `Ctrl` 强制立即发送 |
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
- | `voice` | `zh-CN-XiaoxiaoNeural` | Edge TTS 音色(见下方常用音色表),**即时生效**;行内「试听」按钮可即时预览 |
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` | 发声打断灵敏度:0 高门槛 / 1 中 / 2 低(发声确认约 0.3/0.2/0.1 秒) |
64
- | `bargeInMode` | `auto` | 打断方式:`auto` 自动打断(开口即打断,耳机/安静环境);`manual` 手动打断(**外放推荐**——外放回声会误触发自动打断导致自打断静音,改按住麦克风/Ctrl 显式打断,永不自打断) |
65
- | `echoGateDb` | `6` | 回声门控阈值(dB):自动打断要求残差高于回声地板此值;外放仍误打断调大(8~10),太难打断调小(3~4) |
66
- | `shortcut` | `Ctrl+Shift+V` | 进入/退出语音模式的快捷键(形如 Ctrl+Shift+V;留空禁用快捷键,只用麦克风按钮) |
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
- | `autoResume` | `false` | 切换回上次语音会话时自动恢复语音模式(需麦克风权限已授予;省去每次切换会话后重新点麦克风) |
72
- | `mode` | `toggle` | 交互模式:`toggle` 持续聆听 + 静音自动断句;`hold` 按住说话、松手发送(短按退出) |
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`/`senseVoice` **立即生效**;其余(`silenceMs`/`interruptLevel`/`bargeInMode`/`idleTimeoutMinutes`/`modelHost`/`autoSend`/`autoResume`/`mode`)下次进入语音模式时生效。前六个键的平台默认由插件配置(`base` 层)提供,其余由 schema 提供。
87
+ 生效范围:`voice`/`rate`/`ttsEngine`/`spokenFormat` **立即生效**;其余设置下次进入语音模式时生效。设置项默认值由插件配置(`base` 层)提供。
77
88
 
78
- ### 开发工具
89
+ ### 本地音色(VITS / Kokoro)
79
90
 
80
- - `node scripts/list-voices.mjs`:列出 Edge TTS 音色
81
- - `node scripts/prefetch.mjs`:预下载流式 ASR zipformer 模型(VAD/SenseVoice 首次使用时自动下载)
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
- ### 常用音色(完整清单见 `node scripts/list-voices.mjs`)
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`)与模型缓存目录 `cacheDir`(默认 `~/.cache/dsh-voice-mode/models/`)在安装配置中设置。
111
+ `voice-mode` 命名空间配置可直接写入 `~/.dsh/settings.yaml`;插件总开关 `enabled`(默认 `true`)、模型缓存目录 `cacheDir`、在安装配置中设置。
102
112
 
103
113
  ## 工作原理
104
114
 
105
115
  ```
106
- 麦克风(16kHz, AEC) ─▶ 本地 RMS 门控 ─▶ HOST zipformer2 流式识别(本地 WASM)
107
- Silero VAD 端点断句 + isSpeech 打断判定
108
- 用户说话 ◀── 打断 ◀── 音箱 ◀── Edge TTS 逐句合成 ◀── 分句(text-delta 过滤)
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
- - 朗读由 **Edge TTS**(微软语音服务,无需 API Key)逐句合成,流式播放;
124
+ - 识别在 **host 端本地运行**(zipformer2 中文 int8 WASM + SenseVoice 定稿,模型懒下载),音频不上传第三方;识别定稿由 SenseVoice 补标点;
125
+ - 朗读默认 **Edge 云端**;本地 VITS 纯中文 / Kokoro 原生中英(跑在独立子进程、崩溃自愈)可选(隐私优先);
113
126
  - 同一时间仅一个会话处于语音模式(全局单活);LLM 流被无损观察(不阻塞)。
114
127
 
115
128
  ## 已知限制
116
129
 
117
- - 打断已内置 NLMS 声学回声消除(P3,参考=本页 TTS 播放,互相关 bulk delay 估计 + 64ms 短滤波器)+ host 端 Silero VAD 帧级检测;浏览器 `echoCancellation` 仅兜底;外放极端音量/距离下抑制量需真机标定(`echoGateDb` 等参数)
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)鉴权;`/asr` 有活跃会话归属校验(403),`/toggle` `/cancel` `/preview` 为无鉴权可用性面(仅影响语音模式状态/TTS,本机恶意网页可触发让出/停播,无提权或窃听;读侧受 CORS 限制)
143
+ - **安全说明**:插件 HTTP 面(`/voice-mode/*`)遵循宿主安全模型——请勿将 dsh 端口直接暴露公网;经反向代理发布时由代理层(如 basic auth)鉴权;插件侧对敏感操作保留会话归属校验
129
144
 
130
145
  ## 故障排查
131
146
 
132
147
  | 现象 | 处理 |
133
148
  | --- | --- |
134
149
  | 点麦克风无反应,状态条红字 | 浏览器拒绝麦克风:地址栏(iOS 为 设置 → Safari → 麦克风)开启后重试 |
135
- | 状态条「正在加载模型… x%」卡住 | 检查网络;模型大(160MB)可先 `npm run prefetch`;国内网络 `modelHost` 配 `https://hf-mirror.com` |
136
- | 朗读无声音/无字幕 | 查看状态条「朗读连接失败:正在重试…」(Edge TTS 网络问题,自动退避重试);确认页面前台且未静音 |
150
+ | 状态条「正在加载模型… x%」卡住 | 检查网络;模型较大可先 `npm run prefetch`;国内网络 `modelHost` 配 `https://hf-mirror.com` |
151
+ | 朗读无声音/无字幕 | 本地引擎首次合成需加载模型;若持续失败查看状态条提示(自动退避重试);确认页面前台且未静音 |
137
152
  | 语音模式进不去 | 检查插件 `enabled`;多标签页时确认当前会话为活动会话 |
138
- | 识别到但不是我要说的 | 环境噪声:降低音量或提高 `interruptLevel`(高门槛) |
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 # 单测(segmenter/aec + 发布前自检,无需网络;测试直接 import src/*.ts,需 Node ≥22.18,即 type stripping)
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/capture-e2e.js`、`test/asr-e2e.js`、`test/output-e2e.js`、`test/make-real-voice.mjs` + `test/sherpa-direct.js` + `test/realvoice-e2e.js`)位于仓库根 `test/`,不在 npm 包内;`capture-e2e.js` 用 fake 麦克风做「采集→partial→final」闭环自检,`make-real-voice.mjs` 合成真实中文语音、`realvoice-e2e.js` 用 Chrome file-fake-audio 走**真实语音→浏览器→host ASR→文本**全链路(实测词级准确)。
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 host:单活指针、llm/stream tap、SSE、settings 注册、turn 回合状态机、口语化提示词注入
154
- src/asr-host.ts host:zipformer2 流式识别 + Silero VAD 端点 + SenseVoice 定稿 + 模型懒下载(.part 断点续传)
155
- src/tts-queue.ts host:逐会话 TTS 队列 + 逐 chunk 转发 + epoch 打断机制
156
- src/segmenter.ts host:句子切分(markdown 剥离 + 终止标点)
157
- src/asr.ts client:音频采集、NLMS AEC 注入、VAD 分段、增量识别、端点处理
158
- src/aec.ts client:NLMS 声学回声消除(纯模块,可单测)
159
- src/resample.ts client:线性重采样(采集/回声参考共用)
160
- src/client.tsx client:麦克风按钮 + 状态条 + 字幕浮层 + 播放引擎(Web Audio 队列)+ 打断
161
- src/settings-form.tsx client:设置卡片(Plugins → 插件配置)
162
- src/strings.ts client:中英文案字典(以 dsh 语言设置 <html lang> 为准)
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