Back to home@dusbin

voice-plugin

Dsh(deepseek harness)语音输入插件 Ps: 朗读功能目前还不是很棒。

Stars
0
Language
JavaScript
Created
Aug 27, 2026
Updated
Aug 27, 2026

Introduction

dsh-voice — DeepSeek Harness 语音输入输出插件

给 DeepSeek Harness Web 界面(dsh web)添加语音能力,纯浏览器实现,零后端、零 API key。

当前版本:v0.1.12(变更记录见 CHANGELOG.md

功能一览

  1. 语音输入(模式切换) — 输入框工具行右侧(发送按钮前)有麦克风按钮 🎤:
    • 点击进入「语音模式」(图标变深色并出现绿色边框闪动)→ 长按空格开始听写(Web Speech Recognition)、松开结束,识别文本自动填入输入框(可配置 autoSend 直接发送);
    • 再点按钮退回「文本输入」模式(图标恢复浅色弱化态),键盘完全不受干预。
  2. 语音模式下回车直接发送 — 回车不屏蔽:直接发送输入框内容;录音中按回车 = 结束录音并立即发送已识别内容;焦点在输入框时仍走原生发送,不重复提交。
  3. 文章朗读 — 每条助手消息的操作条上多一个「🔊 朗读」按钮,用浏览器 speechSynthesis 朗读该消息正文(跳过思考过程与工具输出),音色自动优选,符号自动清洗。

本目录是插件的唯一可信源(source of truth),已用 git 管理。 web profile 通过 cordis.patch.yml 直接加载这里的包,改动被 commit 后, 下一次启动(或 patch 热更新)即生效。

文件结构

voice-plugin/                 # ← git 仓库根
├── package.json              # dsh.client 声明(platform: web → 浏览器端 bundle)
├── lib/
│   ├── index.js              # Node 半身:空插件(让 loader 能创建 fiber)
│   └── client.js             # 浏览器端 bundle(全部功能所在,手写、零构建)
├── test/
│   └── smoke.test.mjs        # 冒烟测试:模拟模块表 + 模式切换/听写/回车/朗读全流程
├── README.md
└── CHANGELOG.md

安装(一次性接线)

本机已完成后两步,无需重复执行;重装或迁移时按此操作。

  1. 把插件软链进 web profile 的 node_modules(使 dsh-voice 可被 Node 与 client-modules 从 profile 目录解析):

    mkdir -p ~/.dsh/profiles/web/node_modules
    ln -sfn /Users/robinddu/Desktop/workspace/robinddu/voice-plugin \
      ~/.dsh/profiles/web/node_modules/dsh-voice
    
  2. ~/.dsh/profiles/web/cordis.patch.yml 追加:

    # dsh-voice: 语音输入输出插件(浏览器端)
    - insert:
        - id: voice
          name: 'dsh-voice'
          config:
            holdMs: 250      # 语音模式下长按空格判定阈值(毫秒)
            sttLang: 'zh-CN' # 语音识别语言
            ttsLang: 'zh-CN' # 朗读语言
            ttsRate: 1       # 朗读语速
            autoSend: false  # true = 识别结束直接发送;false = 只填入输入框
    
  3. 重启 dsh web(或依赖 patch 热更新自动加载),浏览器硬刷新(Cmd+Shift+R)。 首次使用语音输入时浏览器会请求麦克风权限,请允许。

为什么是 dsh-voice(包名)而不是绝对路径?client-modules 会用条目名 解析 <name>/package.json 并据此扫描 dsh.client 声明、服务 /plugins/<id>/client.js; 而 loader 的 Node 侧 import 也从 profile 目录解析包名。软链把这两条解析 路径都指向本仓库,一次接线、两处生效。

与 scratch-plugin 不同,这里不需要 node_modules 软链到 harness: 浏览器端 bundle 只 require('react')(平台种子模块),Node 半身零依赖。

使用

语音输入(模式切换)

  1. 点击输入框工具行右侧的 🎤 按钮进入「语音模式」:图标变为深色并出现 绿色边框闪动(文本输入时为浅色弱化态);识别器立即常驻启动并预热, 因此按下空格的同时开口说话也不会漏字(浏览器识别预热在进入语音模式时已完成)。
  2. 按住空格:按住超过 holdMs(默认 250ms)确认长按,提示「请开始说话:松开空格结束」。
  3. 说话,胶囊实时显示本次片段的转写(之前的输入内容不会残留显示); 松开空格结束:只提交按下之后说出的内容(按压前的讲话不会混入), 填入当前会话输入框(autoSend: true 则直接发送)。 多次听写会追加到已有内容(空格分隔),不会清掉前一次输入或手动输入的文字。
  4. 再点 🎤 退出语音模式(识别器停止),恢复正常文本输入。

细节:

  • 短按空格(未超过 holdMs)会放弃本次片段,不提交、不影响输入框。
  • 语音模式下短按空格仍是普通空格,正常打字不受影响。
  • 未开启语音模式时,插件完全不干预键盘输入。

回车直接发送(语音模式下)

  • 非录音时按 回车 → 直接发送输入框内容(语音刚填入的文字不用再点发送按钮)。
  • 录音中按回车 → 结束录音并立即发送已识别内容。
  • 焦点在输入框内时回车走输入框原生发送,不会重复提交
  • 带修饰键的回车(Cmd/Ctrl/Alt+Enter)不拦截。

文章朗读

  • 在任意助手消息的操作条(复制/分支那一行)点 🔊 朗读;再点一次 ⏹ 停止
  • 切换朗读另一条消息会自动停止上一条;Chrome 15 秒停顿 bug 已用 keep-alive 规避。
  • 朗读只读正文 text 块:跳过思考过程、工具输出;朗读前会清洗 Markdown 与装饰符号。

配置项

配置默认说明
holdMs250语音模式下,按住空格多久判定为语音触发(短按仍是普通空格)
sttLangzh-CN语音识别语言(BCP-47,如 en-US
ttsLangzh-CN朗读语言(BCP-47)
ttsRate1朗读语速(0.1–10)
ttsVoice''手动指定朗读音色名(留空 = 自动挑选自然音色)
autoSendfalse识别结束后是否直接发送

修改 cordis.patch.ymlvoice.config 后保存即热生效(刷新页面生效)。

朗读音色与清洗

  • 自动挑选音色:按「语言匹配 > 名称含自然/神经音色线索(Xiaoxiao/Yunxi/Neural/Online/Natural…)> 云端合成(localService:false)」评分,优先用最自然的音色(如 Edge 的 Microsoft Xiaoxiao Online (Natural)、Chrome 的 Google 普通话),不再用生硬的本地默认音色。
  • 手动指定音色voice.configttsVoice: '音色全名'。查看可用音色: 控制台执行 speechSynthesis.getVoices().map(v => v.name + ' [' + v.lang + ']')
  • 朗读前清洗(只读正文,不念符号):
    • Markdown:**加粗***斜体*# 标题[链接](…)(读链接文字)、行内代码、引用、列表标记、表格线;代码块替换为「代码略」。
    • 装饰符号:对勾/叉号/复选框(✓ ✅ ✗ ❌ ☑)、箭头(→ ← ↑ ↓ ⇒ ➜)、emoji(🎉 ✅ ⚠️)、项目符号(• · ▪)、HTML 实体(&nbsp; &amp; 等)全部清除,只保留正文文字。
  • 语音列表异步加载时(getVoices 未就绪)会自动等待 voiceschanged 后再播,避免回落到默认生硬音色;插件激活时预热语音列表。

工作原理

  • 加载package.json 声明 dsh.client.platform: "web"exports["./client"] → 被 client-modules 扫描进 window.__DSH_BOOT__ 图, 浏览器端以 /plugins/dsh-voice/client.js?rev=<hash> 加载;lib/client.js 是手写的 factory bundle(window.__ModuleLoader__.load({id, factory})), 与 tsdown 产物格式一致,改动后 rev 自动变化(HMR 无需手动 ?v=)。
  • 语音输入conversation.input.right 插槽(输入框工具行右侧)注册麦克风按钮, 点击切换 voiceMode(React useSyncExternalStore 订阅驱动按钮高亮)。 进入语音模式即常驻启动 SpeechRecognition(continuous + interimResults) 并保持预热(意外结束自动重启);语音模式下 window 捕获阶段监听键盘:
    • Space:keydown 记录按压起点(当前转写进度),hold 定时器(holdMs) 超时确认长按并显示指示胶囊;keyup 只提交起点之后的转写(按压前讲话不混入)。
    • Enter:非按住时提交当前会话输入框草稿(焦点在输入框则放行原生发送, 避免重复提交);按住空格中按回车 = 提交本次片段并立即发送。
    • 提交经 ctx.sessions(当前会话)→ ctx.conversation.input.for(scope)setDraft() 写入输入框(追加、不清前文),autoSend 时再 submit()
  • 朗读ctx.slots.inject('conversation.chat.assistant-actions') 注册 dsh-voice-speak 条目(order 20,排在反馈之后);组件经标准套件 useSession 从会话快照按 messageId 取该消息的 text 块拼接成文 (真实节点形状 kind='assistant-step',正文在 data.finalNode.blocks), speechSynthesis.speak() 朗读;含音色评分挑选与 Markdown/符号清洗。
  • 无状态、无后端:全部为浏览器原生 Web Speech API;Firefox 无 webkitSpeechRecognition 时语音输入优雅降级(提示不支持),朗读仍可用。

已知限制

  • 语音识别依赖 Chrome / Edge / Safari 的 webkitSpeechRecognition(需 HTTPS 或 localhost,且需麦克风权限);Firefox 暂不支持该 API。
  • 可选音色取决于浏览器/系统:Windows + Edge 的中文神经音色最自然;macOS 上 若无云端音色,效果受限于系统本地音色(可用 ttsVoice 手动指定,或换浏览器)。
  • 语音输入写入当前会话的输入框;未打开会话时长按空格会提示「请先打开一个会话」。
  • 语音模式下若焦点在输入框且长按空格,判定前的 1 个空格可能被输入(极短窗口, 通常无感);长按触发后会自动屏蔽后续重复空格。
  • 识别文本追加到输入框(与已有内容以空格分隔,不清空前文),可人工编辑后再回车。

调试

  • 激活成功:控制台可见 [dsh-voice] 插件已激活 v0.1.12(…)slots / sessions / conversation / stt / tts 五个能力开关。
  • 每次朗读会打印 [dsh-voice] 朗读音色: <音色名> | 语言: <语言>
  • 若刷新后按钮/功能缺失,把控制台报错发回,或在 cordis.patch.yml 检查 voice 条目与软链(ls -la ~/.dsh/profiles/web/node_modules/dsh-voice)。

测试

node test/smoke.test.mjs   # 40 项断言:模块表加载、插槽注册、模式切换、
                           # 长按空格听写、回车发送、音色挑选、符号清洗