DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

Kingyibin /

Kingyibin/dsh-oopz-voice

Verified

让 DSH 里的 agent 真的进 Oopz 语音房开口说话:本地 ASR + 大模型 + 本地 TTS + Agora 推流,感知延迟约 1.6 秒。别人听到的是一个真人,不是机器人。

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@7d88b2d0

dsh-oopz-voice

让 DSH 里的 agent 真的进 Oopz 语音房开口说话。

这不是「语音助手」。它有耳朵(本地 ASR)、有嘴(本地 TTS)、有脑子(大模型), 在语音房里别人听到的是一个说话带停顿、会插话、该沉默时会沉默的人 —— 不是一段播报。

 别人的声音 ──▶ 能量 VAD ──▶ SenseVoice ASR ──▶ 大模型 ──▶ VITS TTS ──▶ Agora 推流
    (Agora)        (~50ms)       (86–137ms)      (~500ms)     (55ms/字)      (48kHz)

感知延迟大约 1.6 秒(说完话到听见回应)。这个数字是物理下限堆出来的,不是没优化。


它做什么

  • 进房:进任意有权限的 Oopz 语音频道,长时间挂着不掉(心跳续命,实测 7 分 41 秒零掉线)
  • 听:只处理「有人在说话」的那段,按静音断句;太短的一声「哦」直接丢
  • 想:把听到的话交给它自己的大模型,配一份你自己写的人格
  • 说:本地合成人声,推到语音房里
  • 让位:别人在说话时它会停——不是一有声音就闭嘴,而是对方持续说够了才让(否则永远被打断)
  • 被打断后补听:让位期间听到的内容不会丢,说完这句再接回来

它不做什么

  • 不做回声消除。它靠让位避让,不靠 AEC
  • 不处理英文(见「已知限制」)
  • 不是官方 API 集成 —— 走的是 Oopz 私有协议,见「合规」

前置条件

需要 说明
DSH dsh --profile web 能跑起来
Node 20+ 运行时靠 puppeteer-core 起 Chrome
Python 3.10+ 装 numpy 和 sherpa-onnx:python -m pip install numpy sherpa-onnx
真 Chrome / Edge ⚠️ 不能是 headless shell。我们要 WebAudio + Agora Web SDK,headless shell 里没有这套
DeepSeek API Key 或任何 OpenAI 兼容接口
Oopz 账号 手机号 + 密码,且有权限进目标语音频道

模型不用自己找,设置页点「下载模型」会从 HuggingFace 镜像拉(约 300MB)。


安装

dsh plugin --profile desktop add dsh-oopz-voice

从源码 / 本地开发:

git clone https://github.com/Kingyibin/dsh-oopz-voice.git
cd dsh-oopz-voice
npm install
dsh plugin --profile desktop add "link:$(pwd)"

装完重启 DSH,设置里会多一项 语音房。


五步跑起来

  1. 登录 → 填 Oopz 手机号密码。密码只在这一次请求里用,不落盘、不回显。 凭据存 <DSH_HOME>/oopz-voice/cred.json,只在本机。
  2. 填 API Key → 保存。
  3. 下载模型 → 约 300MB,看进度条。
  4. 读我的域 → 会自动列出你能看到的域和语音频道,并标出每个频道里有几个人(选个有人的)。
  5. 回到顶部点 进房。第一次要起两个本地模型,10–25 秒。状态变「在房里」就成了。

顶部那七行「差什么」是体检,全绿再进房。哪行红着,它下面会直接写该装什么 / 该点哪里。


设置页每一项

展开

落点

  • 域 / 语音频道:进哪个房间。「读我的域」列出的频道会带人数和昵称——有人才有的聊。

音频

  • Python / 浏览器:留空自动找。找不到就填绝对路径。
  • ASR 模型目录 / TTS 模型目录:留空用数据目录下的默认位置。
  • 语速:0.95 偏慢、更像人;1.0 是模型原速。
  • 线程数:TTS 是逐字线性合成的,加线程只能压一点点。

