公司动态
# 工程化收尾:性能调优、SRT字幕导出、PyInstaller打包成exe(第 5 篇)
纯语音转文字系列 · 共 5 篇第 1 篇系统设计与四层流水线第 2 篇音频采集 语音活动检测第 3 篇流式 ASR —— 从 3 秒延迟重构到亚秒级增量解码第 4 篇PySide6 悬浮字幕窗 —— 透明置顶 双层字幕第 5 篇工程化收尾本篇 · 收官系统跑通了但离真正能用还差最后一公里。前面 4 篇我们搭完了一条完整的实时语音转文字流水线——采集、VAD、流式 ASR、悬浮字幕窗全链路离线可用。但能跑和能用之间还差三步延迟再压一压—— 出字节奏能不能更跟嘴字幕能导出—— 会议结束得拿到 SRT/TXT 文件打包成 exe 发给别人用—— 同事双击就能跑不用装 Python本篇把这三件事一次收尾也是整个系列的收官之作。一、性能调优调参指南这部分是可以直接抄的调参手册。不用重新推导照着改就行。1.1 延迟预算全景先把全链路的延迟拆开看采集 32ms → VAD 1ms → 流式ASR 3.65ms/块 → 字幕刷新(0.8s 节流)瓶颈不在推理RTF 0.114富余 8 倍而在字幕刷新节流。provisional 每 0.8s 才 emit 一次——这是出字节奏的调节旋钮。1.2 核心参数速查表参数位置推荐值说明provisional_interval_sconfig/settings.yaml0.3 ~ 0.8字幕刷新间隔。0.8 最省 CPU0.3 更跟嘴。不要低于 0.3否则字幕会因解码器修正而闪字num_threadsconfig/settings.yaml → asr.streaming2笔记本/4桌面机sherpa-onnx 推理线程数。2 线程 RTF 0.114 已富余4 线程提升有限queue_maxsizepipeline.py100≈3.2s音频队列上限。满了丢最旧保证实时性优先于完整性1.3 各参数详解出字节奏provisional_interval_s设定值效果代价0.8默认字幕约 0.8s 跳一次CPU 最低0.3字幕更跟嘴CPU 略升0.1几乎逐词出CPU 明显升高且会闪字实测结论0.8s 已经够流畅CPU 占用最低。追求跟嘴感可以降到 0.3但绝对不要低于 0.3。推理线程num_threadsasr:streaming:num_threads:2# 笔记本推荐 2桌面机可到 4实测 2 线程 RTF 0.114 已经富余4 线程提升有限但占用更多 CPU。如果还要同时跑其他任务1~2 线程即可。队列缓冲queue_maxsizequeue_maxsize100# 音频队列上限100 块 ≈ 3.2s队列满时丢最旧保证实时性优先于完整性。如果发现字幕偶尔缺字说明下游偶尔跟不上可适当调大到 150~200如果内存占用高调小到 50。1.4 模型加载预热流式模型首次加载 2.2s读 330MB ONNX为避免用户感知到卡顿应用启动时后台预热# 启动时在后台线程加载模型用户无感知threading.Thread(targetself._asr_engine._ensure_loaded,daemonTrue).start()调参口诀节流 0.8 最稳线程 2 就够队列 100 保底预热不能少。二、字幕导出SRT / TXT会议或课程结束拿到带时间轴的字幕文件——这是能真正用的刚需。2.1 核心代码SubtitleRecorderclassSubtitleRecorder:记录带时间戳的字幕段支持导出 SRT 和纯文本。def__init__(self):self._entries[]# [(start_s, end_s, text)]defadd_segment(self,text,start_s,end_s):VAD 语音段结束时调用记录一段字幕。iftext:self._entries.append((start_s,end_s,text))defexport_srt(self,path):导出为标准 SRT 字幕文件。withopen(path,w,encodingutf-8)asf:fori,(start,end,text)inenumerate(self._entries,1):f.write(f{i}\n)f.write(f{_fmt_ts(start)}--{_fmt_ts(end)}\n)f.write(f{text}\n\n)defexport_txt(self,path):导出为纯文本每段一行。withopen(path,w,encodingutf-8)asf:for_,_,textinself._entries:f.write(text\n)def_fmt_ts(seconds):秒数 → SRT 时间戳格式 HH:MM:SS,mmmhint(seconds//3600)mint(seconds%3600//60)sint(seconds%60)msint((seconds-int(seconds))*1000)returnf{h:02d}:{m:02d}:{s:02d},{ms:03d}2.2 时间轴来源时间轴可以直接用 VAD 段的边界更简单可靠不需要依赖 ASR 输出的 timestamps[00:00:01,200 -- 00:00:04,100] 你好现在可以听到我讲话吗 [00:00:05,000 -- 00:00:09,500] 我们开始今天的技术面试2.3 使用示例# 在 pipeline 中初始化recorderSubtitleRecorder()# 每次 VAD 语音段结束时记录recorder.add_segment(你好现在可以听到我讲话吗,start_s1.2,end_s4.1)recorder.add_segment(我们开始今天的技术面试,start_s5.0,end_s9.5)# 会议结束后导出recorder.export_srt(meeting_2024.srt)# → SRT 字幕文件recorder.export_txt(meeting_2024.txt)# → 纯文本文件导出的 SRT 文件可以直接拖进播放器、剪映、PR 做视频字幕无需任何格式转换。三、PyInstaller 打包成 exe目标把整个项目含 330MB 模型打成一个 exe同事双击就能跑。3.1 模型文件路径适配sherpa-onnx 模型是本地文件打包后路径会变化需要做兼容# main.py 中定位模型目录兼容源码运行和打包后运行defget_base_dir():ifgetattr(sys,frozen,False):returnPath(sys.executable).parent# exe 所在目录returnPath(__file__).parent# 源码目录3.2 打包命令pipinstallpyinstaller pyinstaller--noconfirm--onefile--windowed^--nameLiveSubtitle^ --add-datamodels;models^# 模型目录--add-dataconfig;config^# 配置文件--hidden-import sherpa_onnx ^ --hidden-import pyaudiowpatch ^ main.py3.3 踩坑 → 解决 对照表打包过程不是一帆风顺的下面是遇到的坑和对应的解决方案踩坑现象原因解决方案ModuleNotFoundError: sherpa_onnxPyInstaller 无法自动分析 onnx 的 C 扩展依赖加--hidden-import sherpa_onnxTORCH_HOME找不到 silero 模型torch.hub 默认从缓存加载打包后路径丢失打包时设置TORCH_HOME指向内置目录或首次运行联网下载exe 运行时无法写配置/日志--onefile模式下sys._MEIPASS是临时只读目录配置/日志目录改到 exe 旁Path(sys.executable).parent打包体积过大1GB可能包含了 torch GPU 版或无用依赖本系列精简版不含 torch GPU用--onefile压缩排除无用包可显著减小体积提示本系列的纯语音转文字版依赖里没有torchSilero VAD 用 torch.hub 加载但 VAD 模型很小sherpa-onnx 自带 onnxruntime打包体积比含 Qwen3-ASR 的完整版小很多。四、完整工程结构精简版LiveSubtitle/ ├── src/ │ ├── audio/ │ │ ├── audio_capture.py # WASAPI Loopback 麦克风 │ │ ├── vad_detector.py # Silero VAD │ │ └── audio_buffer.py # 缓冲 │ ├── asr/ │ │ ├── streaming_asr.py # sherpa-onnx 流式引擎 │ │ └── hotword_manager.py # 热词 │ ├── ui/ │ │ └── overlay_window.py # 悬浮字幕窗 │ ├── core/ │ │ ├── pipeline.py # 管道编排 │ │ ├── config.py # 配置 │ │ └── event_bus.py # 事件总线 │ └── utils/ │ ├── windows_api.py # 透明/置顶/防捕获 │ ├── logger.py # 日志 │ └── subtitle_exporter.py # SRT/TXT 导出 ├── config/settings.yaml ├── models/ # sherpa-onnx 模型 └── main.py五、踩坑总结全系列精华5 篇写下来踩过的坑比写的代码还多。这里把全系列最关键的坑汇总每一条都是真实调试出来的#坑根因解决1WASAPI Loopback 采样率不是 16k设备原生值44.1k/48kLoopback 直接透传必须做重采样到 16kHz2重采样后块大小不齐重采样器输出长度不固定VAD 内部用输入缓冲凑满 512 样本3decode_stream只识别了一部分一次只处理部分帧while is_ready循环调用直到返回 False4热词不生效直接抛异常热词要求 beam search 解码器hotwords_filemodified_beam_search两者缺一不可5热词设置了但没效果英文整词不在中文单字词表里热词要过词表过滤不过滤就刷警告且不生效6SetWindowDisplayAffinity无效调用时机不对要窗口显示后调用且仅 Windows 10 2004 支持7首次识别卡顿 2.2 秒模型加载耗时启动后台线程预加载用户无感知六、全链路延迟回顾从说话到字幕上屏完整延迟拆解说话 → 2 秒出中间字幕边说边出字流式 provisional → 句末 300ms 静音 → VAD 判定段结束 → 终稿确认 → 字幕历史 → 会议结束 → 导出 SRT/TXT环节延迟备注音频采集32ms每块 512 样本 16kHzVAD 检测1msSilero 极轻量流式 ASR3.65ms/块RTF 0.114富余 8 倍字幕刷新0.8s可调provisional_interval_s 控制句末确认~300msVAD 静音检测阈值整套系统 CPU 实时运行不依赖 GPU、不依赖网络、不依赖任何云端 API——纯本地离线可用。七、系列总结从 0 到 1 的完整旅程回头看这 5 篇我们从一个想让会议自动出字幕的想法出发走完了整个工程链路篇目关键词交付物第 1 篇架构设计四层流水线蓝图采集 → VAD → ASR → 字幕第 2 篇音频输入WASAPI Loopback 麦克风采集 Silero VAD第 3 篇核心引擎sherpa-onnx 流式 ASR从 3s 延迟重构到亚秒级第 4 篇用户界面PySide6 透明置顶悬浮窗双层字幕第 5 篇工程收尾性能调优 SRT 导出 PyInstaller 打包 exe从第一行代码到最终交付的 exe这不是一个 demo而是一个可以真正日常使用的产品——离线可用、双击即跑、字幕能导出。系列导航上一篇第 4 篇 · PySide6 悬浮字幕窗本篇第 5 篇 · 工程化收尾收官本系列 5 篇代码与数据均来自真实项目实战。