返回博客

使用教程

Syzygy 语音输入的工作原理:从一次按键到一条剪贴板条目

拆解 Syzygy 桌面语音输入的完整链路:cpal 录音、Silero VAD 流式分段、多引擎回退、重试预算、整段重转写、热词词典与隐私门。所有数字都来自源码与设计决策记录。

Syzygy 团队发布于 阅读约 15 分钟更新于
  • #语音输入
  • #本地语音识别
  • #语音转文字离线
  • #VAD
  • #剪贴板
本文目录

按下 Alt+Shift+R,Syzygy 的麦克风以 16 kHz 开始采集。Silero VAD 每 512 个样本(32 毫秒)判断一次你是在说话还是在停顿,说完的话被切成段送进识别引擎,转写文字在收尾时物化为剪贴板历史里的一条普通条目。全程没有第二个进程参与,默认情况下也没有一个字节离开这台设备。

这条链路在 1.2.0 版本完整落地,背后是一串可以单独讲清楚的决定:文字为什么落进剪贴板而不是光标处,分段为什么交给一个 VAD 模型而不是能量阈值,识别失败后重试预算怎么花,300 秒这个奇怪的数字从哪来,1153 条热词词条怎么追上识别器奇奇怪怪的分词。本文把这些决定逐个摊开,再给出常见问题的排障入口。文中所有数字都来自源码与设计决策记录(ADR),不是宣传口径。

两个入口共享这条链路:主窗口的听写页和 Quick Panel。热键默认打开面板即开始录音,这个行为可以在设置里拆开;热键本身也可以改。

Syzygy 语音输入面板:波形、当前相位与每段识别的任务详情都在同一列里
Syzygy 语音输入面板:波形、当前相位与每段识别的任务详情都在同一列里
flowchart TD
  A[热键按下] --> B[录音与前置增强]
  B --> C[Silero VAD 流式分段]
  C --> D["分段并行转写<br/>引擎按配置顺序回退"]
  D --> E{"≥2 段且 ≤300 秒?"}
  E -- 是 --> F[整段重转写]
  E -- 否 --> G[保留分段拼接]
  F --> H[确定性标点润色]
  G --> H
  H --> I[热词词典校正]
  I --> J{AI 后处理已开启?}
  J -- 是 --> K[按 profile 加工]
  J -- 否 --> L[物化为剪贴板条目]
  K --> L

文字落在剪贴板,不落在光标处

早期设计稿假设的是另一条路:转写完成后,Syzygy 恢复你录音前的应用焦点,把文字自动粘贴进去。这条路要跨平台处理焦点恢复、Accessibility、Windows UIPI 和 Wayland 的输入限制,还要承担把一句话投进错误窗口的风险。

我们最终把终点定为剪贴板条目。语音输入的整条链路都在 Syzygy 内部完成,成功后产出一条带语音来源标记的普通 Clipboard Item,获得预览、搜索、标签、跨设备同步和 Quick Panel 粘贴的全部既有能力。需要把文字放进别的应用时,由你自己从剪贴板粘贴:Quick Panel 的「选中即输出」序列归粘贴功能所有,语音输入不复用它、不包裹它,也没有任何等价命令。这条禁令不区分自动触发和用户主动触发,不存在「只差一个用户手势就合规」的解释空间。

代价是路径变长了半步。收益是把风险边界从「任意前台应用」收缩到「我们自己的窗口」,而且失败的听写可以保留可理解的失败阶段和恢复动作,不会伪造出一条空文本条目。原始录音同样被排除在剪贴板历史、P2P 同步和日志之外。

每次听写在运行时是一次有显式终态的 Run:录音中、识别中、已完成、已取消、已失败,收尾的每一步(重转写、后处理、润色、词典、物化)都有各自的阶段归属。物化的条目带语音来源标记,所以你能在剪贴板历史里区分「打的字」和「说的话」,后续的同步范围与删除语义也由此单独定义,不默认套用普通复制内容的策略。

录音先过一道清洗

真实麦克风带来的问题在识别之前就存在:直流偏置让波形整体抬高,增益偏低让辅音淹没在量化噪声里,按键瞬间的耳语前导把第一个词切碎。识别引擎看不到这些特征前的原始指纹,只会以空返回或错字回应。

Syzygy 在采集之后、送引擎之前安排了一条 16-bit PCM 的前置增强链:高通滤波、目标峰值归一化、最大增益限制、静音裁剪。每一项都可调,整条链可以关闭;关闭时样本数逐位恒等,引擎收到原始声音。每次增强都产出一份报告(峰值、增益、裁剪量),随听写任务详情一起展示,所以你能看到麦克风到底收到了什么。

