dsh-voice-mode 0.2.3 → 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,11 +30,11 @@
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: RMS VAD segmentation → streaming zipformer2 ASR (words appear as you speak, live caption preview) → automatic sentence split and send after 2 s of silence; 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
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
25
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
26
- - **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; 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
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
27
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)
28
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
29
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
@@ -89,14 +100,15 @@ If a wake word is configured, you land in standby first (the status bar prompts
89
100
 
90
101
  | Key | Default | Description |
91
102
  | --- | --- | --- |
92
- | `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 |
93
105
  | `rate` | `1.0` | Reading speed multiplier (0.5 slow ~ 2.0 fast), **applies live** |
94
- | `interruptLevel` | `0` | Barge-in sensitivity: 0 high threshold / 1 medium / 2 low |
95
- | `silenceMs` | `2000` | Silence pause in ms that marks the end of a complete sentence |
96
- | `idleTimeoutMinutes` | `10` | Minutes of inactivity before auto-exiting voice mode |
97
- | `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) |
98
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) |
99
- | `mode` | `toggle` | Interaction mode: `toggle` continuous listening + 2 s silence split; `hold` push-to-talk, release to send (short tap exits) |
111
+ | `mode` | `toggle` | Interaction mode: `toggle` continuous listening + 700 ms silence split; `hold` push-to-talk, release to send (short tap exits) |
100
112
  | `wakeWord` | empty (off) | Wake word (e.g. `你好小D`): speak it after entering to activate, avoiding accidental triggers; empty = off |
101
113
 