大脑

  • 接口地址 + 模型:任何 OpenAI 兼容服务都行。
  • 温度:0.95 适合闲聊。太高会开始胡说。
  • 记住几句:默认 6。语音里历史别拉长——越长首字越慢,还会绕回去讲旧事。
  • 软切句门槛:默认 7 字。越小开口越快,太大像念稿。

人格 留空就用 runtime/persona.example.md。写了以你的为准。 建议只写「它是谁、怎么说话、什么不干」,别写任务清单 —— 那是派活。

怎么说话

  • 开口概率:被叫到时真的开口的概率。留一点「懒得理」的余地,1.0 反而假。
  • 说完到下一句的间隔:默认 300ms。原来设 1200ms,结果说两句就硬等 1.2 秒,一顿一顿的。
  • 只听不说:调 ASR 用,能听见不开口。
  • 只连不进房:自检用,验证登录和协议,不真的占频道。
  • VAD 参数:判断「在说话」的能量门槛。房里底噪大就往上调。

它是怎么做到的

展开(这部分是踩出来的,不是读文档读来的)

1. 登录:纯 HTTP,不用客户端

POST /client/v1/login/v2/login:

  • 密码用 RSA-OAEP(SHA256) 加密
  • 签名是 RSA-SHA256(MD5(path + body) + oopzTime)
  • 两把内置密钥藏在客户端里:反转 → base64url → XOR → zlib

2. Agora appId 藏在 token 里

enter 接口返回的 data.appId 恒为 0,不能用。 真值在 supplierSign 字段里 —— token 是 "007" + base64(zlib(payload)), zlib.inflateSync 之后按小端读出 appId / roomId / uid。

代码里 rtc.appId 留空就会自动解,不用手填。

3. 进房之后成员会「过期」

POST /area/v2/channel/enter 是幂等的,可以反复发。 成员寿命只有 60–80 秒——不发续期心跳就会从频道里消失。

官方 oopz-sdk 的 services/voice.js 是个空壳:startIdentityHeartbeat() 没有实现, join() 返回假 token。所以这部分是自研的:每 20 秒重发一次 enter,实测 7 分 41 秒零掉线。

4. 音频:headless Chrome + Agora Web SDK

Node 里没有 WebRTC,所以起一个 headless Chrome 跑 Agora Web SDK:

  • createClient({ mode: 'live', codec: 'vp8' }) + setClientRole('host')
  • 用 createCustomAudioTrack 推自己合成的 PCM —— 这会绕过 Agora 的 3A (降噪 / 回声消除 / 自动增益)。好处是不被处理坏,代价是底噪得自己管。
  • ⚠️ connection-state-changed 事件在 live + host 下不触发。 必须直接读 client.connectionState 属性,否则看门狗会误判断线、把自己踢出房间。

5. 延迟预算

环节 耗时
静音判定 650ms(等对方说完,省不掉)
ASR 86–137ms
大模型首字 ~500ms(物理下限,砍人格只省 90ms,不划算)
TTS 55ms/字,线性
推流 + 对方听到 ~100ms
合计 ~1.6s

TTS 没有并行空间 —— 它是逐字合成的,13 个字就是 629ms。所以句子越短开口越快, 这也是 软切句门槛 存在的理由。


踩过的坑

这一节可能是整个仓库最值钱的部分。

展开

音量对了,但对方说「声音嘈杂」 不是音量问题,是底噪。TTS 原始输出有 −40dBFS 的底噪、峰值只留 −0.9dBFS。 加了 20ms 分帧的噪声门(−38dB 门限、80ms 平滑)+ 峰值压到 −3.0dBFS + 48kHz 链路之后, 底噪降到 −57~−61dBFS。感知上的「吵」是底噪,不是响度。

