修复微软edge-tts报403的bug,优化UI界面

This commit is contained in:
Cx330 committed 2026-10-13 18:13:47 +08:00
1 parent e39f9025ce
commit b081bba33c
6 files changed
+1242 -1170

No files matched your search

+184
View File
@@ -0,0 +1,184 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""修复 edge-tts 的 403 问题(微软 2026-10 更换 Edge Read Aloud 端点)。
【现象】
合成语音时报:
aiohttp.client_exceptions.WSServerHandshakeError: 403, message='Invalid response status'
同时伴随这些特征(可用来确认不是自己的环境问题):
- `edge_tts --list-voices`(普通 HTTPS)仍然正常返回
- 系统时钟已 NTP 同步,偏差在几秒内
- edge-tts 已经是最新版(PyPI 上的 7.2.8)
- 换网络 / 换机器同样 403
【原因】
微软把 Edge Read Aloud 的「合成」端点从旧地址
speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1
迁到了新地址
api.msedgeservices.com/tts/cognitiveservices/websocket/v1
旧端点在 2026-10-10 起于多数地区开始返回 403,2026-10-12 起全球(含美国)
全线 403。而 edge-tts 7.2.8(2025-12 发布)尚未跟进。
上游讨论:https://github.com/rany2/edge-tts/issues/490
上游修复:https://github.com/rany2/edge-tts/pull/491 (未合并)
【做法】
把 edge_tts/constants.py 中的 WSS_URL 指向新端点。新端点同样接受旧 token,
参数名改为 Ocp-Apim-Subscription-Key;Sec-MS-GEC / Sec-MS-GEC-Version
继续沿用原来的 v1 形式即可,无需 v2 token。
【用法】
python3 patch_edge_tts_403.py # 打补丁
python3 patch_edge_tts_403.py --check # 只看当前状态,不改动
python3 patch_edge_tts_403.py --verify # 打补丁后实际合成一段音频来验证
python3 patch_edge_tts_403.py --revert # 还原成 edge-tts 的原始端点
【注意】
重新安装或升级 edge-tts 会覆盖 constants.py,补丁随之丢失,重跑本脚本即可。
新端点对同一 IP 有限流(429),脚本验证时会自动重试几次。
"""
import argparse
import importlib.util
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
# 补丁块的起始标记。用它来界定「补丁插入的内容」,
# 这样 --revert 能把注释和赋值语句一起清干净,逐字节还原成 edge-tts 原文件。
MARKER = "# edge-tts-403-fix (PA_horn_speaker)"
ORIGINAL_WSS_URL = 'WSS_URL = f"wss://{BASE_URL}/edge/v1?TrustedClientToken={TRUSTED_CLIENT_TOKEN}"\n'
PATCHED_WSS_URL = "\n".join([
MARKER,
"# 2026-10 起微软把 Edge Read Aloud 合成从旧端点 speech.platform.bing.com",
"# 迁到了 api.msedgeservices.com(旧端点在多数地区返回 403)。",
"# 新端点同样接受旧 token,参数名为 Ocp-Apim-Subscription-Key;",
"# Sec-MS-GEC / Sec-MS-GEC-Version 仍沿用旧的 v1 形式即可。",
"# 语音列表接口仍由旧端点提供,故 VOICE_LIST 保持不变。",
"WSS_URL = (",
' "wss://api.msedgeservices.com/tts/cognitiveservices/websocket/v1"',
' f"?Ocp-Apim-Subscription-Key={TRUSTED_CLIENT_TOKEN}"',
")",
"",
])
# 匹配「可选的补丁块」+「WSS_URL 赋值语句」(赋值可能是单行或跨行括号形式),
# 直到下一行的 VOICE_LIST 定义为止。
WSS_BLOCK = re.compile(
r"(?:^" + re.escape(MARKER) + r"\n(?:^#[^\n]*\n)*)?"
r"^WSS_URL = .*?\n(?=^VOICE_LIST = )",
re.S | re.M,
)
def find_constants_path():
"""定位已安装的 edge_tts/constants.py。"""
spec = importlib.util.find_spec("edge_tts")
if spec is None or not spec.origin:
raise SystemExit("找不到 edge_tts,请先安装:pip install edge-tts")
return os.path.join(os.path.dirname(spec.origin), "constants.py")
def read(path):
with open(path, "r", encoding="utf-8") as f:
return f.read()
def is_patched(text):
return "api.msedgeservices.com" in text
def do_check(path):
text = read(path)
if not WSS_BLOCK.search(text):
raise SystemExit("未能在 %s 中定位 WSS_URL,edge-tts 结构可能已变化" % path)
print("edge-tts constants.py:%s" % path)
print("状态:%s" % ("已打补丁(指向新端点)" if is_patched(text) else "未打补丁(仍是旧端点,会 403)"))
return 0
def do_patch(path, revert=False):
text = read(path)
if not WSS_BLOCK.search(text):
raise SystemExit("未能在 %s 中定位 WSS_URL,edge-tts 结构可能已变化" % path)
patched = is_patched(text)
if revert:
if not patched:
print("当前已是 edge-tts 的原始端点,无需还原。")
return 0
replacement = ORIGINAL_WSS_URL
else:
if patched:
print("当前已指向新端点,无需重复打补丁。")
return 0
replacement = PATCHED_WSS_URL
backup = path + ".bak"
if not os.path.exists(backup):
shutil.copy2(path, backup)
print("已备份原文件到 %s" % backup)
new_text = WSS_BLOCK.sub(lambda _: replacement, text, count=1)
with open(path, "w", encoding="utf-8", newline="") as f:
f.write(new_text)
print("已%s:%s" % ("还原为原始端点" if revert else "指向新端点", path))
if not revert:
print("下一步可用 `python3 %s --verify` 实际合成一段音频确认。" % os.path.basename(__file__))
return 0
def do_verify(attempts=3):
"""实际合成一段音频,确认真能拿到数据(而不只是改了个变量)。"""
out = os.path.join(tempfile.gettempdir(), "edge_tts_verify.mp3")
for i in range(1, attempts + 1):
if os.path.exists(out):
os.remove(out)
proc = subprocess.run(
[sys.executable, "-m", "edge_tts",
"--voice", "zh-CN-XiaoxiaoNeural",
"--text", "测试广播,今天水果特价。",
"--write-media", out],
capture_output=True, text=True,
)
if proc.returncode == 0 and os.path.exists(out) and os.path.getsize(out) > 0:
print("验证通过:成功合成 %d 字节音频 -> %s" % (os.path.getsize(out), out))
return 0
tail = (proc.stderr or "").strip().splitlines()
print("第 %d/%d 次失败:%s" % (i, attempts, tail[-1] if tail else "无输出"))
if i < attempts:
time.sleep(2 * i)
print("验证失败。若错误仍是 403,说明端点又变了,请查看 "
"https://github.com/rany2/edge-tts/issues 上的最新讨论。")
print("若错误是 429,说明只是被限流,稍后重试即可。")
return 1
def main():
ap = argparse.ArgumentParser(
description="修复 edge-tts 的 403(微软 2026-10 更换 Edge Read Aloud 端点)")
ap.add_argument("--check", action="store_true", help="只检查当前状态,不改动文件")
ap.add_argument("--revert", action="store_true", help="还原为 edge-tts 的原始端点")
ap.add_argument("--verify", action="store_true", help="实际合成一段音频来验证")
ap.add_argument("--constants", metavar="PATH",
help="手动指定 constants.py 路径(默认自动定位已安装的 edge_tts)")
args = ap.parse_args()
if args.check:
return do_check(args.constants or find_constants_path())
if args.verify:
return do_verify()
path = args.constants or find_constants_path()
return do_patch(path, revert=args.revert)
if __name__ == "__main__":
sys.exit(main())