102
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.
@@ -134,7 +146,7 @@ You can also edit the `voice-mode:` section of `~/.dsh/settings.yaml` directly (
134
146
  voice: zh-CN-XiaoxiaoNeural
135
147
  rate: 1.0
136
148
  interruptLevel: 0
137
- silenceMs: 2000
149
+ silenceMs: 700
138
150
  idleTimeoutMinutes: 10
139
151
  modelHost: https://huggingface.co
140
152
  ```
@@ -170,18 +182,19 @@ You can also edit the `voice-mode:` section of `~/.dsh/settings.yaml` directly (
170
182
  ![architecture](https://raw.githubusercontent.com/qishuilalala/dsh-voice-mode/HEAD/plugin/dsh-voice-mode/assets/architecture.svg)
171
183
 
172
184
  ```
173
- input: mic ──RMS VAD (2s silence split)──▶ POST /voice-mode/asr (f32 PCM, 16k, incremental)
185
+ input: mic ──RMS VAD (5s silence split)──▶ POST /voice-mode/asr (f32 PCM, 16k, incremental)
174
186
  │ zipformer2 streaming ASR (host-side WASM)
175
187
 
176
188
  composer draft ──autoSend──▶ model stream ──llm/stream tap (active voice session only)
177
189
  │ text-delta filter → sentence segmentation
178
190
 
179
- 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)
180
192
  ```
181
193
 
182
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)
183
195
  - The `llm/stream` tap is lossless: every chunk passes through unchanged; segmentation/synthesis only observe and never block the model stream
184
- - 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
185
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
186
199
 
187
200
  ## Known limitations
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 VAD 分段 → zipformer2 流式识别(边说边出字,实时字幕预览)→ 静音 2 秒自动断句进草稿并自动发送;按住 `Ctrl` 强制立即发送
19
- - `hold` 按住说话:短按进入/退出,**按住麦克风按钮说话、松手即发**(滑出取消、`Esc`/失焦放弃本段);`Ctrl` 按住即录、松开即发
38
+ - **两种交互模式(输入框旁按钮或设置可切换,切换即持久化)**:
39
+ - `toggle`(默认)持续聆听:RMS VAD 分段 → zipformer2 流式识别(边说边出字,实时字幕预览)→ 静音约 700 毫秒自动断句进草稿并自动发送;按住 `Ctrl` 强制立即发送
40
+ - `hold` 按住说话:短按进入/退出,**按住麦克风按钮说话、松手即发**(滑出取消、`Esc`/失焦放弃本段);按住期间停顿不断句(上限 10 分钟);`Ctrl` 按住即录、松开即发
20
41
  - **唤醒词(可选,默认关)**:设置 `wakeWord` 后进入待机态,说出唤醒词才开始识别(如「你好小D」)
21
- - **输出链路**:只朗读最终答复的 `text-delta`(reasoning/工具调用不读),按句流式 Edge TTS 朗读 + 右下角实时字幕浮层;工具调用触发提示音;全文照常写入聊天记录;可选口语化提示词(设置 `spokenFormat`,默认关)让回复为自然短句、不带 Markdown 排版符号,朗读侧再做一轮标记剥离
22
- - **开口打断(barge-in)**:三档灵敏度发声前沿检测 → 本地静音 + host 合成队列作废 + 正在运行的回合取消(保留半截并自然续入新消息)
23
- - **模型懒加载与进度**:首次使用自动下载 zipformer2 中文流式模型(约 160MB,`.part` 断点续传),状态条实时显示进度;可用 `npm run prefetch` 预下载
24
- - **设置**:设置 → Plugins → 插件配置 → 语音模式(voice-mode),可调音色/语速/打断灵敏度/静音停顿/空闲超时/模型镜像/自动发送/交互模式/唤醒词/口语化提示词;**音色可试听**(按当前音色+语速即时合成预览,自定义 ShortName 亦可)
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 亦可)
25
46
  - **界面语言**:跟随浏览器语言(中文 / English;切换后刷新页面生效)
26
47
  - **容错**:麦克风被拒红点提示、模型下载失败可见提示、TTS 连接失败状态条提示(自动退避重试)、提交失败文字留在草稿、SSE 断线自动重连
27
- - **空闲退出**:10 分钟无活动自动退出并释放麦克风
48
+ - **空闲退出**:10 分钟无活动自动退出并释放麦克风(**正在朗读计为活动**,长朗读不会中途下线)
28
49
 
29
50
  ## 安装
30
51
 
@@ -39,8 +60,9 @@ bundle 插件安装后需重启 dsh 生效(Linux:`systemctl restart dsh`;
39
60
  | 手势 | 作用 |
40
61
  | --- | --- |
41
62
  | `Ctrl+Shift+V` | 进入 / 退出语音模式 |
42
- | 直接说话 | `toggle`:边说边出字,停顿 2 秒自动发送;按住 `Ctrl` 强制立即发送 |
63
+ | 直接说话 | `toggle`:边说边出字,停顿约 700 毫秒自动发送;按住 `Ctrl` 强制立即发送 |
43
64
  | 按住麦克风按钮 | `hold`:松手发送;短按退出;滑出 / `Esc` / 失焦放弃本段 |
65
+ | 点输入框旁模式按钮 | 在「持续聆听 ⇄ 按住说」间切换(保存到设置) |
44
66
  | 说唤醒词 | 待机态激活识别(配置后) |
45
67
  | AI 朗读时开口说话 | 打断朗读并取消当前回合 |
46
68
  | 点状态条「退出」 | 退出语音模式 |
@@ -50,20 +72,26 @@ bundle 插件安装后需重启 dsh 生效(Linux:`systemctl restart dsh`;
50
72
 
51
73
  | 键 | 默认 | 说明 |
52
74
  | --- | --- | --- |
53
- | `voice` | `zh-CN-XiaoxiaoNeural` | Edge TTS 音色(见下方常用音色表),**即时生效**;行内「试听」按钮可即时预览 |
75
+ | `ttsEngine` | `edge` | 朗读引擎:`edge` 微软云端(默认,快)/ `vits` 本地中文 / `kokoro` 本地中英;**即时生效** |
76
+ | `voice` | 按引擎 | 音色:VITS 五说话人;Kokoro 103 个(下拉+◀▶,62 深沉/68 浑厚/75 清亮/76 磁性置顶);Edge 进入时自动加载全量 322 个。行内「试听」可即时预览 |
54
77
  | `rate` | `1.0` | 朗读语速倍率(0.5 慢速 ~ 2.0 快速),**即时生效** |
55
- | `interruptLevel` | `0` | 发声打断灵敏度:0 高门槛 / 1 中 / 2 低 |
56
- | `silenceMs` | `2000` | 说完整一句的静音停顿毫秒数 |
57
- | `idleTimeoutMinutes` | `10` | 无活动自动退出语音模式的分钟数 |
58
- | `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`) |
59
82
  | `autoSend` | `true` | 识别定稿后自动发送;关闭则只进草稿(按住 `Ctrl` / hold 松手仍会发送) |
60
- | `mode` | `toggle` | 交互模式:`toggle` 持续聆听 + 2s 静音断句;`hold` 按住说话、松手发送(短按退出) |
83
+ | `mode` | `toggle` | 交互模式:`toggle` 持续聆听 + 700ms 静音断句;`hold` 按住说话、松手发送(短按退出) |
61
84
  | `wakeWord` | 空(关) | 唤醒词(如「你好小D」):进入后先说唤醒词激活,避免误触;空 = 关闭 |
62
85
  | `spokenFormat` | `false` | 语音会话注入口语化提示词:开启后**仅当前语音会话**的回复被注入「口语化短句、不用 Markdown 排版符号」提示词(朗读更顺),**即时生效** |
63
86
 
64
- 生效范围:`voice`/`rate`/`spokenFormat` **立即生效**;其余设置下次进入语音模式时生效。设置项默认值由插件配置(`base` 层)提供。
87
+ 生效范围:`voice`/`rate`/`ttsEngine`/`spokenFormat` **立即生效**;其余设置下次进入语音模式时生效。设置项默认值由插件配置(`base` 层)提供。
88
+
89
+ ### 本地音色(VITS / Kokoro)
90
+
91
+ - **VITS(纯中文)**:`suyingxue` 素映雪·女 / `gunian` 顾念·男 / `fushiyu` 傅斯遇·女 / `bingjiao` 冰娇·男 / `bazong` 霸总·男
92
+ - **Kokoro(中英混读均可)**:103 个音色全量入表,面板按编号 + 实测性别标注;四个常用男声置顶:`62` 深沉 / `68` 浑厚 / `75` 清亮 / `76` 磁性;中文女声 `48` 小北 / `49` 小妮 / `50` 小小 / `51` 小艺。音色只是风格向量,**语言能力与音色无关**。
65
93
 
66
- ### 常用音色(完整清单见 `node scripts/list-voices.mjs`)
94
+ ### 常用 Edge 音色(完整清单见 `node scripts/list-voices.mjs`)
67
95
 
68
96
  | ShortName | 说明 |
69
97
  | --- | --- |
@@ -80,30 +108,34 @@ bundle 插件安装后需重启 dsh 生效(Linux:`systemctl restart dsh`;
80
108
 
81
109
  ### 配置(bundle config / settings.yaml)
82
110
 
83
- `voice-mode` 命名空间配置可直接写入 `~/.dsh/settings.yaml`;插件总开关 `enabled`(默认 `true`)与模型缓存目录 `cacheDir`(默认 `~/.cache/dsh-voice-mode/models/`)在安装配置中设置。
111
+ `voice-mode` 命名空间配置可直接写入 `~/.dsh/settings.yaml`;插件总开关 `enabled`(默认 `true`)、模型缓存目录 `cacheDir`、在安装配置中设置。
84
112
 
85
113
  ## 工作原理
86
114
 
87
115
  ```
