mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 16:00:43 +00:00
用户这一轮报了两个问题,都修了。
== 一、Win7 上语音播报不能用 ==
用户原话:"我 win7 上的语音播报好像不能用,你再试一下别的版本能不能用"
**原来只有一条路**:
powershell -NoProfile -WindowStyle Hidden -Command
"Add-Type -AssemblyName System.Speech; $s = New-Object ...; $s.Speak('...')"
这在 Win7 上有三个坑,任何一个都能让它彻底不出声:
1. **`powershell` 只按名字找**
Win7 的 powershell.exe 在 `%SystemRoot%\System32\WindowsPowerShell\v1.0\`。
精简版系统、或从服务/计划任务拉起时 PATH 里可能没有 ——
CreateProcess 按名字找不到时是**静默失败**的。
2. **Win7 默认 PowerShell 2.0,它按 ANSI 代码页解析 `-Command`**
PS 3.0 起才按 UTF-16 处理。中文经 `-Command` 传进去容易乱码 ——
表现是念出来是乱码,或者干脆不念。
3. **`System.Speech` 需要 .NET Framework 3.0+**
而系统自带的 **SAPI5** 从 Windows XP 就有,**根本不需要 .NET**。
**改成多方案降级**,按"依赖越少越优先"排:
1. cscript + VBScript + SAPI5 <- Win98 起就有,不用装任何东西
2. wscript + VBScript + SAPI5 <- 上一条被安全软件拦时的备选
3. powershell + SAPI5 (COM) <- 绕开 .NET
4. powershell + System.Speech <- 原来的做法,改用绝对路径
5. powershell(PATH) + System.Speech <- 最原始的,兜底
一条通了**记住它**,以后直接用(不用每次从头试)。
**顺带修掉一个"骗人"的行为**:原来 `say()` 用 `Popen` 不等进程结束,
失败了也返回"已播报" —— 明明没出声却报成功。
现在等退出码,**全失败会明确报错并列出试过哪几条**。
代价是同步等待(念多久等多久),但"报成功其实没出声"比"慢一点"糟糕得多。
还加了 `_build/diag_tts.py`:在一台机器上把 5 条方案全试一遍并报告
哪条能用 —— 下次再有"某台机器上不行",跑一下就知道。
VBScript 的细节:**必须用 ANSI(mbcs) 写**,写成 UTF-8 的话
cscript 读出来中文是乱码(VBScript 的老规矩,很隐蔽)。
长文本还要分句 —— VBScript 单行有长度上限。
== 二、快捷栏按钮太小 + 4 格都要能换 ==
用户原话:"屏幕最上面这个按钮有些按不到,按钮太小了,做大一点,
然后呢,这个每每个四格算换一下吧,第一个第二个第三个
第四个都能换东西"
* **按钮做大**:30dp -> 46dp(接近 Android 建议的最小可点区域 48),
字号 11 -> 14。30dp 在表盘上手指按上去一半在栏外。
**栏高做成常量 QUICK_BAR_DP,栏高 / 面板 topMargin / addView
三处用同一个值** —— 分三处写的话改一个忘一个,
表现是"内容被盖住"或"上面空一条"。
* **4 格全部可自定义**。用户最早说"其中有一个按钮不能改,
功能就是一键返回桌面",第一版就按"第 4 格写死 desktop"做的。
现在他改主意了 —— 所以 desktop 变成**普通可选动作**,
默认仍放第 4 格(习惯不用改),但想换也能换。
可选动作从 7 个变成 8 个。
**教训(写进测试注释了)**:别把"用户当时的一句话"当成永久约束
焊进结构里;真是约束的话,注释里要写清是什么时候的要求,
下次他改主意时才知道该动哪儿。
== 三、修一个"测试自己没擦干净"造成的假 bug ==
全量回归报:
[FAIL] 配对码正确写入 实际 '(旧的残留值)'
而 `(旧的残留值)` 是**上一次测试遗留**的配对码 —— 数据目录
`C:\ProgramData\ykxmz\data` 在卸载时**故意不删**(那里存着用户的
中转地址、配对码等,卸载不该删),所以机器上会留着上一轮的
settings.json。测试装的时候虽然传了 `/CODE=517284`,但环境没擦干净时
就可能读到旧值 —— 表现成"安装包有 bug"。
**修法**:`test_installer_cycle.py` 加一步"装前先清残留"
(正规卸载 -> 杀进程 -> 删安装目录 -> **删数据目录**)。
数据目录那一步是关键 —— 卸载器不删它。
== 四、其他 ==
* `docs/项目规矩.md` 第一节改成**按预案执行**:
预案一 = 做完测试没问题就上传;预案二 = 做完测试但不上传,直接给用户。
用户没说预案几时默认按预案一,并在交付时说明。
* 顺手把那份文档里的老域名 `relay.xxmxxs.top` 改成现在的
`yk.xxmxxs.top`(老服务器早就退役了)。
* 修掉自己造的重复定义:往 actions.py 插降级链时,
`list_voices` / `say` 各插了两份(pyflakes 报 redefinition)。
* 一次危险的误操作:`Path.write_text(..., newline='\n')` 会**先截断文件
再校验参数**,参数非法时文件已经空了 —— 我用它把
`_tests/test_quickbar.py` 清空了。已重写。
**以后不给 write_text 传 newline。**
== 验证 ==
全量回归(21 关)全部 PASS
新增关卡:3a13 手表快捷栏 / 3a14 语音播报降级
安装包周期 23 项全过(装 -> 验 -> 卸 -> 再验)
Java 编译通过(android-34, --release 8)
本机实测语音播报:cscript 那条通了(2.4 秒)
全部产物重建,Win7 兼容校验通过
251 lines
10 KiB
Python
251 lines
10 KiB
Python
# -*- 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())
|