dsh-mimo-speak
Reads assistant replies out loud in DeepSeek Harness, using Xiaomi MiMo TTS.
It adds a speaker button to every finalized reply and a settings card on the Plugins page. Click the button and the reply plays through an inline player. The MiMo API key never leaves the host: the browser sends text, the host sends back WAV.
Install
Desktop: open Plugins → Add plugin (添加插件), enter github:F-YXL/dsh-mimo-speak, restart. Or from a terminal:
dsh plugin --profile desktop add github:F-YXL/dsh-mimo-speak
Web: install into the web profile and refresh the page:
dsh plugin --profile web add github:F-YXL/dsh-mimo-speak
Pin a release instead of following main:
dsh plugin --profile desktop add 'github:F-YXL/dsh-mimo-speak#v0.1.0'
No build permission to grant: the package ships a prebuilt lib/ and has no install scripts. It also shows up in the in-app plugin market (Plugins → Discover), because the repo carries the dsh-plugin topic.
After installing, open Plugins → dsh-mimo-speak, paste the MiMo API key and press Audition. You should hear the sample voice. A speaker button appears under each finalized reply from then on.
What it does
The settings card holds the API key (write-only, stored in the host credential store), endpoint, model, one of nine preset voices or a voice-clone sample, default style, character budget and timeout. Audition speaks a sample using the card's current, unsaved edits.
Each finalized reply gets a speaker button. Before reading, the plugin strips code blocks, URLs and formatting marks, then plays what is left. The progress bar is off by default; turn it on for a draggable, keyboard-seekable track (←/→, Home/End). Stop means stop: in-flight requests are aborted, queued audio is dropped, and Esc does the same.
Read scopes: brief (the conversation model summarizes the reply into a sentence or two first), text (prose only), smart (prose plus short code), all.
There is also an agent tool, mimo-tts-say. It synthesizes speech into <session workspace>/tts/speech-<timestamp>-<random>.wav and returns the path. It writes a file; sending it as a voice message is up to the chat channel.
If MiMo fails and the edge-tts CLI is installed, the plugin can retry with a Microsoft Edge neural voice.
Requirements
- DeepSeek Harness
0.2.2-rc.1(the package declaresengines.dsh: ">=0.2.2-rc.1 <0.3.0") - Node.js
>= 22.19 - A MiMo API key from https://api.xiaomimimo.com, needed at runtime only
Install from source or tarball
From a checkout. lib/ is committed prebuilt, so build is only needed after you edit src/:
git clone https://github.com/F-YXL/dsh-mimo-speak.git
cd dsh-mimo-speak
dsh plugin --profile <profile> add ./dsh-mimo-speak
dsh --profile <profile> --dump-config | grep mimo-speak # expect a "# == dsh-mimo-speak" layer
From a packed tarball:
npm pack
dsh plugin --profile <profile> add ./dsh-mimo-speak-0.1.0.tgz
Configure
| Field | Default | Notes |
|---|---|---|
apiKeyEnv |
MIMO_API_KEY |
credential reference, not the key itself |
baseUrl |
https://api.xiaomimimo.com/v1 |
/chat/completions is appended |
model |
mimo-v2.5-tts |
switches to mimo-v2.5-tts-voiceclone automatically when a clone sample is set |
voice |
mimo_default |
preset: mimo_default, 冰糖, 茉莉, 苏打, 白桦, Mia, Chloe, Milo, Dean |
voiceSamplePath |
— | host-local WAV, 24 kHz / 16-bit / mono, 5–15 s |
style |
— | e.g. 开心, 语速慢, 东北话; empty lets the model decide |
maxChars |
2000 |
longer text is clamped at a sentence boundary |
chunkChars |
120 |
size of one playback chunk |
timeoutMs |
60000 |
|
autoRead |
false |
read each newly finalized reply automatically |
readScope |
smart |
brief / text / smart / all |
fallbackEngine |
none |
none or edge |
edgeVoice |
zh-CN-XiaoxiaoNeural (Xiaoxiao) |
any edge-tts voice id |
To keep the key out of the profile config, override the row in your profile's cordis.patch.yml:
- id: mimo-speak
config:
apiKey: !!js process.env.MIMO_API_KEY
Security
The API key is written through the host credential store (credentials/set) and never ends up in a repo file, a log line or the browser. Every /api/mimo-speak/* route passes ctx.connection.requestRejection first, so a request from outside the app's trusted origin is refused before any key is used. Request bodies are capped at 1 MiB; overlong text is cut at a sentence boundary.
npm run check:privacy scans every tracked file for credentials, home paths and email addresses, plus names or hostnames you list in the git-ignored .privacy-markers. npm run check:privacy:history also walks every blob and commit identity in the history, because git log -p publishes the past too. CI runs the file scan on every push.
If you find a vulnerability, use SECURITY.md, not a public issue.
Development
npm install
npm run build # tsc -> lib/node, tsdown -> lib/client.js
npm test # 65 unit tests (node --test)
node scripts/stub-e2e.mjs # full HTTP round-trip against a local stub, no key needed
MIMO_API_KEY=... node scripts/live-e2e.mjs # one real request against the MiMo platform
lib/ is committed on purpose, so dsh plugin add needs no build step. After editing src/, run npm run build and commit lib/ in the same commit; CI fails when the committed build output is stale.
See CONTRIBUTING.md for commit conventions and the version-pinning workflow, MAINTAINING.md for release steps.
Pinning to a dsh release
The package pins one set of @deepseek-ai/dsh-* dependencies; npm will not pick a matching build for the host. Two declarations do the gating:
dsh.compatibility.dshReleaseslists the releases this build supports ({ "0.2.2-rc.1": "compatible" }). The installer refuses or warns when the host version is not on the list.engines.dshis an npm-style range (>=0.2.2-rc.1 <0.3.0).
Two things to watch:
- All
@deepseek-ai/dsh-*packages share one release version (0.2.2-rc.1), but@deepseek-ai/cordisand@deepseek-ai/schemasteryhave their own (~4.1.0/~3.19.0here). Bumping everything together 404s. - Since
0.2.0,dsh-scopeis a peer ofdsh-session/dsh-api-remotesand must be in bothpeerDependenciesanddevDependencies, ornpm installfails withERESOLVE.
Use the script instead of editing version numbers by hand:
node scripts/pin-dsh-version.mjs <release> # rewrite package.json, print next steps
node scripts/pin-dsh-version.mjs <release> --install # ... and run npm install
node scripts/pin-dsh-version.mjs <release> --self # ... and bump this plugin's version too
node scripts/pin-dsh-version.mjs <release> --dry-run # preview only
How it maps to the plugin spec
| Spec | Where it lands here |
|---|---|
| Packaging & installing plugins (bundle) | package.json declares dsh.bundle.patch + cordis.patch.yml inserts the id: mimo-speak row; the same package carries the dsh.client browser half |
| Plugin configuration (no hardcoded knobs) | every parameter is a Schemastery Config field with .volatile(); the key is role('secret') |
| Instant config forms | ctx.configForms (mimo-speak namespace) + the standard SettingsFormModel form |
| Web client slots | two seats: plugins.bundle.config and conversation.chat.assistant-actions |
| Web server | five routes under /api/mimo-speak/*, each behind ctx.connection.requestRejection (Host/Origin + browser auth), so a bare LAN request cannot spend your API quota |
| Client module system | lib/client.js is a lazy-CJS factory (window.__ModuleLoader__.load); the build-time purity gate keeps cross-plugin imports type-only |
MiMo wire format
POST {baseUrl}/chat/completions, header api-key: <key>, body:
{
"model": "mimo-v2.5-tts",
"audio": { "format": "wav", "voice": "mimo_default" },
"messages": [{ "role": "assistant", "content": "<style>开心</style>要说的话" }]
}
Voice cloning switches to mimo-v2.5-tts-voiceclone and puts the reference audio in audio.voice as a data URI:
{
"model": "mimo-v2.5-tts-voiceclone",
"audio": { "format": "wav", "voice": "data:audio/wav;base64,<base64 wav>" },
"messages": [{ "role": "assistant", "content": "要说的话" }]
}
The response carries base64 audio in choices[0].message.audio.data — either a RIFF container (passed through) or raw PCM (wrapped into a 24 kHz / 16-bit / mono WAV by the plugin).
Known limits
- Reading is per message: the action row belongs to one finalized reply, so reasoning blocks and tool calls are not read. Code blocks become "代码略".
- Text longer than
maxCharsis read from the start and cut at a sentence boundary, with a hint next to the action row. - Desktop (Electron) goes through the IPC fetch bridge; the route paths are unchanged.
- Voice input is the official
@deepseek-ai/dsh-experimental-voice-input-bundle, not this plugin. This one stops speaking as soon as the microphone opens, so the two do not feed back into each other.
License
MIT © F-YXL. Commercial use is permitted — that is what MIT means.
中文说明(完整)
这是什么
把助手回复「读出来」的 DSH 插件:「插件」页里的配置卡片 + 每条已定稿助手回复动作行里的音量按钮 + 带可拖动进度条的内联播放器。语音由小米 MiMo TTS(mimo-v2.5-tts)合成,API Key 只存在于宿主侧,浏览器只发送文本、收到 WAV。
面向 deepseek-harness 0.2.2-rc.1(engines.dsh: ">=0.2.2-rc.1 <0.3.0")。
功能
- 「插件」页 → dsh-mimo-speak 详情页的配置卡片
- API Key(只写输入框:粘贴即经官方
credentials/set通道存入宿主凭据存储,页面不回显;显示「已配置/未配置」,清空保存不改动已存密钥) - 接口地址(默认
https://api.xiaomimimo.com/v1)、模型(默认mimo-v2.5-tts) - 预置音色(官方 9 音色下拉:mimo_default、冰糖、茉莉、苏打、白桦、Mia、Chloe、Milo、Dean)/ 音色克隆样本(宿主本地 WAV 路径,24 kHz / 16 bit / 单声道,5–15 s)
- 默认风格(如「开心」「语速慢」「东北话」;留空由模型自行判断,推荐)、单次最大字符数、请求超时
- 试听:用卡片当前(含未保存修改)的风格/音色合成一句示例,内联播放
- 标准表单行为:脏值预览、「已覆盖/恢复默认」徽标、保存/放弃、只读部署提示
- API Key(只写输入框:粘贴即经官方
- 每条已定稿助手回复底部的音量按钮
- 点击 → 宿主合成(Markdown 归一化:不朗读代码块/URL/标记符号)→ 自动播放
- 进度条可关(默认关):关掉只留播放/停止按钮;打开才有可拖动进度条(指针拖拽、单击定位、键盘 ←→/Home/End)
- 停止是真取消:中断在途请求、作废本轮、清空已排程音频;
Esc同样生效 - 同时只有一条在播;播放中动作行不再悬停淡出
- 失败就地提示(未配置/超时/上游拒绝/无文本等稳定错误码 → 本地化文案)
- 朗读范围四档:简要播报(先摘要再念:用当前对话模型把回复浓缩成 1–2 句口播摘要,约 80 字;模型不可用时退回规则摘要)/ 只读正文 / 智能(正文+短代码)/ 全部
- Agent 工具
mimo-tts-say- 参数:
text(必填)、voice、style(可选覆盖) - 行为:合成 → 写
<会话工作区>/tts/speech-<时间戳>-<8位随机>.wav→ 返回绝对路径 + 字节数 + 时长 - 只产出文件,不发消息:要真的发出去,把返回的 path 交给当前聊天渠道自己的发语音工具
- 为什么写进工作区:渠道侧的发送工具通常只接受工作区内的路径
- 参数:
- 安全边界:密钥只在宿主;
/api/mimo-speak/*路由先过 Connection 信任栅栏;请求体 1 MiB 上限;文本超预算在句末截断朗读开头。工具和网页朗读共用同一套 settings/凭据,不额外存密钥。
安装
桌面端:打开「插件 → 添加插件」,填入 github:F-YXL/dsh-mimo-speak,然后重启 DSH;也可以在终端执行:
dsh plugin --profile desktop add github:F-YXL/dsh-mimo-speak
Web:装到 web profile,重启后刷新页面:
dsh plugin --profile web add github:F-YXL/dsh-mimo-speak
仓库公开后,它也会出现在应用内的「插件市场 / Discover」里(仓库带了 dsh-plugin topic,市场会自动收录)。从源码构建请见英文「Install from source or tarball」一节;本包自带预构建的 lib/ 且没有 install 脚本,无需 pnpm 构建授权。
不想把密钥写进配置文件时,在 profile 的 cordis.patch.yml 里覆写该行:
- id: mimo-speak
config:
apiKey: !!js process.env.MIMO_API_KEY
与官方语音输入插件配合
语音指令的识别交给官方组合包,本插件只负责播报,两者互补:
# 版本要与宿主对齐:DSH 0.2.2-rc.1 → 钉到同版本
dsh plugin --profile <profile> add @deepseek-ai/dsh-experimental-voice-input-bundle@0.2.2-rc.1
它是本地 SenseVoice 转写(首次使用下载运行时),识别结果插入输入框草稿,不会自动发送。本插件检测到麦克风被打开时会立即停止朗读,所以「说完 → 发送 → 自动朗读回复」这条链路不会把播报录进去。
目录
dsh-mimo-speak/
├── package.json # dsh.bundle + dsh.client manifest,engines.dsh 锁 >=0.2.2-rc.1 <0.3.0
├── cordis.patch.yml # bundle 层:插入 id: mimo-speak 行(两侧共用)
├── tsconfig.json # tsc → lib/node(Node 半侧 + 类型)
├── tsdown.config.ts # 官方 clientBundle 的树外复刻 → lib/client.js
├── icon.svg locale/{zh,en}.json
├── src/
│ ├── index.ts # Host 插件:Config schema + Web 路由
│ ├── tts.ts # MiMo TTS 客户端(/chat/completions + audio 输出块)
│ ├── wav.ts normalize.ts# PCM→WAV 封装;Markdown→可朗读文本
│ ├── settings.ts # 两侧共享:命名空间/路由/默认值/线格式类型
│ └── client/ # 浏览器半侧:两个 slot 席位、播放器、配置卡片
├── tests/ # 65 项单测(node --test)
└── scripts/
├── stub-e2e.mjs # 本地桩服务端到端(无需真实密钥)
├── live-e2e.mjs # 真实 MiMo API 端到端(需 MIMO_API_KEY)
└── check-privacy.mjs # 推送前的隐私扫描
构建与测试
npm install
npm test # 65 项单测
npm run build # tsc + tsdown → lib/
node scripts/stub-e2e.mjs # 真实 HTTP 桩端到端(96,044 字节 WAV)
npm run check:privacy # 隐私扫描,必须 0 命中
适配版本与「按版本选适配」
分发不会按宿主版本自动下载对应适配版本。 本插件打包时把 @deepseek-ai/dsh-* 依赖锁死在某一个 release(当前 0.2.2-rc.1),别人拉到的就是同一份 package.json。真正的版本闸门是两处兼容性声明:
dsh.compatibility.dshReleases:官方字段,精确列出本包适配的 release。安装器/市场在对方 dsh 版本不在白名单时拒绝或警告。engines.dsh:npm 风格的范围闸门(当前>=0.2.2-rc.1 <0.3.0)。
运行时实际用的 @deepseek-ai/dsh-* 实例由宿主 dsh 自带,插件代码只是复用宿主环境里的版本。想让旧版 dsh 也能用,得单独发一份 pin 回旧版本的包(scripts/pin-dsh-version.mjs 一键生成),即「多版本并行发布」。
三个容易踩的坑:
@deepseek-ai/dsh-*整条线共用同一个 release 版本号。@deepseek-ai/cordis与@deepseek-ai/schemastery是独立版本号,不跟随 release。0.2.0起dsh-scope成为 peer 依赖,需同时写在peerDependencies与devDependencies里,否则npm install报ERESOLVE。
MiMo 线格式
POST {baseUrl}/chat/completions
头:api-key: <key>
体:{ "model": "mimo-v2.5-tts",
"audio": { "format": "wav", "voice": "mimo_default" },
"messages": [{ "role": "assistant", "content": "<style>开心</style>要说的话" }] }
音色克隆时切到 mimo-v2.5-tts-voiceclone 模型,参考音频以 Data URI 放进 audio.voice。响应 choices[0].message.audio.data 为 base64 音频(RIFF 或裸 PCM,裸 PCM 由插件包成 24 kHz / 16 bit / mono WAV)。
已知边界
- 按消息朗读:动作行属于该条已定稿回复,朗读其正文(不读思考块与工具调用);代码块以「代码略」代替。
- 文本超过「单次最大字符数」时朗读开头并在句末截断。
- Desktop(Electron)走 IPC 桥接 fetch,路由路径不变。
- 语音输入(麦克风)是官方
@deepseek-ai/dsh-experimental-voice-input-bundle的职责,本插件不重复实现。
许可证
MIT © F-YXL。MIT 允许商用——这是它和「禁止商用」许可证的关键区别;如果你需要禁止商用,请自行更换为 PolyForm Noncommercial 等非开源许可证。
No comments yet. Be the first to write one.