增强后的缓冲区和 VAD 分段用的是同一个 buffer。清洗只负责把输入拉直,判断何处分段是下一步的事。

分段交给 Silero VAD

长录音必须在用户停止之前决定何处分段送识别,否则一段六十秒的话会作为一个整体排队,中途无法开始下一段录音。这个判断由 Silero VAD 在流上完成:16 kHz 采样、512 样本窗口、0.5 的语音概率阈值。静音持续 0.4 秒闭合一段,语音不足 0.25 秒丢弃,单段最长 5 秒,段尾补 120 毫秒再切,避免把刚说出的尾音切掉。

VAD 模型本身走统一的模型目录:来源、版本、SHA-256 都锁死,下载与校验和其他模型共用一套合同。它有两条退路:模型未安装时用能量检测分段,两条链的窗口与段长参数保持一致;单次检测失败则报告 Run 失败并保留录音,绝不假装静音继续录。模型加载走租约懒加载,不会卡住录音启动。你按停止时,收尾流程会强制 flush 所有未完成的段,正在说出的最后半句不会丢。

引擎按你排的顺序回退

识别引擎清单完全由你配置,回退按清单顺序走。本地引擎有四类:操作系统自带的听写;whisper.cpp 全系,从 tiny 到 large-v3-turbo 连同各档量化版;FunASR Paraformer INT8,中文场景的性价比之选;Qwen3-ASR 0.6B INT8,第三个本地引擎,也是第一个用语音大模型解码器的引擎,它的 ONNX 导出给提示、音频和生成的文字共享一份固定预算,所以适配器按预算分块解码再拼接,单次解码的成本与你说多长无关。首次未配置时默认派生系统识别。

远程引擎同样可选:OpenAI 兼容的 Transcriptions 端点、chat-completions 形式的音频端点、阿里云百炼的 DashScope 原生协议(模型 qwen-audio-3.0-asr-flash,API-only,价格与区域可用性以厂商当前说明为准),以及腾讯云语音账号。DashScope 的模型没有开源权重可下载,远程与本地是两条清晰的边界。

这条边界由一个默认关闭的开关守着:remote_audio_consent。本地引擎和系统识别不需要任何授权;任何远程引擎在开关关闭时直接跳过。授权跟随内容的最终处理目标,而非第一跳的 URL 形状:把端点填成 http://127.0.0.1 不能豁免授权,因为本机网关可以把请求转发到任何远端。候选链按原顺序跳过没有授权的引擎,继续尝试可用的本地或系统候选;你明确指定某个无授权引擎时,失败原因会如实说明。设置页的探测、草稿测试与真实识别走同一道门禁,测通的结论不会和实际请求两套标准。

语言偏好则是一份所有引擎共享的设置。固定语言严格使用指定值,系统语言构造失败不会偷偷换成默认;自动模式下系统识别按系统默认语言和你偏好的语言依次尝试。主窗口和 Quick Panel 的听写页共用同一套能力展示,麦克风权限与系统语音识别权限分开呈现,各自的授权状态单独检测。

失败分类决定重试花在哪

一段识别的预算是三个数字:总共 12 次尝试、单个引擎 3 次、墙钟 60 秒。同引擎的重试和下一个引擎的首次尝试记在同一个账本上,所以无论引擎链怎么配,一段音频最多花掉 12 次。

失败分类决定这些尝试怎么花。Failed 意味着瞬态故障:请求超时、传输中断、服务端 5xx。这类失败值得原地再问一次,重试属于同一个引擎,退避从 500 毫秒起步,每档加 500 毫秒,1.5 秒封顶,退避过程可被取消打断。一次坏请求就跳到下一个引擎,会冤枉并没有坏掉的引擎。Unavailable(引擎或配置不可用)和 NoSpeechDetected(对这段音频的确定性结论)则直接降级到下一个引擎:对「没有语音」反复提问只会浪费预算。墙钟预算只拦截新尝试的启动,已经开始的尝试一定跑到自己的结论,识别永远不会被腰斩在半途。

这套语义后来被提升为全系统契约:语音识别、语音 AI 后处理、AI 对话、视觉 OCR 走同一套失败分类与退避参数,一套参考测试钉住行为。

边录边转:分段并行走,文字按说的顺序落