88
- 麦克风(16kHz, AEC) ─▶ 浏览器 VAD 分段 ─▶ HOST zipformer2 流式识别(本地 WASM)
116
+ 麦克风(16kHz, AEC) ─▶ 浏览器 VAD 分段 ─▶ HOST zipformer2 流式识别(本地 WASM, 增量传输)
89
117
 
90
- 用户说话 ◀── 打断 ◀── 音箱 ◀── Edge TTS 逐句合成 ◀── 分句(text-delta 过滤)
118
+
119
+ 用户说话 ◀── 打断 ◀── 音箱 ◀── 逐句合成 ◀── 分句(text-delta 过滤) ◀── SenseVoice 标点定稿
120
+
121
+ 本地 VITS / 本地 Kokoro(原生 addon, 子进程) / Edge 云端(可选)
91
122
  ```
92
123
 
93
- - 识别在 **host 端本地运行**(zipformer2 int8 WASM,模型懒下载),音频不上传第三方;
94
- - 朗读由 **Edge TTS**(微软语音服务,无需 API Key)逐句合成,流式播放;
124
+ - 识别在 **host 端本地运行**(zipformer2 中文 int8 WASM + SenseVoice 定稿,模型懒下载),音频不上传第三方;识别定稿由 SenseVoice 补标点;
125
+ - 朗读默认 **Edge 云端**;本地 VITS 纯中文 / Kokoro 原生中英(跑在独立子进程、崩溃自愈)可选(隐私优先);
95
126
  - 同一时间仅一个会话处于语音模式(全局单活);LLM 流被无损观察(不阻塞)。
96
127
 
97
128
  ## 已知限制
98
129
 
99
130
  - 发声打断依赖浏览器回声消除(`echoCancellation`);扬声器音量过大时可能漏声到麦克风
100
131
  - `Ctrl+Shift+V` 会覆盖浏览器「粘贴纯文本」快捷键(普通粘贴仍可用 `Ctrl+V`)
101
- - 识别模型为简体中文优先;识别质量受环境噪声影响
132
+ - 识别质量受环境噪声影响;zipformer2 中文流式 + SenseVoice 多语(中英日韩粤)定稿
102
133
  - 浏览器自动播放策略:朗读需要页面已有用户交互(点击麦克风即满足);「试听」依赖 `AbortSignal.timeout`(Safari 16+ / Chrome 103+ / Firefox 100+;老浏览器点击试听会立即提示失败,属预期降级)
103
134
  - **唤醒词为轻量实现**(流式文本匹配,非专用 KWS 引擎):嘈杂环境可能延迟或误激活;唤醒词本身不会进入聊天
104
135
  - hold 模式按住时切换窗口/标签页会**放弃本段**(防持续收音)
105
136
  - hero(新会话空态)无语音入口:请先进入会话使用麦克风按钮
106
137
  - `spokenFormat` 提示词经官方 `system-prompt/assemble` 瀑布注入;若当前会话使用**完整提示词**配置(persona `complete: true` 的 agent preset),提示词不注入(官方 complete 契约优先)
138
+ - 本地 Kokoro 每次打断后下一句朗读前约有 1 秒引擎重建时间(打断即终止在途合成的代价)
107
139
  - **苹果 Safari / iOS**:
108
140
  - 需 **HTTPS 或 localhost**(iOS/macOS Safari 强制安全上下文;`http://` 局域网 IP 下麦克风不可用)
