Qwen3-ASR
TECH DOC 本地离线语音识别 · 部署与实现实录
100% 本地运行

Field Notes / Apple Silicon 本地部署实录

Qwen3-ASR 本地语音识别

在本机(Apple Silicon macOS)完全离线部署 Qwen3-ASR,附带一个支持边说边出字和导出 SRT / VTT 字幕的 Web 应用。本文记录从权重下载、字幕对齐、实时切句到精度损耗与内存回归的完整工程决策。

FastAPI · 模型常驻 单文件 HTML / 零外部依赖 MPS · fp16 qwen-asr · Apache-2.0 M2 Max 64GB / macOS 26.5.2 / torch 2.15.0
~8GB
三份模型权重总体积
0.008
141 秒长音频对齐 RTF
130项
真实 Chrome / HTTP 断言
907ms
首条临时字幕出现
01

快速启动

terminal
# 默认 http://127.0.0.1:8848
./run.sh

# 自定义端口
ASR_PORT=9000 ./run.sh

首次请求才加载模型(约 1–3 秒),之后常驻内存。技术栈一览:

层选型要点
后端FastAPI模型常驻内存,MPS 上 fp16 推理
前端单文件 HTML零外部依赖:无 CDN、无字体请求、断网可用
引擎qwen-asr(Apache-2.0)识别 0.6B / 1.7B + 对齐 Qwen3-ForcedAligner-0.6B
实测机器M2 Max 64GBmacOS 26.5.2 / torch 2.15.0
02

部署:三个坑

坑现象解法
PyPI 直连超时files.pythonhosted.org 连接 timeout,uv 重试 3 次失败换 --index-url https://mirrors.aliyun.com/pypi/simple/
HuggingFace 不通huggingface.co 8 秒无响应走 ModelScope:modelscope download --model Qwen/Qwen3-ASR-1.7B --local_dir ./models/Qwen3-ASR-1.7B,再从本地目录 from_pretrained
官方示例只写 device_map="cuda:0"文档未提 Mac 支持,容易误判为不可用核心推理代码没有 CUDA 硬编码,只读 self.model.device/dtype,device_map="mps" 直接可用

模型下载体积:0.6B 1.8GB · 1.7B 4.4GB · 对齐器 1.7GB,总计约 8GB。

03

字幕(SRT / VTT)

文字只有文本没有时间,做不了字幕。这需要另一个模型:Qwen3-ForcedAligner-0.6B——把已知文本对齐到声学帧上,输出逐字时间戳。

terminal
modelscope download --model Qwen/Qwen3-ForcedAligner-0.6B \
  --local_dir ./aligner/Qwen3-ForcedAligner-0.6B
关键约束

对齐器单独放 aligner/,不要塞进 models/。 两个模型的 config.json 里 architectures 和 model_type 完全一样(都是 Qwen3ASRForConditionalGeneration),而 ModelManager.resolve() 默认取「名字排序最大的目录」——Qwen3-ForcedAligner 恰好排在最前,会被当成默认 ASR 模型加载,然后每次识别都失败。

现在靠两道保险拦住:目录名含 forcedaligner 就跳过,以及读 config.json 看 timestamp_token_id 是否非空(ASR 模型这个字段是 None,对齐器是 151705)。

对齐粒度与切条策略

对齐器把中文逐字、英文整词作为 token(日文走 nagisa、韩文走 soynlp)。不能直接拼 token 文本,否则英文会粘成 TheQwen3ASRmodel。做法是把 token 按「消费等量保留字符」回推原文坐标,切条时直接切原文字符串,空格和标点原样保留。

切条规则(build_cues):

条件行为
强标点 。!?;…必断
软标点 ,、: 且已 ≥10 字断
累计 ≥20 字(max_chars)强制断
单条 ≥6 秒(max_sec)强制断

切完还要过一道 fix_timing:起点不早于上一条终点、时长不足 0.8s 补到 0.8s。播放器遇到重叠或零时长的 SRT 会静默吞掉整条字幕。

实测

场景音频字位字幕条对齐耗时RTF
短片段25.0s9480.21–0.36s~0.01
长音频141.2s552431.11s0.008

141 秒那组:首条 0.48s、末条 140.56s(音频 141.18s),零重叠,最大空隙 1.04s,单条最长 20 字。模型首帧加载 2.8s,之后常驻。

为什么这么快

对齐器输出的是时间戳分类 logits(classify_num=5000)而不是全词表,所以长音频不会爆显存——这是它比「用 ASR 模型自己猜时间」更快更省的原因。

