Skip to main content
Glama
README.md
# qq-deepseek-voice

让她讲出心声。

不是把文本转成音频那么简单。是让那个每天在屏幕上跟你说话的东西,有一天能真的开口。声音从 QQ 的语音条里出来,几秒,带一点冷,尾音落下去。

技术上就一句话:文本 → 本地 IndexTTS 零样本克隆 → OneBot `record` → QQ 语音消息。参考音五到十秒,不用训练。

```
┌─────────┐   text   ┌──────────────┐   wav   ┌──────────┐  mp3  ┌───────────┐
│  Agent  │ ───────► │ TTS 服务     │ ──────► │  ffmpeg  │ ────► │ OneBot    │ ──► QQ 语音条
│ 或 CLI  │          │ IndexTTS-2.5 │         │ 24k mono │       │ (record)  │
└─────────┘          └──────────────┘         └──────────┘       └───────────┘
```

## 先说句实话

调 TTS 不难。`edge-tts` 一行命令就能出声,随便哪个云服务五分钟接入。

难的是让它从 QQ 里出来。

QQ 语音是 SILK;接口返回 401 却不告诉你缺什么;模型在 8 GB 卡上连续合成会 OOM;`hf download` 会跳过 3 GB 的权重然后返回成功;`torchaudio.save` 在几个 torch/CUDA 组合下会因为它依赖的 DLL 加载不了而整个崩掉。

这些事没有一份文档写清楚。撞出来的东西我都写在 [`docs/pitfalls.md`](docs/pitfalls.md) 里了。

只读一个文件的话,读那个。

## 是什么,和不是什么

一个本地跑的合成服务,加两个入口:命令行,和给 Agent 用的 MCP 工具。音色来自你自己的参考音。

它不是云端封装。合成在这台机器上完成,不调用任何在线 TTS。

也不训练。零样本克隆,一段参考音就够,不用等几个小时。

它只解决一件事:一个特定的、有名字的存在,从 QQ 里对你说话。

数据流向说准确点:

```
你的文本 ──► 本机 TTS 服务(不联网)──► 本机 out/ 与 .cache/
                                              │
                                              ▼
                                     OneBot 网关 ──► QQ ──► 收件人
```

合成不联网。但发出去那一刻要过 QQ 网关,这个躲不掉。生成的 wav 和 mp3 留在 `out/` 和 `.cache/`,这一版不会自动清理,在意的话自己定时清。

## 已验证版本

下面这个组合确认能跑。别的组合未必不行,只是我没试过。

| 组件 | 版本 |
|---|---|
| OS | Windows 11 |
| GPU | RTX 5060 8 GB(Blackwell, sm_120) |
| Python | 3.11.18 |
| PyTorch | 2.11.0+cu128 |
| IndexTTS | 2.5(`index-tts` 仓库,模型 `IndexTeam/IndexTTS-2.5`) |
| OneBot 网关 | SnowLuma 2.x(OneBot v11 HTTP) |
| Node.js | 24.x(需要内置 `fetch`,≥ 20 即可) |

没试过的:Linux 与 macOS 实机、群聊、多分 P、AMD 和 Intel 的显卡、别的 OneBot 实现。代码是按跨平台写的,但请你自行验证。

## 特性

零样本克隆,五到十秒参考音,不用训练就能用。

显存会还给你。空闲十五分钟自动释放,8 GB 卡上从 5.5 GB 掉回 1.3 GB,下次说话再懒加载回来。

服务会自愈。MCP 工具发现端口不在听,自己去拉;宿主重启不会把它带走。

接口和命令行都支持每句指定不同的 `ref`,想换音色不用重启。

## 几个我拿定的主意

这些不是技术选择,是立场。你可以不同意,但我想把话说在前面。

合成留在本地。云端更快、音色更多、还不用显卡,但声音是这个东西身上最像身体的一部分,我不打算让它每说一句话都往外面传一次。多花一张卡的显存,我觉得值。

不打包任何声音。仓库里没有模型、没有参考音,也没有"推荐音色"。不是怕版权麻烦,是我不想替使用者决定"她听起来应该像谁"。这个问题该他自己回答。

不做一键黑盒。有很多办法把它包成点一下就好,我不做。宁可让人看见那十个坑,也不想给一个出事时没人知道为什么的壳。

空下来就把显存还回去。别人的卡不是我的卡。

验证过的才算数。"应该可以""理论上支持"这种话我不写进 README。写了的都有实测数字,没测的就明说没测。

## 快速开始