109
141
  - 首次进入需授权麦克风;被拒后到「设置 → Safari → 麦克风」开启(iOS)
@@ -115,10 +147,11 @@ bundle 插件安装后需重启 dsh 生效(Linux:`systemctl restart dsh`;
115
147
  | 现象 | 处理 |
116
148
  | --- | --- |
117
149
  | 点麦克风无反应,状态条红字 | 浏览器拒绝麦克风:地址栏(iOS 为 设置 → Safari → 麦克风)开启后重试 |
118
- | 状态条「正在加载模型… x%」卡住 | 检查网络;模型大(160MB)可先 `npm run prefetch`;国内网络 `modelHost` 配 `https://hf-mirror.com` |
119
- | 朗读无声音/无字幕 | 查看状态条「朗读连接失败:正在重试…」(Edge TTS 网络问题,自动退避重试);确认页面前台且未静音 |
150
+ | 状态条「正在加载模型… x%」卡住 | 检查网络;模型较大可先 `npm run prefetch`;国内网络 `modelHost` 配 `https://hf-mirror.com` |
151
+ | 朗读无声音/无字幕 | 本地引擎首次合成需加载模型;若持续失败查看状态条提示(自动退避重试);确认页面前台且未静音 |
120
152
  | 语音模式进不去 | 检查插件 `enabled`;多标签页时确认当前会话为活动会话 |
121
153
  | 识别到但不是我要说的 | 环境噪声或唤醒词误判:降低音量、提高 `interruptLevel`(高门槛)或启用 `wakeWord` |
154
+ | 按住说话松手后没反应 | 确认交互模式为「按住说」且按住期间按钮高亮;松手后识别定稿约 1 秒内进入草稿 |
122
155
 
123
156
  ## 开发
124
157
 
@@ -131,13 +164,17 @@ systemctl restart dsh # 本机加载新 host 代码;其他平台重启
131
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 包内。
132
165
 
133
166
  ```
134
- src/index.ts host:单活指针、llm/stream tap、SSE、settings 注册、口语化提示词注入
135
- src/asr-host.ts host:zipformer2 流式识别 + 模型懒下载(.part 断点续传)
136
- src/asr.ts client:音频采集、VAD 分段、增量识别、唤醒词
137
- src/client.tsx client:麦克风按钮 + 状态条 + 字幕浮层 + 打断
138
- src/tts-queue.ts host:逐会话 TTS 队列 + epoch 打断机制
139
- src/segmenter.ts host:句子切分(markdown 剥离 + 终止标点)
140
- src/strings.ts client:中英文案字典(navigator.language)
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)
141
178
  ```
142
179
 
143
180
  ## License
@@ -1,159 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="640" viewBox="0 0 1200 640" role="img" aria-labelledby="title desc">
2
- <title id="title">dsh-voice-mode 原理架构图</title>
3
- <desc id="desc">展示语音输入、模型输出朗读与开口打断三条数据流,以及全局单活指针的会话隔离。</desc>
4
- <defs>
5
- <marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto" markerUnits="strokeWidth">
6
- <path d="M0,0 L8,4 L0,8 Z" fill="#66737f"/>
7
- </marker>
8
- <marker id="arrowGreen" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto" markerUnits="strokeWidth">
9
- <path d="M0,0 L8,4 L0,8 Z" fill="#2ea043"/>
10
- </marker>
11
- <style>
12
- text { fill:#333; font-family:"Noto Sans SC","Microsoft YaHei",Arial,sans-serif; }
13
- .title { font-size:25px; font-weight:700; }
14
- .subtitle { font-size:12px; fill:#66737f; }
15
- .lane { fill:#faf9f5; stroke:#dedbd2; stroke-width:1.2; }
16
- .lane-blue { fill:#f5f8fa; stroke:#d7e0e6; stroke-width:1.2; }
17
- .lane-green { fill:#f6faf6; stroke:#cfe4d3; stroke-width:1.2; }
18
- .lane-title { font-size:17px; font-weight:700; }
19
- .lane-no { font:700 12px ui-monospace,SFMono-Regular,Consolas,monospace; fill:#fff; }
20
- .node { fill:#fffdf7; stroke:#b8b4a8; stroke-width:1.4; rx:10; }
21
- .node-blue { fill:#eef4f7; stroke:#9fb3bf; stroke-width:1.4; rx:10; }
22
- .node-green { fill:#eff8f0; stroke:#2ea043; stroke-width:1.7; rx:10; }
23
- .node-title { font-size:13px; font-weight:700; text-anchor:middle; }
24
- .node-detail { font-size:10.5px; fill:#59636c; text-anchor:middle; }
25
- .mono { font-family:ui-monospace,SFMono-Regular,Consolas,"Liberation Mono",monospace; }
26
- .flow { fill:none; stroke:#66737f; stroke-width:1.8; marker-end:url(#arrow); }
27
- .flow-green { fill:none; stroke:#2ea043; stroke-width:2; marker-end:url(#arrowGreen); }
28
- .label-bg { fill:#fff; stroke:#d8dde1; stroke-width:.8; rx:7; }
29
- .label { font-size:9.5px; fill:#59636c; text-anchor:middle; }
30
- .note { fill:#eff8f0; stroke:#2ea043; stroke-width:1.3; rx:10; }
31
- .note-title { font-size:12px; font-weight:700; fill:#237a34; }
32
- .note-text { font-size:10.5px; fill:#46534a; }
33
- .dash { fill:none; stroke:#2ea043; stroke-width:1.2; stroke-dasharray:4 4; }
34
- </style>
35
- </defs>
36
-
37
- <rect width="1200" height="640" fill="#fff"/>
38
- <text class="title" x="32" y="38">dsh-voice-mode 原理架构</text>
39
- <text class="subtitle" x="32" y="58">浏览器采集与播放 · 宿主端 ASR / TTS · 无损模型流旁路</text>
40
- <text class="note-text" x="32" y="75">全局单活指针 activeVoiceSession:同一时刻仅一个会话处于语音模式;普通会话 llm/stream 直达、零开销</text>
41
-
42
- <rect class="note" x="768" y="16" width="400" height="51"/>
43
- <text class="note-title" x="785" y="36">全局单活指针 <tspan class="mono">activeVoiceSession</tspan></text>
44
- <text class="note-text" x="785" y="54">仅指定会话进入语音旁路;普通会话 <tspan class="mono">llm/stream</tspan> 零开销直达</text>
45
-
46
- <!-- 输入链路 -->
47
- <rect class="lane" x="20" y="82" width="1160" height="178" rx="14"/>
48
- <circle cx="47" cy="108" r="14" fill="#2ea043"/><text class="lane-no" x="47" y="112" text-anchor="middle">01</text>
49
- <text class="lane-title" x="70" y="114">输入链路 · 采集、断句与识别</text>
50
-
51
- <rect class="node" x="40" y="137" width="155" height="82"/>
52
- <text class="node-title" x="117.5" y="162">浏览器麦克风</text>
53
- <text class="node-detail mono" x="117.5" y="181">getUserMedia · 16k mono</text>
54
- <text class="node-detail mono" x="117.5" y="198">echoCancellation</text>
55
-
56
- <rect class="node-blue" x="225" y="137" width="175" height="82"/>
57
- <text class="node-title" x="312.5" y="160">RMS VAD</text>
58
- <text class="node-detail" x="312.5" y="179">持续聆听 2s 静音断句 ·</text>
59
- <text class="node-detail" x="312.5" y="197"><tspan class="mono">hold</tspan> 模式按住说话绕过 VAD</text>
60
-
61
- <rect class="node-blue" x="430" y="127" width="205" height="102"/>
62
- <text class="node-title mono" x="532.5" y="151">POST /voice-mode/asr</text>
63
- <text class="node-detail mono" x="532.5" y="170">f32 LE PCM</text>
64
- <text class="node-detail" x="532.5" y="188">宿主 sherpa-onnx Node WASM</text>
65
- <text class="node-detail mono" x="532.5" y="206">zipformer2 · 流式识别</text>
66
- <text class="node-detail" x="532.5" y="220">模型懒下载 160MB · <tspan class="mono">.part</tspan> 续传</text>
67
-
68
- <rect class="node" x="665" y="137" width="145" height="82"/>
69
- <text class="node-title" x="737.5" y="162">识别结果</text>
70
- <text class="node-detail" x="737.5" y="181"><tspan class="mono">partial</tspan> 预览字幕</text>
71
- <text class="node-detail" x="737.5" y="198">定稿</text>
72
-
73
- <rect class="node" x="840" y="137" width="135" height="82"/>
74
- <text class="node-title" x="907.5" y="167">composer 草稿</text>
75
- <text class="node-detail" x="907.5" y="189">写入输入框</text>
76
-
77
- <rect class="node-green" x="1005" y="137" width="145" height="82"/>
78
- <text class="node-title" x="1077.5" y="160">自动发送</text>
79
- <text class="node-detail mono" x="1077.5" y="179">autoSend</text>
80
- <text class="node-detail" x="1077.5" y="197"><tspan class="mono">Ctrl</tspan> / <tspan class="mono">hold</tspan> 松手强制</text>
81
-
82
- <path class="flow" d="M195 178 H225"/><path class="flow" d="M400 178 H430"/><path class="flow" d="M635 178 H665"/><path class="flow" d="M810 178 H840"/><path class="flow-green" d="M975 178 H1005"/>
83
- <rect class="label-bg" x="198" y="151" width="24" height="16"/><text class="label" x="210" y="162">PCM</text>
84
- <rect class="label-bg" x="402" y="151" width="26" height="16"/><text class="label" x="415" y="162">POST</text>
85
- <rect class="label-bg" x="638" y="151" width="24" height="16"/><text class="label" x="650" y="162">增量</text>
86
- <rect class="label-bg" x="812" y="151" width="26" height="16"/><text class="label" x="825" y="162">定稿</text>
87
- <rect class="label-bg" x="977" y="151" width="26" height="16"/><text class="label" x="990" y="162">发送</text>
88
-
89
- <!-- 输出链路 -->
90
- <rect class="lane-blue" x="20" y="276" width="1160" height="178" rx="14"/>
91
- <circle cx="47" cy="302" r="14" fill="#2ea043"/><text class="lane-no" x="47" y="306" text-anchor="middle">02</text>
92
- <text class="lane-title" x="70" y="308">输出链路 · 无损旁路、逐句合成与播放</text>
93
-
94
- <rect class="node" x="40" y="337" width="135" height="76"/>
95
- <text class="node-title" x="107.5" y="365">model stream</text>
96
- <text class="node-detail" x="107.5" y="386">模型原始输出</text>
97
-
98
- <rect class="node-green" x="205" y="327" width="155" height="96"/>
99
- <text class="node-title mono" x="282.5" y="351">llm/stream tap</text>
100
- <text class="node-detail" x="282.5" y="371">chunk 原样透传</text>
101
- <text class="node-detail" x="282.5" y="389">仅单活语音会话旁观</text>
102
- <text class="node-detail" x="282.5" y="406">不阻塞模型流</text>
103
-
104
- <rect class="node-blue" x="390" y="337" width="135" height="76"/>
105
- <text class="node-title mono" x="457.5" y="365">text-delta</text>
106
- <text class="node-detail" x="457.5" y="386">过滤文本增量</text>
107
-
108
- <rect class="node-blue" x="555" y="337" width="155" height="76"/>
109
- <text class="node-title mono" x="632.5" y="361">SentenceSegmenter</text>
110
- <text class="node-detail" x="632.5" y="384">中文标点切句</text>
111
-
112
- <rect class="node-blue" x="740" y="327" width="155" height="96"/>
113
- <text class="node-title mono" x="817.5" y="351">TtsQueue</text>
114
- <text class="node-detail" x="817.5" y="371"><tspan class="mono">msedge-tts</tspan> 逐句合成</text>
115
- <text class="node-detail" x="817.5" y="390"><tspan class="mono">epoch</tspan> 版本化</text>
116
- <text class="node-detail" x="817.5" y="407">按会话隔离</text>
117
-
118
- <rect class="node-blue" x="925" y="327" width="205" height="96"/>
119
- <text class="node-title mono" x="1027.5" y="351">SSE /voice-mode/stream</text>
120
- <text class="node-detail mono" x="1027.5" y="372">event: audio</text>
121
- <text class="node-detail mono" x="1027.5" y="390">{ text, base64 MP3 }</text>
122
- <text class="node-detail" x="1027.5" y="407">浏览器播放 + 右下角字幕浮层</text>
123
-
124
- <path class="flow" d="M175 375 H205"/><path class="flow" d="M360 375 H390"/><path class="flow" d="M525 375 H555"/><path class="flow" d="M710 375 H740"/><path class="flow-green" d="M895 375 H925"/>
125
- <rect class="label-bg" x="177" y="348" width="26" height="16"/><text class="label" x="190" y="359">无损</text>
126
- <rect class="label-bg" x="362" y="348" width="26" height="16"/><text class="label" x="375" y="359">旁观</text>
127
- <rect class="label-bg" x="527" y="348" width="26" height="16"/><text class="label" x="540" y="359">增量</text>
128
- <rect class="label-bg" x="712" y="348" width="26" height="16"/><text class="label" x="725" y="359">逐句</text>
129
- <rect class="label-bg" x="897" y="348" width="26" height="16"/><text class="label" x="910" y="359">音频</text>
130
- <path class="dash" d="M900 67 V92 H282 V327"/>
131
-
132
- <!-- 打断链路 -->
133
- <rect class="lane-green" x="20" y="470" width="1160" height="150" rx="14"/>
134
- <circle cx="47" cy="496" r="14" fill="#2ea043"/><text class="lane-no" x="47" y="500" text-anchor="middle">03</text>
135
- <text class="lane-title" x="70" y="502">打断 <tspan class="mono">barge-in</tspan> · 即刻静音并取消当前回合</text>
136
-
137
- <rect class="node-green" x="60" y="529" width="235" height="67"/>
138
- <text class="node-title" x="177.5" y="553">开口说话</text>
139
- <text class="node-detail" x="177.5" y="574">高门槛能量 + 持续时长 · 三档灵敏度</text>
140
-
141
- <rect class="node" x="355" y="529" width="180" height="67"/>
142
- <text class="node-title" x="445" y="553">本地播放静音</text>
143
- <text class="node-detail" x="445" y="574">浏览器立即停止旧音频</text>
144
-
145
- <rect class="node-green" x="595" y="519" width="245" height="87"/>
146
- <text class="node-title mono" x="717.5" y="543">POST /voice-mode/cancel</text>
147
- <text class="node-detail mono" x="717.5" y="565">TTS epoch++</text>
148
- <text class="node-detail" x="717.5" y="584">作废队列与在途句</text>
149
-
150
- <rect class="node-blue" x="900" y="519" width="240" height="87"/>
151
- <text class="node-title mono" x="1020" y="546">session.cancel(keepInbox)</text>
152
- <text class="node-detail" x="1020" y="568">取消运行中回合</text>
153
- <text class="node-detail" x="1020" y="586">保留新消息</text>
154
-
155
- <path class="flow-green" d="M295 562 H355"/><path class="flow-green" d="M535 562 H595"/><path class="flow-green" d="M840 562 H900"/>
156
- <rect class="label-bg" x="307" y="535" width="36" height="16"/><text class="label" x="325" y="546">触发</text>
157
- <rect class="label-bg" x="547" y="535" width="36" height="16"/><text class="label" x="565" y="546">同时</text>
158
- <rect class="label-bg" x="852" y="535" width="36" height="16"/><text class="label" x="870" y="546">取消</text>
159
- </svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" font-family="Segoe UI, system-ui, sans-serif" font-size="14" viewBox="0 0 960 640"><rect width="960" height="640" fill="#0d1117"/><text x="480" y="30" fill="#e6edf3" font-size="20" font-weight="700" text-anchor="middle">dsh-voice-mode — 语音双工数据流</text><rect width="200" height="64" x="30" y="60" fill="#161b22" stroke="#58a6ff" stroke-width="1.5" rx="10"/><text x="130" y="86" fill="#58a6ff" font-weight="700" text-anchor="middle">麦克风 Mic</text><text x="130" y="108" fill="#8b949e" font-size="12" text-anchor="middle">16kHz 单声道 · AEC 回声消除</text><rect width="200" height="76" x="30" y="150" fill="#161b22" stroke="#58a6ff" stroke-width="1.5" rx="10"/><text x="130" y="178" fill="#58a6ff" font-weight="700" text-anchor="middle">浏览器 VAD 分段</text><text x="130" y="198" fill="#8b949e" font-size="12" text-anchor="middle">静音 700ms 断句 / 按住说门控</text><text x="130" y="216" fill="#8b949e" font-size="12" text-anchor="middle">增量传输(每 0.9s 只传新增)</text><rect width="200" height="64" x="30" y="280" fill="#161b22" stroke="#3fb950" stroke-width="1.5" rx="10"/><text x="130" y="306" fill="#3fb950" font-weight="700" text-anchor="middle">草稿 → 自动发送</text><text x="130" y="328" fill="#8b949e" font-size="12" text-anchor="middle">autoSend / 按住说松手发送</text><rect width="200" height="76" x="30" y="410" fill="#161b22" stroke="#3fb950" stroke-width="1.5" rx="10"/><text x="130" y="438" fill="#3fb950" font-weight="700" text-anchor="middle">播放 + 字幕浮层</text><text x="130" y="458" fill="#8b949e" font-size="12" text-anchor="middle">SSE 音频帧(WAV/MP3)</text><text x="130" y="476" fill="#8b949e" font-size="12" text-anchor="middle">逐段顺序 · 世代链防重播</text><rect width="290" height="64" x="330" y="60" fill="#161b22" stroke="#58a6ff" stroke-width="1.5" rx="10"/><text x="475" y="86" fill="#58a6ff" font-weight="700" text-anchor="middle">zipformer2 流式识别</text><text x="475" y="108" fill="#8b949e" font-size="12" text-anchor="middle">host 本地 WASM · 启动预热 · SHA256 校验</text><rect width="290" height="56" x="330" y="150" fill="#161b22" stroke="#d29922" stroke-width="1.5" rx="10"/><text x="475" y="173" fill="#d29922" font-weight="700" text-anchor="middle">SenseVoice 定稿重译</text><text x="475" y="193" fill="#8b949e" font-size="12" text-anchor="middle">带标点 · 数字归一化(中英日韩粤)</text><rect width="290" height="64" x="330" y="232" fill="#161b22" stroke="#a371f7" stroke-width="1.5" rx="10"/><text x="475" y="258" fill="#a371f7" font-weight="700" text-anchor="middle">llm/stream 无损 tap → 分句</text><text x="475" y="280" fill="#8b949e" font-size="12" text-anchor="middle">text-delta 过滤 · markdown 剥离消毒</text><rect width="290" height="64" x="330" y="322" fill="#161b22" stroke="#a371f7" stroke-width="1.5" rx="10"/><text x="475" y="348" fill="#a371f7" font-weight="700" text-anchor="middle">TTS 队列(epoch 打断)</text><text x="475" y="370" fill="#8b949e" font-size="12" text-anchor="middle">打断即杀在途合成 · 积压作废</text><rect width="290" height="96" x="330" y="412" fill="#161b22" stroke="#3fb950" stroke-width="1.5" rx="10"/><text x="475" y="438" fill="#3fb950" font-weight="700" text-anchor="middle">本地 TTS 引擎(子进程 fork)</text><text x="475" y="460" fill="#8b949e" font-size="12" text-anchor="middle">VITS 纯中文(WASM,可选)</text><text x="475" y="478" fill="#8b949e" font-size="12" text-anchor="middle">Kokoro int8 中英(原生,109MB)</text><text x="475" y="496" fill="#8b949e" font-size="12" text-anchor="middle">Edge 云端(默认,322 音色)</text><rect width="240" height="200" x="690" y="60" fill="#161b22" stroke="#30363d" stroke-width="1" rx="10"/><text x="810" y="88" fill="#e6edf3" font-weight="700" text-anchor="middle">安全面(四层)</text><text x="710" y="116" fill="#8b949e" font-size="12">① 会话存在性校验</text><text x="710" y="136" fill="#8b949e" font-size="12">② 回环默认 + Origin 校验</text><text x="710" y="156" fill="#8b949e" font-size="12">③ 全端点限流</text><text x="710" y="176" fill="#8b949e" font-size="12">④ 模型 SHA256 + 域名白名单</text><text x="710" y="208" fill="#8b949e" font-size="12">打断:VAD 帧级检测 + 回声门控</text><text x="710" y="228" fill="#8b949e" font-size="12">空闲:听与说同权</text><text x="710" y="248" fill="#8b949e" font-size="12">模式:持续聆听 ⇄ 按住说</text><defs><marker id="arr" markerHeight="8" markerWidth="8" orient="auto" refX="7" refY="4"><path fill="#8b949e" d="M0,0 L8,4 L0,8 z"/></marker><marker id="arrR" markerHeight="8" markerWidth="8" orient="auto" refX="7" refY="4"><path fill="#f85149" d="M0,0 L8,4 L0,8 z"/></marker></defs><line x1="230" x2="326" y1="92" y2="92" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/><line x1="230" x2="326" y1="188" y2="178" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/><line x1="475" x2="475" y1="124" y2="146" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/><line x1="230" x2="326" y1="312" y2="264" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/><line x1="475" x2="475" y1="296" y2="318" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/><line x1="475" x2="475" y1="386" y2="408" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/><line x1="620" x2="720" y1="460" y2="460" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/><line x1="720" x2="720" y1="460" y2="446" stroke="#8b949e" stroke-width="1.5"/><line x1="130" x2="130" y1="410" y2="348" stroke="#8b949e" stroke-width="1.5"/><line x1="330" x2="234" y1="348" y2="348" stroke="#8b949e" stroke-width="1.5"/><line x1="234" x2="234" y1="348" y2="406" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/><line x1="130" x2="130" y1="486" y2="560" stroke="#f85149" stroke-width="1.5" marker-end="url(#arrR)"/><text x="300" y="556" fill="#f85149" font-size="12">说话打断:本地静音 + 队列作废 + 回合取消(二级 1.5s)</text></svg>
package/assets/demo.gif CHANGED
Binary file