前端的三个状态

结果区多了「字幕」按钮:点了才对齐(懒加载,不用字幕的人不付这份内存)。面板给出条数 / 耗时 / 字位数和前 6 条预览,两个按钮直接下载 .srt 与 .vtt,文件名取自音频名。

  • 录音停止后音频留在内存(int16),导出时补 44 字节 WAV 头直接送后端——绕开 webm 有损压缩那条坑(见下文精度损耗)。
  • 改过文本旧字幕立即失效,因为时间轴对应的是旧文本。
  • 从历史回看必须同时清掉内存里的音频并禁用字幕按钮。这条是真 bug:最初 lastFile 没清,给历史记录生成字幕会拿上一份音频,时间轴全错却不报错。
04

实时识别怎么做的

Qwen3-ASR 官方的流式推理只走 vLLM 后端,Mac 没有 CUDA,跑不了。所以这里用不上 qwen-asr-demo-streaming,而是在前端做能量 VAD 按句切分,逐句送后端识别。

01 / INPUT麦克风getUserMedia
02 / DSPAudioContextScriptProcessor
03 / VADRMS 能量检测静音处切句
04 / POSTint16 PCM/api/asr/pcm
05 / OUT追加结果边说边出字

几个关键参数(web/index.html 的 VAD 常量):

参数值作用
SILENCE_CUT_MS450静音超过这么久就断句
MAX_SEG_MS8000单句最长,防止一直说不停憋不出字
PREVIEW_MIN_VOICE600有 0.6 秒语音就先给临时结果
PREVIEW_EVERY_MS1200当前句的临时预览刷新间隔
LAYER 1临时预览灰色「识别中」框,边说边刷新
LAYER 2确认文本断句后追加进结果框
LAYER 3整段精修停止后自动重跑一遍

实测首字延迟 759ms–2.0s(取决于说话节奏),一句 8 秒的话落定约 0.8s。

为什么要「整段精修」

分段识别丢了上下文,精度必然低于整段。这不是调参能解决的——同一段音频按不同长度切分喂给模型,结果如下:

音频长度参考「数据合在一起」参考「总出力」
8s十聚合 ✗总处理 ✗
12s十聚合 ✗总处理 ✗
16s十句合 ✗总处理 ✗
20s十句合 ✗总处理 ✗
25s(完整)数据合在一起 ✓总出力 ✓

只有拿到完整音频才收敛到正确结果,16–20 秒反而退化成更离谱的「十句合」。所以录完必须让整段音频再过一遍模型:停止后若录音超过 3 秒,自动用完整音频重新识别并替换文本(refineWhole(auto=true)),也可以在结果区手动点「整段精修」重跑。

另一条实测结论

context 参数救不了分段精度:把前文作为 context 传给模型,总处理 依然是 总处理,只是标点变好。缺的是声学上下文,不是文本上下文。

05

精度:三种损耗,都是可复现的

除了上面说的「音频长度」,还有两条更隐蔽的损失,都已实测确认:

损耗来源实验结果
有损压缩同一段 25s 音频转 webm/opus 96kbps稳定把「总出力」识别成「总处理」,wav 版每次都对
重采样往返16k → 44.1k → 16k(ffmpeg 转两趟)一次往返就把「总出力」变成「总处理」
硬截断ffmpeg -t 8 直接切在语音中间开头「数据合」退化成「十聚合」;VAD 切在静音处则不会

三条加起来解释了为什么「浏览器录音」的结果总比「上传原始 wav」差一档。工程上的选择是:

  • 不做浏览器端降采样:试过 new AudioContext({sampleRate: 16000}) 让浏览器直接输出 16k,结果反而更差(Chrome 内部重采样质量不如 soxr),已回退,让后端 soxr 统一降到 16k。
  • VAD 切在静音处:宁可多留 450ms 尾部静音,也不硬切。
  • 整段精修兜底:最终稿以整段识别为准。

模型选型

真实人声 25s 对比:

参考文本0.6B1.7B
数据合在一起时距和在一起 ❌✅
总出力总处理 ❌✅
虚拟电厂虚拟电场 ❌✅

1.7B 全面胜出,默认用它。但要注意用 TTS 合成音频测试会得出相反结论:macOS say 生成的音频上 0.6B 全对,1.7B 把「通义千问」读成「通亿千问」(fp32 复测一致,排除精度因素)。合成语音的声学伪影会骗人,选型必须用真实人声。

