张洋 1 month ago
parent
commit
9a92b7b9f2
1 changed files with 373 additions and 0 deletions
  1. 373 0
      zhipuzi_pos_windows/tool/tts/README.md

+ 373 - 0
zhipuzi_pos_windows/tool/tts/README.md

@@ -0,0 +1,373 @@
+# 腾讯云语音合成(TTS)Windows C++ 对接指南
+
+本目录包含:
+- REST 版轻量客户端(`TextToVoice`,带情感参数)
+- 流式客户端(`TextToStreamAudioWS`,OpenSSL 自建 WSS)+ waveOut 实时播放器,**全程不落盘**
+- 给官方语音 SDK 打情感补丁的说明(可选方案)
+
+依赖只有两个:**libcurl** + **OpenSSL**。支持 **Windows 7 及以上**(`_WIN32_WINNT=0x0601`),
+Win7 的适配说明见 [第九节](#九win7--_win32_winnt0x0601-兼容)。
+
+---
+
+## 一、三条接入路径,先选一条
+
+| 路径 | 协议 | 情感参数 | C++ 成本 | 适合场景 |
+|---|---|---|---|---|
+| **A. 基础语音合成 `TextToVoice`**(本目录 `TtsClient`) | HTTPS REST | ✅ 原生支持 `EmotionCategory`/`EmotionIntensity` | 低:libcurl + OpenSSL | 短文本播报、配音、生成文件 |
+| **B. 实时/流式语音合成 `TextToStreamAudioWS`**(本目录 `StreamTtsPlayer`) | WebSocket (wss) | ✅ 支持 | 低:OpenSSL 自建 WSS,不依赖 Win8+ API | 边合成边播放、LLM 逐字输出、实时客服 |
+| **C. 长文本语音合成 `CreateTtsTask`** | REST 异步 | ⚠️ 免费额度不含 | 低(同 A,换接口) | 有声书、10 万字以内批量导出 |
+
+**建议路径**:先用 A 跑通业务闭环 → 需要"边生成边出声"再上 B。
+
+> 官方语音 SDK(`tencentcloud-speech-sdk-cpp`)也能做 B,但它要编 Boost/OpenSSL/websocketpp,
+> 且未暴露情感 setter。本目录自己实现 WSS,依赖更少、情感参数直接支持,且 Win7 可用
+> (WinHTTP 的 WebSocket API 是 Win8+ 才有的,Win7 工程编不过)。
+> 若你已在用官方 SDK,见 `patch/官方语音SDK增加情感参数.md`。
+
+---
+
+## 二、开通与准备(10 分钟)
+
+1. 开通服务:https://console.cloud.tencent.com/tts
+2. 领取免费额度(**必须手动领,不领默认没有**):控制台 → 资源包,三类:
+   - 基础/精品音色 **800 万字符**
+   - 大模型音色 **10 万字符**
+   - 超自然大模型音色 **2 万字符**
+   - 自领取起 **3 个月内有效,一个账号限领一次**
+3. 建密钥:https://console.cloud.tencent.com/cam/capi → 新建密钥,拿到 `SecretId` / `SecretKey`
+4. **取账号 `AppId`**(只有 B 路径的流式接口需要,REST 接口不用):
+   官网右上角**头像** → **账号信息** → 基本信息栏里的 **APPID** 一行,纯数字(如 `1300123456`)。
+   直达链接:https://console.cloud.tencent.com/developer
+
+   > ⚠️ 同一个页面还有个「**账号 ID(UIN)**」,也是一串数字,但**不是 AppId**,别拿错。
+   > 填错的典型现象是流式接口握手返回 **403**。
+   > 另外别和 `SecretId`(`AKID` 开头的字符串)混为一谈。
+5. **试听选音色(最关键的一步)**:https://cloud.tencent.com/document/product/1073/92668
+   - 筛选"情感"场景 → 只有**多情感音色**才支持 `EmotionCategory`,普通音色传了会被忽略
+   - 记下音色 ID(如 `101001`),这就是代码里的 `voice_type`
+   - 注意每个音色标注的档位(基础/精品/大模型/超自然),直接决定单价
+
+---
+
+## 三、跑通路径 A(本目录代码)
+
+### 1. 装依赖
+
+只需要 **libcurl** 和 **OpenSSL** 两个,都是常见库:
+
+```powershell
+git clone https://github.com/microsoft/vcpkg
+.\vcpkg\bootstrap-vcpkg.bat
+.\vcpkg\vcpkg install curl:x64-windows openssl:x64-windows
+# Linux: sudo apt install libcurl4-openssl-dev libssl-dev
+```
+
+各自的分工:
+
+| 依赖 | 用在哪 |
+|---|---|
+| libcurl | 路径 A 的 REST 请求(`TextToVoice`) |
+| OpenSSL | ① TC3 签名 / 流式签名的 HMAC;② 流式接口的 WSS 通道(自建,见下) |
+
+> 流式接口(WebSocket)**没有用 WinHTTP**,而是用 OpenSSL + 原生 socket 自己实现的,
+> 原因见 [第九节](#九win7--_win32_winnt0x0601-兼容)。这是 Win7 能跑起来的关键。
+
+### 2. 编译
+
+```powershell
+cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=D:/vcpkg/scripts/buildsystems/vcpkg.cmake
+cmake --build build --config Release
+```
+
+### 3. 运行
+
+```powershell
+set TENCENTCLOUD_SECRET_ID=AKIDxxxxxxxxxxxxxxxx
+set TENCENTCLOUD_SECRET_KEY=xxxxxxxxxxxxxxxx
+.\build\Release\tts_demo.exe
+```
+
+产出 4 个文件:`out_normal.wav`(默认语气)、`out_happy.wav`(happy×150)、
+`out_sad.wav`(sad×180)、`out_long.mp3`(长文本分段拼接)。
+
+### 4. 接进你的工程
+
+```cpp
+#include "TtsClient.h"
+
+txcloud::TtsClient client(secret_id, secret_key);
+
+txcloud::TtsOptions opt;
+opt.voice_type       = 101001;      // 换成你选的多情感音色
+opt.codec            = "wav";       // wav / mp3 / pcm
+opt.sample_rate      = 16000;
+opt.speed            = 0.5f;        // [-2, 6]
+opt.volume           = 0.0f;        // [-10, 10]
+opt.emotion_category = "happy";     // 留空 = 默认语气
+opt.emotion_intensity= 150;         // [50, 200]
+
+std::string err;
+client.SynthesizeToFile(u8"今天天气真好,我们出去走走吧。", opt, "a.wav", &err);
+```
+
+拿到内存数据而非文件,用 `Synthesize()` 返回 `TtsResult::audio`(`std::vector<uint8_t>`),直接喂给你的播放器。
+
+---
+
+## 四、实时播放:不落盘 / 边合成边播
+
+两种情况,别混为一谈:
+
+| 你想要 | 用哪个 | 延迟 |
+|---|---|---|
+| 合成完立刻播出来,不要文件 | `PlayWavMemory()`(REST + waveOut) | 整段合成完才响,约 1~2 秒 |
+| 边合成边出声(LLM 逐字、实时播报) | `StreamTtsPlay()`(WebSocket 流式) | 首包约 300ms |
+
+### 做法 1:REST 合成 → 内存直接播放
+
+```cpp
+txcloud::TtsOptions opt;
+opt.voice_type = 101001;
+opt.codec      = "wav";       // 关键:wav 可解析出 PCM,mp3 需要额外解码器
+opt.sample_rate= 16000;
+opt.emotion_category = "happy";
+opt.emotion_intensity= 150;
+
+txcloud::TtsResult r = client.Synthesize("今天天气真好。", opt);
+if (r.ok) PlayWavMemory(r.audio);   // 阻塞直到播完,全程无临时文件
+```
+
+`PcmPlayer` 底层是 waveOut,8 个缓冲区轮转,可随时 `Stop()` 打断:
+
+```cpp
+PcmPlayer player;
+player.Open(16000, 1, 16);   // 16kHz / 单声道 / 16bit
+player.Feed(pcm_data, len);  // 可分多次喂
+player.FlushAndWait();       // 播完剩余
+player.Close();
+```
+
+### 做法 2:WebSocket 流式,边下边播(推荐做实时场景)
+
+网络层是 OpenSSL + 原生 socket 自建的 WSS(`WssClient`),**不用 WinHTTP**,
+所以 Win7 上照样能跑(WinHTTP 的 WebSocket API 是 Win8+ 才有的)。详见[第九节](#九win7--_win32_winnt0x0601-兼容)。
+
+```cpp
+StreamTtsOptions sopt;
+sopt.appid      = 1300000000;         // 控制台「账号信息」里的 AppId,流式接口必填
+sopt.secret_id  = secret_id;
+sopt.secret_key = secret_key;
+sopt.voice_type = 101001;
+sopt.codec      = "pcm";              // 实时播放必须是 pcm,裸流无需解码,收到即播
+sopt.sample_rate= 16000;
+sopt.emotion_category  = "story";
+sopt.emotion_intensity = 120;
+
+PcmPlayer player;
+player.Open(16000, 1, 16);
+std::string err;
+StreamTtsResult r = StreamTtsPlay("欢迎使用腾讯云实时语音合成。", sopt, player, nullptr, &err);
+player.Close();
+```
+
+需要自己接管音频(比如送进自研播放器 / 推流 / 落盘),用 `StreamTtsRun()` 拿分片回调:
+
+```cpp
+std::atomic<bool> cancel{false};
+StreamTtsRun(text, sopt,
+    [](const uint8_t* data, size_t len) { my_player.Feed(data, len); return true; },
+    &cancel, &err);
+```
+回调返回 `false` 或把 `cancel` 置 true,都能立刻中断。
+
+### 签名实现说明(重要)
+
+流式接口的签名与 REST 完全不同,且**官方文档描述与官方 SDK 实际实现不一致**,本实现按能跑通的 Python SDK
+(`tencentcloud-speech-sdk-python/tts/speech_synthesizer_ws.py`)对齐:
+
+- 签名原文 = `GET` + `tts.cloud.tencent.com` + `/stream_ws?` + 字典序 `k=v&k=v`,**值不做 urlencode**
+- 签名值 = `Base64(HMAC-SHA1(SecretKey, 签名原文))`
+- 最终 URL = `wss://tts.cloud.tencent.com/stream_ws?` + 字典序参数(**只有 Text 做 percent-encoding**)
+  + `&Signature=` + `urlencode(签名值)`
+- 必填参数:`Action / AppId / SecretId / Timestamp / Expired / SessionId / Text / Codec`
+
+常见失败:upgrade 握手失败(HTTP 401/403)→ 99% 是 **AppId 填错、时间戳过期、或 Signature 没 urlencode**。
+
+### 流式接口的能力边界
+
+- 单次文本:中文 600 汉字 / 英文 1800 字母(比 REST 的 150 字宽裕得多)
+- 采样率:仅 8000 或 16000;24k 不支持
+- 音频格式:仅 `pcm` / `mp3`,实时播放选 pcm
+- 并发:精品 20 路、大模型 20 路、超自然 10 路(与 REST 接口**不共享**)
+- 超自然音色不支持时间戳
+
+---
+
+## 五、情感调参要点
+
+| 参数 | 取值 | 说明 |
+|---|---|---|
+| `EmotionCategory` | `neutral / sad / happy / angry / fear / news / story / radio / poetry / call / sajiao / disgusted / amaze / peaceful / exciting / aojiao / jieshuo` | 中文音色常用:`happy / sad / angry / fearful→fear / exciting / peaceful / story / news / call / poetry` |
+| `EmotionIntensity` | `50~200`,默认 100 | 建议先从 100 起,每次 ±25 试听,超过 180 容易"演过头" |
+
+提效技巧:**同一段文本,用不同情感值和语速批量合成一批 wav,一次性对比试听**,比单次调参快得多。
+`demo.cpp` 里的三个输出就是这个思路。
+
+补充:腾讯云 TTS 也支持 SSML(控制停顿、局部语速/音调、多音字发音),需要更细的表现力时再上。
+
+---
+
+## 六、工程化注意事项(容易踩的坑)
+
+1. **长度限制**:基础语音合成单次 **中文 150 字**(英文 500 字母)。超长必须分段 ——
+   `SynthesizeLongTextToFile()` 已实现按标点切分 + 拼接。
+2. **拼接格式**:`wav` 后续分段会自动跳过 44 字节头的 data 块;`mp3` 直接顺序拼接;`pcm` 无头直接追加。
+   推荐长文本直接用 `mp3`。
+3. **并发限制**(默认,超出返回错误码 10002 / 并发超限):
+   - 基础/精品音色 20 路,大模型音色 20 路,超自然大模型音色 10 路
+   - 需要更高并发要买并发叠加包(精品 200 元/并发/月,大模型 350 元/并发/月)
+4. **密钥安全**:`SecretKey` 绝对不能打进客户端。桌面软件请走**后端中转** —— 客户端请求你的服务器,
+   服务器持有密钥调腾讯云并返回音频。要放客户端也至少做混淆 + 子账号最小权限 + 用量告警。
+5. **缓存**:同一文本+参数的结果存本地(按 `md5(text+voice+emotion)` 做 key),成本和延迟都能降一个量级。
+6. **成本**(后付费,按字符,汉字/字母/标点/空格都算):
+   - 精品音色 **0.3 元/万字符**
+   - 大模型音色 1.2 元/万字符起,量大降到 0.55
+   - 超自然大模型音色 6.5 元/万字符起,量大降到 4.9
+   - 例:每天 10 万字符的播报量,用精品音色约 **3 元/天**
+7. **UTF-8**:MSVC 必须加 `/utf-8`(CMakeLists 已配),否则中文字符串按本地 GBK 解析,合成出来是乱码发音。
+8. **时间戳**:`enable_subtitle=true` 可拿到逐字时间戳,做字幕同步/口型驱动;超自然音色暂不支持。
+9. **播放侧**:`PcmPlayer` 内部 8 个 8KB 缓冲区,`Feed()` 在缓冲满时会阻塞 —— 这是正常背压,
+   不要放到 UI 线程;实时场景建议开一个工作线程专门收数据 + 播放。
+
+---
+
+## 七、排障清单
+
+| 现象 | 原因 / 处理 |
+|---|---|
+| `AuthFailure.SignatureExpire` | 本机时间与标准时间偏差过大,开 Windows 时间同步 |
+| `AuthFailure.SignatureFailure`(REST) | ①`Content-Type` 必须是 `application/json; charset=utf-8`;②参与签名的 header 与实际发送的不一致 |
+| `InvalidParameterValue.TextTooLong` | 超过 150 字,走分段(或改用流式接口,上限 600 字) |
+| 返回成功但音频无声 | `Codec` 与播放方式不匹配;pcm 裸流没有文件头,播放器识别不了,先换 wav 验证 |
+| `FailedOperation.ServiceNotEnabled` | 没开通服务或免费额度没领取 |
+| 并发超限 | 代码里做请求队列 + 重试退避,或购买并发包 |
+| 情感没变化 | 音色不是"多情感音色",去音色列表页重新挑一个带"情感"标签的 |
+| 流式:`WebSocket upgrade rejected ... 403` | 签名错。依次查:AppId 是否为**数字账号 AppId**、Signature 是否 urlencode、Text 编码是否只做了一次 |
+| 流式:`certificate verify failed ...` | OpenSSL 在 Windows 上找不到根证书。设环境变量 `SSL_CERT_FILE` 指向 `cacert.pem`,或调 `SetCaFile()`;代码默认会尝试导入 Windows 系统根证书库 |
+| 流式:`TCP connect timeout` | 公司网络/代理拦了 443 出站。本机 `WssClient` 不走系统代理,需要在代理侧放行 `tts.cloud.tencent.com:443` |
+| 流式:`TLS handshake failed` | 见[第九节](#九win7--_win32_winnt0x0601-兼容)的 TLS 1.2 说明 |
+| 流式:有数据但无声/像噪音 | `Codec` 不是 `pcm`,或采样率传了 24000(流式只支持 8k/16k) |
+| 播放卡顿/断续 | `PcmPlayer` 在 UI 线程跑导致背压阻塞,移到工作线程;或把缓冲区调大 |
+| REST:`Problem with the SSL CA cert` / `SSL peer certificate or SSH remote key was not OK` | 证书校验失败。代码在 Windows + curl ≥ 7.71 下会自动走系统证书库(`CURLSSLOPT_NATIVE_CA`);若你的 curl 更老,下载 `cacert.pem` 后设 `TTS_CA_FILE` 或调 `client.SetCaFile()`。仅本地调试可 `client.SetVerifyPeer(false)` |
+| REST:Win7 上 TLS 握手失败 | curl 若是 SChannel 后端,Win7 默认没开 TLS 1.2,见[第九节](#九win7--_win32_winnt0x0601-兼容) |
+
+---
+
+## 八、下一步
+
+1. **接实时对话**:LLM 流式输出 → 按句号/分号攒句 → 每攒够一句就起一个 `StreamTtsPlay()`,
+   即可实现"边想边说"(官方推荐做法,比等全文生成完再合成体验好很多)。
+2. **字幕/口型同步**:`enable_subtitle=true`,`StreamTtsRun` 的文本帧里带逐字 `BeginTime/EndTime`,
+   用 `on_audio` 累计已播放字节数换算成播放进度,即可驱动字幕高亮或数字人口型。
+3. **想换官方 SDK**:见 `patch/官方语音SDK增加情感参数.md`,给 `tencentcloud-speech-sdk-cpp`
+   补两处即可支持流式 + 情感(代价是要编 Boost/OpenSSL/websocketpp)。
+
+---
+
+## 九、Win7 / `_WIN32_WINNT=0x0601` 兼容
+
+工程目标是 Win7(`_WIN32_WINNT=0x0601`)时,有 **两个硬伤** 要处理。下面是全部改动点,
+按这个改完就能在 Win7 上跑通。
+
+### 9.1 硬伤一:WinHTTP 的 WebSocket 是 Win8+ 的 API
+
+`WinHttpWebSocketCompleteUpgrade` / `WinHttpWebSocketReceive` 这些函数在 Windows SDK 里
+被 `#if (NTDDI_VERSION >= NTDDI_WIN8)` 包着。你把 `_WIN32_WINNT` 设成 `0x0601`,
+它们**直接不声明**,编译期就报「未定义标识符」,没有绕的余地。
+
+**解法**:流式接口的 WSS 通道换成 OpenSSL + 原生 socket 自己实现(`WssClient`):
+
+| | 原方案 | 现在 |
+|---|---|---|
+| 传输 | WinHTTP(Win8+) | OpenSSL + `socket()`(Win7 起全支持) |
+| WebSocket 协议 | 系统实现 | 自己实现 RFC6455 握手 + 帧编解码 |
+| 依赖 | `winhttp.lib` | `libssl` / `libcrypto` + `ws2_32.lib` |
+
+顺带两个好处:不依赖系统 SChannel 的 TLS 版本策略;Win7/Win10/Linux 一份代码。
+
+`WssClient` 已实现的细节(省得你再踩):
+
+- TLS 下限锁死 1.2(`SSL_CTX_set_min_proto_version`)、设 SNI、做主机名校验
+- 非阻塞 `connect` + `select` 实现连接超时(`SO_RCVTIMEO` 管不到 connect)
+- `SSL_read` **不保留 TLS 记录边界** —— 握手时很可能一次就把 HTTP 响应头和紧随其后的
+  WebSocket 帧一起返回,多出来的字节必须缓存下来给帧解析用,否则首帧音频会被吞掉
+- 分片(continuation)帧自动拼成完整消息;mask 索引按帧内偏移算(RFC6455 5.3)
+- 能区分「对方干净关闭」和「超时/中断」(`clean_eof()`),避免超时被误判成播放成功
+
+### 9.2 硬伤二:Win7 的 SChannel 默认不开 TLS 1.2
+
+腾讯云接口只接受 TLS 1.2+。Win7 的 SChannel 默认只到 TLS 1.0,
+要打 **KB3140245** 并改注册表 `SslProtocols` 才开得起来 —— 你没法要求每个客户都去打补丁。
+
+按你 curl 的编译后端分两种情况:
+
+| curl 的 TLS 后端 | Win7 表现 | 处理 |
+|---|---|---|
+| **OpenSSL**(推荐) | 正常,OpenSSL 自带协议栈 | 代码里已用 `CURLOPT_SSLVERSION` 锁死 TLS 1.2 |
+| SChannel | 握手失败 | 装 KB3140245,或换 OpenSSL 后端重编 curl |
+
+vcpkg 装 curl 时显式指定后端:
+
+```powershell
+.\vcpkg\vcpkg install curl[openssl]:x64-windows openssl:x64-windows
+```
+
+> 流式接口(WSS)不受这个影响 —— 它走的是你自己的 OpenSSL,跟 SChannel 没关系。
+
+### 9.3 其它改动点
+
+| 项 | 说明 |
+|---|---|
+| 签名算法 | 原 Windows 走 BCrypt、其它平台走 OpenSSL 的分支**已删除**,统一走 OpenSSL(`CryptoUtil`)。少链一个 `bcrypt.lib`,也不用维护两套实现 |
+| `_WIN32_WINNT` | CMakeLists 里原来写死了 `0x0602`,**已删除** —— 那是给 WinHTTP WebSocket 用的,现在不需要了,留着反而会覆盖你工程的设置 |
+| 链接库 | `winhttp.lib`、`bcrypt.lib` → `ws2_32.lib`、`crypt32.lib`、`libssl`、`libcrypto`。`winmm.lib` 保留(waveOut 播放) |
+| OpenSSL 版本 | 要求 **>= 1.1.0**(用到 `SSL_CTX_set_min_proto_version` / `SSL_set1_host`),低于此版本会 `#error` 提示 |
+| 编译器 | VS2013+(`std::strtoll`、C++11)。MSVC 必须加 `/utf-8`,否则中文按 GBK 解析 |
+| 证书 | Windows 上 OpenSSL 通常找不到根证书,`WssClient` 默认会导入 **Windows 系统根证书库**(`CertOpenSystemStoreA`),一般不用额外配置;真出问题再设 `SSL_CERT_FILE` 或调 `SetCaFile()` |
+
+### 9.4 手工接进 VS 工程(不用 CMake)
+
+把 `src/` 下这几个文件加进工程即可:
+
+```
+CryptoUtil.h/.cpp      签名与编解码(OpenSSL)
+WssClient.h/.cpp       流式接口的 WSS 通道(OpenSSL + socket)
+StreamTtsPlayer.h/.cpp 流式合成与播放
+TtsClient.h/.cpp       REST 合成(libcurl)
+PcmPlayer.h/.cpp       waveOut 内存播放(仅 Windows)
+```
+
+VS 工程属性要改这几处:
+
+| 位置 | 值 |
+|---|---|
+| C/C++ → 常规 → 附加包含目录 | curl 和 OpenSSL 的 `include` 目录 |
+| C/C++ → 命令行 → 附加选项 | `/utf-8` |
+| C/C++ → 预处理器 → 预处理器定义 | `OPENSSL_SUPPRESS_DEPRECATED`(消 OpenSSL 3.0 的 deprecation 警告) |
+| 链接器 → 输入 → 附加依赖项 | `libcurl.lib` `libssl.lib` `libcrypto.lib` `ws2_32.lib` `crypt32.lib` `winmm.lib` |
+| 链接器 → 常规 → 附加库目录 | curl 和 OpenSSL 的 `lib` 目录 |
+
+最后把 `libcurl.dll`、`libssl-3-x64.dll`、`libcrypto-3-x64.dll` 放到 exe 同目录。
+
+### 9.5 验证清单
+
+改完按这个顺序验,能快速定位问题在哪一层:
+
+1. `TtsClient::SynthesizeToFile()` 能出 `out_normal.wav` → REST + 签名 + curl TLS 都 OK
+2. `StreamTtsRun()` 收到 `audio_bytes > 0` → WSS 握手 + 签名 OK
+3. `StreamTtsPlay()` 能听出声音 → 播放链路 OK
+4. 情感切换有明显语气差异 → 音色是多情感音色,参数生效
+
+第 2 步失败时重点看 `err` 里的前缀:`TCP connect` / `TLS handshake` / `certificate verify` /
+`WebSocket upgrade rejected` 分别对应网络、TLS 版本、证书、签名四个不同环节。