dsh-speech
Speech capability plugin for the DeepSeek Harness (dsh) web host: a token-gated /s/api route family serving audio transcription (ASR) and synthesis (TTS) over configurable providers
安装
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:elskly-cmyk/dsh-speech说明文档
阅读完整 README ↗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](https://github.com/elskly-cmyk/dsh-speech/blob/77fa5f7d80e7c645246dbcf068e8b29b3d02cd41/dsh-mobile-gateway) 一起装时,端口绑定由 mobile-gateway
负责(`0.0.0.0`),本插件的路由自动对局域网可达。
## 配置
token 必填,其余有默认值。配置写在 profile patch
(`~/.dsh/profiles/web/cordis.patch.yml`)的插件行:
```yaml
- 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 呈现;留空插件拒绝启动 |
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=查询参数
| 方法 | 路径 | 请求 | 响应 |
|---|---|---|---|
| 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= 查询参数或 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
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 示例
## 配置
token 必填,其余有默认值。配置写在 profile patch
(`~/.dsh/profiles/web/cordis.patch.yml`)的插件行:
```yaml
- 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 呈现;留空插件拒绝启动 |
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 / |