性能

场景音频时长耗时RTF
wav 整段25.0s2.2s0.089
m4a 长音频141.2s12.0s0.085
实时单句(2s)2.0s0.61s0.305

fp16 相对 fp32 提速 2.5 倍(8.64s → 3.52s),文本逐字不变。短片段 RTF 偏高是固定开销占比大,绝对值仍在 1 秒内。

06

架构

web / index.html 单文件前端 录音 · 上传 · 字幕 · 状态 · 零外部依赖 录音 · PCM 上传 · 任意格式 字幕 · SRT / VTT 状态 · 健康检查 /api/asr/pcm /api/transcribe /api/subtitle /api/health · /api/model server.py · FastAPI 推理放线程池 · 全局推理锁 · 模型懒加载 ModelManager 懒加载 + 推理锁 AlignerManager 懒加载 · 共用同一把锁 Qwen3-ASR · MPS / fp16 Qwen3-ForcedAligner · MPS / fp16

图 1 从单文件前端到两个本地模型的完整链路:四个 API 入口汇入 FastAPI,两个管理器共享同一把推理锁。

五个关键实现决策

  1. 推理放线程池。transcribe 是 CPU/MPS 密集的同步调用,直接写在 async def 里会阻塞事件循环,导致页面 15 秒一次的健康轮询全部超时。用 run_in_threadpool 包一层。
  2. 全局推理锁。Apple MPS 不是线程安全的,并发请求必须串行。前端用 Promise 链排队,后端再加一道锁;对齐器和 ASR 共用同一把锁,两个模型交替推理也不会互相打断。
  3. PCM 接口不走 ffmpeg。实时链路每 1–2 秒发一次请求,每次都起 ffmpeg 子进程不划算。/api/asr/pcm 直接收 raw int16 LE,非 16k 才用 soxr 重采样。
  4. max_new_tokens 按时长动态给(duration × 12 + 128,上限 8192),避免长音频被截断。
  5. 对齐器懒加载。只有点「字幕」才加载,用不上字幕的人省 1.7GB 内存。
07

前端与验证

界面按桌面应用组织,不是网页文档流:外壳固定为 标题栏 / 工作区 / 状态栏,工作区内再分 会话栏 · 录音台 · 转写面 · 检查器,整页高度锁死 100dvh,滚动只发生在转写正文和列表里。

界面示意 · web/index.html
┌──────────────────────────────────────────────────────────────┐
│ ▮ Qwen3·ASR  LOCAL │ ● 已就绪                    ⌘K    ◐      │ 标题栏
├──────────┬───────────────────────────────────────────────────┤
│ 会话  4  │  ⬤   00:12.4   ▁▂▅▇▅▃▂▁▂▃▅   已识别 3句   ⌗导入音频 │ 录音台
│ ───────  │  ─────────────────────────────────────────────    │
│ 数据合…  │  [实时识别] [字幕] [语言][模型]                    │
│ 11/11 …  ├─────────────────────────────────────┬─────────────┤
│          │ 转写                    [精修][字幕] 复制 ↓ ⌫   │ 源音频       │
│          │                                     │ 指标         │ 检查器
│          │   数据合在一起,右上角是园区实时指标…  │ 字幕(时间轴)│
├──────────┴─────────────────────────────────────┴─────────────┤
│ 设备 mps │ 精度 fp16 │ 内存 1.4M·1.1G   音频 25.0s RTF .088   │ 状态栏
└──────────────────────────────────────────────────────────────┘
  • 录音台是横置仪表:左边录制键(主操作位)→ 计时 → 波形 → 分句计数,导入收在行的最右端、贴窗口右边缘;DOM 顺序与视觉一致,Tab 键也最后才走到它
  • 波形是滚动电平带:待机时是一条静止的起伏剪影(不画假数据,也不留空白),录音时变成红色电平带、左端渐隐、右端一条「当前时刻」亮线
  • 转写面是文档:17px / 1.88 行高、72ch 栏宽、居中排版,点空白即可编辑;实时预览像一条钉在正文底部的字幕条
  • 检查器(右侧 306px)放源音频、指标、字幕时间轴;空态用骨架条占位
  • 明暗双主题(默认浅色,因为这是以读长文本为主的应用),色值全部走 CSS 变量,切换即时生效
  • 窄屏(<900px)会话栏变抽屉、检查器折叠成底部条带;<720px 检查器默认收起,只有真的生成了字幕才浮出来

