@@ -23,6 +23,8 @@ use crate::live_session::LiveSessionParams;
2323pub struct LiveSessionStatus {
2424 pub active : bool ,
2525 pub session_id : Option < i64 > ,
26+ /// P3:引擎预热是否已就绪(前端"开始即录"提示)
27+ pub prepared : bool ,
2628}
2729
2830/// 启动实时捕获会话(REQ-007~012 汇总入口)。
@@ -83,20 +85,73 @@ pub async fn start_live_session(
8385/// 停止实时捕获会话;返回被停止的会话 id(None=无活动会话)。
8486#[ tauri:: command]
8587pub async fn stop_live_session ( state : State < ' _ , AppState > ) -> Result < Option < i64 > , String > {
86- let stopped = state. live_session . stop_active ( ) . map_err ( |e| e. to_string ( ) ) ?;
88+ // 停止含 5s 有界等待——异步 command 不得直接阻塞运行时线程(审查要求;
89+ // LiveSessionManager 为 Clone+Send,可安全移入 spawn_blocking)
90+ let manager = state. live_session . clone ( ) ;
91+ let stopped = tauri:: async_runtime:: spawn_blocking ( move || manager. stop_active ( ) )
92+ . await
93+ . map_err ( |e| format ! ( "停止任务失败: {}" , e) ) ?
94+ . map_err ( |e| e. to_string ( ) ) ?;
8795 stop_clipboard_monitor ( & state) ;
8896 Ok ( stopped)
8997}
9098
91- /// 查询实时会话状态(活动会话 id)。
99+ /// 查询实时会话状态(活动会话 id + 预热就绪标记 )。
92100#[ tauri:: command]
93101pub fn live_session_status ( state : State < ' _ , AppState > ) -> LiveSessionStatus {
94102 LiveSessionStatus {
95103 active : state. live_session . active_session_id ( ) . is_some ( ) ,
96104 session_id : state. live_session . active_session_id ( ) ,
105+ prepared : matches ! (
106+ state. live_session. prepare_status( ) ,
107+ crate :: live_session_prepare:: PrepareStatus :: Ready
108+ ) ,
97109 }
98110}
99111
112+ /// 预热流式 ASR 引擎(P3):选窗口阶段后台加载,点"开始"即录。
113+ ///
114+ /// @ai-context: 返回 PrepareStatus(loading/ready/failed/idle)供前端提示;
115+ /// 幂等;模型文件缺失预检快速返回 Failed(不白起线程);
116+ /// 页面卸载时由 release_live_prepare 释放(15min TTL 兜底)。
117+ #[ tauri:: command]
118+ pub fn prepare_live_session (
119+ state : State < ' _ , AppState > ,
120+ ) -> Result < crate :: live_session_prepare:: PrepareStatus , String > {
121+ // 防御性预检:四件套任一缺失 → 预热必失败,快速返回不白起线程
122+ let m = & state. streaming_models ;
123+ let missing: Vec < & str > = [
124+ ( & m. encoder , "encoder" ) ,
125+ ( & m. decoder , "decoder" ) ,
126+ ( & m. joiner , "joiner" ) ,
127+ ( & m. tokens , "tokens" ) ,
128+ ]
129+ . iter ( )
130+ . filter ( |( p, _) | !std:: path:: Path :: new ( p) . exists ( ) )
131+ . map ( |( _, name) | * name)
132+ . collect ( ) ;
133+ if !missing. is_empty ( ) {
134+ return Ok ( crate :: live_session_prepare:: PrepareStatus :: Failed ( format ! (
135+ "模型缺失: {}" ,
136+ missing. join( ", " )
137+ ) ) ) ;
138+ }
139+ let env = crate :: live_session_prepare:: PrepareEnv {
140+ streaming_models : state. streaming_models . clone ( ) ,
141+ engines : state. engines . clone ( ) ,
142+ vocab : state. vocab . clone ( ) ,
143+ // ADR-012 F4-2:标点恢复模型路径(与 start 同口径)
144+ punctuation_model : crate :: punctuation_model ( & state. model_dir ) ,
145+ } ;
146+ Ok ( state. live_session . prepare ( env) )
147+ }
148+
149+ /// 释放预热引擎(P3:离开课堂助手页时调用;有界 join ≤1s,超时 detach)。
150+ #[ tauri:: command]
151+ pub fn release_live_prepare ( state : State < ' _ , AppState > ) -> Result < ( ) , String > {
152+ state. live_session . release_prepare ( ) . map_err ( |e| e. to_string ( ) )
153+ }
154+
100155/// 暂停实时会话(2026-08 A1 硬暂停:完全停采,时间轴冻结)。
101156///
102157/// @ai-context: 只置共享标志——实际暂停由捕获线程边沿检测执行(WASAPI 端点
0 commit comments