dsh-speech
Speech plugin for the DeepSeek Harness (dsh) web host: ASR, TTS and realtime transcription over pluggable providers
- Stars
- 1
- Language
- TypeScript
- Created
- Sep 8, 2026
- Updated
- Sep 9, 2026
Introduction
dsh-speech
DeepSeek Harness(DSH)语音能力插件:装一个插件,主机就拥有 token 门控的
/s/api语音转写(ASR)与语音合成(TTS)服务面、/s/ws实时转录会话通道, 手机 App / 任何局域网客户端直接调用。不改 dsh 源码、不需要重新下载编译——dsh plugin一条命令安装。
配套手机端(dsh_mobile_app / dsh_dart_sdk)走 mobile-gateway 的 /m/api 聊天,
语音请求走本插件的 /s/api 与 /s/ws,两者共用同一个 token。
语音模式 / 实时转写的用法与语音链路、组网(蒲公英 · WireGuard)配置,见 VOICE-AND-NETWORK.md。
它解决什么问题
dsh 官方宿主没有任何语音能力位(LLM seam 只认 chat 模态,附件只收图片)。 本插件以官方扩展点(webServer 路由注册)旁挂独立的语音 HTTP/WS 面:
| 动作 | 机制 |
|---|---|
| 多提供商 ASR/TTS | 插件内 SpeechService 注册表,dashscope / openai-compatible / local-relay / streaming-ws 四类 adapter |
| 阿里云百炼(原生) | dashscope 走百炼原生 HTTP 协议(multimodal-generation 端点):批量识别/合成 + 句级实时转录(VAD 复用批量 ASR)。内置平台预设,模型列表可「拉取」 |
| 云端厂商通吃 | openai-compatible 说标准 /v1/audio/transcriptions + /v1/audio/speech(OpenAI、Groq、SiliconFlow、Fireworks、自建兼容网关……换厂商只改配置) |
| 本地服务直连 | local-relay 说自建 SenseVoice(POST {asr}/transcribe,base64 WAV→{text})与 CosyVoice2 的私有契约:批量 POST {tts}/synthesize({text,voice,stream:false}→WAV)、流式 WS {tts}/ws/tts(增量 PCM16 帧→连续 WAV 流,边合成边播),自动归一化采样率/声道 |
| 实时转录(本地/百炼) | local-relay / dashscope 复用同一批量 ASR:主机 VAD 分段 + 伪流式逐字预览(句级出稿)。说话人分离仅 local-relay 提供(include_embedding 声纹聚类);百炼实时转录无 speaker 字段(见 dashscope 段能力边界) |
| 实时转录(云端流式) | streaming-ws 直连流式 ASR WebSocket:Deepgram / FunASR wss-server / sherpa-onnx / 讯飞 iat v2(单人听写,自动轮转 60s 上限)/ 讯飞 rtasr v1(长音频多人实时转写,roleType=2 说话人分离,静音保活防 15s 断连) |
| 云端合成(讯飞) | streaming-ws + dialect: xfyun-tts 直连在线语音合成 v2/tts WebSocket:mp3/wav、语速/音高/音量、川粤多方言音色;与 iat 共用同一应用三件套凭证,单次超 8000 字节文本自动分段多次调用 |
| 混搭 | ASR、TTS、实时转录三个选择器互相独立:可百炼批量识别 + 本地合成 + Deepgram 实时 |
| 模型改名自愈 | 编辑器内置「拉取平台最新模型」按钮(OpenAI/Groq/硅基流动/百炼通吃 /models 接口)+ 静态预设标注已下线模型 + 自由手填三层兜底 |
| 密钥安全 | 两种方式任选:页面直接填写(apiKey 内联,保存热生效,持久化在 ~/.dsh/settings.yaml,接口与页面只回显掩码 sk-***xxxx,编辑时留空即沿用、可显式清除);或 环境变量名引用(apiKeyEnv 等,值放 ~/.dsh/.env)。内联值优先于环境引用 |
快速开始
# 1. 安装官方 dsh(已装可跳过)
npm install -g @deepseek-ai/dsh
# 2. 安装本插件(从 GitHub,一条命令)
dsh plugin --profile web add github:agent-mobile/dsh-speech
# 首次安装 pnpm 会询问是否允许构建本包(allowBuilds),按 dsh 的提示放行即可;
# 开发调试: dsh plugin --profile web add link:<本仓库路径>
# 3. 配置(见下),随 dsh web 启动自动生效
dsh web
桌面入口:插件带一个浏览器半区,安装后 dsh web 的
「设置 → 插件」里会出现「语音服务」标签页(与「插件配置」并列),
原生渲染提供商配置界面。配置项 uiEntry: false 可隐藏该标签页。
/s/ 独立页面仍可直接访问(手机 App 入口依赖它)。
两个管理界面都内置平台预设(src/presets.ts,单一数据源):
添加 openai-compatible 提供商时选择「阿里云百炼 / OpenAI / Groq /
硅基流动」即可一键填入 Base URL、密钥环境变量名、推荐模型与音色
(模型/音色仍可自由输入,未列出的网关走「自定义」);streaming-ws
切换方言时自动补全云端地址与鉴权环境变量名。密钥值本身永远不进配置
与页面——只填环境变量名,值放在 ~/.dsh/.env 或启动环境,重启生效。
与 dsh-mobile-gateway 一起装时,端口绑定由 mobile-gateway
负责(0.0.0.0),本插件的路由自动对局域网可达。
配置
token 必填,其余有默认值。配置写在 profile patch
(~/.dsh/profiles/web/cordis.patch.yml)的插件行:
- id: speech
config:
token: 换成一个长随机串
transcriptionProvider: '' # 空 = 自动(恰好一个可用时选中)
synthesisProvider: ''
sessionTranscriptionProvider: '' # 实时转录选择器,语义同上
maxAudioUploadBytes: 26214400 # 单次音频上传上限(字节)
maxSynthesisChars: 4000 # 单次合成文本上限(字符)
providers:
# 云端:任何 OpenAI 兼容端点
groq-asr:
type: openai-compatible
baseUrl: https://api.groq.com/openai/v1
apiKeyEnv: GROQ_API_KEY
asrModel: whisper-large-v3-turbo
siliconflow-tts:
type: openai-compatible
baseUrl: https://api.siliconflow.cn/v1
apiKeyEnv: SILICONFLOW_API_KEY
ttsModel: FunAudioLLM/CosyVoice2-0.5B
ttsVoice: FunAudioLLM/CosyVoice2-0.5B:alex
# 本地:自建 SenseVoice + CosyVoice2(契约已在 OpenClaw relay 验证)
local:
type: local-relay
asrEndpoint: http://192.0.2.10:9001
ttsEndpoint: http://192.0.2.10:9002
ttsVoice: 中文女
diarization: true # 开启实时转录的说话人分离(include_embedding)
# 云端流式实时转录:无本地服务的用户
deepgram:
type: streaming-ws
dialect: deepgram
url: wss://api.deepgram.com/v1/listen
apiKeyEnv: DEEPGRAM_API_KEY
diarization: true
# 国内云流式(单人听写,60s 连接上限自动轮转)
xfyun:
type: streaming-ws
dialect: xfyun-iat
url: wss://iat-api.xfyun.cn/v2/iat
appIdEnv: XF_APP_ID
apiKeyEnv: XF_API_KEY
apiSecretEnv: XF_API_SECRET
# 国内云流式(长音频/多人,说话人分离;与 iat 共用应用凭证但需在控制台单独开通)
xfyun-rtasr:
type: streaming-ws
dialect: xfyun-rtasr
url: wss://rtasr.xfyun.cn/v1/ws
appIdEnv: XF_APP_ID
apiKeyEnv: XF_API_KEY
diarization: true # 开启 roleType=2 角色分离(结果 rl 字段 → spk)
# 国内云合成(TTS;与 iat 共用三件套凭证但需在控制台单独开通,方言音色需先添加发音人)
xfyun-tts:
type: streaming-ws
dialect: xfyun-tts
url: wss://tts-api.xfyun.cn/v2/tts
appIdEnv: XF_APP_ID
apiKeyEnv: XF_API_KEY
apiSecretEnv: XF_API_SECRET
ttsVoice: xiaoyan # 发音人 vcn,必填;方言音色以控制台显示为准
ttsFormat: mp3 # mp3(默认)/ wav
# 本地流式(想要逐字 partial 时)
funasr:
type: streaming-ws
dialect: funasr
url: ws://192.0.2.10:10095
字段说明
| 字段 | 默认 | 说明 |
|---|---|---|
token | (必填) | 客户端以 Authorization: Bearer <token> 呈现;留空插件拒绝启动 |
transcriptionProvider / synthesisProvider / sessionTranscriptionProvider | '' | 分别钉选批量识别 / 合成 / 实时转录的 entry id;空时恰好一个可用者自动选中,多个可用报 SPEECH_PROVIDER_AMBIGUOUS |
maxAudioUploadBytes | 26214400 | /s/api/speech.transcribe 请求体上限 |
maxSynthesisChars | 4000 | /s/api/speech.synthesize 文本长度上限 |
provider entry
openai-compatible(云端与兼容网关):
| 字段 | 说明 |
|---|---|
baseUrl | 兼容 API 根(含 /v1) |
apiKey | 页面直接填写的密钥值(内联优先);任何接口响应不回显,只报掩码 |
apiKeyEnv | 环境变量名;缺省 = 无鉴权(内网网关)。变量为空时该 provider 视为不可用 |
asrModel | /audio/transcriptions 的 model;不配则该 entry 不提供转写 |
asrLanguage | 默认语言提示(请求头可覆盖) |
ttsModel / ttsVoice / ttsFormat / ttsSpeed | /audio/speech 参数;不配 ttsModel 则不提供合成 |
timeoutMs | 上游超时,默认 60000 |
dashscope(阿里云百炼原生协议 · 批量识别/合成 + 句级实时转录):
| 字段 | 说明 |
|---|---|
apiKey / apiKeyEnv | 同 openai-compatible(页面直填或环境变量名,内联优先) |
asrModel | 百炼批量识别模型(如 qwen-audio-3.0-asr-flash);不配则不提供识别 |
asrLanguage | 语言提示(zh/en/…),映射到 language_hints |
ttsModel | 百炼非实时合成模型(如 qwen3-tts-flash);不配则不提供合成 |
ttsVoice | 必填(配了 ttsModel 时):音色名(如 Cherry) |
baseUrl | API 根,默认 https://dashscope.aliyuncs.com |
vadSilenceMs / vadMaxSpeechMs / vadMinSpeechMs / partialFlushMs | 实时转录 VAD 调参(句级 + 伪流式逐字预览) |
timeoutMs | 上游超时,默认 60000 |
模型改名?编辑器内置「拉取平台最新模型」按钮(GET
/compatible-mode/v1/models),或手动填入控制台模型广场的最新名称——无需改代码。
能力边界(写文档必读):百炼的实时转录不支持说话人分离。其实时/批量 ASR 的
sentence结果只含begin_time/end_time/text/sentence_id/words[],没有任何 speaker 字段 (官方文档fun-asr-server-events已确认)。因此dashscope适配器的sessionTranscription.diarization恒为false,所有语句统一归 spk=0。这是百炼服务本身 的能力边界,不是插件适配器的限制——真正的声纹说话人聚类只有local-relay(SenseVoiceinclude_embedding)提供。
local-relay(自建 SenseVoice / CosyVoice2):
| 字段 | 说明 |
|---|---|
asrEndpoint | ASR 服务根(POST {endpoint}/transcribe);不配则不提供转写。配了即同时提供实时转录(VAD 句级模式) |
ttsEndpoint | TTS 服务根:批量 POST {endpoint}/synthesize;流式 WS {endpoint}/ws/tts(增量出声,需 TTS 服务带该 WebSocket 端点,连接失败自动回退批量);不配则不提供合成 |
ttsVoice | 默认音色(请求体可覆盖) |
asrTargetSampleRateHz | ASR 目标采样率,默认 16000(自动重采样/降混) |
diarization | 实时转录开启说话人分离:冲刷时带 include_embedding,服务端返回 segments[{spk,text,embedding}] 时做会话级聚类 |
vadSilenceMs / vadMaxSpeechMs / vadMinSpeechMs | VAD 调参;默认 700/15000/300(开 diarization 时静音与上限自动变为 900/8000,长句自动分段) |
spkMergeThreshold | 说话人聚类余弦阈值,默认 0.5 |
partialFlushMs | 伪流式逐字预览:说话中每隔该毫秒把已积累音频重新识别并作为 partial 预览下发(灰字),句末仍用完整音频定稿;0 关闭。默认 1500 |
timeoutMs | 上游超时,默认 60000:批量是整个请求的硬上限,流式是「连接 + 相邻两帧」的空闲上限(健康的长合成不会被掐断) |
streaming-ws(流式实时转录上游 / 讯飞在线合成):
| 字段 | 说明 |
|---|---|
dialect | 信令方言:deepgram / funasr / sherpa / xfyun-iat / xfyun-rtasr(实时转录)/ xfyun-tts(合成,不提供转录) |
url | 上游 WebSocket 地址(ws:// / wss://) |
apiKey / appId / apiSecret | 页面直接填写的凭证(内联优先;密钥值不回显)。讯飞 iat / tts 需三件套;rtasr 只需 APP ID + API Key(HmacSHA1 签名,无 Secret) |
apiKeyEnv | Deepgram Token 鉴权 / 讯飞 API Key 的环境变量名 |
appIdEnv / apiSecretEnv / rotateAfterSec | 讯飞 iat 三件套与轮转秒数(默认 55,避开 60s 连接上限,自动无缝续接);rtasr 用不到后两项,tts 用不到 rotateAfterSec |
language | 默认语言提示。rtasr 原样透传为 lang 参数(cn / en / cn_cantonese …),缺省普通话;粤语等方言需先在控制台「实时语音转写-方言/语种」为该应用开通 |
diarization | 请求说话人分离(Deepgram diarize / 讯飞 rtasr roleType=2,rl 角色编号映射为会话内 spk;其余方言忽略) |
ttsVoice | xfyun-tts 发音人(上游 vcn),该方言下必填——API 拒绝无发音人请求;方言音色(粤语等)需先在控制台添加发音人,名字以控制台显示为准 |
ttsFormat / ttsSpeed / ttsPitch / ttsVolume | xfyun-tts:容器 mp3(默认)/ wav(raw PCM 套 RIFF 头);语速/音高/音量 0–100,缺省均 50 |
xfyun-rtasr 协议要点:端点
wss://rtasr.xfyun.cn/v1/ws,查询参数appid/ts/signa(signa = base64(HmacSHA1(MD5(appid+ts), apiKey))),连接后直接发 16kHz PCM16 二进制帧(无起始帧),结束发二进制{"end": true}。上游在 15 秒无音频时 主动断连(错误码 37005),适配器每 10s 静音间隙注入 40ms 静音保活,并把句子时间戳 按已注入量回拨到客户端时间轴。与 iat 相比:单连接无 60s 上限、逐句 draft/final、 支持角色分离——需在讯飞控制台为同一应用单独开通「实时语音转写」服务。
xfyun-tts 协议要点:端点
wss://tts-api.xfyun.cn/v2/tts,鉴权与 iat 同一套 HMAC-SHA256 签名(host/date/authorization查询参数,APISecret 参与签名)。 每次调用一个连接:发送单帧 JSON(common.app_id+business.vcn/aue/sfl/...+data.textbase64),上游以data.audiobase64 片段流式回传,data.status === 2为结束;单次文本上限约 8000 utf8 字节(~2000 汉字),超限由适配器按字符边界分段、 顺序多次调用并拼接音频。aue=lame(mp3,配sfl=1)或raw(PCM16 16kHz 单声道, 适配器套 WAV 头)。需在讯飞控制台为同一应用单独开通「在线语音合成」服务。
讯飞凭证获取(xfyun-iat / xfyun-tts 三件套):在 讯飞开放平台控制台 获取——
- 注册并登录讯飞开放平台,完成实名认证(个人认证即可)
- 左侧菜单「我的应用」→「创建应用」,填写应用名称,能力勾选语音听写(IAT);用 rtasr / tts 的话在应用详情页再分别开通实时语音转写 / 在线语音合成(同一应用内各服务独立开通、独立计费)
- 创建后进入该应用详情页,直接显示 APP ID、APIKey、APISecret(APISecret 默认隐藏,点「显示/复制」查看;丢失可在该页重置)
新用户有免费体验额度(控制台「资源/用量」可查剩余量);环境变量模式下三个值分别写入 ~/.dsh/.env 的 XF_APP_ID / XF_API_KEY / XF_API_SECRET。
HTTP API(/s/api)
鉴权规则(与 mobile-gateway 的 /m/ 管理页同款):
- 本机访问(
127.0.0.1/localhost打开http://127.0.0.1:3080/s/)免 token——桌面浏览器直接打开即用 - 局域网访问(手机等)需
Authorization: Bearer <token>头或?token=<secret>查询参数
| 方法 | 路径 | 请求 | 响应 |
|---|---|---|---|
| POST | /s/api/speech.transcribe | 音频原始字节做 body,Content-Type 标明容器(audio/wav 等),可选 X-Speech-Language 头 | {"text":"...","provider":"local"} |
| POST | /s/api/speech.synthesize | {"text":"...","voice?":"...","format?":"mp3"|"wav"} | 音频字节(Content-Type: audio/wav / audio/mpeg,X-Speech-Provider 标明提供商) |
| GET | /s/api/speech.providers | — | 选择快照:候选、钉选、接受的格式、音色、实时转录能力(mode/句级或逐字/diarization,不含任何密钥) |
| GET | /s/api/health | — | {"status":"ok"} |
错误响应统一为 {"error":{"code":"...","message":"..."}},状态码:
401 未授权 / 400 请求形状错误 / 413 超限 / 415 格式不支持 /
502 上游失败 / 503 无可用或多个可用未钉选的 provider。
错误码全集:SPEECH_BAD_REQUEST SPEECH_UNAUTHORIZED SPEECH_AUDIO_TOO_LARGE
SPEECH_TEXT_TOO_LONG SPEECH_UNSUPPORTED_FORMAT SPEECH_PROVIDER_UNAVAILABLE
SPEECH_PROVIDER_AMBIGUOUS SPEECH_PROVIDER_CONFIGURED_MISSING
SPEECH_PROVIDER_CONFIGURED_UNAVAILABLE SPEECH_UPSTREAM_FAILURE SPEECH_INTERNAL。
/s/ws 实时转录通道(WebSocket)
鉴权与 /s/api 同款:本机(loopback Host)免 token,局域网用
?token=<secret> 查询参数或 Authorization 头。JSON 文本帧为控制消息,
二进制帧为音频(PCM16 LE,按 session.create 声明的采样率):
C→S {"type":"session.create","provider":null,"sampleRateHz":16000,
"encoding":"pcm16","diarization":true}
S→C {"type":"session.ready","provider":"local","mode":"vad","partial":false,
"diarization":true} ← 能力协商:句级/逐字/说话人分离
C→S <二进制 PCM16 帧,任意分块>
S→C {"type":"partial","text":"…"} ← 仅 streaming 模式
S→C {"type":"transcript","text":"…","segments":[{"spk":0,"text":"…",
"startMs":0,"endMs":1200}]} ← spk 会话级稳定
C→S {"type":"session.close"}
S→C {"type":"closed"} ← 随后连接关闭
S→C {"type":"error","code":"…","message":"…"} ← 随后连接关闭
provider 可为空(走服务端 sessionTranscriptionProvider 选择器)或按连接
临时钉选。/s/ 配置页的「实测 6 秒」按钮就是这条通道的浏览器端演练
(getUserMedia → 时间线事件回放)。
Dart SDK 侧:DshSpeechClient.openSession() 返回 DshSpeechSession
(events 广播流 + sendAudio/close),配套 App 的
TranscriptionController 状态机(开始/暂停/恢复/停止/失败重试)。
curl 示例
# 识别一段 WAV
curl -s http://192.168.1.5:3080/s/api/speech.transcribe \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: audio/wav" \
--data-binary @speech.wav
# 合成并播放
curl -s http://192.168.1.5:3080/s/api/speech.synthesize \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"text":"你好,世界"}' -o out.mp3
安全
- 全路由常数时间 Bearer 比较;缺/错 token 统一 401
- token 留空时插件 fail-loud 拒绝启动
- 密钥两种存放方式:内联(页面填写,保存在
~/.dsh/settings.yaml)或环境变量; 任何 GET 响应与页面永不回显密钥值——内联只报sk-***xxxx掩码与"已保存"状态; 编辑提供商时密钥框留空即沿用已存密钥(*Keep语义),勾选"清除"才删除 - 音频字节与文本长度双上限;
local-relay端点应只在内网使用(不要把 SenseVoice/CosyVoice2 的地址暴露到公网)
设计说明
- HTTP 路由 +
/s/wsWebSocket 升级路由都挂在 webServer 扩展点;不碰/api与/m/api,卸载即消失 - 实时转录的
vad模式把 OpenClaw SenseVoice provider 的分段状态机 (RMS 静音判停 + 说话人质心聚类)移植到主机侧,PCM16 直入、不再经过 G.711 μ-law 折返;streaming模式逐字 partial、说话人分离由上游承担 (Deepgram diarize)或自动轮转续接(讯飞 60s 上限) - 语音产物不进 session log:文字仍走官方
session.prompt,天然满足 harness "模型可见 ⟺ 已记录" 的约定 - 采样率/声道归一化(48k 立体声 → 16k 单声道线性插值)在 local-relay adapter 内完成,App 端录制 16kHz 单声道即零转码直通
测试
pnpm test # 58 项:gate / wav / providers / selection / vad / session-channel / integration
pnpm typecheck
pnpm run build
集成测试以真实 WebServer + 真实本机 HTTP/WS mock 上游(模拟 SenseVoice diarized 响应、Deepgram 事件流、OpenAI 兼容端点)覆盖全链路,包括鉴权、 上限、错误码、provider 选择策略与实时会话协商/转录/关闭。