设计约束:4px 栅格、发丝分隔(不堆卡片阴影)、一个信号色(录制红 #c8402c)加一个状态色(确认绿 #0d7a52),数字一律等宽 + tabular-nums 防跳动。所有图标是内联 <symbol>,字体用系统栈,仍然零外部请求。

⌘K 命令面板空格 录音 / 停止⌘O 导入⌘⇧C 复制⌘⇧L 明暗切换
Qwen3-ASR 浅色初始界面
浅色初始态 录音台置顶、转写面居中、右侧检查器骨架占位。
字幕生成结果界面
字幕结果态 8 条字幕、25s 音频、对齐 0.22s,右侧显示时间轴与 SRT / VTT 下载。
窄屏移动端界面
窄屏布局 会话栏抽屉化、检查器折叠成底部条带。

图 2 项目真实 Chrome 截图(点击可放大)。取自 shots/ 验证产物。

功能面

  • 点击录音(或按空格)→ 实时波形 + 计时 → 边说边出字
  • 拖拽文件、点选文件、⌘V 粘贴音频都可触发整段识别
  • 结果可直接编辑,支持复制、下载 txt、清空
  • 「字幕」是一个开关,不是一次操作:选中后每确认一句就追加一条带时间码的字幕,边录边出;定稿前还会先显示一条临时行(弱化样式 + 光标),中间不留空档
  • 录完想要逐字精确的时间码,再点工具栏「精确对齐」用 forced aligner 覆盖一版
  • 历史记录存 localStorage(最近 20 条),点击可回填
  • 语言可选自动检测或强制指定;模型可在线切换(录音中锁定)
  • 状态栏常显「内存:页面 JS 堆 · 服务常驻」,⌘K →「释放内存」可把加速器缓存池还给系统

字幕开关为什么是「持续输出」

实时识别 按句切分,句子边界一到就把这一段送去识别。字幕开关接在这一步上:段落被切出来的那一刻记下 absSamples / sampleRate,等识别结果回来直接用它当时间码,所以字幕和录音是同一条时间轴,结果回得晚一点时间也不会漂。

正式条目要等 VAD 判定句子结束(350ms 有声 + 450ms 静音)再回传,中间必然有几秒空档,所以还有一条临时行:预览识别(每 1.2s 一次)一回来就先把那句话铺上去,定稿后自然被正式条目替换。实测首条临时字幕在 907ms 出现,首条正式字幕在 9.4s。

时间轴只许向前

新条的 start 会被夹到上一条的 end,长度不足 0.6s 补齐——播放器遇到重叠或倒序 SRT 会直接把字吞掉。开着字幕时不做自动整段精修:精修会换掉文本,边录边出的时间轴立刻对不上。

验证:五套真实 Chrome / HTTP 断言

全部通过(30 + 34 + 37 + 24 + 5 = 130 项):

verify/
# 30 项:页面结构 / 导入组件靠右 / 零依赖 / 上传识别 / 375px 溢出 / WCAG AA 对比度
NODE_PATH=.../node_modules .../node verify/check-ui.js

# 34 项:模拟麦克风跑完整链路 —— 实时出字 / 状态机 / 自动精修 / 连点防护
NODE_PATH=.../node_modules .../node verify/check-live.js

# 37 项:字幕 —— 对齐质量(不丢字、不重叠、关键词不被切碎)+ 真实下载落盘
NODE_PATH=.../node_modules .../node verify/check-subtitle.js

# 24 项:字幕开关 —— 录音中就有字幕 / 条数持续增长 / 时间码不重叠且单调 / 停录音不丢
NODE_PATH=.../node_modules .../node verify/check-subs-live.js

# 5 项:内存回归 —— 连续导入 6 次,JS 堆、DOM 节点、服务 RSS、加速器缓存都必须收敛
NODE_PATH=.../node_modules .../node verify/check-memory.js

check-live.js 用 Chrome 的 --use-file-for-fake-audio-capture 把真实音频喂成麦克风,所以走的是完整链路(getUserMedia → AudioContext → VAD → 后端)。三套脚本的分工是刻意分开的:

  • API 层断言模型精度(送原始 16k PCM / wav,无重采样损失,必须识别出「总出力」、对齐必须不丢字)
  • UI 层只断言链路可用性(模拟麦克风是 16k→44.1k→16k 双重重采样,精度必然掉,不该拿它考精度)

改界面时先跑 node verify/shot.js 取四张图看效果,再跑三套断言——视觉改动不该靠想象验收。check-subtitle.js 还做了件容易被跳过的事:用 CDP 打开真实下载,等文件落到磁盘再读回内容校验格式。只断言「点了按钮没报错」是抓不到「文件是空的」或「扩展名对但内容是 txt」这类问题的。

08

内存

用户反馈「每选一个音频文件,内存就往上抬一格」。先量,再改——

观察点改前改后
页面 DOM 节点(连续 6 次导入)每轮 +13,不回落稳定在 850–870(多出的就是历史项)
页面 JS 堆1.4 → 2.0MB稳态增量 0.01MB
服务常驻 RSS(连续 6 次长音频)998 → 1160MB 后停住1165.4MB,六轮完全不动
MPS 缓存池每次顶到新高,不再归还5387↔5517 反复回吐(每轮归还 80–90MB)

四个真实原因,都修了

  1. 波形每帧新建缓冲区:drawScopeFrame 里 new Uint8Array(analyser.fftSize),60 帧/秒 × 2KB = 每秒 120KB 垃圾,长录音时 GC 一直追不上。改成一个复用的缓冲区。
  2. 15 秒一次的健康轮询重建 <select>:模型列表没变也 innerHTML="" 再重建 option,一天下来几百个脱离文档的节点。加了列表指纹,没变就一个节点都不动。
  3. 历史列表每次全量重建:同样加了指纹(at + 文本长度)。
  4. 请求体和解码结果同时驻留:raw 字节留到推理结束才释放,大文件时峰值凭空翻倍(200MB 文件 + 400MB float32)。解码完立刻 del raw,推理完 del wav。

另外,MPS 缓存分配器会把历史峰值一直攥着——这不是泄漏,但确实不还,外部看就是「内存只涨不降」。所以一次性任务(导文件、做字幕)结束后主动 empty_cache(),并加了两件可验证的东西:

  • GET /api/stats:rss_mb + 加速器 allocated_mb / driver_mb
  • POST /api/free:gc.collect() + empty_cache(),返回前后对比。连点两次,第二次几乎不掉,就说明之前涨的只是缓存
  • 状态栏常显这两个数,⌘K →「释放内存」

判读方法(写在 check-memory.js 注释里):基线取第 2 次(第 1 次含模型预热和分配器首次扩容),后 4 次的增量才是判断依据。读取前必须强制 GC,否则 DOM 节点数统计的是「已脱离文档但还没被扫走」的对象,看起来像泄漏。

09

踩过的坑

01第二次点录音停不下来(用户报的 bug)

旧代码 · web/index.html
function toggleRec() {
  if (!rec || rec.state === "inactive") startRec();
  else { rec.stop(); rec = null; }   // ← 问题在这
}
rec.onstop = () => {
  const type = rec.mimeType || mime;  // ← 此时 rec 已是 null,抛 TypeError
  ...
  stopStream();                       // ← 永远执行不到
};

MediaRecorder.onstop 是异步回调,而 toggleRec 同步把 rec 置成了 null。回调里读 rec.mimeType 直接抛异常,后面最关键的那行 stopStream() 被跳过——麦克风不释放、按钮卡在录音态、计时器不停;再点一次因为 rec 为 null 又会重开一路录音。

修法是换成显式状态机(IDLE/STARTING/RECORDING/STOPPING),所有入口先过状态检查,onstop 只做数据收尾,清理逻辑放 teardown() 且不依赖闭包变量。顺带修了另一个竞态:startRec 是 async,getUserMedia 等待期间 rec 仍是 null,连点会并发开两路麦克风——现在 STARTING 态的点击直接忽略。

断言覆盖:二次点击回 IDLE、麦克风轨道 readyState !== live、计时器读数冻结、可重复启停、同一 tick 连点 5 次最多申请 1 次麦克风。

02hidden 属性被 CSS 吃掉

<button hidden> 的 UA 样式是 display:none,但元素同时有 .btn{display:inline-flex} 时,类选择器优先级更高,按钮照样显示。表现是文件识别后不该出现的「整段精修」按钮跑了出来。加 [hidden]{display:none !important} 兜底。这个坑之前的断言完全抓不到——它只检查了 hidden 属性值,没检查实际可见性。现在断言改成量 getBoundingClientRect() 宽高 + offsetParent。

03375px 下页面被撑到 1002px

canvas 的 width 属性是固有尺寸,grid 项默认 min-width:auto 不肯收缩,而 sizeCanvas() 又去读 getBoundingClientRect(),形成「越读越宽」的正反馈。改为读 clientWidth + grid 项和卡片加 min-width:0 + canvas 加 max-width:100%。

04次级文字对比度只有 2.41:1

--faint:#9aa1ab 在灰底上远不达 WCAG AA 的 4.5:1。调深到 #5f666e 后全部达标。

05对齐器抢走了「默认模型」

对齐器下载到 models/ 后,健康检查突然开始报识别失败。原因是 resolve() 在没有指定模型时取 sorted(avail, reverse=True)[0]——按名字排序最大的那个,而 Qwen3-ForcedAligner-0.6B 排在 Qwen3-ASR-* 前面,直接被当成默认 ASR 模型。更麻烦的是它没法用配置区分:两个模型 config.json 里 architectures 和 model_type 完全一样,唯一差别是对齐器的 timestamp_token_id 非空(151705),ASR 模型是 None。处理方式:权重放到 aligner/ 单独目录,同时在 available() 里加两道过滤,只靠名字过滤太脆——改个目录名就失效。

06历史回看生成的字幕时间轴是错的

show() 同时服务「刚识别完」和「点历史回看」两种场景。历史里只存了文本,没存音频,但内存里的 lastFile 还留着上一份音频——于是给旧记录点「字幕」时,拿的是另一段音频去做强制对齐。文本和音频不匹配,对齐器照样输出一串看起来正常的时间戳,不报错。这类 bug 最难发现:有输出、格式正确、只是全错。 修法是历史回看时一并清掉 lastWhole/lastFile 并禁用按钮,按钮 title 写明「这条结果没有对应音频」。

断言本身也会错

值得单独记:check-ui.js 最初有两条 FAIL 是断言写错而不是产品 bug——

  • 「零外部依赖」:CSS 里 data URI 的 xmlns="http://www.w3.org/2000/svg" 被 new URL() 当成了外部域名;
  • 「历史已写入」:只取 innerText.slice(0, 60),模型名在末尾被截掉了。

同一套脚本里,「首字延迟」最初的 12.2s 也是半误报:只读了 textarea,没算临时预览框——用户其实 2 秒就看到字了。断言误报和产品 bug 必须分开处理,否则会去修不存在的缺陷。

10

已知边界

限制说明出路
不是真流式官方的 token 级流式只支持 vLLM,Mac 无 CUDA当前是「按句流式」:一句说完 0.5–0.8 秒出字。要 token 级流式只能上 NVIDIA + vLLM
分段精度低于整段见上文长度梯度实验,结构性限制已用自动「整段精修」兜底
字幕要求文本与音频一致强制对齐是把给定文本贴到声学帧上,文本与音频不符时时间轴会偏用同一份结果生成;换过文本要重新识别
字幕不做二次断句优化按标点 + 长度机械切条,不保证语义完整已保证关键词不被切碎;要更自然得接语义分句
单文件上限 30 分钟整段推理会吃满内存长音频应分段
整段缓冲上限 10 分钟约 58MB,超了就不再留存给精修够用,超出部分仍走实时结果
无说话人分离模型不输出 speaker label需外挂 pyannote 之类
单并发MPS 推理锁串行多并发得换 CUDA + vLLM
音频不出本机除首次下载权重外全程离线—
11

目录

server.pyFastAPI 服务(转写 / PCM 流式 / 字幕对齐 / 内存统计与回收)
web/index.html单文件前端(录音 + 实时 VAD 切句 + 整段精修 + 边录边出字幕)
run_asr.py命令行最小验证脚本
verify/check-ui.js页面结构与交互断言(30 项)
verify/check-live.js实时链路与状态机断言(34 项,含模拟麦克风)
verify/check-subtitle.js字幕断言(37 项:对齐质量 + 真实下载落盘)
verify/check-subs-live.js字幕开关断言(24 项:录音中就出字幕、条数持续增长)
verify/check-memory.js内存回归(5 项:连续导入 6 次,四项指标必须收敛)
verify/shot.js快速取景(浅色 / 深色 / 命令面板 / 窄屏),只截图不断言
audio/测试音频(TTS 合成 + 真实人声切片 + PCM/48k/16k 变体)
models/ASR 权重(ModelScope 下载,未纳入版本管理)
aligner/Qwen3-ForcedAligner 权重(必须与 models/ 分开,见上文)
shots/验证截图与下载产物
放大查看点击任意处关闭 · ESC