Skip to content

Commit 802a370

Browse files
committed
fix(live): 预热双跑语义收敛——StrictMode 免疫 + 加载期取消即时释放
1 parent 8b2c5e6 commit 802a370

2 files changed

Lines changed: 47 additions & 29 deletions

File tree

‎app/src-tauri/src/live_session_prepare.rs‎

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,22 @@ pub fn run_prepared(rx: mpsc::Receiver<PrepareMsg>, env: PrepareEnv, status: Arc
123123
return;
124124
}
125125
};
126+
// 加载期间可能已收到 Cancel/断开(release 先于加载完成——引擎加载不可中断,
127+
// 请求排队的语义收口):先消费再宣告 Ready,避免"已完成→随即取消"的自相
128+
// 矛盾日志(dev StrictMode 双挂载下可稳定复现的误读观感)。
129+
// Start 理论上不会出现在加载期(start 仅在 status=Ready 时发交接;Loading
130+
// 期 start 走内联回退并先取消预备),但不可吞消息——交接到手就就地转会话
131+
match rx.try_recv() {
132+
Ok(PrepareMsg::Cancel) | Err(mpsc::TryRecvError::Disconnected) => {
133+
eprintln!("[LiveSession] 加载期间已请求取消,释放引擎");
134+
return;
135+
}
136+
Ok(PrepareMsg::Start(handoff)) => {
137+
run_with_handoff(engine, *handoff);
138+
return;
139+
}
140+
Err(mpsc::TryRecvError::Empty) => {}
141+
}
126142
*status.lock().expect("prepare status lock poisoned") = PrepareStatus::Ready;
127143
eprintln!("[LiveSession] 引擎预热完成(等待开始/取消)");
128144

@@ -131,26 +147,7 @@ pub fn run_prepared(rx: mpsc::Receiver<PrepareMsg>, env: PrepareEnv, status: Arc
131147
loop {
132148
match rx.recv_timeout(Duration::from_millis(PREPARE_POLL_MS)) {
133149
Ok(PrepareMsg::Start(handoff)) => {
134-
let StartHandoff {
135-
params,
136-
session_id,
137-
stop,
138-
latest_frame,
139-
pause,
140-
session_info,
141-
} = *handoff;
142-
// 会话纪元在交接后创建(模型已加载——无 A1 秒级偏移)
143-
let epoch = Instant::now();
144-
crate::live_session::run_session_after_engine(
145-
engine,
146-
stop,
147-
params,
148-
session_id,
149-
latest_frame,
150-
pause,
151-
epoch,
152-
session_info,
153-
);
150+
run_with_handoff(engine, *handoff);
154151
return;
155152
}
156153
Ok(PrepareMsg::Cancel) | Err(RecvTimeoutError::Disconnected) => {
@@ -166,3 +163,20 @@ pub fn run_prepared(rx: mpsc::Receiver<PrepareMsg>, env: PrepareEnv, status: Arc
166163
}
167164
}
168165
}
166+
167+
/// Start 交接执行(run_prepared 的就绪前/就绪后两处共用——防语义漂移)。
168+
fn run_with_handoff(engine: StreamingAsrEngine, handoff: StartHandoff) {
169+
let StartHandoff {
170+
params,
171+
session_id,
172+
stop,
173+
latest_frame,
174+
pause,
175+
session_info,
176+
} = handoff;
177+
// 会话纪元在交接后创建(模型已加载——无 A1 秒级偏移)
178+
let epoch = Instant::now();
179+
crate::live_session::run_session_after_engine(
180+
engine, stop, params, session_id, latest_frame, pause, epoch, session_info,
181+
);
182+
}

‎app/src/pages/ClassroomPage.tsx‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* @ai-context: 2026-08 审查硬拆(>600 硬上限):右栏内容区拆至 ClassroomRightPane,
1010
* 文件素材输入与提取拆至 MaterialInputPanel——本文件回归装配层职责。
1111
*/
12-
import { useCallback, useEffect, useState } from "react";
12+
import { useCallback, useEffect, useRef, useState } from "react";
1313
import { invoke } from "@tauri-apps/api/core";
1414
import { listen } from "@tauri-apps/api/event";
1515
import { WindowSelectCard } from "../components/WindowSelectCard";
@@ -104,16 +104,20 @@ export default function ClassroomPage({ onOpenSessions }: { onOpenSessions?: (se
104104
.catch(() => setPrepareState("idle"));
105105
}, []);
106106

107+
const warmedRef = useRef(false);
107108
useEffect(() => {
109+
// dev StrictMode 会双跑 effect(挂载→cleanup→挂载):预热是秒级重资源
110+
// 加载,第二次直接跳过——否则首次预热会被 replay 的 cleanup 取消(1s
111+
// join 超时 detach)后再开第二个加载线程,白跑一次引擎加载
112+
if (warmedRef.current) return;
113+
warmedRef.current = true;
108114
warmUp();
109-
// 离开页面释放预热引擎(内存 ~数百 MB;后端另有 15min TTL 兜底)
110-
return () => {
111-
// Low 清扫:不吞异常——释放失败记录上下文(TTL 兜底仍会回收)
112-
void invoke("release_live_prepare").catch((e) => {
113-
console.warn("[ClassroomPage] 释放预热引擎失败(TTL 兜底仍生效):", e);
114-
});
115-
};
116-
}, [warmUp]);
115+
// TD-004:课堂助手页常驻挂载(display:none 切换不卸载)——本 cleanup 只
116+
// 在应用卸载/StrictMode replay 时触发,**不在此释放**:release 会取消正在
117+
// 加载的预热线程造成 dev 双加载与"已完成→随即取消"误读;真实回收由后端
118+
// 15min TTL 与显式 release_live_prepare 命令承担(浮窗/退出路径保留)
119+
// eslint-disable-next-line react-hooks/exhaustive-deps
120+
}, []);
117121

118122
// 实时会话事件监听(v0.2.0;字幕/语音实时内容由右侧 LiveActivityPanel 自监听展示)
119123
useEffect(() => {

0 commit comments

Comments
 (0)