OpenLess 听写主链路源码解读:从全局快捷键到 ASR、AI 润色与光标插入的完整数据流
OpenLess 听写主链路源码解读从全局快捷键到 ASR、AI 润色与光标插入的完整数据流【免费下载链接】openlessHold a key, speak, release — AI-polished text appears at your cursor in any app. Open-source voice input for macOS Windows. (按住快捷键说话松开即得润色后的文字)项目地址: https://gitcode.com/gh_mirrors/op/openlessOpenLess是一款开源语音输入工具按住快捷键说话松开即得——经过 ASR 转写和 AI 润色后的文字会自动插入到当前光标位置适用于 macOS 与 Windows 上的任意应用。本文带你沿着听写主链路走一遍看看一次「按住 → 说话 → 松开 → 落字」背后数据是如何从键盘钩子一路流到光标处的。一次听写的完整数据流 先建立整体地图。OpenLess 采用「界面 / Host / 共享 Core」三层架构React 界面只负责展示平台 Hostsrc-tauri/提供键盘钩子、录音和插入等原生能力真正的业务编排全部收敛在共享 Core 库中。官方架构文档对这条主链有一句话总结听写主链由dictation_engine.rs管理触发会话 → 录音/ASR → 清理与润色 → Host 插入 → 历史与事件。简化后的链路是键盘钩子边沿事件→ Core 热键解释器 → 听写引擎录音 ASR→ AI 润色 → Host 光标插入 → 历史记录与界面事件下面逐段拆解。全局快捷键边沿事件如何被捕获 ⌨️听写的起点是原生键盘钩子入口在 hotkey.rs。三个平台各有实现平台钩子方式macOS原生 CGEventTapWindowsWH_KEYBOARD_LL低级键盘钩Linuxfcitx5 插件通过 DBus 信号上报值得注意的一个设计Host 只上报原始边沿带代次编号press_id和单调时间戳的按下/抬起事件不解释业务语义。这样 macOS 钩子、Windows 钩子、Linux fcitx5 路径天然共享同一套判定逻辑不会出现两个平台按键手感不一样的问题。另外源码里特别提到一条独立的低延迟通道取消Esc和组合键撤销不走普通的HotkeyEvent队列而是走单独的HotkeyCombinedEdge通道并配专用消费线程——因为按下按键时桥接线程要同步完成开麦和 ASR 握手如果取消事件排在同一队列里用户会感到取消晚了几百毫秒才生效。按下、抬起怎么被判定成开始/结束 ️原始边沿交给 Core 的 hotkey_interpreter.rs 解释。它本质上是一个按键状态机输出四种意图Noop忽略、Start开始听写、Stop结束、Cancel取消。里面藏了不少打磨细节防抖 250ms抑制按键抖动和误触避免无谓地分配录音器与 ASR 会话结束后冷却 450ms会话刚完成、胶囊动画还没退场时不会让一次随手按键惊起新的录音Auto 模式 350ms 阈值短按视为 Toggle点按开始/再点按结束长按视为 Hold-to-talk按住说话修饰键仲裁窗口 150ms单独按 Ctrl/Option 可能是开始说话但配上普通字母键就只是普通打字——系统会等待 150ms 看有没有伴随键再下结论。听写会话状态机一次会话经历哪些阶段 会话状态定义在 types.rs 的DictationPhase枚举中阶段含义Idle空闲等待触发Starting开麦 ASR 握手Recording正在录音Transcribing音频转文字PolishingAI 润色中Inserting向光标处写入文字Failed任一步失败全局同时只允许一个活跃语音会话voice_session.rs 里的VoiceSessionGate用互斥锁保证听写、QA、选区语音等不同类型的会话不会互相踩踏取消令牌也在同一处统一撤销。听写引擎录音、ASR 与取消 ️整条流水线的编排者是 dictation_engine.rs 中的PipelineDictationEngine它持有三个可替换的部件AudioRecorder录音、TranscriptionEngine转写、TextPolisher润色。两个值得留意的设计边录边传的缓冲会话BufferedTranscriptionSession把麦克风 PCM 同时交给录音归档与流式 ASR支持边说边出字的实时转写而停止后再转写transcribe_after_stop的稳定模式下则丢弃中间增量只出最终结果。处处有取消护栏每个异步阶段准备 provider、开录音、挂载转写流前后都会检查取消标志一旦取消立即停止录音、撤销 provider 会话并清理资源保证 Esc 随时生效。转写能力本身是插件式的云端 ASR火山引擎、讯飞、百炼等与本地 ASRsherpa-onnx 离线解码都实现同一套TranscriptionEngine端口由配置决定走哪条路配置说明见 火山引擎 ASR 配置 与 qwen-asr 子模块升级清单。AI 润色超时预算里的工程权衡 ✨转写完成后进入润色阶段核心在 polish.rs。源码注释非常坦诚地记录了它踩过的坑最初是整个请求 30 秒的单一超时结果把推理型模型误杀了——某些模型在吐出正文前会先跑一整段思考一条 1758 字的长录音实测首字要 43~75 秒30 秒一到就被拦腰砍断用户拿回的是未润色的原始转写。于是超时被拆成了两把尺子StreamingTimeouts首字预算max(30, 字符数 × 0.05 30)秒——用户盯着空屏干等的上限覆盖模型思考期空闲预算出字后 20 秒没有新 chunk 才算真正卡死。只要还在稳定出字长稿就让它写完。润色失败时默认回退原文PolishFailurePolicy::UseRawText语音输入的核心承诺是总能拿到文字。提示词模板编译在 crates/openless-core/src/prompts/ 中按角色/任务/通用规则/输出/示例的分段结构维护。光标插入与剪贴板兜底 ⌨️最后一步是把文字送进光标处。insertion.rs 的策略很务实通用步骤先写剪贴板模拟失败时用户能手动粘贴→ 模拟粘贴快捷键。macOSCoreGraphics CGEvent 直接发送 CmdVWindows / Linux用 enigo 模拟粘贴快捷键终端可配置为 CtrlShiftVLinux优先走 fcitx5CommitText插件不可用再回退剪贴板。粘贴后 750ms 会恢复用户原来的剪贴板内容。如果整条链路失败界面上会弹出兜底卡片把文字放进剪贴板供手动粘贴——这也是copy_text_to_clipboard单独开口子的原因卡片浮在别的 App 之上时前端navigator.clipboard会因文档未聚焦而抛错只能走原生通道。关键源码文件速查 环节文件平台键盘钩子src-tauri/src/hotkey.rs按键语义解释器crates/openless-core/src/hotkey_interpreter.rs听写流水线编排crates/openless-core/src/dictation_engine.rs会话互斥与取消crates/openless-core/src/voice_session.rsAI 润色客户端crates/openless-core/src/polish.rs跨平台光标插入src-tauri/src/insertion.rsTauri 侧热键接入src-tauri/src/coordinator/dictation_core.rs整体架构说明docs/architecture.md2.0 平台范围与验收docs/2.0-requirements.md从一次物理按键到光标处多出的几个字OpenLess 用边沿事件 → 状态机 → 流水线 → 原生插入四级接力把这条链路串了起来Host 只提供最薄的原生能力所有业务判定集中在 Core这也是它能在 macOS、Windows、Linux 上保持一致听写手感的原因。【免费下载链接】openlessHold a key, speak, release — AI-polished text appears at your cursor in any app. Open-source voice input for macOS Windows. (按住快捷键说话松开即得润色后的文字)项目地址: https://gitcode.com/gh_mirrors/op/openless创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考