Skip to content
dsh.fish
Bundle

dsh-voice-mode

Full-duplex voice plugin for DeepSeek Harness: local zipformer2 streaming ASR (no API key) → editable draft; Edge TTS or local VITS / Kokoro read-aloud with live captions; true barge-in; hardened HTTP surface + model SHA256 pinning; compatible with all ds

License
MIT
Updated
Updated 15 hours ago

Readme

# dsh-voice-mode

[![License: MIT](https://img.shields.io/github/license/qishuilalala/dsh-voice-mode?style=flat-square&color=blue)](LICENSE)
[![Latest Release](https://img.shields.io/github/v/release/qishuilalala/dsh-voice-mode?style=flat-square&color=brightgreen&include_prereleases)](https://github.com/qishuilalala/dsh-voice-mode/releases)
[![npm version](https://img.shields.io/npm/v/dsh-voice-mode?style=flat-square&color=orange)](https://www.npmjs.com/package/dsh-voice-mode)
[![Tests: 380 passing](https://img.shields.io/badge/tests-380%20%E2%9C%93-2ea043?style=flat-square)](../../docs/rules/STATE.md)

DeepSeek Harness 语音双工对话模式:会话内一键进入 → 边说边出字的流式识别 → 停顿自动发送 → 最终答复按句流式朗读 + 实时字幕,开口即可打断(真 barge-in)。无需 API Key,识别模型在本地宿主端推理。

> **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.

![dsh-voice-mode 全双工语音对话](https://raw.githubusercontent.com/qishuilalala/dsh-voice-mode/HEAD/assets/hero-banner.png)

![语音模式真实录制:流式转写 → 自动发送 → 按句朗读 + 实时字幕](https://raw.githubusercontent.com/qishuilalala/dsh-voice-mode/HEAD/plugin/dsh-voice-mode/assets/demo-voice-flow.gif)

![全双工对话闭环:声音 → 文字 → 声音](https://raw.githubusercontent.com/qishuilalala/dsh-voice-mode/HEAD/plugin/dsh-voice-mode/assets/duplex-banner.png)

> **运行时变更(最近一批:v0.7.10,2026-09-18)**:**输出链路静默丢音根治**——① 云端 TTS 单句重试耗尽后不再静默跳句(现在状态条提示「有一句朗读失败,已跳过」,此前表现为「AI 回复偶尔不朗读」且无从判断);② 浏览器挂起 AudioContext 后的「无声播放」(UI 显示朗读中、字幕照走)现在每次入队 + 任意点击/按键自动恢复;③ SSE 丢帧导致的坏句丢弃留诊断痕迹。其余同 v0.7.9:**唤醒词链路全面修复**(Issue #10 + 真机复测)——台架驱动真引擎逐项定位并修掉五处流程缺陷:① 朗读期 TTS 回声污染待机段(喊不醒);② 只上传超门限帧导致尾字不 flush(唤醒词只剩半截);③ 唤醒命中丢整段(连说「唤醒词+命令」只发出去尾部几个字);④ 命中晚于停口时命令悬挂不定稿;⑤ 只喊唤醒词后停顿会关掉命令窗口(命令丢失)。**现在唤醒词可与命令连说、词头自动剥离不进消息**,待机态实时显示「它听到了什么」,支持「唤醒词…停顿…命令」。其余同 v0.7.7:朗读默认 Edge 云端,本地 TTS(VITS / Kokoro)可选;静音断句默认 1500 毫秒。

---

## 🤝 Fork 增强(本仓库新增)

本仓库在上游 [haoku123/dsh-voice](https://github.com/haoku123/dsh-voice) 基础上加入了大量增强,核心如下(完整清单见 git 历史与 [docs/rules/STATE.md](../../docs/rules/STATE.md)):

- **朗读默认 Edge 云端;本地 TTS 可选(隐私优先)**:选本地则回复文本不出本机——
  - 本地 VITS(`sherpa-onnx-vits-zh-ll`,纯中文,5 说话人);
  - 本地 Kokoro(**中英混读**,103 音色;`int8` 默认约 109MB / 可选 `fp32` 约 311MB 音质更好),经 `sherpa-onnx-node` **原生 addon** 运行(无 WASM 内存上限,连续合成不崩);
  - Edge 云端朗读保留为可选(设置 `ttsEngine: edge`);设置面板「朗读引擎」热切换。
- **Kokoro 音色全量 103 个**(F0 实测标定性别),四个常用男声置顶带编号;音色面板用 **下拉列表 + ◀▶ 步进**切换;
- **增量传输**:partial 只传新增 0.9 秒,长段按住说话松手**秒出定稿**(不再整段重传重解码);
- **交互增强**:输入框旁**模式切换按钮**(持续聆听 ⇄ 按住说话,保存到设置);按住说模式下**按住才录、不按住不打断**;
- **长段支持**:持续聆听连续多段自动拼成一条消息(内部按 30s 分块识别,跨块累积拼接),静音断句默认 1500 毫秒;按住说停顿不断句;
- **朗读稳定性**:打断即终止在途合成释放 CPU;句间不再有 3-5 秒停顿;长朗读不触发空闲下线;
- **安全加固**:会话存在性校验 / 回环+Origin 校验 / 全端点限流 / **ASR+TTS 全模型 SHA256 固定** / 下载域名白名单 / 重定向守卫。

> ℹ️ 顶部 `demo-voice-flow.gif` 是**当前界面的真实录制**(语音模式 → 边说边出字 → 停顿自动发送 → 按句朗读 + 实时字幕),由 `screenshots/scripts/capture-demo.mjs` 驱动真实链路产出。
> ⚠️ 仓库内 `assets/demo.gif` 与根目录 `assets/` 下的旧截图仍为**上游旧版界面**(单按钮时期);当前界面在语音按钮旁多一颗「模式切换」按钮。

---

## ✨ 功能

- **语音模式**:输入框工具排麦克风按钮或全局快捷键 `Ctrl+Shift+V` 进入/退出;全局单活(同一时刻仅一个会话处于语音模式,切换会话自动让出)
- **两种交互模式(输入框旁按钮或设置可切换,切换即持久化)**:
  - `toggle`(默认)持续聆听:RMS VAD 分段 → zipformer2 流式识别(边说边出字,实时字幕预览)→ 静音约 1500 毫秒断句进草稿,连续多段拼成一条消息,再静音约 1500 毫秒(合计约 3 秒)自动发送;按住 `Ctrl` 强制立即发送
  - `hold` 按住说话:短按进入/退出,**按住麦克风按钮说话、松手即发**(滑出取消、`Esc`/失焦放弃本段);按住期间停顿不断句(上限 10 分钟);`Ctrl` 按住即录、松开即发
- **唤醒词(可选,默认关)**:设置 `wakeWord` 后进入待机态,说出唤醒词才开始识别(如「你好小D」)。**可直接与命令连说**(「你好小D,帮我查天气」)——唤醒词会在定稿/字幕里自动剥掉、不进消息;匹配带容错(同音字替换「小莫→小墨」、首字错、前置语气词「呃/喂/我说」),建议 3-4 字;待机态状态条实时显示「说『x』开始 · 它听到的转写」,唤醒成败可自查。边界(重要):**仅 `toggle` 模式生效**(`hold` / 手动打断下唤醒词静默失效);**说完整一句后回到待机态需重说唤醒词**(只喊唤醒词、还没说命令时保持聆听,可停顿想好再说);**朗读期说唤醒词不触发**(打断由 VAD 开口即触发,与唤醒词无关)
- **字幕档位**(批 3):`captionFontSize` 4 档(0=12px / 1=14px / 2=18px / 3=24px)+ `captionMaxWidth` 3 档(0=50vw / 1=70vw / 2=90vw),窄屏自适应
- **让位语义**(批 5 / ADR-0008):`backchannelYield` 默认开(I10 豁免)—— 朗读期用户插话「嗯/对」自动让位 1.5s,真要说走硬打断;让 LLM 主动让出话轮(人格层让位);关掉恢复改造前行为
- **输出链路**:只朗读最终答复的 `text-delta`(reasoning/工具调用不读),按句流式朗读(默认 Edge 云端;可切本地 VITS/Kokoro,中英混读选 Kokoro)+ 右下角实时字幕浮层;工具调用触发提示音;全文照常写入聊天记录;口语化提示词(设置 `spokenFormat`,默认开)让回复为自然短句、不带 Markdown 排版符号
- **开口打断(barge-in)**:服务端 Silero VAD 帧级检测 + 回声门控(echoGateDb)三档灵敏度 → 本地静音 + host 合成队列作废 + 正在运行的回合取消(保留半截并自然续入新消息);朗读中自动切超灵敏档。开启唤醒词后打断**仍是开口即打断**(打断门控是 VAD,不是唤醒词;打断后回待机态)
- **模型懒加载与进度**:首次使用自动下载识别/合成模型(`.part` 断点续传),状态条实时显示进度;可用 `npm run prefetch` 预下载
- **设置**:设置 → Plugins → 插件配置 → 语音模式(voice-mode),可调朗读引擎/音色/语速/打断灵敏度/静音停顿/空闲超时/模型镜像/自动发送/交互模式/唤醒词/口语化提示词/字幕档位/让位语义;**音色可试听**(按当前音色+语速即时合成预览,自定义 ShortName 亦可)
- **界面语言**:跟随浏览器语言(中文 / English;切换后刷新页面生效)
- **容错**:麦克风被拒红点提示、模型下载失败可见提示、TTS 连接失败状态条提示(自动退避重试)、提交失败文字留在草稿、SSE 断线自动重连
- **空闲退出**:5 分钟无活动自动退出并释放麦克风(**正在朗读计为活动**,长朗读不会中途下线;批 J 已微调默认 10→5)

---

## 🚀 5 分钟上手(Quick Start)

```sh
dsh plugin --profile web add dsh-voice-mode
```

bundle 插件安装后需重启 dsh 生效(Linux:`systemctl restart dsh`;其他平台重启 dsh 进程)。

**第一次用**:

1. 进入任一会话,按 `Ctrl+Shift+V`(或点输入区麦克风按钮)进入语音模式,状态条显示「聆听中…」;
2. 说一句完整的话(如「帮我看看今天的天气」)→ 实时字幕立即出现,停顿后自动发送;
3. AI 回复开始朗读时,**开口说话 → 朗读即刻停止,你的话被听见**(这就是 barge-in)。

---

## ⌨️ 操作手势

| 手势 | 作用 |
| --- | --- |
| `Ctrl+Shift+V` | 进入 / 退出语音模式 |
| 直接说话 | `toggle`:边说边出字,停顿约 1500 毫秒断句进草稿、再静音约 1500 毫秒(合计约 3 秒)自动发送;按住 `Ctrl` 强制立即发送 |
| 按住麦克风按钮 | `hold`:松手发送;短按退出;滑出 / `Esc` / 失焦放弃本段 |
| 点输入框旁模式按钮 | 在「持续聆听 ⇄ 按住说」间切换(保存到设置) |
| 说唤醒词 | 待机态激活识别(配置后) |
| AI 朗读时开口说话 | 打断朗读并取消当前回合 |
| 点状态条「退出」 | 退出语音模式 |
| 点字幕浮层「跳过」 | 跳过当前句朗读 |

---

## ⚙️ 设置(设置 → Plugins → 插件配置 → 语音模式)

| 键 | 默认 | 说明 |
| --- | --- | --- |
| `ttsEngine` | `edge` | 朗读引擎:`edge` 微软云端(默认,快)/ `vits` 本地中文 / `kokoro` 本地中英;**即时生效** |
| `kokoroModel` | `int8` | Kokoro 模型精度:`int8`(默认,109MB,纯 CPU/低带宽推荐)/ `fp32`(311MB,音质更好,独显/大内存推荐);两档共用 103 音色,**即时生效** |
| `voice` | 按引擎 | 音色:VITS 五说话人;Kokoro 103 个(下拉+◀▶,62 深沉/68 浑厚/75 清亮/76 磁性置顶);Edge 进入时自动加载全量 322 个。行内「试听」可即时预览 |
| `rate` | `1.1` | 朗读语速倍率(0.5 慢速 ~ 2.0 快速),**即时生效**(批 J 1.0→1.1) |
| `interruptLevel` | `0` | 发声打断灵敏度(服务端 VAD 帧级检测 + 回声门控):0 高门槛(3 帧)/ 1 中(2 帧)/ 2 低(1 帧) |
| `bargeInMode` | `detect` | 打断方式:`detect` 自动探测本机原生回声消除状态(默认;未生效时切为长按打断)/ `auto` 强制开口即打断(耳机、安静环境推荐)/ `manual` 长按打断(外放推荐)。批 7O 默认,I10 豁免(ADR-0006) |
| `echoGateDb` | `6` | 回声门控阈值(dB):自动打断要求残差高于回声地板此值。**原生 AEC 生效时此门控闲置**(Safari / 耳机等无原生 AEC 环境才兜底生效);打不断降 3-4,噪音误打断升 8-10。**不要为「打不断」调它**(详见 ADR-0006) |
| `silenceMs` | `1500` | 说完整一句的静音停顿毫秒数 |
| `idleTimeoutMinutes` | `5` | 无活动自动退出语音模式的分钟数(朗读计为活动;批 J 10→5) |
| `modelHost` | 默认源 | 模型下载源(国内网络填 `https://hf-mirror.com`) |
| `autoSend` | `true` | 静音到点自动发送(连续多段拼成一条消息);关闭则只进草稿(按住 `Ctrl` / hold 松手仍会发送) |
| `autoResume` | `false` | 切回上次语音会话时自动恢复语音模式(默认关)。开启后:下次进入语音会话即自动进入语音模式 + 恢复上次会话;关闭则需手动按 `Ctrl+Shift+V` 重新进入 |
| `mode` | `toggle` | 交互模式:`toggle` 持续聆听 + 1500ms 静音断句;`hold` 按住说话、松手发送(短按退出) |
| `shortcut` | `Ctrl+Shift+V` | 进入 / 退出语音模式的快捷键(修饰键 Ctrl/Shift/Alt/Meta + 一个字母键);**留空 = 禁用快捷键**,改用麦克风按钮 |
| `wakeWord` | 空(关) | 唤醒词(如「你好小D」):进入后先说唤醒词激活,避免误触;空 = 关闭。**可与命令连说**,词头自动剥掉不进消息;匹配带容错(编辑距离 ≤1 + 前 3 字符前导窗口,吸收同音字/语气词);**建议 3-4 字**——单字词无容错、2 字词的容错会连带吸收所有同首字的 2 字词(如「小莫」也会唤醒「小张」),介意误唤醒用 3 字以上;每句断句或打断后回待机需重说;**仅 toggle 模式生效**(hold / 手动打断下不生效);朗读期说唤醒词不触发 |
| `toolBeep` | `false` | 工具调用提示音(默认关):开启后 AI 调用工具时「滴」一声;关闭则全程静默 |
| `spokenFormat` | `true` | 语音会话注入口语化提示词:开启后**仅当前语音会话**的回复被注入「口语化短句、不用 Markdown 排版符号」提示词(朗读更顺),**即时生效** |
| `senseITN` | `true` | 批 2 P0:SenseVoice 逆文本归一化(数字/日期/货币规范化;默认开) |
| `senseVoice` | `true` | 定稿是否用 SenseVoice 重译(带标点 + 数字归一化,更准;默认开)。**关闭可省 ~228MB 模型**,只走流式识别(更快、精度下降) |
| `captionFontSize` | `0` | 批 3 P0:字幕字号档位 0=12px / 1=14px / 2=18px / 3=24px(默认 0 与现状字节等价) |
| `captionMaxWidth` | `1` | 批 3 P0:字幕宽度档位 0=50vw / 1=70vw / 2=90vw(视口 <686px 接近 480px,>686px 宽于 480px) |
| `backchannelYield` | `true` | 批 5 P1:让位语义(ADR-0008);朗读期说「嗯/对」自动让位 1.5s + 真要说走硬打断。I10 豁免(默认开是产品决策);关 = 行为等同改造前 |
| `yieldMs` | `1500` | 让位窗口时长(ms,500-3000):`backchannelYield` 命中后 TTS 丢帧持续时长;窗口内用户真要说则由原 `hardBreak` 接管,窗口到点自动恢复播放 |

生效范围:`voice`/`rate`/`ttsEngine`/`kokoroModel`/`spokenFormat` **立即生效**;其余设置下次进入语音模式时生效。设置项默认值由插件配置(`base` 层)提供。

### 本地音色(VITS / Kokoro)

- **VITS(纯中文)**:`suyingxue` 素映雪·女 / `gunian` 顾念·男 / `fushiyu` 傅斯遇·女 / `bingjiao` 冰娇·男 / `bazong` 霸总·男
- **Kokoro(中英混读均可)**:103 个音色全量入表,面板按编号 + 实测性别标注;四个常用男声置顶:`62` 深沉 / `68` 浑厚 / `75` 清亮 / `76` 磁性;中文女声 `48` 小北 / `49` 小妮 / `50` 小小 / `51` 小艺。音色只是风格向量,**语言能力与音色无关**。

### 常用 Edge 音色(完整清单见 `node scripts/list-voices.mjs`)

| ShortName | 说明 |
| --- | --- |
| `zh-CN-XiaoxiaoNeural` | 晓晓 · 女声(默认) |
| `zh-CN-XiaoyiNeural` | 晓伊 · 女声 |
| `zh-CN-YunxiNeural` | 云希 · 男声 |
| `zh-CN-YunjianNeural` | 云健 · 男声 |
| `zh-CN-YunyangNeural` | 云扬 · 男声 |
| `zh-CN-YunxiaNeural` | 云夏 · 男声 |
| `zh-CN-liaoning-XiaobeiNeural` | 小北 · 东北话 · 女声 |
| `zh-HK-HiuMaanNeural` | 晓曼 · 粤语 · 女声 |
| `zh-TW-HsiaoYuNeural` | 小雨 · 台湾腔 · 女声 |
| `en-US-AriaNeural` | Aria · English · 女声 |

---

## 🔧 配置(bundle patch / settings.yaml)

也可以直接编辑 `~/.dsh/settings.yaml` 的 `voice-mode:` 段(设置面板与 RPC 写的是同一份文档层):

```yaml
- id: voice-mode
  name: dsh-voice-mode
  config:
    enabled: true                 # false = 完全禁用语音模式(toggle 会被拒)
    cacheDir: ~/.cache/dsh-voice-mode/models   # 可覆盖;否则用平台默认
    # 以下为设置项播种的默认值(设置面板优先级更高,面板是权威源):
    voice: zh-CN-XiaoxiaoNeural
    rate: 1.1                     # 批 J 1.0→1.1
    interruptLevel: 0
    silenceMs: 1500
    idleTimeoutMinutes: 5         # 批 J 10→5
    modelHost: https://huggingface.co
```

> 注意:`voice` / `rate` / `interruptLevel` / `silenceMs` / `idleTimeoutMinutes` / `modelHost` / `autoSend`
> 的**生效值来自设置面板**;bundle 配置只负责为这些键播种默认值
> (`enabled` / `cacheDir` 则仅由 bundle 配置决定)。
> 插件 HTTP 命名空间固定为 `/voice-mode`(与客户端打包契约一致,不可配置)。

---

## 🌐 API

| 路由 | 说明 |
| --- | --- |
| `GET /voice-mode/stream` | SSE:`event: audio`(`{sessionId, seq, text, audio(base64 MP3)}`)、`event: mode`(全局单活归属)、`event: tool`(提示音)、`event: asr-progress / asr-ready / asr-error / tts-error` |
| `POST /voice-mode/toggle` | `{sessionId, on}` 进入 / 退出语音模式(全局单活) |
| `POST /voice-mode/asr` | 裸 f32 LE 16k PCM → `{text}`(流式 zipformer2);模型未就绪返回 `202 {loading}`;`?reset=1` 丢弃在途段(唤醒词命中时使用) |
| `POST /voice-mode/cancel` | `{sessionId}` 作废 TTS 队列并丢弃在途 ASR 段 |
| `POST /voice-mode/preview` | `{voice, rate?}` 一次性合成试听 → `audio/mpeg`(400 缺 voice / voice 过长;502 合成失败,如无效 ShortName;403 插件 `enabled=false`)。不要求语音模式处于激活态;使用独立合成连接,不影响朗读队列 |
| `GET /voice-mode/config` | 客户端启动参数(静音阈值 / 灵敏度 / 音色与语速等)——含 ASR 侧字段 `senseITN` / `senseVoice` / `captionFontSize` / `captionMaxWidth` / `backchannelYield` |
| `GET /voice-mode` | 健康检查 `{ok, name, enabled, active}` |

---

## 💾 模型与缓存

- 识别模型:`csukuangfj/sherpa-onnx-streaming-zipformer-zh-int8-2025-06-30`(encoder ≈154 MB / decoder / joiner / tokens,合计约 160 MB),宿主端经 sherpa-onnx(Node WASM,Apache-2.0,原生跨平台)运行
- 缓存目录的平台默认:
  - **Windows**:`%LOCALAPPDATA%\dsh-voice-mode\models`
  - **macOS / Linux**:`~/.cache/dsh-voice-mode/models`
  - 两者都可用 `cacheDir` 覆盖
- 下载用 `.part` 断点续传;`huggingface.co` 失败时回退 `hf-mirror.com`(可用 `modelHost` 配置)

---

## 🏛️ 工作原理(Architecture)

```mermaid
flowchart LR
    subgraph Client[浏览器 Client]
        Mic[麦克风 16kHz<br/>AudioWorklet<br/>echoCancellation:true] --> VAD[客户端 VAD<br/>RMS 分段]
        VAD -->|partial 0.9s| UI[状态条 + 字幕浮层]
    end

    subgraph Host[宿主 dsh.host]
        ASR[zipformer2 流式识别<br/>host 端 WASM]
        SV[SenseVoice 定稿<br/>+ ITN + 标点]
        Tap[llm/stream tap<br/>仅观察·不阻塞]
        Seg[sentence segmenter]
        Q[TtsQueue<br/>epoch 打断]
        TTS{引擎}
        Edge[Edge 云端]
        Vits[本地 VITS WASM]
        Kokoro[本地 Kokoro<br/>原生 addon]
    end

    UI -->|audio f32 PCM<br/>POST /voice-mode/asr| ASR
    ASR --> SV
    SV --> Draft[composer draft<br/>autoSend]
    Draft --> Tap
    Tap --> Seg
    Seg --> Q
    Q --> TTS
    TTS -->|edge| Edge
    TTS -->|vits| Vits
    TTS -->|kokoro| Kokoro
    Edge -.->|SSE audio frame| UI
    Vits -.->|SSE audio frame| UI
    Kokoro -.->|SSE audio frame| UI
    VAD -.->|唤醒词/打断| Q
```

- 识别在 **host 端本地运行**(zipformer2 中文 int8 WASM + SenseVoice 定稿,模型懒下载),音频不上传第三方;识别定稿由 SenseVoice 补标点;
- 朗读默认 **Edge 云端**;本地 VITS 纯中文 / Kokoro 原生中英(跑在独立子进程、崩溃自愈)可选(隐私优先);
- 同一时间仅一个会话处于语音模式(全局单活);LLM 流被无损观察(不阻塞)。

详细架构决策:见 [`docs/adr/`](../../docs/adr/README.md) 8 个 ADR。

---

## 🔍 与 dsh 内置语音模式对比

| 维度 | dsh 内置 | dsh-voice-mode(本插件) |
| --- | --- | --- |
| 识别模型 | 云端 API(需 key) | **本地 zipformer2 + SenseVoice**(零 key) |
| 多语种 | 英文为主 | **SenseVoice 自动识别(zh/en/ja/ko/yue)+ ITN** |
| 朗读引擎 | 云端 TTS | **Edge 云端 + 本地 VITS/Kokoro** 三选一 |
| 打断检测 | 基础 VAD | **三档灵敏度 + 回声门控 + 让位语义** |
| 热词偏置 | 无 | 无(已移除,详见 v0.7.7 文档说明) |
| 字幕 a11y | 无 | **4 档字号 + 3 档宽度 + 主题跟随** |
| 唤醒词 | 无 | **轻量流式匹配 + 前缀语气词白名单** |
| 兼容 dsh | — | **0.1.1-rc.2 → 0.1.5-rc.2 全版本(+ 0.1.6-alpha.2 预览)** |

---

## 🚧 已知限制

- 发声打断依赖浏览器回声消除(`echoCancellation`);扬声器音量过大时可能漏声到麦克风
- `Ctrl+Shift+V` 会覆盖浏览器「粘贴纯文本」快捷键(普通粘贴仍可用 `Ctrl+V`)
- 识别质量受环境噪声影响;zipformer2 中文流式 + SenseVoice 多语(中英日韩粤)定稿
- 浏览器自动播放策略:朗读需要页面已有用户交互(点击麦克风即满足);「试听」依赖 `AbortSignal.timeout`(Safari 16+ / Chrome 103+ / Firefox 100+;老浏览器点击试听会立即提示失败,属预期降级)
- **唤醒词为轻量实现**(流式文本匹配,非专用 KWS 引擎):嘈杂环境可能延迟或误激活;唤醒词本身不会进入聊天
- hold 模式按住时切换窗口/标签页会**放弃本段**(防持续收音)
- hero(新会话空态)无语音入口:请先进入会话使用麦克风按钮
- `spokenFormat` 提示词经官方 `system-prompt/assemble` 瀑布注入;若当前会话使用**完整提示词**配置(persona `complete: true` 的 agent preset),提示词不注入(官方 complete 契约优先)
- 本地 Kokoro 每次打断后下一句朗读前约有 1 秒引擎重建时间(打断即终止在途合成的代价)
- **苹果 Safari / iOS**:
  - 需 **HTTPS 或 localhost**(iOS/macOS Safari 强制安全上下文;`http://` 局域网 IP 下麦克风不可用)
  - 首次进入需授权麦克风;被拒后到「设置 → Safari → 麦克风」开启(iOS)
  - iOS 后台/锁屏时识别与朗读暂停,回前台自动恢复(可能丢句);建议语音模式期间保持前台
- **安全说明**:插件 HTTP 面(`/voice-mode/*`)遵循宿主安全模型——请勿将 dsh 端口直接暴露公网;经反向代理发布时由代理层(如 basic auth)鉴权;插件侧对敏感操作保留会话归属校验

---

## 🛠️ 故障排查

| 现象 | 处理 |
| --- | --- |
| 点麦克风无反应,状态条红字 | 浏览器拒绝麦克风:地址栏(iOS 为 设置 → Safari → 麦克风)开启后重试 |
| 状态条「正在加载模型… x%」卡住 | 检查网络;模型较大可先 `npm run prefetch`;国内网络 `modelHost` 配 `https://hf-mirror.com` |
| 朗读无声音/无字幕 | 本地引擎首次合成需加载模型;若持续失败查看状态条提示(自动退避重试);确认页面前台且未静音 |
| AI 回复**偶尔不朗读**(某句没声/整条没声) | 已定位三类原因并修(v0.7.10):① 云端 TTS 偶发超时——单句自动重试 3 次,仍失败会**跳过该句并在状态条提示**「有一句朗读失败,已跳过」(此前是静默丢句);② 浏览器挂起 AudioContext(切到后台标签页/长时间静音后回来)——播放被调度但无声、UI 却显示朗读中;现在每次入队与**任意点击/按键**都会自动恢复;③ SSE 断线丢帧导致整句不完整——按设计丢弃该句(避免播坏音频),可开 `localStorage['dsh-voice-mode.telemetry']='1'` 看 `tts-drop-sentence` 诊断 |
| 语音模式进不去 | 检查插件 `enabled`;多标签页时确认当前会话为活动会话 |
| 识别到但不是我要说的 | 环境噪声或唤醒词误判:降低音量、提高 `interruptLevel`(高门槛)或启用 `wakeWord` |
| 唤醒词唤不醒 | 先看待机态状态条的实时转写(它听到了什么):同音字 1 字内已自动容错;停顿约 1.5 秒(= 静音断句时长)再说,清掉待机段残留语音、从段首重新匹配;换一个转写稳定的词(建议 3-4 字) |
| 打断后说话没反应(开了唤醒词) | 唤醒词只管「开始识别」的门:打断/每句断句后回到待机态,**需重说唤醒词**再继续说;待机态状态条会显示「说『x』开始」提示 |
| 唤醒词完全没反应 | ① 确认交互模式是 `toggle`(`hold` 与手动打断下唤醒词不生效);② 确认 `bargeInMode`:`manual`(或 `detect` 在本机无原生回声消除时自动落 manual)会关闭常驻聆听 → 唤醒词失效,改回 `auto` 或按住麦克风/Ctrl;③ 朗读期说唤醒词不触发(先等 AI 说完);④ 看待机态实时转写,确认它听到了什么(同音字 1 字内已容错,建议 3-4 字词) |
| 按住说话松手后没反应 | 确认交互模式为「按住说」且按住期间按钮高亮;松手后识别定稿约 1 秒内进入草稿 |
| 打不断(朗读中开口无反应) | 调高 `interruptLevel`(更敏感档 = 0 或 1)或检查麦克风权限;若 VAD 持续不触发可临时切到「手动打断」(`mode: hold` / 唤醒词定时延后);**不要**调 `echoGateDb`——真机 3.1 分钟朗读期里 Silero 0/777 帧把回声判成语音,原生 AEC 生效时此阈值从未被执行(详见 ADR-0006) |
| 字幕被输入框挡住 | 默认 `captionMaxWidth=1`(70vw)+ `captionFontSize=0`(12px)在窄屏可能与底部输入框重叠;调高档位或点字幕浮层「×」收起 |
| 让位行为异常(朗读期说「嗯」不停 / 真话被打断) | 「嗯/对」类短词触发让位 1.5s 后继续朗读;继续说真话会走硬打断;不要时关 `backchannelYield` 即可恢复改造前行为(ADR-0008) |
| 朗读期说「嗯」没让位 | 确认 `backchannelYield=true`(默认开);hold 模式松手后让位 1.5s 内继续说话会变硬打断 |
| 空闲 5 分钟自动退出(不想退) | 调高 `idleTimeoutMinutes`(默认 5 分钟,**朗读计为活动**) |

---

## 🛣️ 路线图(Roadmap)

完整 backlog(43 项 P0-P3)见 [`docs/competitive/backlog.md`](../../docs/competitive/backlog.md)。

- ✅ **已完成(v0.7.7)**:11 批次周全修复(字幕档位 / 让位语义 / 模型预热 / 默认值微调 / 死代码清理等)
- 🚧 **P0(近期)**:ADR-0003 VAD 下沉 / ADR-0006 第一级探测接通 manual / F1 emotion DSL 全量上线
- 📋 **P1(中期)**:MCP `voice_*` 工具集 / 卡片表单 draft validate / 状态条 idle 优化
- 💡 **P2(远期)**:声音克隆(用户已决定推迟)/ ADR-0004 WebSocket transport
- ⏸️ **已推迟**:xAI fallback / C1 人格层(用户已决定推迟)

---

## 🛠️ 开发

```sh
pnpm install && pnpm build    # esbuild:lib/index.js(host)+ lib/client.js(browser)
pnpm test                     # segmenter/wakeword 单测 + 发布前自检(无需网络)
systemctl restart dsh         # 本机加载新 host 代码;其他平台重启 dsh 进程
```

> 注意: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 包内。

```
src/index.ts         host:单活指针、llm/stream tap、SSE、settings 注册、口语化提示词注入
src/asr-host.ts      host:zipformer2 流式识别 + SenseVoice 定稿 + 模型懒下载(.part 断点续传)+ 增量喂料
src/models.ts        host:模型下载/校验(SHA256 固定 + 域名白名单)
src/security.ts      host:限流器与安全守卫
src/tts-local.ts     host:本地 TTS 引擎(VITS WASM / Kokoro 原生 addon,子进程管理)
src/tts-vits-worker.ts 子进程:合成执行(base64 IPC,空文本静音守卫)
src/tts-queue.ts     host:逐会话 TTS 队列 + epoch 打断机制
src/segmenter.ts     host:句子切分 + 文本消毒(markdown 剥离 + 噪声字符剔除)
src/asr.ts           client:音频采集、VAD 分段、增量识别、唤醒词、按住说门控
src/client.tsx       client:麦克风按钮 + 模式切换按钮 + 状态条 + 字幕浮层 + 打断
src/strings.ts       client:中英文案字典(navigator.language)
```

---

## 📄 License

[MIT](../../LICENSE)

> 部分实现借鉴 [haoku123/dsh-voice](https://github.com/haoku123/dsh-voice)(派生声明见子包 LICENSE)。

Install

dsh plugin --profile web add dsh-voice-mode@0.7.14

Profile: web

Source