DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

lunnlew /

lunnlew/dsh-voice-typing

Verified

语音输入与对话朗读插件

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@0e82bed3

dsh-voice-typing

DeepSeek Harness 的语音输入与对话朗读插件。

点一下麦克风按钮(或按 Alt+V)就能进入语音模式:你说话,文字自动打进输入框; 助手的回复可以自动朗读出来。全程本地离线识别,不需要 API Key、断网也能用。

本 README 面向使用者,按「从零到会用」的顺序一步步引导。每一步都告诉你 做什么、为什么、以及做完会看到什么。维护者须知与技术参考见文末。


目录

  1. 它是什么
  2. 安装(一步完成)
  3. 第一次使用(跟着走一遍)
  4. 日常操作速查
  5. 设置面板详解(按你的需求调)
  6. 模型与下载(该选哪个)
  7. 自定义词表(修同音字与专名)
  8. 故障排查
  9. 技术参考(维护者/进阶)
  10. 许可

1. 它是什么

一句话:「说话代替打字,回复自动念给你听。」

它把两件独立的事合在一起,但各自可控:

功能 做什么 什么时候用
语音输入 你说话 → 文字自动进输入框 → (可选)自动发送 不想打字、手在忙、想快点说
对话朗读 助手的回复自动念出来 不想盯着屏幕、听回复更方便

关键特性:

  • 点一下就开,再点一下就关:麦克风按钮是开关,不是「按住说话」。
  • 本地离线识别:默认用 SenseVoice 模型,复用 DSH 已装好的那份,零下载、零密钥、断网可用。
  • 边说边出字:说话时文字实时回填到输入框,不用等说完。
  • 三种朗读引擎:在线 Edge(音质最好)、本地神经模型(离线、音质接近在线)、系统语音(最快)。

2. 安装(一步完成)

前置条件:你已经装好并运行过 DeepSeek Harness 桌面应用。

第 1 步 — 安装插件。在 DSH 的终端/命令行里执行:

dsh plugin --profile desktop add dsh-voice-typing

第 2 步 — 重启 DSH。装完插件必须重启才会生效。

第 3 步 — 启用组合包。打开 DSH →「设置 → 插件」,把 dsh-voice-typing 加进 dsh.profile.bundles(勾选启用)。

第 4 步 — 确认出现麦克风按钮。重启后,在聊天输入框附近应该能看到一个 麦克风图标。看到它,安装就算成功了。

💡 看不到麦克风按钮? 先确认「第 3 步」的启用组合包有没有勾上,再重启一次。 还不行就跳到故障排查。


3. 第一次使用(跟着走一遍)

下面按顺序做,每一步都说明你会看到什么。

3.1 打开语音模式

  • 点一下麦克风按钮,或按全局热键 Alt+V。
  • 看到:按钮变「实心/高亮」,旁边状态行出现 「聆听中」。

语音模式是一个开关。再点一下(或右键)就关闭,不是按住说话。

3.2 开始说话

  • 对准麦克风说一句完整的话,例如「今天天气怎么样」。
  • 看到:你说话时,文字实时出现在输入框里(边说边出字)。

3.3 停顿,自动发送

  • 说完停下来。默认静音 1.5 秒 后判定「说完了一句」。
  • 看到:文字被发送给助手,助手开始回复。

想改成「说完不自动发,我自己按回车」?在设置里关掉「识别后自动发送」。 见 5.2 语音识别。

3.4 (可选)听回复

  • 勾选设置里的「朗读回复」后,助手的回复会自动念出来。
  • 朗读时麦克风旁边会出现一个 ■ 停止朗读 按钮,点它就不念了。
  • 想打断朗读并自己说话?直接开口说,朗读会自动停,你说的话会发出去。

「朗读回复」不需要开语音模式,只要勾上就生效。

3.5 退出语音模式

  • 再点一下麦克风按钮(或按 Alt+V),或右键麦克风按钮。
  • 看到:按钮恢复原样,状态行显示「已停止」。

4. 日常操作速查

你想做什么 怎么操作
开/关语音模式 点一下麦克风按钮,或按 Alt+V
说完一句并发送 说一段话后停顿 1.5 秒(默认)
说完立刻退出语音模式 设置「交互模式」→ 说完就关
停止朗读这一轮 点 ■ 停止朗读 按钮
打断朗读、自己说话 直接开口说(朗读自动停)
临时不自动发送 关掉「识别后自动发送」,说完手动回车
只朗读、不开语音模式 只勾「朗读回复」,不点麦克风

麦克风按钮只有两个动作:

手势 行为
点一下 开 → 进入语音模式并开麦;关 → 只关麦,朗读继续播完
右键 同「点一下」(顺带屏蔽浏览器默认菜单)

全局热键(Alt+V):按一下切换开/关,行为与点麦克风一致。 热键必须带至少一个修饰键,且主键不能是单个字母(否则打字会误触发)。


5. 设置面板详解(按你的需求调)

打开 DSH →「设置 → 语音输入」(settings.section 槽位)。 所有设置分 6 个可折叠分区(前 5 组默认展开,最后一组「高级与诊断」默认收起): 语音识别 / 语音交互 / 文本处理 / 语音合成 / 模型与下载 / 高级与诊断。 改完记得点 保存。

5.1 先知道这两个总开关

开关 控制什么 默认
语音模式(麦克风按钮) 是否聆听你的说话 关
朗读回复(设置里) 是否朗读助手的回复 关

两者独立。想「只听不说」就只开麦克风;想「只听回复不发言」就只勾朗读回复。


5.2 语音识别

设置 作用 怎么调
识别模型 用哪套模型认字 见 第 6 节。默认 SenseVoice,零下载。
识别语言 告诉模型你说的是哪种语言(仅 SenseVoice 生效;Whisper/zipformer2 会忽略,UI 会按模型禁用/提示) 自动(推荐,由模型判断)/ 中文 / 英文 / 粤语 / 日文 / 韩文
实时字方式 说话时实时出字用哪种方式 见下方说明。默认「整段重识别」(精度最好)。

「实时字方式」三种选法:

方式 优点 缺点 何时用
整段重识别(默认) 精度最好,与最终稿同源 句子越长越慢 多数情况
流式解码 延迟低,长句不变慢 精度略低 说长句、要快
自动 流式模型用流式,否则整段 — 不确定时

最终稿(断句后的那次识别)始终用当前模型,不受「实时字方式」影响。 「流式解码」固定依赖 zipformer2-zh(与当前所选识别模型无关);它没下载时 UI 会提示,实际会回退整段重识别。


5.3 语音交互

