# -*- coding: utf-8 -*- r'''诊断:这台机器上哪几种语音播报方案能用。 == 为什么写这个 == 用户反馈:**Win7 上语音播报不能用**。 现在的实现只有一条路: powershell -NoProfile -WindowStyle Hidden -Command "Add-Type -AssemblyName System.Speech; $s = New-Object ...; $s.Speak('...')" 这在 Win7 上有几个可能致命的点: 1. **`powershell` 可能不在 PATH 里** Win7 的 powershell.exe 在 `%SystemRoot%\System32\WindowsPowerShell\v1.0\`。 正常装的机器 PATH 里有,但被精简过、或者从服务/计划任务里拉起时 可能没有 —— 而**我们是用 CreateProcess 按名字找的**,找不到就静默失败。 2. **Win7 默认是 PowerShell 2.0,它用 ANSI 代码页解析 `-Command`** PS 3.0+ 才改成按 UTF-16 处理命令行。所以中文经 `-Command` 传进去 在 PS 2.0 上会乱码 —— 表现是"念出来是乱码"或"什么都不念"。 3. **`System.Speech` 需要 .NET Framework 3.0+** 而系统自带的 **SAPI5**(`SAPI.SpVoice`)从 Windows XP 就有, 根本不需要 .NET。绕开 .NET 更稳。 == 所以这里试 5 条路 == A. powershell(按 PATH 找)+ System.Speech <- 现在的实现 B. powershell(按**绝对路径**找)+ System.Speech <- 修"不在 PATH" C. powershell(绝对路径)+ SAPI.SpVoice COM <- 绕开 .NET D. **cscript + VBScript + SAPI.SpVoice** <- 完全不依赖 PowerShell E. wscript + VBScript + SAPI.SpVoice <- 同上,另一个宿主 D/E 最值得期待:**VBScript 从 Windows 98 就有,SAPI5 是系统组件**, 两个都不需要装任何东西,也不受 PowerShell 版本影响。 == 怎么跑 == python _build\diag_tts.py 它会一条条试并打印结果。**在有问题的机器上跑最有价值** —— 比如那台 Win7。把输出发回来就知道该换哪条路。 ''' from __future__ import annotations import os import shutil import subprocess import sys import tempfile import time from pathlib import Path IS_WINDOWS = os.name == "nt" # 让它念一句中文。中文最能暴露编码问题 —— # 纯英文在 PS 2.0 上也可能碰巧是对的(ASCII 不受代码页影响)。 TEXT = "语音播报测试,一二三" NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0) FOUND: list = [] FAILED: list = [] def report(name: str, ok: bool, detail: str = "") -> None: mark = "可用 " if ok else "不可用" print(" [%s] %s%s" % (mark, name, (" —— " + detail) if detail else "")) (FOUND if ok else FAILED).append(name) def run(cmd: list, timeout: int = 30) -> tuple: """跑一条命令,返回 (退出码, 输出)。不让它弹窗。""" try: p = subprocess.run(cmd, capture_output=True, timeout=timeout, creationflags=NO_WINDOW, encoding="mbcs", errors="replace") out = ((p.stdout or "") + (p.stderr or "")).strip() return p.returncode, out except FileNotFoundError: return -1, "文件找不到" except subprocess.TimeoutExpired: return -2, "超时" except Exception as exc: # noqa: BLE001 return -3, "%s: %s" % (type(exc).__name__, exc) def find_powershell() -> str: """先按 PATH 找,再按绝对路径找。""" p = shutil.which("powershell") if p: return p cand = Path(os.environ.get("SystemRoot", r"C:\Windows")) / \ "System32" / "WindowsPowerShell" / "v1.0" / "powershell.exe" return str(cand) if cand.is_file() else "" def main() -> int: print("=" * 62) print(" 语音播报方案诊断") print("=" * 62) print() if IS_WINDOWS: try: wv = sys.getwindowsversion() print(" 系统: Windows %d.%d (build %d)" % (wv.major, wv.minor, wv.build)) except Exception: # noqa: BLE001 print(" 系统: Windows(版本号取不到)") else: print(" 系统: 非 Windows") print(" Python: %s (%d 位)" % (sys.version.split()[0], 64 if sys.maxsize > 2 ** 32 else 32)) print(" 测试文本: %r" % TEXT) print() if not IS_WINDOWS: print(" 非 Windows,没有 TTS 方案可试。") return 0 # ---------------- 0. 环境事实 ---------------- print("── 0. 环境 ──") ps_path = find_powershell() ps_in_path = shutil.which("powershell") print(" powershell 在 PATH 里: %s" % (ps_in_path or "**没有**")) print(" powershell 绝对路径: %s" % (ps_path or "**找不到**")) if ps_path: code, out = run([ps_path, "-NoProfile", "-Command", "$PSVersionTable.PSVersion.Major"]) print(" PowerShell 主版本: %s" % (out.strip() or "查不到")) print(" cscript: %s" % (shutil.which("cscript") or "**没有**")) print(" wscript: %s" % (shutil.which("wscript") or "**没有**")) print(" SystemRoot: %s" % os.environ.get("SystemRoot", "?")) print() # ---------------- A. 现有实现 ---------------- print("── A. powershell(按 PATH)+ System.Speech ← 现在产品用的 ──") if not ps_in_path: report("A", False, "powershell 不在 PATH 里 —— **这就是失败原因**") else: script = ("Add-Type -AssemblyName System.Speech;" "$s = New-Object System.Speech.Synthesis.SpeechSynthesizer;" "$s.Speak('%s')" % TEXT.replace("'", "''")) code, out = run(["powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", script]) report("A", code == 0, "退出码 %d %s" % (code, out[:70])) time.sleep(0.4) # ---------------- B. 绝对路径 + System.Speech ---------------- print("── B. powershell(绝对路径)+ System.Speech ──") if not ps_path: report("B", False, "找不到 powershell.exe") else: script = ("Add-Type -AssemblyName System.Speech;" "$s = New-Object System.Speech.Synthesis.SpeechSynthesizer;" "$s.Speak('%s')" % TEXT.replace("'", "''")) code, out = run([ps_path, "-NoProfile", "-WindowStyle", "Hidden", "-Command", script]) report("B", code == 0, "退出码 %d %s" % (code, out[:70])) time.sleep(0.4) # ---------------- C. SAPI.SpVoice COM(绕开 .NET)---------------- print("── C. powershell(绝对路径)+ SAPI.SpVoice COM ──") if not ps_path: report("C", False, "找不到 powershell.exe") else: script = ("$v = New-Object -ComObject SAPI.SpVoice;" "$v.Speak('%s')" % TEXT.replace("'", "''")) code, out = run([ps_path, "-NoProfile", "-WindowStyle", "Hidden", "-Command", script]) report("C", code == 0, "退出码 %d %s" % (code, out[:70])) time.sleep(0.4) # ---------------- D. cscript + VBScript + SAPI ---------------- # # 最值得期待的一条:VBScript 从 Windows 98 就有, # SAPI5 是系统组件,**两个都不需要装任何东西**, # 也完全不受 PowerShell 版本 / PATH / .NET 的影响。 print("── D. cscript + VBScript + SAPI.SpVoice(不依赖 PowerShell)──") vbs = ( 'Dim v\r\n' 'Set v = CreateObject("SAPI.SpVoice")\r\n' 'v.Speak "%s"\r\n' ) % TEXT.replace('"', '""') # ⚠ VBScript 文件要 ANSI(=系统代码页) —— 写成 UTF-8 的话 # 中文在 cscript 里会变乱码。这里用 mbcs 编码写。 tmp = Path(tempfile.gettempdir()) / "ykxmz_tts_test.vbs" try: tmp.write_bytes(vbs.encode("mbcs", errors="replace")) except Exception as exc: # noqa: BLE001 report("D", False, "写 vbs 失败: %s" % exc) else: code, out = run(["cscript", "//nologo", str(tmp)]) report("D", code == 0, "退出码 %d %s" % (code, out[:70])) time.sleep(0.4) # ---------------- E. wscript(不弹黑框)---------------- print("── E. wscript + VBScript + SAPI.SpVoice ──") if tmp.is_file(): code, out = run(["wscript", "//nologo", str(tmp)]) report("E", code == 0, "退出码 %d %s" % (code, out[:70])) else: report("E", False, "没有 vbs 文件") time.sleep(0.4) # ---------------- 音色列表(两种方式对比)---------------- print() print("── 可用音色 ──") if ps_path: code, out = run([ ps_path, "-NoProfile", "-Command", "$v = New-Object -ComObject SAPI.SpVoice;" "$v.GetVoices() | ForEach-Object { $_.GetDescription() }"]) print(" SAPI5(COM):") for ln in (out or "").splitlines(): if ln.strip(): print(" " + ln.strip()) if not out.strip(): print(" (列不出来)") code2, out2 = run([ ps_path, "-NoProfile", "-Command", "Add-Type -AssemblyName System.Speech;" "(New-Object System.Speech.Synthesis.SpeechSynthesizer)" ".GetInstalledVoices() | ForEach-Object { $_.VoiceInfo.Name }"]) print(" System.Speech:") for ln in (out2 or "").splitlines(): if ln.strip(): print(" " + ln.strip()) if not out2.strip(): print(" (列不出来 —— .NET 那套可能有问题)") # ---------------- 结论 ---------------- print() print("=" * 62) print(" 可用 %d 条 / 不可用 %d 条" % (len(FOUND), len(FAILED))) if FOUND: print(" 可用方案: %s" % ", ".join(FOUND)) print() print(" → 产品应该按**这个顺序**降级尝试:D(cscript) -> C(SAPI COM)") print(" -> B(绝对路径) -> A(PATH),全失败才报错。") else: print(" ⚠ 一条都不通。可能是:没装任何语音、系统被精简过、") print(" 或者 SAPI 组件损坏。") print("=" * 62) return 0 if __name__ == "__main__": sys.exit(main())