Skip to content

Commit 25cd23d

Browse files
committed
refactor(rust): 拆 live_session_frame 第 5 步 — 档案运行期
1 parent 0d20ad7 commit 25cd23d

3 files changed

Lines changed: 219 additions & 187 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
//! 画面档 / 档案三维运行期(live_session_frame.rs 的拆分子模块)。
2+
//!
3+
//! @ai-context: 采样 tick 上的**档案决策三块**集中于此——①画面价值观测注入(帧切换
4+
//! 上升沿 + OCR 面积)与升档静默/降档确认;②`profile_override` 三维热切换
5+
//! 消费(command 写入,本 worker 是唯一消费者,`guard.take()`);③领域
6+
//! 自动重评(150s 窗口 + B站选集 OCR 证据 + ASR 开场白)。
7+
//! @ai-context: 锁顺序(不得倒置,D3 相邻约束):消费 override 时持 `profile_override`
8+
//! → 取 `applied_tier`(同拍),emit 在持锁中发出;即
9+
//! `profile_override → applied_tier/applied_profile`。
10+
//! @ai-context: 副作用 = `scheduler.retune`(采样档变更)/ 写 `applied_tier`、
11+
//! `applied_profile` 共享槽 / `app.emit`(live:tier-changed、
12+
//! live:tier-downgrade-request、live:profile-updated)/ 防御性 eprintln。
13+
//! 领域重评读 `subtitle_segments` 时守卫保持短(仅 filter/map/join)。
14+
15+
use tauri::Emitter;
16+
17+
use super::FrameWorkerState;
18+
19+
impl FrameWorkerState {
20+
/// 采样 tick 的档案决策拍(tier 观测 → 升降档 → override 消费 → 领域重评)。
21+
///
22+
/// @ai-context: `now_ms` 由调用方注入(同一采样 tick 内、process_frame **之后**
23+
/// 现取的补偿纪元毫秒)——顺序不可前移:tier 观测依赖
24+
/// `stats.diff_pass/ocr_ok` 在本拍 `process_frame` 内已更新。
25+
pub(super) fn profile_tick(&mut self, now_ms: u64) {
26+
// v0.9.0 M2(REQ-189):画面价值观测注入(每采样 tick)——帧切换
27+
// 上升沿(diff_pass 增量)、OCR 面积占比(ocr_ok 增量:本版以
28+
// 固定 0.4 近似——全帧变化路径即画面有文字;区域构成留 M4 迭代)
29+
// @review C12: has_structure 恒 false(区域构成信号暂缺实际注入)
30+
self.tier_observer.observe(
31+
now_ms / 1000,
32+
self.stats.diff_pass > self.last_tier_diff_pass,
33+
(self.stats.ocr_ok > self.last_tier_ocr_ok).then_some(0.4),
34+
false,
35+
);
36+
self.last_tier_diff_pass = self.stats.diff_pass;
37+
self.last_tier_ocr_ok = self.stats.ocr_ok;
38+
// 重评窗口结算后:升档静默生效(retune 采样器);降档需确认——
39+
// 确认结果经 tier_override 共享状态回流(前端 confirm_tier_downgrade)
40+
if let Some(new_tier) = self.tier_observer.current_tier() {
41+
let applied = self.tier_applied_tier;
42+
if applied != Some(new_tier) {
43+
let change = crate::video_tier_detect::decide_change(applied, Some(new_tier));
44+
let budget = crate::video_profile_spec_data::sampling_for_tier(new_tier);
45+
match change {
46+
crate::video_tier_detect::TierChange::UpgradeSilent
47+
| crate::video_tier_detect::TierChange::None => {
48+
// 升档/首定档静默应用(更积极采样无损失);同档无需动作
49+
self.scheduler.retune(budget);
50+
self.tier_applied_tier = Some(new_tier);
51+
if let Ok(mut guard) = self.applied_tier.lock() {
52+
*guard = Some(new_tier);
53+
}
54+
let _ = self.app.emit(
55+
"live:tier-changed",
56+
serde_json::json!({
57+
"tier": new_tier.as_str(),
58+
"reason": "upgrade-silent",
59+
}),
60+
);
61+
}
62+
crate::video_tier_detect::TierChange::DowngradeConfirm => {
63+
// 降档需确认:读取共享确认状态——用户已确认 → 应用;
64+
// 未确认 → 保持现状档(不丢信息),下轮重评再询
65+
let confirmed = self.tier_override
66+
.lock()
67+
.ok()
68+
.and_then(|g| *g)
69+
.filter(|t| *t == new_tier);
70+
if confirmed.is_some() {
71+
self.scheduler.retune(budget);
72+
self.tier_applied_tier = Some(new_tier);
73+
if let Ok(mut guard) = self.applied_tier.lock() {
74+
*guard = Some(new_tier);
75+
}
76+
if let Ok(mut guard) = self.tier_override.lock() {
77+
*guard = None;
78+
}
79+
let _ = self.app.emit(
80+
"live:tier-changed",
81+
serde_json::json!({
82+
"tier": new_tier.as_str(),
83+
"reason": "downgrade-confirmed",
84+
}),
85+
);
86+
} else {
87+
let _ = self.app.emit(
88+
"live:tier-downgrade-request",
89+
serde_json::json!({
90+
"from": self.tier_applied_tier.map(|t| t.as_str()),
91+
"to": new_tier.as_str(),
92+
}),
93+
);
94+
}
95+
}
96+
}
97+
}
98+
}
99+
// ── v0.11.5 Task 6: 消费档案三维覆写 ──
100+
if let Ok(mut guard) = self.profile_override.lock() {
101+
if let Some(po) = guard.take() {
102+
let mut changed = false;
103+
if let Some(t) = po.tier {
104+
let budget = crate::video_profile_spec_data::sampling_for_tier(t);
105+
self.scheduler.retune(budget);
106+
self.tier_applied_tier = Some(t);
107+
if let Ok(mut ag) = self.applied_tier.lock() { *ag = Some(t); }
108+
changed = true;
109+
}
110+
if let Some(f) = po.form { self.current_form = Some(f); changed = true; }
111+
// v0.11.5 审查修复(A3):domain 为 None(用户未选领域)→
112+
// 重置锁定,重新启用自动检测(领域重评不再跳过覆盖);
113+
// v0.13.6(审查修复):领域一并清空——避免 emit 出
114+
// domain=旧/fine=[] 的不一致快照("领域自动"语义)
115+
if po.domain.is_none() && self.domain_user_locked {
116+
self.domain_user_locked = false;
117+
self.current_domain_kind = None;
118+
self.current_fine_ids.clear();
119+
}
120+
if let Some(d) = po.domain {
121+
self.current_domain_kind = Some(d);
122+
// 用户手动覆写 → 锁定该维度(重评不覆盖)
123+
self.domain_user_locked = true;
124+
changed = true;
125+
// v0.13.6(REQ-220):细目随领域覆写(空=仅粗领域,合法)
126+
self.current_fine_ids = po.fine.clone();
127+
}
128+
if changed {
129+
let snapshot = crate::live_session::ProfileOverride {
130+
form: self.current_form,
131+
tier: self.tier_applied_tier,
132+
domain: self.current_domain_kind,
133+
fine: self.current_fine_ids.clone(),
134+
};
135+
if let Ok(mut ag) = self.applied_profile.lock() { *ag = Some(snapshot); }
136+
let _ = self.app.emit("live:profile-updated", serde_json::json!({
137+
"form": self.current_form.map(|f| f.as_str()),
138+
"tier": self.tier_applied_tier.map(|t| t.as_str()),
139+
"domain": self.current_domain_kind.map(|d| d.as_str()),
140+
"fine": self.current_fine_ids,
141+
}));
142+
} else {
143+
// v0.11.5 审查修复(A2):override 取到全空值(command 层
144+
// 应已拒绝全空,此为防御)
145+
eprintln!("[LiveSession] profile_override 取到全空值(command 层应已拒绝全空,此为防御)");
146+
}
147+
}
148+
}
149+
// ── v0.11.5 Task 6: 领域自动重评(同画面档窗口节拍)──
150+
let profile_reeval_now = now_ms / 1000;
151+
if profile_reeval_now >= self.last_profile_reeval_secs + 150 {
152+
self.last_profile_reeval_secs = profile_reeval_now;
153+
// v0.11.5 Task 7: B站 选集 OCR 证据增强——标题确认 B站 且累计
154+
// OCR 中选集命中(`P3/12`/`第X集`,adapt_bilibili_episode 解析)
155+
// → 累计 OCR 文本提升为平台证据(命中才加权,不命中不惩罚)
156+
let mut platform_tags: Vec<String> = Vec::new();
157+
if crate::platform_adapter::infer_platform(
158+
Some(&self.window_title),
159+
None,
160+
) == Some(crate::platform_adapter::PlatformKind::Bilibili)
161+
&& self.accumulated_ocr_text
162+
.iter()
163+
.any(|t| crate::platform_adapter::adapt_bilibili_episode(t).is_some())
164+
{
165+
platform_tags = self.accumulated_ocr_text.clone();
166+
}
167+
// v0.11.5 Task 7: ASR 开场白——前 30s 段文本(现有段累计可达,
168+
// 不为它新建数据流);无段 → None 诚实降级
169+
let asr_opening: Option<String> = self.subtitle_segments
170+
.lock()
171+
.ok()
172+
.map(|g| {
173+
g.iter()
174+
.filter(|s| s.start_ms <= 30_000)
175+
.map(|s| s.text.clone())
176+
.collect::<Vec<_>>()
177+
.join(" ")
178+
})
179+
.filter(|t| !t.trim().is_empty());
180+
let domain_signal = crate::video_profile_domain::DomainSignals {
181+
title: Some(self.window_title.clone()),
182+
platform_tags,
183+
user_confirmed: None,
184+
term_freq: self.accumulated_ocr_text.clone(),
185+
asr_opening,
186+
};
187+
let detected = crate::video_profile_domain::detect_domain(&domain_signal);
188+
// 终审 I-1:用户已手动覆写领域 → 跳过自动覆盖(用户裁决优先)
189+
if !self.domain_user_locked
190+
&& detected.kind.is_some()
191+
&& detected.kind != self.current_domain_kind
192+
&& detected.confidence >= 0.6
193+
{
194+
self.current_domain_kind = detected.kind;
195+
// v0.13.6:自动重评命中的细目随之生效(curated 预选;空则仅粗领域)
196+
self.current_fine_ids = detected.fine_ids;
197+
let snapshot = crate::live_session::ProfileOverride {
198+
form: self.current_form,
199+
tier: self.tier_applied_tier,
200+
domain: self.current_domain_kind,
201+
fine: self.current_fine_ids.clone(),
202+
};
203+
if let Ok(mut ag) = self.applied_profile.lock() { *ag = Some(snapshot); }
204+
let _ = self.app.emit("live:profile-updated", serde_json::json!({
205+
"form": self.current_form.map(|f| f.as_str()),
206+
"tier": self.tier_applied_tier.map(|t| t.as_str()),
207+
"domain": self.current_domain_kind.map(|d| d.as_str()),
208+
"fine": self.current_fine_ids,
209+
}));
210+
}
211+
}
212+
}
213+
}

0 commit comments

Comments
 (0)