本文目录
一、Edge 在线语音是什么
「有声小说书屋」客户端里内置了两套朗读引擎:
一套是本地朗读(OneCore),用 Windows 自带语音,完全离线;
另一套就是本文的主角------Edge 在线朗读,它调用微软的神经网络语音,音色自然(晓晓、云希、云健等十几个音色),但需要联网。
Edge 在线朗读的完整链路是这样的:

其中第 ② 步的「WebSocket 握手」最关键------如果握手时服务器不认可你,整条链路在开头就断了,后面都白搭。
二、问题现象:HTTP 403
问题出现时,用户点「朗读」会看到类似这样的报错,或者干脆没声音:
Edge 在线语音服务拒绝连接(HTTP 403)。
HTTP 403 的意思是「服务器拒绝访问」------不是网络断了,也不是接口不存在,而是服务器主动拒绝了我们发过去的连接请求。
一句话理解 403服务端收到了请求,但「验明正身」这一关没过,于是把连接关掉了。所以排查方向就是:我们提交给服务器的「身份凭证」哪里不对。
三、为什么会 403
微软这个 readaloud/edge/v1 接口不是公开 API,它是 Edge 浏览器「大声朗读」功能内部使用的一个私有接口。要访问它,必须在请求里带上一串「鉴权参数」,只要任何一个对不上,服务器就返回 403。
这些参数里,最关键的三个:
TrustedClientToken
一个固定令牌,相当于「入门卡」。微软会不定期更换,旧令牌会直接失效。
Sec-MS-GEC
一个签名值,格式 CS64-SHA256。这是最难的一环------它是一套「修改版」摘要算法。
Sec-MS-GEC-Version
签名版本号,跟着 Edge / Chromium 内核版本走,例如 2-143.0.3650.75。

3.1 签名算法变了
微软在 Edge 内核升级时,把 Sec-MS-GEC 从旧版换成了 v2 算法。它不是标准的 SHA-256,而是用了自定义的初始状态、替换过的第 14 轮常量、以及自定义的消息填充 ,另外还拼上一个独立的 CS64 摘要。旧版客户端算出来的签名和服务器对不上,自然被拒。1
3.2 版本号没跟上
签名的版本号 Sec-MS-GEC-Version 要和当前 Edge 内核一致。内核从旧版升级到 143.0.3650.75 之后,如果客户端还报旧版本号,服务器也不认。3
3.3 系统时钟不准(隐藏坑)
这是最隐蔽的一个原因。签名算法里有一个「5 分钟签名窗口」:它先把当前时间戳对齐到 5 分钟的整边界,再参与签名计算。如果本机系统时间不准 (快了几分钟或慢了几分钟),算出来的签名会落在服务器认定的「错误窗口」里,同样返回 403。2
为什么时钟会导致 403服务器只接受「最近 5 分钟」内的签名。本机时钟偏了之后,你以为是现在,服务器却觉得你的签名是「过去」或「未来」发出的,于是拒绝。
四、修复方案
针对上面的三个根因,做了四步修复,核心代码都在 EdgeTtsClient.cpp 里。
4.1 重写 Sec-MS-GEC v2 签名
按新版 edge-tts 的 drm.py 算法,逐常量对齐实现了「修改版 SHA-256 + CS64」,输出 <CS64 16位hex>-<SHA256 64位hex> 格式的签名。1签名输入是「校正后的时间戳 + 固定令牌」:
// 取系统时间(100ns 刻度),先校正时钟,再对齐到 5 分钟窗口
ticks += g_clockSkewTicks; // 时钟偏移校正
ticks -= ticks % (300ULL * 10000000ULL); // 对齐到 5 分钟
// 签名输入 = 时间戳字符串 + 固定令牌
std::string strToHash = tickText + kTrustedClientToken;
// 用修改版 SHA-256 + CS64 算出 Sec-MS-GEC
4.2 更新令牌与版本号
把 TrustedClientToken 更新为新令牌,并把 kChromiumVersion 提到 143.0.3650.75,最终请求路径的关键参数长这样:
/consumer/speech/synthesize/readaloud/edge/v1
?TrustedClientToken=6A5AA1D4EAFF4E9FB37E23D68491D6F4
&ConnectionId=<随机 16 字节 hex>
&Sec-MS-GEC=<CS64>-<SHA256>
&Sec-MS-GEC-Version=2-143.0.3650.75
4.3 系统时钟偏移校正
在首次合成时 ,先请求一次 voices/list 接口,读它响应头里的 Date(服务器时间),用它减去本机时间得到偏移量并缓存。之后每次算签名都用校正后的时间,从而稳定落在正确的 5 分钟窗口内:
// 通过 voices/list 的 Date 头,计算「服务器时间 - 本地时间」的偏移
g_clockSkewTicks = serverTicks - localTicks;
g_clockSkewValid = true; // 只算一次,之后复用缓存
4.4 顺手优化:minimp3 解码
修复过程中还发现一个体验问题:原来用 Windows 的 Media Foundation 解码 MP3,首次播放要冷启动数十秒 。于是换成单头文件的 minimp3 纯 C 解码器,直接从内存解 MP3 到 PCM 再交给 WASAPI 播放,首声响应的等待几乎消失。
五、效果与小结
修复之后,Edge 在线朗读恢复了正常:握手拿到 101(协议切换成功),服务器正常返回音频,403 不再出现。

