|
| 1 | +//! Tauri setup 装配(line-limit-exemptions.md 登记计划落地:lib.rs >600 行硬拆)。 |
| 2 | +//! |
| 3 | +//! @ai-context: 本模块承载 setup 的 AppState 初始化(数据目录/DB/引擎池/可校准 |
| 4 | +//! 配置/内存态存储)与装配期辅助函数(模型路径构造/捆绑同步/ORT |
| 5 | +//! 运行时目录注入)——lib.rs 只保留声明与 command 注册,职责单一。 |
| 6 | +//! @ai-context: 无业务逻辑(业务自底向上分布:types → concat/db → asr/ocr → |
| 7 | +//! engine → commands);全部装配失败均显式返回错误串,不静默。 |
| 8 | +
|
| 9 | +use std::path::Path; |
| 10 | + |
| 11 | +use tauri::Manager; |
| 12 | + |
| 13 | +use crate::asr::AsrModels; |
| 14 | +use crate::commands::AppState; |
| 15 | +use crate::db::Db; |
| 16 | +use crate::device_config::{decide, OcrDeviceConfig, OcrDeviceStatus}; |
| 17 | +use crate::engine::EnginePool; |
| 18 | +#[cfg(target_os = "windows")] |
| 19 | +use crate::live_session::LiveSessionManager; |
| 20 | +use crate::model_downloader::ModelDownloader; |
| 21 | +use crate::ocr::{OcrModels, OcrParams}; |
| 22 | +use crate::streaming_asr::StreamingAsrModels; |
| 23 | + |
| 24 | +/// 装配 AppState(setup 调用):数据目录/DB/引擎池/可校准配置/内存态存储。 |
| 25 | +/// |
| 26 | +/// @ai-context: 顺序敏感:① ORT 运行时目录注入(引擎线程加载 DLL 前)→ |
| 27 | +/// ② 数据/模型目录创建 → ③ 捆绑模型同步 → ④ DB 打开 + 崩溃恢复 |
| 28 | +/// → ⑤ OCR 设备决策(DXGI 探测/折叠模式)→ ⑥ 引擎池(后台加载) |
| 29 | +/// → ⑦ 可校准配置与内存态存储 → ⑧ app.manage 注入。 |
| 30 | +pub fn setup_app_state(app: &mut tauri::App) -> Result<(), String> { |
| 31 | + // ADR-009:引擎线程加载 ORT 之前注入运行时目录(CUDA 多 DLL 依赖) |
| 32 | + ensure_ort_runtime_search_dir(); |
| 33 | + let data_dir = app |
| 34 | + .path() |
| 35 | + .app_data_dir() |
| 36 | + .map_err(|e| format!("无法获取应用数据目录: {}", e))?; |
| 37 | + std::fs::create_dir_all(&data_dir).map_err(|e| format!("创建数据目录失败: {}", e))?; |
| 38 | + |
| 39 | + // 模型根目录(ASR 模型由下载/捆绑放入;OCR 模型经 ModelScope 自动缓存) |
| 40 | + let model_dir = data_dir.join("models"); |
| 41 | + std::fs::create_dir_all(&model_dir).map_err(|e| format!("创建模型目录失败: {}", e))?; |
| 42 | + |
| 43 | + // 同步随安装包捆绑的模型(方式①;开发环境缺 resources 时自动跳过) |
| 44 | + sync_bundled_models(app.handle(), &model_dir); |
| 45 | + |
| 46 | + // SQLite 数据库(本地优先,数据不出本机) |
| 47 | + let db_path = data_dir.join("entropy.db"); |
| 48 | + let db = Db::open(&db_path.to_string_lossy()).map_err(|e| format!("初始化数据库失败: {}", e))?; |
| 49 | + |
| 50 | + // 崩溃恢复:上次异常退出残留的 recording 会话标记为 failed(ADR-004) |
| 51 | + // 日志可观测:用户反馈会话"异常中断"时据此判断是否来自重启打断 |
| 52 | + let interrupted = db.mark_interrupted_sessions().map_err(|e| e.to_string())?; |
| 53 | + if interrupted > 0 { |
| 54 | + eprintln!("[Db] 启动恢复:{} 个中断会话标记为 failed", interrupted); |
| 55 | + } |
| 56 | + |
| 57 | + // 常驻引擎池(后台线程加载模型,不阻塞启动) |
| 58 | + // ADR-009(v0.4.0 M1):OCR 推理后端启动期决策—— |
| 59 | + // 三层检测:DXGI 硬件门槛(select_best)→ decide 折叠模式/校准 → ORT 原生回退(引擎内) |
| 60 | + let ocr_device_config_path = data_dir.join("ocr_device.json"); |
| 61 | + // TD-044 同批修复:配置内存态单点(命令层锁内 read-modify-write,防 TOCTOU) |
| 62 | + let ocr_device_config = std::sync::Arc::new(std::sync::Mutex::new(OcrDeviceConfig::load( |
| 63 | + &ocr_device_config_path, |
| 64 | + ))); |
| 65 | + #[cfg(target_os = "windows")] |
| 66 | + let adapters = crate::device_probe::probe_adapters(); |
| 67 | + #[cfg(not(target_os = "windows"))] |
| 68 | + let adapters = Vec::new(); |
| 69 | + let best_device = crate::device_probe::select_best(&adapters); |
| 70 | + let nvidia_count = crate::device_probe::nvidia_candidate_count(&adapters); |
| 71 | + let ocr_cfg_snapshot = ocr_device_config |
| 72 | + .lock() |
| 73 | + .map(|c| c.clone()) |
| 74 | + .unwrap_or_default(); |
| 75 | + let ocr_backend = decide(ocr_cfg_snapshot.mode, best_device, nvidia_count, ocr_cfg_snapshot.bench); |
| 76 | + eprintln!( |
| 77 | + "[Engine] OCR 设备决策: 模式 {:?} → 后端 {:?}{}", |
| 78 | + ocr_cfg_snapshot.mode, |
| 79 | + ocr_backend, |
| 80 | + best_device |
| 81 | + .map(|id| format!("(候选设备 index={},NVIDIA 候选数 {})", id, nvidia_count)) |
| 82 | + .unwrap_or_else(|| format!("(无候选,NVIDIA 候选数 {})", nvidia_count)) |
| 83 | + ); |
| 84 | + let ocr_device_status = |
| 85 | + std::sync::Arc::new(std::sync::Mutex::new(OcrDeviceStatus::new( |
| 86 | + ocr_cfg_snapshot.mode, |
| 87 | + ocr_backend, |
| 88 | + ocr_cfg_snapshot.bench, |
| 89 | + ))); |
| 90 | + let ocr_models = ocr_models(); |
| 91 | + let ocr_params = OcrParams::default(); |
| 92 | + // M5/REQ-040:共享词表(热词注入 ASR 流、替换词纠错 OCR;JSON 持久化) |
| 93 | + let vocab_path = data_dir.join("vocab.json"); |
| 94 | + let vocab = |
| 95 | + std::sync::Arc::new(std::sync::Mutex::new(crate::vocab::VocabStore::load(&vocab_path))); |
| 96 | + // v0.5.0 M1(REQ-043):档案记忆偏好(混合检测的用户确认记忆;JSON 持久化) |
| 97 | + let profile_memory_path = data_dir.join("video_profile_memory.json"); |
| 98 | + let profile_memory = std::sync::Arc::new(std::sync::Mutex::new( |
| 99 | + crate::video_profile::ProfileMemory::load(&profile_memory_path), |
| 100 | + )); |
| 101 | + // v0.5.0 M8(REQ-055):补缝式 AI 护栏骨架(配额/缓存/审计;云端 V1.0 生效) |
| 102 | + let ai_guardrails = |
| 103 | + std::sync::Arc::new(std::sync::Mutex::new(crate::ai_guardrails::AiGuardrails::default())); |
| 104 | + // v0.6.0 M1(REQ-083/060):可校准配置——ui_junk.json 黑名单与 |
| 105 | + // symbol_map.json 映射表(缺失走内置默认;损坏回退默认,不阻断启动) |
| 106 | + let ui_junk = crate::ui_junk::UiJunkList::load(&data_dir.join("ui_junk.json")); |
| 107 | + let symbol_normalize = |
| 108 | + crate::symbol_normalize::SymbolNormalizeConfig::load(&data_dir.join("symbol_map.json")); |
| 109 | + // v0.7.5(REQ-173/168):净化阈值与 OCR 纠错表可校准配置—— |
| 110 | + // purify_config.json / ocr_correction.json(同 ui_junk 先例: |
| 111 | + // 缺失走内置默认;损坏回退默认,不阻断启动) |
| 112 | + let purify = crate::purify_config::PurifyConfig::load(&data_dir.join("purify_config.json")); |
| 113 | + let ocr_corrections = |
| 114 | + crate::ocr_correction::OcrCorrectionTable::load(&data_dir.join("ocr_correction.json")); |
| 115 | + // v0.5.0 模型版(REQ-047/049/050):结构模型下载器(按需下载,独立状态机) |
| 116 | + let structure_downloader = crate::structure_models::StructureModelDownloader::new(); |
| 117 | + // v0.5.0 模型版(REQ-050):结构档位配置(公式档位持久化,审查 H3 修复) |
| 118 | + let structure_tier_path = data_dir.join("structure_tier.json"); |
| 119 | + // 结构模型装配目录(models/structure;下载器/引擎共用) |
| 120 | + let _ = std::fs::create_dir_all(model_dir.join("structure")); |
| 121 | + let engines = EnginePool::start( |
| 122 | + asr_models(&model_dir), |
| 123 | + ocr_models.clone(), |
| 124 | + ocr_params.clone(), |
| 125 | + ocr_backend, |
| 126 | + ocr_device_status, |
| 127 | + Some(vocab.clone()), |
| 128 | + ) |
| 129 | + .map_err(|e| format!("启动引擎池失败: {}", e))?; |
| 130 | + |
| 131 | + let streaming_models = streaming_asr_models(&model_dir); |
| 132 | + // v0.7.0 M1(REQ-104/132):剪贴板信号内存态存储 + 监听句柄占位 |
| 133 | + // (start_live_session 成功后启动监听线程,stop 时置位停止) |
| 134 | + let clipboard = std::sync::Arc::new(crate::clipboard_signal::ClipboardSignalStore::new()); |
| 135 | + let clipboard_monitor = std::sync::Arc::new(std::sync::Mutex::new( |
| 136 | + None::<crate::clipboard_signal::ClipboardMonitorHandle>, |
| 137 | + )); |
| 138 | + // v0.7.0 M2(REQ-115):VAD 阈值共享槽(会话线程发布、诊断面板读取) |
| 139 | + let vad_slot = |
| 140 | + std::sync::Arc::new(crate::vad_threshold_slot::VadThresholdSlot::default()); |
| 141 | + app.manage(AppState { |
| 142 | + db, |
| 143 | + engines, |
| 144 | + streaming_models, |
| 145 | + #[cfg(target_os = "windows")] |
| 146 | + live_session: LiveSessionManager::new(), |
| 147 | + model_downloader: ModelDownloader::new(), |
| 148 | + app: app.handle().clone(), |
| 149 | + ocr_device_config_path, |
| 150 | + ocr_device_config, |
| 151 | + ocr_models, |
| 152 | + ocr_params, |
| 153 | + vocab, |
| 154 | + vocab_path, |
| 155 | + model_dir, |
| 156 | + profile_memory_path, |
| 157 | + profile_memory, |
| 158 | + data_dir, |
| 159 | + ai_guardrails, |
| 160 | + structure_downloader, |
| 161 | + structure_tier_path, |
| 162 | + // v0.6.0 M1:可校准配置(UI 垃圾黑名单 / 口语符号映射表) |
| 163 | + ui_junk, |
| 164 | + symbol_normalize, |
| 165 | + // v0.7.5:净化阈值配置 / OCR 错字纠错表(purify_config.json / |
| 166 | + // ocr_correction.json 可校准) |
| 167 | + purify, |
| 168 | + ocr_corrections, |
| 169 | + // v0.7.0 M1(REQ-104/132):剪贴板信号(内存态存储 + 监听句柄占位; |
| 170 | + // start_live_session 启动监听 / stop_live_session 置位停止) |
| 171 | + clipboard, |
| 172 | + clipboard_monitor, |
| 173 | + // v0.7.0 M2(REQ-115):VAD 阈值共享槽 |
| 174 | + vad_slot, |
| 175 | + }); |
| 176 | + Ok(()) |
| 177 | +} |
| 178 | + |
| 179 | +/// 由模型根目录构造 ASR 模型路径(约定布局,禁止硬编码绝对路径)。 |
| 180 | +/// |
| 181 | +/// @ai-context: 正式版将随安装包捆绑模型(bundle.resources)并在首启复制到该目录, |
| 182 | +/// 路径约定保持不变即可无缝切换。 |
| 183 | +fn asr_models(model_dir: &Path) -> AsrModels { |
| 184 | + AsrModels { |
| 185 | + model: model_dir.join("asr/sensevoice/model.int8.onnx").to_string_lossy().into_owned(), |
| 186 | + tokens: model_dir.join("asr/sensevoice/tokens.txt").to_string_lossy().into_owned(), |
| 187 | + } |
| 188 | +} |
| 189 | + |
| 190 | +/// 构造流式 ASR 模型路径(ADR-003:models/streaming-zipformer/ 四件套)。 |
| 191 | +/// |
| 192 | +/// @ai-context: 2026-08 升级为 2025-06-30 新版中文 zipformer(fp16)——替代 2023-02-20 |
| 193 | +/// 旧双语包(性能与准确性显著提升,用户决策);文件名与官方仓库 |
| 194 | +/// csukuangfj/sherpa-onnx-streaming-zipformer-zh-fp16-2025-06-30 一致。 |
| 195 | +fn streaming_asr_models(model_dir: &Path) -> StreamingAsrModels { |
| 196 | + let dir = model_dir.join("streaming-zipformer"); |
| 197 | + StreamingAsrModels { |
| 198 | + encoder: dir.join("encoder.fp16.onnx").to_string_lossy().into_owned(), |
| 199 | + decoder: dir.join("decoder.fp16.onnx").to_string_lossy().into_owned(), |
| 200 | + joiner: dir.join("joiner.fp16.onnx").to_string_lossy().into_owned(), |
| 201 | + tokens: dir.join("tokens.txt").to_string_lossy().into_owned(), |
| 202 | + } |
| 203 | +} |
| 204 | + |
| 205 | +/// 构造 OCR 模型标识(oar-ocr 注册名,首次使用从 ModelScope 国内源自动下载缓存)。 |
| 206 | +fn ocr_models() -> OcrModels { |
| 207 | + OcrModels { |
| 208 | + det: "pp-ocrv6_tiny_det.onnx".to_string(), |
| 209 | + rec: "pp-ocrv6_tiny_rec.onnx".to_string(), |
| 210 | + dict: "ppocrv6_tiny_dict.txt".to_string(), |
| 211 | + } |
| 212 | +} |
| 213 | + |
| 214 | +/// 同步随安装包捆绑的模型到数据目录(模型集成方式①)。 |
| 215 | +/// |
| 216 | +/// @ai-context: bundle.resources 把 models/ 打进安装包,首启解压到数据目录供引擎加载; |
| 217 | +/// 开发环境无 resources 目录或 models 为空时自动跳过(走按需下载路径)。 |
| 218 | +/// 已存在的目标文件不覆盖(避免回退用户已更新的新版模型)。 |
| 219 | +fn sync_bundled_models(app: &tauri::AppHandle, model_dir: &Path) { |
| 220 | + let Ok(resource_dir) = app.path().resource_dir() else { return }; |
| 221 | + let bundled_root = resource_dir.join("models"); |
| 222 | + if !bundled_root.is_dir() { |
| 223 | + return; |
| 224 | + } |
| 225 | + if let Err(e) = copy_dir_skip_existing(&bundled_root, model_dir) { |
| 226 | + eprintln!("同步捆绑模型失败: {}", e); |
| 227 | + } |
| 228 | +} |
| 229 | + |
| 230 | +/// 递归复制目录,跳过已存在的文件。 |
| 231 | +fn copy_dir_skip_existing(src: &Path, dst: &Path) -> std::io::Result<()> { |
| 232 | + std::fs::create_dir_all(dst)?; |
| 233 | + for entry in std::fs::read_dir(src)? { |
| 234 | + let entry = entry?; |
| 235 | + let from = entry.path(); |
| 236 | + let to = dst.join(entry.file_name()); |
| 237 | + if from.is_dir() { |
| 238 | + copy_dir_skip_existing(&from, &to)?; |
| 239 | + } else if !to.exists() { |
| 240 | + std::fs::copy(&from, &to)?; |
| 241 | + } |
| 242 | + } |
| 243 | + Ok(()) |
| 244 | +} |
| 245 | + |
| 246 | +/// 把 ORT 运行时目录加入 DLL 搜索路径(ADR-009:CUDA 运行时含 cudart/cublas/cudnn |
| 247 | +/// 等数十个 DLL,不复制进 target;AddDllDirectory 让 onnxruntime.dll 按需找到它们)。 |
| 248 | +/// |
| 249 | +/// @ai-context: 必须在引擎线程首次加载 onnxruntime.dll 之前调用(setup 最早期); |
| 250 | +/// 失败仅告警——CPU-only 运行时无需额外 DLL,不阻断启动。 |
| 251 | +/// @ai-context: SetDefaultDllDirectories(DEFAULT_DIRS) 是 AddDllDirectory 生效的前提; |
| 252 | +/// 搜索序 = 应用目录(build.rs 已复制 onnxruntime.dll,压过 system32 旧版) |
| 253 | +/// + 系统目录 + 本目录。 |
| 254 | +fn ensure_ort_runtime_search_dir() { |
| 255 | + let Ok(lib_dir) = std::env::var("ORT_LIB_LOCATION") else { return }; |
| 256 | + let path = Path::new(&lib_dir); |
| 257 | + if !path.join("onnxruntime.dll").exists() { |
| 258 | + return; |
| 259 | + } |
| 260 | + unsafe { |
| 261 | + // 注意:本 crate 存在本地 `mod windows`(窗口枚举模块), |
| 262 | + // 外部 windows crate 必须以 ::windows:: 全路径引用(遮蔽问题) |
| 263 | + use ::windows::Win32::System::LibraryLoader::{ |
| 264 | + AddDllDirectory, SetDefaultDllDirectories, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS, |
| 265 | + }; |
| 266 | + let _ = SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); |
| 267 | + let wide = ::windows::core::HSTRING::from(path.to_string_lossy().as_ref()); |
| 268 | + // AddDllDirectory 返回句柄(null=失败,windows 0.61 无 Result 包装) |
| 269 | + if AddDllDirectory(::windows::core::PCWSTR(wide.as_ptr())).is_null() { |
| 270 | + eprintln!( |
| 271 | + "[Setup] AddDllDirectory 失败: {}(CUDA 运行时 DLL 可能无法加载)", |
| 272 | + path.display() |
| 273 | + ); |
| 274 | + } |
| 275 | + } |
| 276 | +} |
0 commit comments