分段真正的收益在流水线上。一次听写被切成多段之后,前一段还在识别,你就可以开始说下一段;各段的识别并发进行,结果却按说话顺序写回。运行时用一条提交链让后关闭的分段等待前一段应用结果之后再提交,所以先说完的话一定先出现在文本里,即使它识别得更慢。早先的分段失败也不会中断会话:失败被记录在该段的账上,你继续说,已提交的文字原样保留。

每个分段都有一份完整的账:录音报告(时长、峰值与电平 dBFS)、前处理报告(高通、裁静音、增益,未启用则为空)、识别尝试(成功与失败都记,含耗时)。会话层面则记录后处理尝试。成功、失败、取消的 Run 用同一个组件渲染同样的五个阶段:录音、前处理、识别、后处理、剪贴板。听写结果页脚展开的就是这份账,历史列表里的详情面板也是。哪一段慢了、哪个引擎试了几次、麦克风到底收到了多大声,都在里面。

本地批量引擎没有中间文本,实时引擎则会在说话时给出预览:预览窗口按固定节奏推进、最长八秒,闭合后做完整段识别。预览可以修订,它只是让你看到进展,终稿以段闭合后的完整识别为准。

300 秒:整段重转写的资格线

分段是为了即时性,代价恰是跨段上下文。你分五次说完一段话,五段各自识别再拼接,引擎看不到段与段之间的关联。所以按下停止后,运行时会做一次补课:把会话自己的分段录音按口语序拼接,交回这个会话胜出的引擎(最后一段成功识别所用的那个),再做一次整段批量识别,用结果替换分段拼接文本,作为后处理的输入。

重转写有资格线:至少两个分段,总时长不超过 300 秒。这个数字对齐的是远程识别 10 MB 的请求上限,300 秒 16 kHz 单声道音频大约就是这个体量。单段会话不重转,它本来就是一段完整录音;全链失败没有胜出引擎也不重转。被保留的分段音频只活在会话存续期,终态即清空,不落盘。重转写进行时界面会显示「润色中」阶段,录音按钮照常可用,你不必等它。

重转写是升级而非关卡。引擎拒绝、拼接失败、用户取消,任何一种失败都回退到分段拼接文本,会话照常走完后续步骤。不因为追求更好的结果而丢掉已有的字。

标点润色是纯函数

识别引擎返回的文字里,标点是它顺手给的:中文里夹半角句号,省略号打成一串点,删除语气词后留下孤立的逗号。这些都不需要模型来修。七条规则处理全部常见情况:全角拉丁字母与数字写回 ASCII、点串收敛为省略号、重复标点合并、中文内的 ASCII 标点转全角、行首孤立标点丢弃、标点周围的空格清理、以及给没有句号的中文句子补句号。最后这条默认关闭,它是唯一会添加「识别器没听到的文字」的规则,口述搜索词或文件路径时补句号是帮倒忙。

每条规则都是文本的纯函数,同一份听写永远润色出同一个结果,与是否配置了 AI 无关。每条规则改了多少处,报告里逐条记录。润色还保证一点:非空进去、非空出来,行首标点清理不会剥掉一份转写仅剩的内容。

热词:1153 条词条和空格的形状

中文引擎按自己的分词返回术语:open ai、chat gpt、G P T。精确匹配对这类形状无能为力,于是内置词库曾经用 12 条手工短语改写逐条补偿。12 条改写的存在本身就是缺口的证据:另外 1153 条规范拼写词条对同样的输出毫无作用,每多覆盖一个术语就要多写一条改写。

现在的匹配规则是去掉空白后再比对,把命中回映射到原文。识别器返回 type script,规则 TypeScript 直接命中;返回 postgre sql,规则 PostgreSQL 同样命中。匹配有边界:片段必须整体落在字素和标识符边界上,xopen ai 和 openair 不会被误改;空白总量不能超过它中断的术语本身,a 和 b 之间隔九个空格不会被拼成 ab;不跨行。启用一个词条就覆盖它全部的空格变体,不再逐个枚举。

热词还有第二条路径:打进识别请求本身。whisper 兼容的 prompt 字段有 224 字符预算,词条以逗号连接,装不下的整条跳过(截断半个词恰好毒化那个词的识别),结尾补一个句号防止模型把 prompt 续写进正文。词典改写则是最后一道工序,在标点润色之后执行:用户写的规则是决定,任何前置步骤都不得撤销它。

词典的匹配语义只作用于显式规则。拼音纠错走另一条更保守的路:它只接受显式候选和无歧义的读音,多音字的逐字组合证明不了词级读音正确,放宽会连带改写普通词。

AI 后处理可选,而且被看管

配置了 AI Profile 后,转写文本可以再过一遍大模型做整理。这一步有开关,关掉不丢配置;但接受 AI 的改写前要过三道机械检查:改写为空、改写长度达到原文两倍以上(模型开始发挥或编造)、改写与原文的字符二元组重叠低于 0.2(它回答了另一个问题)。任何一道拒绝,回退到听写原文。丢失用户的原话是唯一不可接受的结局,这三道闸门把 AI 的角色限制在润色,而不是重写。

后处理也不会挡住下一次录音。按下停止的瞬间麦克风槽已经释放,AI 请求在后台按各自的 Run 归属回填,你再按一次热键就是新一轮听写,AI 耗时多久都与你无关。AI 步骤与识别链共用同一套重试预算与失败分类:瞬态故障在预算内重试,密钥错误这类永久失败不浪费第二次尝试。

免费版的 60 秒

基础版的语音输入单次连续录音上限 60 秒。到达上限时麦克风暂停,这一段不转写,你点继续可以接着说,最后停止时整段一起转写。暂停按音频样本计量而非壁钟,到达上限后、麦克风真正关闭前送达的样本照常保留,不丢半个词。每次重新打开麦克风重新计数,说满一次上限就又有一整个完整的录音段。实时引擎是唯一例外:到点直接结束该段并转写,因为挂起一条实时连接跨越一次时长未知的暂停,得到的往往是提供方空闲超时下的整段失败。

60 这个数字只在一处定义:特性目录里的一条限制,代码、界面文案与订阅页都从它读取。运行时不查订阅状态,上限随听写计划在开始时进入运行时,中途升级不影响进行中的这段录音;计划没有携带上限时,录音没有时长限制。

出问题时从哪里看

No speech detected,但明明说了话。 最常见的原因是语言不一致:系统识别按系统默认语言解码,你说的语言和它不一致时,它会给出确定性的「没有语音」结论而不是报错。检查语言偏好设置;自动模式下系统识别会依次尝试系统默认语言与你的偏好语言,指定语言则严格使用指定值。

引擎迟迟没有出字。 本地模型首次使用前要下载并做 SHA-256 校验,FunASR 和 Whisper 的加载发生在第一次解码前,这份成本由第一次识别承担。模型驻留有租约管理,用过的模型在内存里保留一段时间,下一次识别直接从内存走。耐心等第一次,或者提前在模型页把模型下载好。

长录音被切成了很多段。 这是设计行为:单段上限 5 秒,段间以 0.4 秒静音闭合,段尾补 120 毫秒。每段的录音时长、峰值电平、识别尝试与耗时都在任务详情里逐段可见,文本仍按说话顺序拼接。

远程引擎连续失败。 先确认 remote_audio_consent 已打开、引擎清单里的远程账号凭据有效。超过单引擎 3 次尝试或 60 秒墙钟后,链路会自动降级到下一个引擎;远程识别连续失败不会拖垮本地引擎的可用性。另外注意整段重转写的 300 秒上限:更长的会话自动跳过重转写,保留分段拼接结果。

录音到 60 秒自己停了。 免费版的连续录音上限触发的是暂停,麦克风关闭、界面出现上限说明,点「继续」就在同一段录音上接着说。这是订阅限制,不是故障;如果这个限制影响了你的用法,升级后同一界面不再出现。


语音输入在 1.2.0 版本随整条流式链路落地,各版本的变更见 1.2.0 更新说明。热键可以改,全部默认值见快捷键速查表;第一次安装与权限配置见安装与首次运行指南。

常见问题

Syzygy 的语音识别默认会把录音上传到云端吗?

不会。系统识别和已安装的本地模型(Whisper、FunASR Paraformer、Qwen3-ASR)完全在本机完成。任何远程识别引擎都受 remote_audio_consent 开关控制,该开关默认关闭,关闭时音频不允许离开设备。

本地语音识别支持哪些引擎?

本地引擎包括系统自带听写、whisper.cpp 全系模型(tiny 到 large-v3-turbo 及其量化版)、FunASR Paraformer INT8 和 Qwen3-ASR 0.6B INT8(经 sherpa-onnx 解码)。远程引擎支持 OpenAI 兼容转写端点、chat-completions 音频端点、阿里云百炼 DashScope 和腾讯云语音账号,按你配置的顺序回退。

免费版语音输入有什么限制?

单次连续录音最长 60 秒。到达上限时麦克风暂停而不是转写,点继续可以接着说,最后停止时整段一起转写。暂停按音频样本计量,不会丢半个词。