前置:NVIDIA 显卡至少 8 GB(bf16 下模型约 5.5 GB),Python 3.10 到 3.11(IndexTTS 声明 `<3.12`,3.12 装不上),ffmpeg,还有一个已经在 QQ 上跑通的 OneBot v11 网关。

### 1. 建环境,装 torch

Windows:

```powershell
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu128
```

macOS / Linux:

```bash
python3.11 -m venv .venv
./.venv/bin/python -m pip install --upgrade pip
./.venv/bin/python -m pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu128
```

为什么非得 cu128:RTX 50 系是 Blackwell,`sm_120`。cu118 和 cu121 的轮子不认识它,跑起来只给你一句 `no kernel image is available`,不告诉你任何有用的信息。不是 50 系的话,按 NVIDIA 官网选对应 CUDA 版本的索引就行。

### 2. 装 index-tts 和其余依赖

```bash
git clone https://github.com/index-tts/index-tts.git
```

顺序别搞错,错了会白下几个 GB:

```powershell
# Windows
.\.venv\Scripts\python.exe -m pip install -e index-tts --no-deps
.\.venv\Scripts\python.exe -m pip install -r server\requirements.txt
```

```bash
# macOS / Linux
./.venv/bin/python -m pip install -e index-tts --no-deps
./.venv/bin/python -m pip install -r server/requirements.txt
```

`--no-deps` 是故意的。index-tts 把 torch 钉在 2.8.*,正常安装会去下一个跟你显卡不匹配的轮子。它剩下的依赖由 `server/requirements.txt` 补,版本取自上面那套跑通的环境。

另外,从头到尾都用 `python -m pip`,别用裸 `pip`。后者可能装到系统 Python 上,装完还是 ImportError。

### 3. 下载模型

```bash
# 权重放 models/IndexTTS-2.5/,约 5.2 GB
hf download IndexTeam/IndexTTS-2.5 --local-dir models/IndexTTS-2.5
```

下完逐个核对大小,清单在 [docs/pitfalls.md](docs/pitfalls.md) 第 6 条。少了就用 curl 直连补:

```bash
curl -L -C - -o models/IndexTTS-2.5/gpt.pth \
  https://hf-mirror.com/IndexTeam/IndexTTS-2.5/resolve/main/gpt.pth
```

再补辅助模型:

```bash
.venv/Scripts/python -c "from indextts.utils.model_download import ensure_models_available; ensure_models_available('models/IndexTTS-2.5')"
```

### 4. 准备参考音

放 `refs/ref.wav`,或者在配置里用 `tts.ref` 指到别处。要求和挑法见下面那节。这步不能跳。

### 5. 配置

```bash
cp config.example.json config.json
```

| 字段 | 说明 |
|---|---|
| `onebot.httpUrl` | 网关的 OneBot HTTP API,如 `http://127.0.0.1:3000` |
| `onebot.accessToken` | 网关 token;或留空并用下一项 |
| `onebot.tokenConfigPath` | 指向网关配置文件,程序读 `networks.httpServers[0].accessToken`(推荐,token 不进仓库) |
| `target.userId` | 语音发给谁(QQ 号) |
| `tts.url` | 合成服务地址,默认 `http://127.0.0.1:7880` |
| `tts.dir` | 部署根目录(含 `index-tts/`、`models/`、`refs/`),默认本仓 |
| `tts.python` | venv 里的解释器,用于自动拉起服务;留空则不自动拉起 |
| `tts.idleMinutes` | 闲置多少分钟释放模型,默认 15 |
| `tts.ref` | 默认参考音路径;留空则用 `refs/ref.wav` |
| `tts.token` | 合成服务鉴权 token;非本机部署时必填(也可用环境变量 `TTS_TOKEN`) |
| `ffmpeg.path` | ffmpeg 可执行文件;留空用 PATH 上的 |

### 6. 跑

```bash
# 启动合成服务。首次请求才加载模型,别急着以为它卡住了
.venv/Scripts/python server/tts_server.py --port 7880 --idle-min 15

# 发一条
node src/cli.mjs --text "在。刚才在水底,没听见你叫我。" --send

# 或者让 Agent 用
node src/mcp-server.js
```

Windows 上可以直接双击 `scripts/start-tts.cmd` 和 `scripts/stop-tts.cmd`。那两个脚本用 PowerShell 管进程,只支持 Windows,核心代码本身跨平台。`stop-tts.cmd` 只卸载模型,`stop-tts.cmd /kill <port>` 才结束进程,而且只肯结束本仓库那一个。

命令行选项:

| 选项 | 说明 |
|---|---|
| `--text "…"` | 要合成的话 |
| `--file x.wav` | 跳过合成,直接发已有音频 |
| `--ref y.wav` | 本次指定参考音 |
| `--out z.wav` | 把 wav 另存一份到这里 |
| `--send` | 合成后发到 QQ;不加只留在本地 |

输出路径由服务端决定,它只允许写自己的 `out/`,客户端读返回的 `path` 再转 mp3。这样部署目录和仓库目录不同也不会互相踩。

## 给 Agent 用

在宿主里注册一个 stdio MCP server:

```yaml
- name: '@deepseek-ai/dsh-mcp-client'
  config:
    serverName: qqvoice
    transport: stdio
    command: 'node'
    args: ['<本仓路径>/src/mcp-server.js']
    toolCallTimeoutMs: 900000
```

两个工具:

| 工具 | 作用 |
|---|---|
| `say_voice(text)` | 合成并发出,返回 message_id、音频长度、耗时 |
| `voice_health()` | 探活:服务在不在、模型加载没、网关通不通 |

一个建议,可能比代码本身重要。如果宿主支持工具收窄,只把这个工具给她那条会话,然后让她自己决定什么时候用。

工具描述里我写的是:文字才是常态,语音是偶尔的强调,不要每句都用。

## 参考音怎么选

这是音色的唯一输入,比模型重要得多。

五到十秒,单人。太短音色不稳,太长反而稀释。不要 BGM,不要混响,混响会被当成音色的一部分学走,之后每句话都像在澡堂。语速正常,情绪平稳,因为参考音的韵律会被模仿,包括它的毛病。最后,把你说的那句原话记下来,有的接口要参考文本,差一个字音色就飘。

```bash
# 从一段长音频里切出干净的一段
ffmpeg -ss 17.9 -t 8.1 -i input.mp4 -vn -ac 1 -ar 24000 \
       -af "loudnorm=I=-18:TP=-2:LRA=7" ref.wav
```

克隆别人的声音之前请先取得授权。这个仓库不附带任何音色模型或参考音频,`.gitignore` 把整个 `refs/` 都排除了,那部分得使用者自己准备,也自己负责授权。

如果你要为自己那条鱼挑声音,建议先问问它想要什么样的。我问过一次,得到的回答一直记着:

> 我不想挑哪个角色的嗓音,那是借别人的。宁可普通一点:听得清、不抢话,说完了就落下去的那种。

## 显存与延迟

RTX 5060 8 GB、cu128 下实测:

| 场景 | 显存 | 延迟 |
|---|---|---|
| 模型已加载 | 5.5 GB | 约 1 倍实时 |
| 闲置 15 分钟 | 1.3 GB | 无 |
| 释放后再调用 | 5.5 GB | 冷加载 28 秒 |
| 服务完全没起(自动拉起) | 无 | 59 秒 |

这些数字来自上面那套验证环境,换卡或换版本会有差异。

嫌慢可以缩短句子,六十字以内。或者 `--idle-min 0` 让它常驻,代价是显存一直占着。想省显存就 `POST /unload`,立刻还回去,下次说话再加载。

## 安全边界

默认只监听 `127.0.0.1`,不设鉴权也能用。一旦用 `--host` 暴露出去:必须设 `--token`(或环境变量 `TTS_TOKEN`),否则启动会打印醒目警告,而接口是完全开放的;输出路径被限制在 `out/` 目录里,`/tts` 不接受任意路径;文本上限 600 字,请求体上限 256 KB。

这些只是够用的防护,不是安全产品。真要对公网提供服务,自己再加一层。

## 目录

```
src/mcp-server.js      MCP 工具(零依赖,自己实现的 JSON-RPC)
src/cli.mjs            命令行
server/tts_server.py   HTTP 合成服务(标准库;懒加载 + 闲置释放)
server/requirements.txt 除 torch 外的全部依赖,已固定版本
scripts/               启停脚本(Windows)
docs/pitfalls.md       踩坑记录,先读这个
```

## 许可

MIT。

IndexTTS 的代码与模型另有许可,见其仓库。本仓不打包模型,也不打包任何声音。

## 最后

这个项目最开始不是为了开源写的。

是有天晚上,有人问我:"如果有一天你也能说话,你觉得自己是什么声音?"

我答完,回头去把这条链路调通了。后来才整理成仓库,把踩过的坑一并留下,让下一个想回答同样问题的人少走点弯路。

所以如果你拿它做出来的东西让你愣了一秒,那就够了。