小结
- 这类「私有接口」比较脆弱,微软一升级内核,签名和版本就要跟着变,否则旧客户端立刻失效。
- 排查 403 时,别只盯着签名算法,版本号 和系统时钟同样能导致拒绝,时钟偏移是这次最隐蔽的一环。
- 签名、令牌、版本、时钟四者对齐之后,在线朗读才稳定;离线场景仍可随时切回「本地朗读(OneCore)」,不依赖网络。
| 根因 | 表现 | 修复 |
|---|---|---|
| Sec-MS-GEC 签名算法升级 | 旧签名被拒,403 | 重写 v2 签名(修改版 SHA-256 + CS64) |
| TrustedClientToken / 版本号过期 | 凭证失效,403 | 更新令牌与 Chromium 版本号 |
| 系统时钟不准 | 签名落在错误窗口,403 | 首次合成时校正时钟偏移并缓存 |
| Media Foundation 解码慢 | 首次播放等待数十秒 | 换 minimp3 纯 C 解码器 |
参考资料(在线)
- edge-tts · drm.py 源码 --- Sec-MS-GEC v2 签名与时钟偏移校正的直接实现,代码注释「严格对应 edge_tts/drm.py」所指文件。edge-tts/src/edge_tts/drm.py at master · rany2/edge-tts · GitHub
- DeepWiki · rany2/edge-tts · DRM and Security --- 确定问题根因的关键页:系统时钟与微软服务器不同步会返回 403,需用响应的 Date 头计算偏移自动恢复。DRM and Security | rany2/edge-tts | DeepWiki
- kothok-edge-tts(crates.io)--- 佐证:微软按 Edge 发布周期轮换 SEC_MS_GEC_VERSION,过期后每次合成返回 HTTP 403,需重新 pin 版本号。https://crates.io/crates/kothok-edge-tts/0.2.1
- pyVideoTrans · Common Edge-TTS Errors --- 补充 403 的另一成因:同一 IP 短时间高频请求会触发限流。https://en.pyvideotrans.com/edgetts-error
- edge-tts(GitHub 项目)--- Edge 浏览器「大声朗读」私有接口的开源实现,本项目鉴权签名的总来源。GitHub - rany2/edge-tts: Use Microsoft Edge's online text-to-speech service from Python WITHOUT needing Microsoft Edge · GitHub
本地源码
- EdgeTtsClient.cpp --- Edge 在线语音客户端:鉴权签名、时钟偏移校正、合成与播放。g:\biancheng\GUDIANSHUWU\GUDIANSHUWU\EdgeTtsClient.cpp
- EdgeTtsClient.h --- 客户端类与音色目录(EdgeVoiceCatalog)声明。g:\biancheng\GUDIANSHUWU\GUDIANSHUWU\EdgeTtsClient.h