设置 作用 怎么调
交互模式 说完一段之后怎么办 连续对话(默认):说完自动接下一轮,麦克风不关;说完就关:说完就退出语音模式。依赖自动发送:关闭自动发送时该选项会被禁用(否则说完不会发送、也不会退出)。
静音断句(毫秒) 静音多久算「说完一句」 默认 1500。太短会把长句切断;太长回复会慢。
识别后自动发送 说完是否自动发送 开(默认):停顿后自动发;关:文字只留在输入框,手动回车发送。
发送前等待(毫秒) 说完后再等多久才发 默认 1200。用来合并短句、避免抢话。只在「自动发送」开时显示/生效;清空会回退默认。
语音模式快捷键 切换语音模式的全局热键 默认 Alt+V。格式如 Alt+V,改完立即生效;非法组合会阻止保存并提示。

快捷键注意事项:

  • 必须带至少一个修饰键(Ctrl / Alt / Shift / Meta)。
  • 主键是单个字母时,必须搭配 Ctrl / Alt / Meta(不能只配 Shift,否则打字会误触发)。
  • 别用 Ctrl+Shift+V(浏览器「粘贴为纯文本」)、Ctrl+Space(输入法占用)。

静音断句与发送前等待在面板里并排显示,方便对照调整「说完」与「发送」的节奏。


5.4 文本处理

设置 作用 怎么调
清理语气词 去掉「嗯」「啊」「那个」等口头禅 勾选即生效(只处理识别结果,不改写说话内容)。
自定义词表 热词与替换规则 一行一条;正确=错误 表示把「错误」替换成「正确」,单行表示热词。热词仅 zipformer2(transducer)档位生效;保存时按行 trim、去空、上限 100 字/条、200 条。空格/换行输入不会被逐键吞掉。

5.5 语音合成(朗读)

本区顶部会显示「当前合成方式」(引擎 + 音色/模型),改完设置后保存即生效。

设置 作用 怎么调
朗读回复 总开关。关掉则下面所有朗读参数都不生效 勾选即朗读,不需要开语音模式。朗读关闭时本区仍可编辑/保存(仅保存、不生效),方便先配置。
朗读追赶最新 生成远快于朗读时,允许落后屏幕多少秒;超过就整段跳旧段、保留最新 3 段(从最新第 3 条开始读,不从段落中间开始念) 默认 7.5 秒(0.5 秒步进);0 = 全部朗读、不跳段(长回复会越读越滞后);>120 按 120 秒生效。只在 flow(客户端 ack)模式生效。
语音引擎 用哪种方式念 Edge 在线 / 本地神经模型 / 本地系统语音 / GPT-SoVITS 外部服务,见下方对比。引擎值非法时保存会被拒绝(不会静默落在线)。
朗读音色 用哪个发音人 在线用 Edge 音色(zh-CN-XiaoxiaoNeural 等);本地用系统语音;神经模型用「说话人」(单说话人模型会自动隐藏该项)
GPT-SoVITS 服务地址 外部 GPT-SoVITS 服务地址 默认 http://127.0.0.1:9880;根地址或 /tts 均可
服务端参考音频路径 官方 api_v2 必填 GPT-SoVITS 所在机器上可访问的 wav 路径(插件只透传)
参考音频文本/语言、合成语言 参考音色与多语种 参考文本与音频逐字匹配效果最好;语言 zh/en/ja/ko/yue
朗读语速 快慢 默认 +10%;+0% 为正常,+50% 更快,-50% 更慢。四种引擎都生效(SoVITS 转 speed_factor 0.5~2)
朗读音调 高低 只有在线 Edge 支持(如 +0Hz、-5Hz)
朗读音量 大小 在线/本地支持(如 +0%),神经模型与 SoVITS 不支持
音频格式 MP3 码率 只有在线 Edge 支持。24kHz/48kbps(推荐)/ 24kHz/96kbps(音质更好)

四种语音引擎对比:

引擎 音质 联网 速度 备注
Edge 在线 ★★★ 需要 快(预热后 ~600ms) 音质最好,音色选择多
本地神经模型 ★★☆ 离线 中(~1.6s) 比系统语音自然;需下载约 170MB 模型
本地系统语音 ★☆ 离线 最快(~800ms) 机器感较强;Windows SAPI / macOS say / Linux espeak
GPT-SoVITS 外部服务 取决于你的服务与模型 本机 HTTP 取决于服务端(GPU 快) 用户自己跑 GPT-SoVITS api_v2;插件只转发文本合成,不内置模型、不做克隆

在线引擎合成失败时自动回退到本地,保证「总有声音」;x-tts-engine-fallback 会在 UI 提示一次。 GPT-SoVITS 外部服务不回退(避免把文本发给微软、也避免偷偷换音色),失败见 /diagnostics 的 sovitsTts。

配置保存会做校验:非法枚举/类型/范围(如 ttsEngine 未知、sovitsUrl 非 http(s)、readAloudLag 非数字)返回 400 并显示错误,不会被静默改成别的值。

外部 GPT-SoVITS 引擎(可选)

插件不内置 GPT-SoVITS、也不做声音克隆:需要你自己运行官方 api_v2 服务, 把服务端可访问的参考音频/参考文本/语言填到设置里;插件只把每句文本 POST 到 /tts, 拿回 wav 走现有播放管线。

  1. 在 GPT-SoVITS 目录启动 API 服务: python api_v2.py -a 127.0.0.1 -p 9880 -c GPT_SoVITS/configs/tts_infer.yaml
  2. 设置 → 语音引擎选「GPT-SoVITS 外部服务」,填:
    • 服务地址(默认 http://127.0.0.1:9880,填根地址或 /tts 都行);
    • 服务端参考音频路径(官方 api_v2 每个请求都要求它,必须是服务端机器上可访问的 wav);
    • 参考音频文本 / 语言(可选;与参考音频逐字匹配时音质更好);
    • 合成语言(zh / en / ja / ko / yue)。
  3. 模型/音色/参考音频都由 GPT-SoVITS 侧管理,插件不下载模型、不克隆声音。

服务未启动或参考音频路径不对时,/tts 会返回可读错误(含官方 api_v2 的 message 与提示), 计数进 /diagnostics.sovitsTts;插件不会替你拉起 Python 服务。


5.6 模型与下载

设置 作用 怎么调
启用后预加载模型 选了模型就后台先加载好 开(推荐)。第一次说话就不用干等(Zipformer2 首次要 40 秒)。只覆盖当前识别/神经 TTS 模型,不覆盖实时字用的 zipformer2。
空闲释放(分钟) 模型闲置多久后释放内存 10 分钟(默认)/ 永不释放 / 5 / 30 / 60 分钟。不释放当前正在使用的模型。
运行环境自检 检查平台、显卡、各引擎可用性 点「重新检查」,看绿色 ✓ / 红色 ✗(含四种 TTS 引擎与 GPT-SoVITS 配置项)。

模型常驻内存(Zipformer2 约 160MB,神经 TTS 约 170MB)。闲置释放能把内存还给系统。 下方的模型列表负责下载/删除识别与合成模型;已下载的档位才会出现在识别模型下拉里。


5.7 高级与诊断

设置面板最后一组默认收起,只提供排查入口,不含任何配置项:

  • GET /dsh-voice-typing/diagnostics:朗读调度(lagMs/trimmed/configWarnings 等)、各 TTS 引擎失败、模型下载状态、环境自检结果;
  • 浏览器调试快照 __DSH_VOICE_DEBUG__():客户端播放/队列/回退/高亮等运行时状态。

6. 模型与下载(该选哪个)

6.1 识别模型

档位 大小 中文精度 特点 推荐场景
SenseVoice(推荐) 0(内置)/ 230MB ★★★ 带标点与数字规范化;零下载 大多数人
Zipformer2 中文 160MB ★★ 流式,延迟最低;追求实时字速度、说长句
Whisper tiny 99MB ★ 最小 先跑通
Whisper base 153MB ★★ 轻量折中 中文可用
Whisper small 358MB ★★★ 精度明显优于 base 均衡
Whisper medium 902MB ★★★★ 更准 重精度
Whisper large-v3-turbo 989MB ★★★★☆ 接近 large-v3、体积小一半 综合最优
Whisper large-v3 1.7GB ★★★★★ 精度天花板 重精度、不怕慢

怎么选:默认 SenseVoice 就行。它零下载、中文强、带标点。 想要实时字更快选 Zipformer2;想要更准选 Whisper 大档(代价是体积和耗时)。

6.2 合成(TTS)模型

档位 大小 特点
MeloTTS 中英 170MB 多说话人、本地离线、音质接近在线

6.3 怎么下载

第 1 步 — 打开「设置 → 语音输入 → 模型管理」。

第 2 步 — 在「识别模型」或「合成模型」列表里,找到你要的档位,点 下载。

第 3 步 — 看进度。下载时显示百分比;完成后出现 ✓ 和「已就绪」。

第 4 步 — 回到「语音识别」或「语音合成」,把「识别模型 / 神经模型」切到你刚下载的档位。

下载失败? 确认网络能访问模型源(国内走 hf-mirror.com 镜像)。 进度卡在 100% 不动通常是校验失败,重试一次或换档位。


7. 自定义词表(修同音字与专名)

词表里两种写法,作用完全不同:

写法 作用 示例
正确=错误 替换:识别出「错误」时改写成「正确」 深度求索=深度球搜
只写 正确 热词提示:不改文本,只给识别器做偏置 专业术语

怎么用:

  1. 打开「设置 → 语音输入 → 交互与后处理」。
  2. 在「自定义词表」里一行一条填写。
  3. 保存。

注意事项:

  • 中文精确匹配;英文忽略大小写并保持原词形态。
  • 热词提示只有 Zipformer2(transducer)档位支持。用 SenseVoice 时,只写「正确」的条目不会有任何效果(想要替换效果请用 = 写法)。

8. 故障排查

按症状找对策。

8.1 看不到麦克风按钮

  1. 确认插件已安装:dsh plugin --profile desktop list 里有没有 dsh-voice-typing。
  2. 确认已在「设置 → 插件」的 dsh.profile.bundles 里启用。
  3. 重启 DSH(插件加载需要重启,不是刷新页面)。

8.2 点麦克风没反应 / 显示「麦克风被阻止」

  1. 检查浏览器/系统是否给了麦克风权限(地址栏权限图标)。
  2. 确认没有别的程序(会议软件等)占着麦克风。
  3. 重试;若仍不行,重启 DSH。

8.3 能说话,但文字没出来 / 显示「未听到声音」

  1. 确认麦克风没静音、音量够大。
  2. 靠近麦克风,清晰地说一句。
  3. 若提示「模型加载中…」,等首次加载完成。
  4. 检查「识别模型」是否已就绪(见 6.3)。

8.4 能识别,但不对 / 同音字错

  1. 用自定义词表修正:正确=错误。
  2. 若错得离谱,换更准的识别模型(见 6.1)。
  3. 确认「识别语言」设为 自动 或你的语言。

8.5 朗读没声音

按顺序排查(最常见的是「在线引擎需联网」):

  1. 确认设置里「朗读回复」已勾选。
  2. 确认「语音引擎」:
    • Edge 在线 需要联网(断网请换本地引擎)。
    • 本地系统语音 在当前平台需支持(Windows SAPI / macOS say / Linux espeak-ng)。
    • 本地神经模型 需要已下载该档位模型(见 6.3)。
  3. 打开浏览器控制台,输入 __DSH_VOICE_DEBUG__() 看 ttsFailures。
  4. 若显示「朗读失败」,看 诊断接口里的 onlineTts / localTts / neuralTts / sovitsTts 的 lastError。
  5. 若本地引擎失败但显示「朗读中」却没声,多半是自动回退到在线但没联网。

8.6 朗读不完整 / 句子被切碎 / 把代码念出来

这些都是有意设计的防御,不是 bug:

  • 代码块(```)一个字都不会念 —— 这是刻意的,防止把代码念出来。
  • 列表编号不会当句子念 —— 1. 第一步… 只念「第一步…」,不念「一」。
  • 长句不会被拦腰切断 —— 尽量切在句末标点(。!?;)。
  • 按段落朗读 —— 先按空行 / 列表项 / 标题 / 引用把回复分成段落,段落内再按句末标点分句。一段话从头读到尾才进入下一段,绝不从段落中间开始念。
  • 朗读完整、不跳段 —— 宿主按客户端的真实播放进度逐句补发(客户端每开始播一句回一条 ack),生成再快也不会积压、不会丢掉「接下来要播的句子」。

8.7 快捷键不生效

  1. 确认设置里「语音模式快捷键」的格式正确(如 Alt+V)。
  2. 别与浏览器/系统的快捷键冲突(Ctrl+Space、Ctrl+Shift+V 等)。
  3. 确认主键不是单个字母。

9. 技术参考(维护者/进阶)

9.1 宿主接口

全部路由带 /dsh-voice-typing 前缀。

路由 说明
POST /transcribe 请求体为 16 kHz 单声道 f32 PCM,返回 {text, model, language, hotwords, speech}(最终稿;speech 表示是否检测到人声)
POST /asr-stream?sessionId=… 送入增量 PCM,返回当前实时文字(流式 zipformer2;模型不可用时返回 {unavailable:true, reason})
POST /asr-stream/reset?sessionId=… 结束该会话的流式解码并丢弃状态
GET /asr-ready 识别模型是否已下载 + 已加载(客户端开麦前先问,首次加载要 20~40 秒)
POST /asr-warmup 确保识别模型真的加载好(没就绪就等到就绪)
GET /tts?text=… 文字 → 音频(在线出 MP3,本地系统语音与本地神经模型出 WAV)。在线失败自动回退到本地系统语音,响应头 x-tts-engine-fallback: local 标出
GET /voices?engine=online|local 音色列表(在线 Edge 列表 / 本地系统语音)
GET /tts/env 合成运行环境自检(引擎可用性、平台能力)
GET /tts/warmup 预热在线 TTS 连接(本地引擎没有连接概念,直接返回)
GET /latest-message?sessionId=… 本轮 assistant 回复全文
GET /busy 助手是否正在生成。降级用 —— 首选是 SSE 的 assistant-busy 事件(零延迟),SSE 断了才回退到轮询
GET /diagnostics 诊断数据(统计、忙状态、各引擎合成失败计数、下载进度、朗读调度器状态 readAloud[]:mode/outstanding/acks/emitted/stalls/lagMs/playingMs/backlogMs/budgetMs/streaming/skim/trimmed/trimmedParagraphs/trimmedSentences/truncatedParagraphs/awaitingFlush,以及 readAloudScheduler 的 rejectedAcks/lastRejectedAck)。不在 /config 里 —— 混进去会被客户端当配置回传
POST /voice-session 客户端心跳上报(诊断用)
GET/POST /config 读写生效配置
GET /models 模型档位清单与本地状态(识别 + 合成)
POST /model 下载/准备指定档位
GET /stream SSE:turn-begin / turn-end / call-end / sentence / assistant-busy / model-progress / model-ready / model-error
POST /reading-ack 客户端朗读进度回报(流控)。请求体 {sessionId, turnId, count, durationMs?}(durationMs = 这一句真实音频时长,仅 count=1 时用,宿主据此精确计「正在播放的剩余时长」):每开始播放一句回一条,宿主随即补发下一句。收不到 ack 时宿主自动降级为旧的 400ms 时间窗口节奏(老客户端可用)

9.2 SSE 事件信封

所有 SSE 事件的 data 都是同一个信封,业务字段平铺在里面:

event: sentence
data: {"v":3,"seq":12,"ts":1735000000000,"sessionId":"s1","text":"你好","turnId":"turn-x","index":0,"para":0,"paraStart":true}
字段 含义
v 协议版本(当前 3;v3 之后只做只增不改的字段扩展,同包同版本一起加载)
seq 全局单调递增。跳号 = 丢了事件
ts 事件产生时刻(epoch 毫秒)
sessionId 归属会话;null 表示全局事件(如模型下载进度)

call-end 只表示「这一次 llm/stream 调用结束」,不表示该次调用的句子已经全部发出: flow(流控)模式下剩余句子仍会按客户端 ack 逐句补发,所以 call-end 之后仍可能出现 sentence。 真正的「一轮结束」是 turn-end(下一轮开始时广播上一轮)。

sentence 事件另有业务字段:text(要念的句子)、turnId(属于哪一轮)、 index(本轮第几句,客户端据此去重)、para(自然段序号,一轮内从 0 递增)、 paraStart(是否该段第一个发出的片段 —— 客户端永不丢弃它)。

客户端据此可以:按 sessionId 过滤(多会话不串音)、按 seq 发现丢事件、 按 turnId 精确到某一轮做静音。

9.3 识别链路

浏览器采集 → AudioContext 原始 float32 → 线性重采样到 16 kHz → 上传宿主 → 工作线程里用 sherpa-onnx 识别(SenseVoice / Whisper / Zipformer2,按设置选择)→ Silero VAD 切段(灵敏度 0.5 / 最短语音 0.25 s / 静音断句 0.5 s / 单段最长 20 s, 音点前置补偿 0.3 s)→ 自定义词表与语气词后处理 → 写入草稿并发送。

识别跑在独立工作线程(lib/asr-worker.js)里,主线程不会被 0.5~3 秒的同步推理卡住。 本地神经合成同理,放在 lib/tts-worker.js 里跑。

⚠️ Zipformer2 是流式模型,必须用 OnlineRecognizer + isReady/inputFinished 的解码方式。 用 OfflineRecognizer 配 transducer 会让 sherpa 的原生代码 直接终止进程(整个桌面应用退出)。

9.4 诊断接口

接口 用途
GET /dsh-voice-typing/config 当前配置(诊断/统计走 /diagnostics)
GET /dsh-voice-typing/voices?engine=online|local 音色列表
GET /dsh-voice-typing/tts?text=… 合成测试
GET /dsh-voice-typing/tts/warmup 预热连接
GET /dsh-voice-typing/busy 助手是否在生成
GET /dsh-voice-typing/stream SSE:sentence / turn-begin / turn-end / call-end / assistant-busy / model-* 事件

浏览器控制台里 __DSH_VOICE_DEBUG__() 能看到 ttsFailures、playSkipped 等运行时状态。

9.5 配置文件位置

底层配置落盘在 ~/.dsh/voice-typing.json,设置面板是它的编辑器(单一数据源)。

9.6 代码结构

文件 职责
lib/index.js 宿主侧:HTTP 路由、配置读写、模型管理、朗读调度
lib/client.js 浏览器侧:麦克风按钮、VAD 断句、草稿管理、播放队列、设置面板
lib/asr-worker.js 识别工作线程(SenseVoice / Whisper / Zipformer2)
lib/tts-worker.js 本地神经合成工作线程(sherpa-onnx VITS)
lib/edge-tts.js Edge 在线朗读(共享连接 + 串行锁)
lib/local-tts.js 跨平台系统语音(Windows SAPI / macOS say / Linux espeak-ng)
lib/neural-tts.js 本地神经 TTS 的宿主侧封装(线程管理、预加载、释放)
lib/sovits-tts.js 外部 GPT-SoVITS 引擎客户端:把每句文本 POST 到用户自己的 api_v2 服务;不内置模型、不做克隆、失败不回退
lib/model-catalog.js 模型档位定义、并行分片下载、缓存、SHA256 校验
lib/text-rules.js 词表解析、替换、语气词清理、输出规范化
lib/read-aloud-policy.js 朗读发射规则(纯函数):时间窗口、单句上限、splitLongText(切成完整句子,绝不断成半句)、packWindowSentences(一批取几句,不合并成长文本)
lib/paragraph.js 自然段落切分(纯函数):空行 / 列表项 / 标题 / 引用为段落边界,splitParagraphs 支持流式累积(末尾未完成段进 rest)
lib/speech-clean.js 文本清洗与分句(纯函数):stripForSpeech(Markdown → 纯文本 + SSML 转义)、splitSentenceParts
lib/read-aloud-scheduler.js 朗读调度器:按自然段落收集、按客户端 ack 的播放进度发句(流控),无 ack 时降级为时间窗口;事件带 para / paraStart
lib/fence-filter.js 流式代码围栏过滤器(纯函数):跨 chunk 识别 ``` / ~~~ 围栏(围栏标记本身可能被 chunk 边界切开),围栏内内容一个字都不交给朗读
lib/tts-env.js 运行环境自检
src/client/pure/*.js 浏览器侧的纯函数源码(门限状态机、草稿合并、事件信封、定时器登记表)。不依赖浏览器 API,测试可直接原生 import
scripts/bundle-client.mjs 把 src/client/pure/ 内联进 lib/client.js(浏览器模块的 require 只能解析包名,不能 import 本地相对路径,所以纯函数只能有一份源码)

9.7 维护者须知

这一节记录这个插件反复出问题的地方,以及对应的硬规则。 每一条都对应一个真实修过的 bug,改动这块代码前请先读一遍。

作用域:三个地方拿不到东西

宿主侧有三层作用域,引用错了会抛 ReferenceError, 而表现常常是「某个功能安静地不工作」,不报错。

位置 能拿到 拿不到
模块级函数 只有模块级变量 settings、scope、t
ctx.on('llm/stream', ...)(注册在 inject 之外) 模块级变量 scope、settings
ctx.inject([...], cb) 回调内 全部 —

规则

  • llm/stream 钩子里要用配置,走模块级的 currentSettings,不要写 settings。
  • 翻译函数 t 不能在模块级函数里用;模块级 label: () => ... 只能写死文案(写 () => t('x') 会让设置面板白屏)。

组件必须在模块级定义

Label / Section / Toggle 这类小组件如果定义在组件体内, 每次渲染它们都是新的函数 → React 认为组件类型变了 → 卸载并重建 → 设置面板里的输入框每敲一个字符就失焦。

规则

  • 任何被 React.createElement 当作类型用的组件,都定义在模块级。
  • 组件体内只放「每次渲染都该新建」的东西(style 对象、回调)。

引用稳定性:effect 的依赖必须稳定

useCallback 写成裸箭头函数时,每次渲染都是新引用, 依赖它的 effect 会反复重跑:连接刚建立就被关掉、无限重渲染、 点一下按钮就滚到顶部。

规则

  • 所有传给 useEffect 依赖数组的值都要 useCallback 包住,且依赖完整 (漏依赖 = 读到过期值)。
  • 翻译函数 t 必须 useCallback 包装。

静默失败:空 catch 会掩盖故障

catch {} 到处都是。改动时至少要区分「可忽略」和「要上报」。

规则

  • 面向用户的失败必须有可见反馈(提示条 / 状态文字),不能只写日志。
  • catch {} 里至少留一行注释说明为什么可以忽略。

状态复位:关掉什么就要清什么

规则

  • 任何「停止」类动作,都要把该动作涉及的所有状态一次清干净: 定时器、队列、标志位、草稿基准。
  • 定时器必须跟着状态一起清。漏清的后果是「关了麦草稿还自己发出去」。
  • 清状态的职责要单一:stopListening(聆听侧)、stopPlayback(朗读侧)、 stopAll(两者,仅卸载时用)。一个动作不要隐式包含多个不相关职责。

共享状态:过期快照不能写回

异步请求拿到的数据是发起请求那一刻的快照。

规则

  • 拿到配置后不要无条件写回共享存储(configStore.set(next))。 用户在这期间改过设置的话,旧快照会把他的修改覆盖掉。
  • 共享存储的权威来源只有一个(设置面板:加载时和保存时)。
  • 读配置时要合并,不要整份取舍: configRef = { ...宿主返回的, ...共享存储 } —— 存储里有值的键以存储为准(它是用户刚保存的),其余用宿主这份补齐。
  • 拉配置要能重试:只拉一次的话,宿主刚启动时那一次失败就再也补不回来。

回合边界:不要用启发式

「这是不是新的一轮对话」曾经用「会话里的用户消息条数变化」判断 —— 但本轮用户消息写入 session 的时机和 LLM 开始输出不一致。

规则

  • 判定新回合必须容忍「消息延迟写入」:同一回合内多次 llm/stream (工具调用链)不能重复触发打断。
  • 用 llm/stream 请求里的 options.messages 数用户消息条数, 不要读 session 里的。只认「条数增加」为新回合。
  • 这套回合判定抽在独立模块 lib/turn-model.js(createTurnModel 工厂)。
  • 每一轮对话有唯一的 id(turnId,递增序号),句子里带着它。 ⚠️ 轮次 id 不能用「本轮最后一条 user 消息的内容锚点」生成。
  • 回合事件只有 turn-begin / turn-end 两个,不要再加别名事件。
  • turn-end(协议 v3)表示真正的一轮结束;call-end 表示一次 llm/stream 调用结束。
  • 回合状态表不会无限增长:用空闲超时兜底 —— turnModel.prune() 每 60 秒 清掉超过 10 分钟没活动的会话的回合状态。

外部 GPT-SoVITS:只做转发,不内置、不克隆、不回退

GPT-SoVITS 是 PyTorch 推理体系(官方只提供 api_v2.py HTTP 服务),没有能在 Electron 里直接跑的 ONNX 管线。 所以插件把它当成外部 TTS 引擎:

  • 插件不管理模型/参考音频、不做声音克隆,只按官方 /tts 契约转发 text/text_lang/prompt_lang (以及用户配置的 ref_audio_path/prompt_text),拿回 wav;
  • 失败绝不回退 Edge/系统语音:用户选它就是要自己的声音与隐私,不能偷偷把文本发给微软、也不能换音色;
  • 宿主超时(SOVITS_TIMEOUT_MS=20s)必须小于客户端单句超时(25s),否则客户端先放弃、错误只剩「没声音」;
  • 失败计入 /diagnostics.sovitsTts(failures/lastError/lastAt)。

回归:test/sovits-tts.test.mjs(契约/URL 规范化/失败/超时/不回退 + 宿主接线 + 构建产物 UI)。

朗读失败不静默(可观测性)

「朗读没声」最怕无从排查。现在三种情况都留痕:

  • 宿主:/diagnostics 里 localTts / onlineTts / neuralTts 各带 failures / lastError / lastAt。
  • 客户端:synth 失败时 ttsFailuresRef 计数 + 节流(>4 秒)弹一次短词 「朗读失败」,完整说明走悬停。
  • 调试:浏览器控制台 __DSH_VOICE_DEBUG__() 能看到 ttsFailures。

朗读无声:共享连接坏掉后必须丢弃(最坑的一条)

在线(Edge)合成复用同一个 WebSocket 实例,换连接单句要贵 4~5 秒。 复用本身没错,错在什么时候该丢掉它。真实故障是「合成结束但没有收到音频数据」 —— 这也是连接坏了。这条原来不丢连接,坏连接被一直复用,之后每一句都返回空音频。

规则

  • 判断「该不该丢连接」必须走同一个判据 isBrokenConnection(),不要四处各写正则。
  • 它的正则必须包含「没有收到音频数据 / 超时」。只判 WebSocket 字面错误必然漏。
  • chunks.length === 0 那条分支要自己调 disposeTts() 再抛错,别指望外层 catch。
  • 回归测试 test/edge-tts.test.mjs(合并了原 tts-pending/reconnect/lock-hardtimeout):用假合成器模拟「坏连接返回空音频」, 断言第二次调用会重建实例并成功。
  • 改完要重启 DSH(lib/edge-tts.js 是宿主侧模块)。

朗读无声:播放上下文必须等恢复

浏览器自动播放策略会把新创建的 AudioContext 留在 suspended。 若在上下文恢复完之前就 source.start(0),音频会一直排队不响。

修复:play() 在 start 前 await resume(),并在首次用户手势(点击/按键)时 预热并恢复播放上下文。

等多久、超时怎么办:

  • 不能只等 1 秒就跳过。resume() 是异步的,实测要 1~3 秒。
  • 现在的做法:先 await playCtx.resume(),再轮询到 state === 'running', 最多等 8 秒。
  • 8 秒还挂起才算彻底失败,这时跳过这一句并 playSkippedRef += 1, 调试快照 __DSH_VOICE_DEBUG__().playSkipped 能查到。
  • 回归测试:test/client-behavior.test.mjs 用真实构建产物 lib/client.js(vm + 假 DOM/AudioContext) 驱动真实播放循环,覆盖 resume/gen 复查、ack 恰好一次、停止/换轮后不出声等行为; 仓库内原 read-aloud-resume.test.mjs 测的是复制出来的逻辑(已与真实实现漂移),已删除。

朗读队列:代次机制

speakGenRef 递增即作废所有旧音频。

规则

  • 停止朗读要三件事一起做:清队列 + 代次自增 + 停掉当前播放。 少一件就会出现「停了还在播」。
  • 停当前播放不能只靠「当前那一个取消句柄」:要登记所有还活着的音频源,停止时逐个 stop()。
  • SSE 是常驻的,所以「本轮不再朗读」必须有单独的标记(mutedTurnIdRef)。
  • 队列为空时不要把状态设成「朗读中」。

朗读切分:按段落收集,段内再按完整句子,代码块绝不能念

层级:

  1. 先按段落收集(splitParagraphs,lib/paragraph.js):空行 / 列表项 / 标题 / 引用为段落边界。段落是朗读的基本单元,段内句子连贯、不混入其它段落。
  2. 段落内按句末标点切句(splitLongText):保证完整句子,绝不在句子中间 / 词汇中间断开。

两条硬规则:

  1. 不要在句子中间切断。切分优先级(splitLongText):
    • 句末标点 。!?; → 完整句子;
    • 句内标点 ,、: → 半句,仅在远超上限(>2 倍)时才用;
    • 硬切(无任何标点)→ 停顿怪,但总比无限长好。
    • 略超上限但不到 2 倍 → 整句不切。
  2. 代码块一个字都不能念。围栏识别必须在流层面做,而且必须跨 chunk(与旧行为一致:任意位置;`~~~` 围栏按 Markdown 规范要求**行首**,避免正文里 3 个连续 `~` 把后文静默吞掉)(`lib/fence-filter.js`):模型按 token 流式输出,围栏标记 `` 可能正好被 chunk 边界切成两半("" + "js")。旧实现逐块 split('```')` 会在这种切法下状态错位:代码被念出来、围栏后的正文反而被当代码丢掉。

回归:test/read-aloud-fence.test.mjs、test/read-aloud-list.test.mjs。

当前策略速查(2026-11-01 定稿)

项 值/行为
发句节奏 客户端每开始播放一句回 /reading-ack,宿主补发;未确认上限 READ_ALOUD_LEAD=3
追赶预算 readAloudLag,默认 7.5 秒(0 = 关闭追赶,全部朗读)
追上去哪 跳过未开始的整段旧段落,保留最新 3 段(从最新第 3 段开始读)
生成很快时 skim:正在读的段只读首句就跳走(判据 backlogMs 超预算且仍在生成)
计时口径 lagMs = 正在播放的剩余时长 + 已发未确认 + 队列未发 + 当前段尾巴(含流水线提前量)
收尾 pending(无 ack)+ 超预算:等 1.2s grace 再排空;flow:按 ack 逐句补发
兜底 watchdog 10s 放行一条;SSE 重连清零旧未确认;客户端安全网按整段软上限裁剪
诊断 /diagnostics 的 readAloud[]:lagMs/playingMs/backlogMs/budgetMs/skim/trimmed*/keepParagraphs/awaitingFlush

朗读发送节奏:客户端播放进度流控(不是固定时间窗口)

助手一次可能吐出一大段文本,而浏览器真正「念出来」受音频播放时长限制 (23 秒/句)。如果宿主只管按固定节奏猛发(历史上是 400ms/句 = 2.5 句/秒), 生产速度是消费速度的 58 倍 —— 客户端必然积压,最后只能丢内容, 表现为「朗读不按自然段落、总挑到段落中间」。所以节奏必须由播放进度驱动。

规则(唯一实现在 lib/read-aloud-scheduler.js):

  • 客户端播放进度驱动:宿主维护「已发未确认」句数,上限 READ_ALOUD_LEAD(3)。 客户端每开始播放一句就 POST /reading-ack 回一条,宿主才补发下一句。 于是朗读节奏 = 真实播放节奏:不积压、不跳段,和屏幕内容的差距恒定在几句以内。
  • 首句不排队:第一句无条件立刻发(实测 0ms),用户尽早听到声音。
  • 段落是朗读单元:一段的句子全部发出后才轮到下一段;短段落(≤60 字)整段作为一个朗读片段 (仍然是完整句子),超过单句上限才按句末标点切句。正常情况下绝不从段落中间开始念 (事件带 para / paraStart,客户端据此按整段裁剪);例外是下面的 「生成很快时只读首句」——此时正在读的段允许在首句后截断(用户明确要求)。
  • 老客户端兼容:收不到 ack 时自动降级为旧的 400ms 时间窗口节奏; pending(旧)模式下调用结束时同步发完剩余句子(不丢尾巴)。
  • 追赶最新(可调,默认 7.5 秒):估算「待朗读音频时长」= 正在播放的剩余时长 + 已发未确认 + 队列里没发的 + 当前段落剩余文本(按约 210ms/字估算,客户端会回传真实音频时长所以一般更准);超过设置项 readAloudLag 就跳到最新几段:保留「已开读的段」+「最新 3 段」,中间未开始发的完整段落整段跳过(当前段读完后,下一段是最新第 3 段,再顺序读到最新段 —— 既追上最新内容,又保留一点上下文)。
  • 生成很快时每段只读首句就跳:仍在生成中、且「还没发出去的内容」(backlogMs,不含正在播放/已发未播的流水线提前量)超过预算时,正在读的段落不再强制读完 —— 发出首句后就把该段剩余句子放弃,直接跳到下一段/最新段(任务要求:跳走时只需要读首句)。判断用 backlogMs 而不是 lagMs:生成慢时流水线提前量本身就可能超过预算,用 lagMs 会把正常朗读也截断。生成结束后(或 backlogMs 回到预算内)恢复正常完整朗读,剩余段落不会被读残。 跳过时只整段丢「还没开始发」的段;例外:生成很快、仍在流式输出时,正在读的段可以 只读首句就跳走(下方的 skim),不再强制整段读完;生成结束后的剩余段落完整读完。 0 = 关闭追赶(全部朗读,长回复会越读越滞后)。
  • watchdog 只在真发句 / 收到 ack 时重新计时:持续收到文本不算进度 —— 否则客户端不 ack 时 watchdog 永远不会触发(评审实测 2.2s 放行 0 次)。
  • 收尾 grace:pending(老客户端 / 首条 ack 未到)且积压超预算时,一次调用结束**先等 FLUSH_GRACE_MS(1.2s)**再同步排空;期间 ack 到了就切 flow 走追赶(否则"结束即排空"会让追赶在这条调用上完全失效)。老客户端没有 ack,grace 到点后照旧发完,不丢尾巴。
  • SSE 重连释放配额:页面刷新 / 断线重连时,旧连接遗留的「已发未确认」永远不会再被 ack,新连接建立即清零(否则新页面首句要等 watchdog 10 秒)。
  • watchdog:flow 模式下某句发出去 ACK_STALL_MS(10 秒)还没 ack (客户端掉线 / 播放被跳过;长句播放本身也可能十几秒,所以阈值不能太短), 放行一条,保证朗读不会永久卡死。

回归:test/read-aloud-scheduler.test.mjs(调度器纯逻辑:流控/追赶/保留 3 段/skim/watchdog/重连)、 test/read-aloud-catchup.test.mjs(真实钩子 + 真实 ack 的追赶端到端)、 test/reading-ack-contract.test.mjs(ack 契约/边界)、 test/read-aloud-catchup.test.mjs 的 e2(pending 收尾时限)、端到端 test/read-aloud-chain.test.mjs。

客户端安全网:按整段裁剪(不承担「追最新」职责)

「追赶最新」由宿主侧负责(readAloudLag,见上一节):积压超预算就整段跳旧段、 保留最新 3 段,生成很快时每段只读首句。 下面客户端的安全网只兜底「老宿主 / ack 丢失 / 重连」这类异常,正常流控下不会触发。

生成通常比朗读快。客户端待播队列积压太多时,听到的内容会离屏幕越来越远; 但绝不能丢「接下来马上要播的那几句」 —— 那会让播放位置向前跳、落点在段落 中间(用户听到的就是「不是按自然段落朗读,总挑到非首句」)。

规则

  • 只读当前这一轮的句子:句子的 turnId 与当前轮次不一致就丢掉。
  • 安全网裁剪(MAX_PENDING)按整段进行,且是软上限:超限时从最旧开始整段丢弃 (同一 para 的整组一起丢),并保护 ①正在播放的段落 ②本轮第一段 ③最新一段。 可丢的组都在受保护组之后时宁可不丢(允许暂时超过上限),绝不为了压上限丢掉最新内容。 于是播放永远是「完整段落」的向前推进,不会从段落中间开始、也不会丢「正在读那段的剩余句子」。
  • 不重复播放:客户端按 (turnId, index) 去重;宿主段落补全时按已发字符数 减前缀,绝不把已发句子再发一遍(旧实现用字符串 startsWith 且无兜底, 一旦不匹配就把整段重发 —— 那是「读完后面又回去读前面」的来源)。
  • 正常情况下流控已把队列压到几句,这条安全网只在异常(客户端卡顿 / ack 丢失)时生效。

朗读高亮覆盖层:绝不能画在弹框/输入框上面

朗读时会在页面上叠一层半透明色块,圈出「正在念的那一句」。它是 position: fixed 的覆盖层,z-index 不能取整型最大值:DSH 的弹框/遮罩是 z-index:1000 级别的浮层, 覆盖层取 2147483647 就会在设置弹框打开时画到弹框上面(用户反馈)。

规则

  • 覆盖层 z-index: 900:高于消息正文(正文没有 z-index),低于弹框/遮罩(1000+);
  • 第二道保险:检测到 [aria-modal="true"] / [role="dialog"] / [role="alertdialog"] 时直接隐藏覆盖层(data-voice-reading-hidden),并用 MutationObserver 监听 弹框挂载/卸载 —— 弹框关闭后自动恢复并重画;
  • 旧样式迁移:已注入的旧 <style>(z-index 2147483647)会在下次 set() 时被覆写, 页面不刷新也能生效。
  • 裁剪到会话滚动视口:消息滚出视口后,Range 的矩形仍然会报「原位置」 (父级 overflow 只是视觉裁掉),覆盖层会把高亮画到输入框/顶栏上面 (用户反馈:滚动到输入框下方时高亮显示在输入框上)。所以每个色块都与 「滚动视口 ∩ 窗口 ∩(底部输入区以上)」求交集,完全在视口外的不画; 底部输入区只认「下半屏、够宽」的 [data-slot*="composer"] / textarea / contenteditable, 避免把顶栏搜索框当底栏;

回归:test/highlight-overlay.test.mjs(对构建产物 lib/client.js 断言 z-index/隐藏/ 旧样式迁移/启动清理/视口裁剪)。

朗读管道不能永久卡死

pumpQueue 的消费循环 await 每句的合成 promise。若某句挂住,await 永不 resolve。

规则

  • 每句合成都要有超时兜底:await Promise.race([promise, sleep(...)])。
  • 播放上下文要在 try 内取。
  • object URL 必须在 finally 里释放。
  • 宿主 llm/stream 里 next() 同步抛错时必须复位 busy。

状态行只放短词

规则

  • 状态行只显示短词(「加载模型…」「朗读中」「已停止」)。
  • 完整说明放到 …Detail 文案里,鼠标悬停显示。
  • 状态行给一个 minWidth:换状态时宽度不变,界面不跳。

草稿:我们记的和用户看到的不一定一致

规则

  • 写新内容之前先调 syncUserEdit() 检测用户是否改过。
  • 「我们有没有写过」不能只看内容是否为空(首次写入会被误判)。

流式识别:门限不能一刀切

规则

  • 底噪只在静音时更新。
  • 进入说话态后,单帧偏低不能中断;连续若干帧都低才退出。

定时器:必须登记到登记表

规则

  • 源码里不允许出现裸的 window.setTimeout / window.setInterval。
  • 定时器按 group 登记(mic / send / ui / app / request),状态复位时按 group 一次清干净。
  • 短时等待统一用 sleep(ms)。

同一判据只能有一处实现

规则

  • 能共享就共享(宿主侧无限制,可以自由拆文件)。
  • 因浏览器模块不能 import 本地文件而必须写两份时, 两份要由同一份用例表(test/fixtures/)驱动,断言两端行为一致。
  • 配置清单的唯一真源在宿主:GET /config 返回生效配置。

下载:数据必须是真的

规则

  • 档位里声明的文件大小必须是实际大小,不能用估计值。
  • 文件名要对着仓库实际的名字写。
  • 进度用绝对字节语义,不要用增量语义。

原生库:类别用错会杀进程

规则

  • 流式 transducer 必须用 OnlineRecognizer + isReady/decode/inputFinished。
  • 离线模型才用 OfflineRecognizer。

改动之后

  • npm test 必须全绿(42 个文件;先跑 bundle-sync 校验产物同步,再跑 client-behavior 行为回归)。
  • 需要连活插件的集成测试单独跑:npm run test:silence -- http://127.0.0.1:<端口>。
  • 需要跑「真加载模型」的慢集成(SenseVoice 预加载等):$env:DSH_VT_SLOW_TESTS=1; npm test。 默认套件走快速路径并显式打印 ⏭ 跳过,不会悄悄漏测。
  • 客户端行为回归在 test/client-behavior.test.mjs:直接加载 lib/client.js 产物, 用假 React/DOM/AudioContext 驱动真实播放循环,断言 ack 恰好一次 / 顺序 / 段落完整 / 停止与换轮后不出声。改完 src/client/ 并重建产物后一定要跑它(形状断言测不出时序缺陷)。
  • 改完 lib/index.js(宿主侧)要重启 DSH; 改完 lib/client.js(浏览器侧)要浏览器强制刷新。
  • 改完 src/client/pure/ 要跑 npm run build:client 重新生成 lib/client.js。 npm test 里的 bundle-sync.test.mjs 会检查产物是否同步。

配置项的配合关系(生效条件矩阵)

配置统一走 GET/POST /config:GET 返回已归一化的完整配置;POST 是 patch(只接受已声明键)。 加载时非法/越界值回退默认并记入 /diagnostics.configWarnings;保存时非法值返回 400 {ok:false, errors},不会静默改值。

配置 生效条件 / 配合
readAloud 朗读总开关。关闭时 /tts 不会被调用;但朗读相关设置仍可编辑/保存
ttsEngine online→voice/pitch/volume/ttsFormat;neural→neuralModel/neuralSpeaker;local→localVoice;sovits→sovits*(rate 转 speed_factor)。非法值保存被拒,运行时不静默落 online
rate 四种引擎都生效(online/local 百分比、neural 倍率、sovits speed_factor 0.5~2)
pitch / volume / ttsFormat 仅特定引擎有效:pitch/format 仅 online,volume 仅 online/local
readAloudLag 只在 flow(客户端 ack)模式生效;0=关闭追赶;非有限值回退默认(7.5)
language 只对 SenseVoice 生效;Whisper/zipformer2 忽略(UI 按模型禁用/提示)
liveAsr stream 固定依赖 zipformer2-zh(与 model 无关);不可用时提示并回退整段
model 必须是已声明档位;非法值保存被拒,运行时不再静默回退 SenseVoice
autoSend 关闭时 autoSendExtraMs 隐藏;mode=once 的「说完退出」只在自动发送后发生,因此会禁用并说明
preloadModels × idleReleaseMinutes 预加载只覆盖 model/neural(不覆盖 liveAsr 的 zipformer2);idle 释放不释放当前选中模型;0=永不释放
sovits* 只对 ttsEngine=sovits 生效;sovitsRefAudio 是服务端机器可访问路径(官方 api_v2 每请求必填),留空则不带该字段由服务端决定
words 热词仅 zipformer2(transducer)生效;按行保存,保存时 trim/去空

CI 与自动发布(GitHub Actions)

仓库自带两个 workflow(.github/workflows/),都走 pnpm:

  • ci.yml:每次 push / PR 到 main 自动跑 check:client + npm test。 改完代码提交后,GitHub 上的红绿状态就是最直接的回归信号。
  • npm-publish.yml:GitHub 上发布一个 Release(release 事件 published)时, 先跑一遍同样的测试,全绿后自动 npm publish --access public。

发布到 npm 前要做的两件事:

  1. 在 GitHub 仓库设置里加一个 NPM_TOKEN secret(值来自 npm 官网的 Access Token, 建议用 publish 权限的 token,不要用自动生成的 npm_token)。
  2. 在 GitHub 上创建一个 Release(vX.Y.Z),tag 就打在要发布的提交上。

发布成功与否看 workflow 的 publish job;测试失败时不会发出版本,修复后重新打 Release 即可。

版本号以 package.json 的 version 为准,tag 只用来触发工作流。


10. 许可

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit 0e82bed3f648

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout