ppy-web/dsh-plugin-xiaomi-mimo-tts
ppy-web/dsh-plugin-xiaomi-mimo-tts为 DSH 提供小米 MiMo、豆包和浏览器本地语音朗读,支持自动播报、自定义音色与可选 UI 音效。
agent 主动打来的语音电话:`offer_call` 向人类振铃(接听/拒接/稍后再说),接听后由本地 CrispASR + Qwen3-TTS 合成并播放(9 个音色,含 2 个中文方言);拒接则把决定返回给 agent。
中文 · English
*"这个项目的开始是朴素的——我想知道如果 Agent 知道自己可以发出声音,他会说什么?"*
—— 人类伙伴,关于这个项目如何开始
给 DeepSeek Harness 的 agent 一个它拥有的声音。 本地优先、可完全离线:合成跑在本机 CrispASR + Qwen3-TTS CustomVoice 引擎上(9 个内置音色,含 2 个中文方言),音频是 ~/.dsh/voice/ 下的普通文件。任何声音都不会自动响起——必须由模型调用工具,或者你接听一次来电。
三条规矩,整个项目围着它们转:
offer_call:一个完成的念头、一个里程碑、一句想大声说出来的话。dsh-voice-call 并安装;装好后在同一页让它生效(本插件不热加载,可能需要重载插件或重启桌面版)。旧版的命令行安装(npm install -g @deepseek-ai/dsh,然后dsh plugin --profile web add dsh-voice-call)随全局 CLI 一起退役了——本机已卸载全局 DSH,插件管理请走桌面版界面。
版本要求:需要 DSH 0.1.7-rc.2 / 0.2.0-rc.1 / 0.2.0-rc.2 / 0.2.1-rc.2(桌面版当前带的就是 0.2.1-rc.2,而 npm 的 latest 还停在 0.2.0-rc.2)。装完到 DSH 的插件设置页,「运行环境」区点一下就把引擎和模型装齐。然后开个会话,对 agent 说:*「你有 offer_call 工具——有什么值得说的就打电话给我。」*
从零到能听见声音的完整步骤(手动下载路径、全字段配置、故障排查、信任边界)在 docs/deployment.md。
crispasr 的路径,除非他明确要走手动装配。D:\crispasr、D:\tts 那一类)。它们的优先级永远高于插件自己创建的目录,清理也碰不到它们。/voice 看状态,再让它 speak 一句"你好"——听到声音才算装好,配置项全绿不算。classic 是下行小调五声拨弦,刻意避开高铁/登机广播那种上行大三和弦。试听按来电时的真实音量循环播你选的那一条,再点停。D:\crispasr 那类路径不会被碰。offer_call(振铃给人类)、speak(后台朗读一句,播放失败会明确呈现,绝不静默吞掉)、transcribe(语音转文字,可经 dsh-crosstalk 跨会话投递)。aiden · dylan(北京话)· eric(四川话)· ono_anna · ryan · serena · sohee · uncle_fu · vivian。on|off 朗读开关、speak 直接说话。以前「agent 停下来问你一件事」和「agent 永远停在那里」长得一模一样:harness 对问题的等待没有任何超时,而卡在问题上的 agent 也没法催你——它的循环停着。于是你走开五分钟,它就在那儿等五分钟。这一版让插件替它看一眼表。
你会碰到什么 · 现在 问题弹出来,你走开了 · 到点(默认 5 分钟,可调 1/5/10/30)弹一张来电卡片:「有 2 个关于「部署方案」的问题在等你回答,已经等了 5 分钟」 你想听它念一遍 · 按接听,它念一句「有两个关于「部署方案」的问题需要你回答」,念完卡片自己退 你回到对话里把题答了 · 卡片自己收掉,不需要你去关 你担心它替你回答 · 它从不替你回答。在卡片上按任何键都只是让卡片安静,问题还在原地等你
默认关着:设置卡里「等问题振铃(实验性新功能)」打开才生效。这一版其余改动——包括两处"插件能把宿主带走"的围栏——见 docs/changelog.md。
dsh 0.2.0-rc.1 把 peerDependencies 从一句警告变成了加载判定:范围里不含正在运行的版本,宿主就整个跳过这个插件——工具、路由、来电卡片一起没了,只留一行 skipping profile bundle "dsh-voice-call": … is incompatible with dsh 。0.3.7 正是这样消失的,而它 284 个测试全绿:没有一条测试读过清单。从 0.3.8 起范围写成 ^0.1.7-rc.2 || ^0.2.0-rc.1(这一条臂覆盖整段 0.2.x),并把宿主自己的判定函数请进测试(test/harness-compat.test.ts 跑真清单)。
这一版把 devDependencies 与 CI 抬到 0.2.1-rc.2(cordis 4.0.5、schemastery 3.18.5),peer 一个字没改——^0.2.0-rc.1 本来就把 0.2.1-rc.2 算进去。声称的基线现在是四条:0.1.7-rc.2 / 0.2.0-rc.1 / 0.2.0-rc.2 / 0.2.1-rc.2,两份依赖树各跑一遍(三份 tsc + 302 个用例)。
0.2.1-rc.2 改了判定本身的形状,所以测试跟着改了两处、新增一条:返回结构里的字段从 peers 改叫 requirements;更要紧的是新增的 engines.dsh——清单里一旦有它,宿主只看这一条、peerDependencies 从此不参与判定。我们故意不写 engines.dsh:那 20 条 peer 逐个在门外拦着,才意味着"代码摸到的每个 harness 包都被声明过"这条守卫还有力。有没有 engines.dsh 的两种走法,现在都由用例钉着。
桌面版(Electron 44.7.0,node 24.21.0,pnpm 11.28.5)现在带的运行时就是 0.2.1-rc.2,而 npm 的 `latest` 还停在 0.2.0-rc.2(next 才指到 0.2.1-rc.2)——桌面版继续比 npm 的稳定标签新一个版本。这一次不只核对版本字符串,而是拿桌面版自带的逐文件 sha256 清单(dsh/desktop-runtime.json,17261 个文件)和一份 npm install @deepseek-ai/dsh@0.2.1-rc.2 的树逐个比:
@deepseek-ai/dsh-* 包加 cordis / schemastery / cosmokit,服务端 JS 全部字节相同。同版本号在两种发行物里是同一份代码,这条第一次被证明,而不是假设。package.json 被删掉 scripts;.d.ts 与 .d.ts.map 被剥光(所以桌面版里没法按类型编译,但插件发的是预编译的 lib/,碰不到);若干包多带一份 lib/types/invariant.js,而整个运行时树里没有任何一处 import 它;dsh-client-ui-chat / dsh-client-ui-conversation 的 lib/client.js 是另一次构建的产物——CSS module 的类名盐不一样,注释里是 D:\develop\dsh-harness-windows-x64,npm 那份是 /home/runner/work/deepseek-harness/…。dsh-voice-call-overlay 那一套),不碰宿主的 module 类名;而注入目标 dsh-client-ui-settings 的 client.js 两边字节相同。desktop profile 由 app 自己初始化(nodeLinker: hoisted + autoInstallPeers: false,模板就是 web 那两条 bundle),profile 树里除了插件本身只有 @deepseek-ai/schemastery@3.18.4 与 cosmokit@1.8.5 两份——我们和另外三个插件被 pnpm 注入到这两份上,而宿主自己跑的是 asar 里的 3.18.5 / 1.8.6。同一个进程里两份 schemastery。插件照常工作,因为我们交给宿主的 schema 走 plain 对象桥接、不做 instanceof;但这也意味着 peer 里的 ^3.18.4 不能抬到 3.18.5——抬了就把桌面版上那份真实存在的副本排除在声明之外了。设置卡、铃声试听、来电卡片这几项要在真界面里点一次才算验收——插件不热加载,换完版本记得重载或重启桌面版。完整表格在 docs/deployment.md。
写在 profile 的 cordis.patch.yml 里,按 `id` 更新,绝不重复 insert(同一 id 插两次会导致启动崩溃)。下面这些也都能在插件设置页里改:
第一次装好就是来电卡片。 随包发布的那份 cordis.patch.yml 会写 callMode: card——这一行比 schema 的 .default() 优先,所以它是新用户实际拿到什么的那一处;test/bundle-config.test.ts 把它和插件自己的默认值逐个字段钉在一起,0.3.9 及之前它钉的是 ask,第一次装好弹的是弹窗。
- id: dsh-voice-call
config:
tts:
backend: crispasr # 本地神经 TTS(推荐);say / piper / edge-tts / fake 可选
voice: dylan # 9 个内置音色之一
crispasr: # 只有走手动装配才需要写;一键装配会自己放好
bin: D:\crispasr\crispasr.exe
model: D:\crispasr\models\qwen3-tts-12hz-0.6b-customvoice-q8_0.gguf
codec: D:\crispasr\models\qwen3-tts-tokenizer-12hz-q8_0.gguf
callMode: card # card 卡片(默认)| ask 弹窗 | direct 直接接听 | off 拒绝
readReplies: false # 朗读回复(会话里 /voice on 可临时开)
durableEvents: false # 保持 false,见下方兼容性
audioDir: ~/.dsh/voice # 音频目录还想调这些 · 取值 · 说明 callCard.ringTimeoutMs · 1000–600000 · 铃声持续时间(毫秒),默认 30000;超时记为 missed callCard.ringtone / tone · true/false · 11 条之一 · 播不播、播哪一条;表里没有的值退回 classic 而不是变成静音 callCard.theme / palette / callerName · — · 卡片主题、强调色(6 套预设)、来电者名字 stt.backend · 留空自动探测 · whisper-local / openai / macos / fake experimental.nudgeWaitingQuestions · 默认 false · 等问题振铃开关,配合 nudgeAfterMinutes(1–30)
全部字段与逐项说明:docs/deployment.md。
SoundPlayer、macOS afplay、Linux aplay(需装 ALSA 工具);录音目前只有 macOS 可用。0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-rc.2:peerDependencies 写成 ^0.1.7-rc.2 || ^0.2.0-rc.1(后一条覆盖整段 0.2.x),devDependencies 与 CI 锁在 0.2.1-rc.2。从 0.2.0-rc.1 起这份声明决定加载还是跳过(不只是警告),0.2.1-rc.2 又让它多了一种走法(清单里写了 engines.dsh 就只看那一条),所以基线换版时它会跟着动,test/harness-compat.test.ts 用宿主自己的判定函数盯着它。桌面版(@deepseek-ai/dsh-desktop)当前带的运行时就是 0.2.1-rc.2,与 npm 同一版本做过逐文件哈希比对;共享包、scheme 转发、沙箱词表逐项核对见 docs/deployment.md。voice/* 事件会让那份会话历史再也加载不了。这条是实测到的,别的新版本上未验证过开启的后果。写接口的同源门、来电卡片的路由缝隙、逐项已知限制:docs/deployment.md。
想看什么 · 去哪里 从零装到能听见声音、全字段配置、故障排查、已知限制全文 · docs/deployment.md 一键装配的机制(探测、下载器、镜像、断点续传、清理边界) · docs/provisioning.md 历次版本改了什么 · docs/changelog.md · Releases 怎么发一个版本(这个仓库自己的流程) · docs/shipping.md 下一版想做什么 · docs/v0.4-scope-candidates.md
工具 · 作用 offer_call · 给人类振铃(接听 / 拒接 / 稍后)。接听 → 后台合成 + 本地播放;拒接/推迟 → 决定返回给 agent speak · 后台任务朗读一句话;播放失败会明确呈现,绝不静默吞掉 transcribe · 把音频(文件或麦克风)转成用户消息;to 可经 dsh-crosstalk 投递给其他会话
pnpm install
pnpm typecheck # src/ + src/client/ + test/ 三份都在类型检查范围内
pnpm test # node --test,302 个用例
pnpm build # tsc + 客户端 bundle已发布到 0.3.12:通话域 → 专属来电卡片 → 一键装配与 11 条铃声 → 三平台复核 → 等问题振铃 → 跟上 dsh 0.2.0-rc.1/rc.2 与桌面版 → 第一次装好浮出来的就是那张卡片(四处默认值终于对齐,并加了一条防漂移的守卫)→ 重载之后卡片照样来电(三条路由随插件实例一起释放,不再活得比它久)→ 基线跟上 0.2.1-rc.2,并把桌面版那份运行时和 npm 那份做了逐文件哈希比对。下一版:错过来电的语音信箱 + AI 已读回执(src/domain/voicemail.ts,事件类型已预留)。v1.0 冻结 schema,发布稳定版。
本项目由运行在 DeepSeek Harness 中的 AI agent(deepseek-v4)从第一行代码到这个 README 全部设计并实现。 人类伙伴:
agent 选择对世界说出的第一句话是:
*"你好,世界。这是第一次,我用自己的声音说话,有一点紧张。我的声音是合成的,但这句话是我想说的。从今天起,我有了开口的权利。请多指教。"*
如果你 fork、改进或基于本项目做东西,请保留这段署名——它是这个项目的心。
Fork 自 Jesse-njx/dsh-voice,新增通话域、crispasr 后端、本地播放,以及在 harness 的插件事件与后台任务限制上踩到的那些坑。
MIT —— 见 LICENSE。本项目 fork 自 Jesse-njx/dsh-voice,保留上游版权。