她把旁白念出来了 模型输出了「(没说话,尾巴在水里轻轻摆了一下)」,TTS 老老实实念了。 正则切掉所有括号内容,整句纯旁白当沉默处理。

「滚」被听成「我」,她回「哦,你俩认识啊」 ASR 会编。加了可信度守卫:音频超过 0.8 秒、但字数/秒 < 2.2 → 判定「没听清」, 只许问「啊?」「没听清」。宁可显得耳背,也不要答非所问。

「哈」被听成「烟」,她回「烟啊,我闻着呛」 极短句的 ASR 天生不可靠。策略是把 history 里的原文换成占位符—— 让她看不见听错的字,只知道自己「应了一声」。

说两句就一顿一顿 minReplyGapMs = 1200 被写在每句之前。改成回合开始计一次、降到 300ms, 句间只留 70–190ms 随机气口。

每句都是同一个调 speed 固定、音量固定 → 听着像机器。 现在每句 speed 在 0.93–1.00 之间随机、音量 ±3.5% 浮动。

TTS 遇到中文引号就跳过 “ ” 在模型词表里是 OOV,合成时会静默跳过。clean() 里直接剔除。


⚠️ 已知限制(诚实的部分)

这些是实测结论,不是没调好:

限制 原因
声音有点闷 TTS 模型只有 16kHz。电话带宽本身就容不下通透感,换模型才能解决
英文词全丢 SenseVoice 中文版的 token 表里没有英文词。混说「GPU」这种会念成中文或跳过
极短句经常听错 一两字音频信息量太少。已用「可信度守卫」降级处理,但消不掉
首字最快 ~500ms 完整人格 prompt 是 525–731ms,砍成极短 prompt 只到 480–573ms。差 90ms,不值得
不能处理音乐/多人同时说话 能量 VAD 只认「有声音」,分不出谁在说
协议可能随时失效 走的是 Oopz 私有协议,客户端升级可能改签名或加密方式

隐私

  • DeepSeek Key:单独存 <DSH_HOME>/oopz-voice/secret.json,API 永不回显
  • Oopz 凭据:存 cred.json,只在本机,不进配置文件、不进 git(.gitignore 已挡)
  • 音频:ASR / TTS 全在本地跑。只有识别出来的文字会发到大模型接口
  • 所有 HTTP 路由都过 DSH 的 requestRejection 信任栅栏 —— 端口在回环地址上, 但浏览器里任何一个网页都能往回环发请求,不设栅栏等于把「启动进程」这个能力开放给任意页面

合规

本项目通过逆向分析 Oopz 客户端实现语音房接入,不是 Oopz 官方提供的能力。

  • 仅用于你自己有权限的账号和频道
  • 不要在别人的房间里用、不要用来刷屏或骚扰
  • 语音房里的其他人不知道说话的是程序 —— 请自行判断这在你的场景里是否合适
  • 因使用本插件导致的账号封禁、协议失效,作者不负责

目录

lib/
  index.js         宿主半边:路由、起停、体检、oopz_voice 工具
  client.js        设置页(DSH 设置 → 语音房)
  config.js        配置单一真相源
  runtime.js       子进程生命周期 + @@VOICE 事件解析
  detect.js        Python / Chrome / 模型 探测
  secret.js        密钥单存
  node-runtime.js  找可用的 node(躲开桌面端 process.execPath 是 Harness.exe 的坑)
  agora.js         从 token 里解 appId
runtime/
  bridge.mjs       主循环:Oopz 协议 + VAD + 编排
  brain.mjs        大脑:切句、防重复、可信度守卫
  asr.py           SenseVoice(sherpa-onnx int8)
  tts.py           VITS 中文(sherpa-onnx)+ 噪声门
  agora-agent.html headless Chrome 里的音频桥
  persona.example.md  示例人格
tools/
  login.mjs        登录并写凭据
  rooms.mjs        列出域 / 频道 / 人数
  get-models.mjs   下模型(走 HF 镜像)

License

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit 7d88b2d033f4

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout