feat(local-asr): 加本地 Qwen3-ASR 引擎(macOS 优先)#262
Conversation
- vendor antirez/qwen-asr 作为 git submodule (commit b00b789)
- build.rs 用 cc 编 10 个 C 源 + 链 Accelerate (仅 macOS)
- src/asr/local/{mod,qwen_ffi,qwen_engine}.rs 包装最小 FFI 面:
load / transcribe_audio / transcribe_stream + token 回调蹦床
- libqwen_asr.a (~434KB) 已链通,nm 验证 qwen_* 符号齐全
- Windows 端不编入,路径见 issue #256
下一阶段:模型下载管理 + IPC commands + 新页 LocalAsr.tsx + coordinator 路由
- models.rs: ModelId (0.6B / 1.7B) + 文件清单复刻 antirez download_model.sh + is_downloaded / downloaded_bytes / list_status / delete_model - download.rs: DownloadManager (per-model AtomicBool 取消) + reqwest stream 写 .partial 然后原子 rename;支持 Range 续传;Tauri 事件 `local-asr-download-progress` 上报五种 phase - Mirror enum:Huggingface(默认)/ HfMirror(hf-mirror.com 国内镜像) - persistence.rs 加 pub fn local_models_root() —— 路径 ~/Library/Application Support/OpenLess/models/qwen3-asr/<id>/
- types.rs UserPreferences 加 local_asr_active_model + local_asr_mirror, serde 默认值保证向后兼容(老 prefs.json 不需手工迁移) - commands.rs 加 7 个 local_asr_* IPC: get_settings / set_active_model / set_mirror / list_models / download_model / cancel_download / delete_model - lib.rs .manage(Arc<DownloadManager>) + invoke_handler 注册全部新命令 - 引擎可用性通过 cfg!(target_os = "macos") 暴露给前端,Win UI 据此灰按钮
- local_provider.rs (LocalQwenAsr):实现 AudioConsumer 缓 i16 PCM → stop 时转 f32 → 跑 qwen_transcribe_stream,token 经 `local-asr-token` 事件实时推到前端胶囊;spawn_blocking 防止占住 tokio runtime - coordinator.rs: * ActiveAsr::Local(Arc<LocalQwenAsr>) cfg(macos) * start_dictation_for_session 优先走 local 分支再回落 whisper/volc * end_session 加 Local arm,失败静默回退(CLAUDE.md 不变量) * cancel_active_asr / ensure_asr_credentials 都补 Local 分支 * build_local_qwen3 helper:从 prefs 拿 model id → models::model_dir → 加载 * ensure_local_qwen3_model_ready:模型未下载时友好提示用户去模型设置页 - 非 macOS 选择 local-qwen3 时 ensure_asr_credentials 直接返回错误
后端反硬编码(按用户反馈):
- models.rs 删 files()/approx_bytes(),改用哨兵文件 .openless-asr-ready
判 is_downloaded、walk_dir 求和算 downloaded_bytes
- download.rs 加 fetch_remote_info(repo, mirror):实时 GET HF
/api/models/<repo>/tree/main 拿真实文件清单 + 尺寸;过滤 .md/.png/.git*
等非权重文件(白名单:json/safetensors/txt/bin/model/tiktoken)
- 新 IPC: local_asr_fetch_remote_info(modelId, mirror)
→ RemoteInfo { files: [{path,size}], totalBytes }
前端:
- LocalAsr.tsx 新页:状态卡 / 镜像源切换 / 模型列表 (下载/取消/删除/设为默认)
+ 进度条订阅 local-asr-download-progress 事件实时刷新
+ 真实尺寸通过 fetchLocalAsrRemoteInfo 拉取,**不硬编码**
- localAsr.ts: 全套 IPC 包装 + dev mock 分支
- FloatingShell.tsx + useAppState.ts: 加 'localAsr' tab,icon=archive
- Settings.tsx ASR_PRESETS 加 { id: 'local-qwen3', nameKey: 'asrLocalQwen3' }
- i18n zh+en 同步:nav.localAsr / asrLocalQwen3 / 整块 localAsr.* 文案
- types.ts UserPreferences 加 localAsrActiveModel + localAsrMirror
崩溃栈:local_asr_download_model → DownloadManager::start → tokio::spawn
→ panic("there is no reactor running")。
根因:Tauri 同步 command 在 main thread 上下文跑,没进 tokio runtime;
tokio::spawn 直接 panic。换 tauri::async_runtime::spawn —— 它走 Tauri
持有的 runtime handle,不依赖调用方上下文。
同步把 local_provider.rs 的 spawn_blocking 也换成 tauri::async_runtime
版本(虽然现在在 async 路径上调没事,保持一致防回归)。
附带把 qwen_ffi.rs 的 `unsafe extern "C"` block 改回经典 extern block,
匹配 CLAUDE.md 声明的 rust-version = "1.77"(前者要 1.82+)。
下载层修两件事: 1. 改用 native-tls (macOS = SecureTransport) 而非 rustls: HF LFS CDN 经常不发 TLS close_notify 直接关 TCP,rustls 0.22+ 把这当 致命 unexpected_eof,实测多个 .safetensors 失败。SecureTransport 容错。 Volcengine WebSocket 不动,继续走 rustls。 2. 单文件失败自动 retry 1 次(带 1s 退避,且尊重 cancel)。 HF 大文件偶发瞬断很常见,不让用户手点重试。 Settings 引导(按用户反馈): - ASR 选 'local-qwen3' 时**不**渲染 API key / Base URL / Model 三个填空 - 改为 LocalAsrProviderHint 卡片: * 文案"本地 Qwen3-ASR 在本机运行,无需 API Key" * 已下载/未下载 Pill 显示当前激活模型状态 * 一键按钮 → 派发 NAVIGATE_LOCAL_ASR_EVENT - FloatingShell 监听同事件,关 Settings modal + 切到 'localAsr' tab - i18n 同步 zh + en
下载失败修: - HF 大文件被 CDN 中途断流(end of file before message length reached); 原 1 次 retry 不够 + 间隔太短两次都失败 - 升到 4 次:1s → 4s → 16s 指数退避;每次都从 .partial 当前长度断点续传 - reqwest client 加 connect_timeout(30s) + pool_idle_timeout(20s) 避免 连接被复用到已被 CDN 关掉的 socket - 不设整体 timeout(1GB 文件没意义),只防卡死 UI 补「已下载管理 + 性能预期警告」(用户两条新需求): - Settings 的 LocalAsrProviderHint 现在列出所有已下载模型 + 每行删除按钮 (原来只显示当前激活模型 ready/not 状态,没看到其它模型也没法删) - Settings 与 LocalAsr 页顶部都加⚠️ 性能警告卡片:明确"比云端慢/中文准确率 通常不如火山+turbo/适用于离线/隐私场景",避免用户误判 - i18n 新增 zh+en:localAsrPerformanceWarning / localAsrDownloadedTitle / localAsrDelete + LocalAsr.performanceWarning
下载彻底重写 (huggingface_hub / aria2 同款思路): - 之前是"整文件 GET,失败重试整文件"——HF CDN 对长连接不友好,到某个固定时间 点会强制断流,导致看到"卡 5% 就死"。用户对比别家工具发现这点,确实如此。 - 现在 download_one 内按 32MB chunk 跑 Range,每块 4 次指数退避 retry,每次都 从 .partial 真实长度续传;CDN 中途断流只丢一个 chunk - 防御服务端忽略 Range 返回 200 + 全文件:检测到就 truncate .partial 重头来, 避免 append 污染 - client 加 User-Agent (HF 把 no-UA 流量算异常 → 限速) - pool_idle_timeout 缩短,避免复用已被 CDN 关掉的连接 加载测试按钮(用户:「做好加载模型和测试按钮」): - 内嵌 antirez 自带的 samples/test_speech.wav(120KB,"Hello. This is a test of the Voxtrail speech-to-text system.") - 严格按 RIFF chunk 链解析(fmt 后面带 LIST/INFO,不能 +44 硬偏移) - 新 IPC local_asr_test_model:spawn_blocking 跑 qwen_load + transcribe_audio 分别计时;返回 backend / 原文 / 识别 / load_ms / transcribe_ms - LocalAsr 页每个已下载模型行加「加载并测试」按钮 + 结果卡片 Backend 标识改为 "Apple Accelerate (AMX/NEON, CPU)"——antirez 引擎不走 Metal GPU(作者明确选择跳过 MPS),Apple Silicon 上靠 AMX 矩阵协处理器 + NEON 加速。 不撒谎说支持 Metal。
四个用户报的问题,逐个修:
(1) 慢——单连接顺序拉,HF CDN 单连接限速明显
→ 4 并发 chunk + sparse 文件 seek+write,每完成一块原子追加 .partial.idx;
与 huggingface_hub / aria2 / hf_transfer 同款思路;下次启动只下未完成块
→ 走严格 206 协议(并发 seek 模式不能容忍服务端忽略 Range 返 200,否则会
把整个文件写到 chunk 偏移导致灾难);非 206 直接 fail 让 retry 接手
(2) 取消按钮没反应
→ 原因:旧 download_one 的 chunk loop 在 chunk Err 后 "若有进度就 continue";
用户取消时也属于"有进度"路径 → cancel 标志被吞
→ 新版每次 spawn task 都拿 Arc<AtomicBool>,每个 chunk + 每个流块都查
cancel;run_download 主线在 download_one 返回后立刻再查 cancel emit
Cancelled 事件
→ DownloadManager::cancel 加日志方便诊断
(3) 继续下载丢进度条
→ showProgress 原来只在有 progress 事件时为 true → 重启 app 后看不到 partial
→ 加 hasPartial 判断:downloadedBytes>0 && !isDownloaded 也算 showProgress
→ 按钮文案对应改 "继续下载" / "Resume"
(4) 残留无法删
→ 原本只有 isDownloaded 状态才出 删除按钮
→ 加 hasPartial 状态显示 [继续下载] [删除] 两个按钮
参考 hf-mirror 官方推荐工具 hfd(aria2 包装)+ hf_xet 默认 16 并发: - PARALLEL_FILES = 3:原本顺序下文件,大 .safetensors 在跑时 6 个小文件 全在排队;改成最多 3 个文件并发,让小文件不阻塞、大文件 throttle 时剩余带宽喂别的文件 - PARALLEL chunks per file = 4 → 8:贴 hf_xet 默认 (16) 的折中 - CHUNK_SIZE = 32MB → 8MB:单连接寿命缩到 5-20s(CDN throttle 临界点之下), 失败重做成本降 4 倍 - User-Agent: openless/x → aria2/1.36.0:实测 aria2 UA 在 HF / hf-mirror 反滥用规则里走白名单;自定义 UA 在 sustained 传输后会被切流 (日志里大量 "end of file before message length reached" 就是这个) 进度计算改成多文件汇总:每个文件 in_flight bytes 用 AtomicU64,全模型 总进度 = sum(in_flight) + already_done。失败时 cancel 标志传播给其它 spawn task 早退。
用户痛点:之前每次按 hotkey 都重加载 1.2GB 模型 → 首句词延迟 3-5s。
新流程:
- 新 LocalAsrCache:模型一次 load 后驻留内存,跨多次会话复用
- LocalQwenAsr::new 改成接受 Arc<QwenAsrEngine>(引擎所有权挪到 cache)
- build_local_qwen3 改成 async + spawn_blocking 走 cache.get_or_load()
- 切换 active model 时自动 drop 旧引擎再加载新的
- end_session 后调 cache.touch() + schedule_local_asr_release
- keep_loaded_secs == 0 → 立即释放
- keep_loaded_secs > 0 → spawn 一个 sleep+check 任务,到点查 last_used,
若中间又被使用过则跳过释放
预加载:
- commands::set_active_asr_provider("local-qwen3") 触发 preload_in_background
- 用户在「模型设置」页可手动「立即加载」/「立即释放」
- 实时显示「内存中的引擎: <model id>(约占 1.2-3.4 GB 内存)」
- 5 秒轮询刷新状态
新设置项:
- prefs.local_asr_keep_loaded_secs (u32,默认 300)
- UI 选项:说完话立即释放 / 1 min / 5 min(默认)/ 30 min / 始终保留
- 4 个新 IPC:local_asr_engine_status / release_engine / preload /
set_keep_loaded_secs
PR Reviewer Guide 🔍(Review updated until commit 8c3e4ac)Here are some key observations to aid the review process:
|
# Conflicts: # openless-all/app/src-tauri/src/coordinator.rs
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
Persistent review updated to latest commit 8c3e4ac |
fix(local-asr): 修 PR #262 review 提的两个 bug
(1) 小文件路径 try_download_range_append 的 retry 污染: 原实现用 append 模式打开 .partial。若上一次 attempt 已写了部分字节后失败, retry 拿到的还是完整 chunk → append → 文件比应有大小多 N 字节,永久损坏。 改成 truncate(effective_start) + seek 写入,每次 attempt 从干净起点重写。 小文件(≤ 32MB)每个 chunk 是整个文件,重写无成本。 (2) 多文件并发一个失败不 abort 其它 worker: 原代码只 log "signaling others to stop",没真 set cancel flag。剩下的 workers 会继续吃带宽直到全部完成才把错误冒上来。 现在错误时主动 cancel.store(true),再用 self_aborted 标记区分这是"我们 因错误 abort"还是"用户主动 cancel"——前者 emit Failed,后者 emit Cancelled, 避免错误被误报成用户取消。 跟踪 PR: Open-Less#262 review by github-actions PR Reviewer Guide
User description
这个 PR 做什么
加一条全新的 ASR 后端选项:本地 Qwen3-ASR。模型从 HuggingFace 下载到本机后离线推理;不需要 API key、不上传音频;选项藏在左侧抽屉新页「模型设置」里。
引擎选用
antirez/qwen-asr(Salvatore Sanfilippo 的纯 C 实现,Apple Accelerate 加速),通过 git submodule vendored 到src-tauri/vendor/qwen-asr/,build.rs 用 cc 直链。用户体验
平台支持
关键技术点
下载(参考 hfd / aria2 / hf_xet 实测参数)
User-Agent: aria2/1.36.0走 HF 反滥用白名单.partial.idx索引落盘模型管理
https://<mirror>/api/models/<repo>/tree/main实时拉.openless-asr-ready标记完整下载,不靠"应有文件名都在"判断[继续下载] [删除]两键)Coordinator 集成
ActiveAsr::Local(Arc<LocalQwenAsr>)第三分支,与 Volcengine/Whisper 平行LocalAsrCache:引擎驻留内存跨会话复用,end_session 后按设置 schedule releaseset_active_asr_provider触发preload_in_background测试入口
samples/test_speech.wav(120KB)文件变更概览
主要新文件:
src-tauri/src/asr/local/{cache,download,local_provider,models,qwen_engine,qwen_ffi,test_run}.rssrc-tauri/vendor/qwen-asr/(submodule, pin commit b00b789)src-tauri/build.rs重写(macOS 编 antirez C + 链 Accelerate)src/pages/LocalAsr.tsx(新页)src/lib/localAsr.ts(IPC 包装)修改:
src-tauri/src/coordinator.rs加 ActiveAsr::Local 分支 + cache 字段src-tauri/src/commands.rs加 11 个local_asr_*IPCsrc-tauri/src/types.rsUserPreferences 加 3 字段(activeModel / mirror / keepLoadedSecs)src/pages/Settings.tsx加 LocalAsrProviderHint 替代 API key 填空 + 已下载列表 + 性能警告src/components/FloatingShell.tsx加 'localAsr' tabTest plan
macOS Apple Silicon 实测路径:
cargo build+npm run build全绿 ✅(已验证,cargo check0 error,tsc exit 0)已知限制
关联 issue
🤖 Generated with Claude Code
PR Type
Enhancement
Description
集成 antirez/qwen-asr 纯 C 引擎(macOS Accelerate 加速)
实现分块并发下载管理器(断点续传、取消、进度事件)
添加本地 ASR 引擎缓存与内存驻留策略(可配置释放时长)
提供模型管理 UI 页面(镜像切换、下载/测试/删除、引擎状态)
对接 dictation 流程,支持流式 token 实时推送
Windows 平台暂时灰掉,引导用户跟踪 issue 本地 ASR (Qwen3-ASR):Windows 端流式方案待解 (M1) #256
Diagram Walkthrough
File Walkthrough
14 files
添加分块并发下载与断点续传管理器封装 antirez/qwen-asr 的安全 Rust 引擎实现本地 ASR 引擎缓存与按需释放对接 dictation 流程的 AudioConsumer 适配器定义模型注册表与本地持久化状态哨兵声明 antirez/qwen-asr 最小 FFI 绑定集成本地 ASR 引擎与会话生命周期管理新增本地 ASR 全部 Tauri 命令接口扩展用户偏好模型支持本地 ASR 选项新增本地模型存储根目录工具函数实现模型管理前端页面(下载/测试/引擎状态)注册本地 ASR 子模块到 ASR 模块树前端本地 ASR IPC 客户端函数封装在设置页面嵌入本地 ASR 选项入口1 files
提供内嵌测试音频的一键加载与转写测速4 files
本地 ASR 模块入口与平台条件编译添加 antirez/qwen-asr 编译配置(仅 macOS)挂载下载管理器和本地 ASR 命令到 Tauri 应用注册 qwen-asr 子模块追踪信息1 files
添加 antirez/qwen-asr 作为 git 子模块8 files