Skip to content

Commit 90bb5f6

Browse files
committed
fix(live): 档位下拉初值改用后端记忆消除显示滞后
U2(控制方 §C44 授权):检测卡档位下拉的初值此前来自本地 KIND_TO_TIER 映射 ⇒ 重启 应用后卡片显示「低/中」而新会话实际按记忆档「高」起 —— §C11.4 把后端定为真源之后的 一致性缺口(显示与真源矛盾)。 - `DetectResult` 加 `memory_tier`(`#[serde(default)]` ⇒ 旧响应/旧 JSON 零迁移),由 `apply_profile_memory` 在 **kind 判定之外**回填(tier 是独立通道:用户只改过档位、 没改过形态时同样要带回),取值与 `start_live_session` 的解析**同源**(显式 > 记忆) - 前端 `DetectResult` 补 `memory_tier`;`ProfileDetector` 初值改 `r.memory_tier ?? KIND_TO_TIER[top] ?? "medium"`(无记忆 ⇒ 仍回落本地映射兜底) - 测试:Rust +3(带回 / 冲突象限仍带回 / 负控 + 旧 JSON 缺省);前端 +2 (「生效档 ≠ 本地映射档」用例 + 负控)——修前该用例红在 `expected 'low' to be 'rich'` - 同步豁免表 `ProfileDetector.tsx` 行 446 → 448(§C18.2 blob 构造) 门禁:`cargo test` **2351 passed / 0 failed / 6 ignored**(串行;一次并行复跑被 `ffmpeg` 墙钟超时假红,已按 P9 串行复绿)· clippy 唯一位置集合 **19 = SET-IDENTICAL** (post-only 0)· `tsc` 0 错 · 12 个受影响 vitest 文件 153 用例全绿 · line-limits --full / docs-check / registry 三闸全绿(未用 --no-verify)。
1 parent 08717e8 commit 90bb5f6

7 files changed

Lines changed: 93 additions & 4 deletions

File tree

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

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::video_profile::{
1212
apply_profile_memory, vote_detect, DetectResult, ObservedSignals, ProfileCandidate,
1313
ProfileKind, ProfileMemory,
1414
};
15-
use crate::video_profile_spec::ContentForm;
15+
use crate::video_profile_spec::{ContentForm, VisualTier};
1616

1717
/// 构造观测信号辅助(默认全 None)。
1818
fn signals() -> ObservedSignals {
@@ -173,3 +173,48 @@ fn detect_result_old_json_missing_memory_conflict() {
173173
assert_eq!(r.memory_conflict, None);
174174
assert_eq!(r.candidates[0].kind, ProfileKind::Lecture);
175175
}
176+
177+
/// 批 7 T19(U2):记忆档随检测结果带回(检测卡档位下拉初值 ⇒ 显示 == 实际生效档)。
178+
#[test]
179+
fn memory_tier_reported_for_display() {
180+
// Arrange:只记过档位(未记形态/kind)
181+
let memory = {
182+
let mut m = ProfileMemory::default();
183+
m.remember_tier("零基础化妆教程", VisualTier::Rich);
184+
m
185+
};
186+
// Act
187+
let result = apply_profile_memory(vote_detect(&signals()), &memory, "零基础化妆教程");
188+
// Assert:档位独立通道回填;kind/form 通道**未被** tier-only 条目污染
189+
assert_eq!(result.memory_tier, Some(VisualTier::Rich), "记忆档应随检测结果带回");
190+
assert_eq!(result.memory_hit, None, "tier-only 条目不得伪造 kind 命中");
191+
assert_eq!(result.memory_form, None, "tier-only 条目不得伪造形态命中");
192+
}
193+
194+
/// 批 7 T19(U2)边界:② 冲突象限(检测为准)**仍**带回记忆档——档位是独立轴,
195+
/// 与形态判定无关,且与 start_live_session 的解析(显式 > 记忆)口径一致。
196+
#[test]
197+
fn memory_tier_reported_even_when_detection_wins() {
198+
let memory = {
199+
let mut m = ProfileMemory::default();
200+
m.remember_tier("实操演练", VisualTier::Low);
201+
m
202+
};
203+
let result = apply_profile_memory(vote_detect(&strong_hands_on_signals()), &memory, "实操演练");
204+
// Assert:形态按检测(无 memory_hit),档位仍按记忆
205+
assert_eq!(result.memory_hit, None, "前置条件:高置信冲突 ⇒ 检测为准");
206+
assert_eq!(result.memory_tier, Some(VisualTier::Low), "独立轴:档位不随形态冲突被丢弃");
207+
}
208+
209+
/// 批 7 T19(U2)负控:无记忆档 ⇒ None(前端回落本地映射兜底,AGENTS.md §3.4)。
210+
#[test]
211+
fn memory_tier_none_without_memory() {
212+
let result = apply_profile_memory(vote_detect(&signals()), &ProfileMemory::default(), "未记过的标题");
213+
assert_eq!(result.memory_tier, None);
214+
// 旧 JSON 兼容:缺 memory_tier 字段 → None(零迁移)
215+
let old: DetectResult = serde_json::from_str(
216+
r#"{"candidates":[{"kind":"lecture","score":1.0}],"needs_confirmation":false,"memory_hit":null}"#,
217+
)
218+
.unwrap();
219+
assert_eq!(old.memory_tier, None);
220+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,11 +254,16 @@ pub fn profile_by_kind(kind: ProfileKind) -> VideoProfile {
254254
/// ④ 无记忆 → 纯检测(结果原样)
255255
/// @ai-context: 本函数不触碰 domain(命令层赋值);记忆命中时按 REQ-188 回填
256256
/// memory_form(检测为准的②场景不设——形态随检测)。
257+
/// @ai-context: 批 7 T19(U2):`memory_tier` 是**独立通道**(tier_entries)⇒ 在 kind 判定
258+
/// **之外**回填(用户只改过档位、没改过形态时同样要带回);其取值与
259+
/// `start_live_session` 的档位解析**同源**(显式 > 记忆)⇒ 检测卡的档位
260+
/// 下拉初值 == 新会话实际生效档(§C11.4 后端真源的显示面)。
257261
pub fn apply_profile_memory(
258262
mut result: DetectResult,
259263
memory: &ProfileMemory,
260264
title: &str,
261265
) -> DetectResult {
266+
result.memory_tier = memory.lookup_tier(title);
262267
if let Some(kind) = memory.lookup(title) {
263268
let top = result.candidates.first().map(|c| c.kind);
264269
let high_conf = !result.needs_confirmation;

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ pub struct DetectResult {
6464
/// 高于记忆/标题候选:影视/直播分区直接定叙事/直播形态);未命中 None。
6565
#[serde(default)]
6666
pub platform_form: Option<crate::video_profile_spec::ContentForm>,
67+
/// 批 7 T19(U2):命中标题的**画面档记忆**(独立通道——不随 kind 命中与否)。
68+
///
69+
/// @ai-context: 检测卡 v2 用它做档位下拉**初值** ⇒ 显示档 == 新会话实际生效档
70+
/// (与 `start_live_session` 的档位解析同源:显式 > 记忆);
71+
/// 旧后端/旧 JSON 缺省 None(零迁移)⇒ 前端回落本地映射兜底。
72+
#[serde(default)]
73+
pub memory_tier: Option<crate::video_profile_spec::VisualTier>,
6774
}
6875

6976
/// 检测得分阈值:top 得分低于该值视为信号不足。
@@ -106,6 +113,7 @@ pub fn vote_detect(signals: &ObservedSignals) -> DetectResult {
106113
domain: None,
107114
memory_conflict: None,
108115
platform_form: None,
116+
memory_tier: None,
109117
};
110118
}
111119
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
@@ -118,7 +126,7 @@ pub fn vote_detect(signals: &ObservedSignals) -> DetectResult {
118126
.get(1)
119127
.map(|(_, s)| scored[0].1 - *s < CONFLICT_GAP)
120128
.unwrap_or(false);
121-
DetectResult { candidates, needs_confirmation, memory_hit: None, memory_form: None, domain: None, memory_conflict: None, platform_form: None }
129+
DetectResult { candidates, needs_confirmation, memory_hit: None, memory_form: None, domain: None, memory_conflict: None, platform_form: None, memory_tier: None }
122130
}
123131

124132
/// 单档案得分(纯函数,可注入 fake 信号单测)。

‎app/src/components/ProfileDetector.test.tsx‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,31 @@ describe("ProfileDetector v0.13.6 三维交互", () => {
171171
expect(src.includes("TODO(" + "后端)"), "TODO(后端) 未删(它说的命令已存在)").toBe(false);
172172
expect(src.includes("仅本次" + "会话生效"), "旧文案仍在(写后端之后它是谎话)").toBe(false);
173173
});
174+
175+
it("批 7 T19 U2:档位下拉初值取后端记忆——「生效档 ≠ 本地映射档」时显示真值", async () => {
176+
// Arrange:候选 talking-head ⇒ 本地 KIND_TO_TIER 会是 "low"(该值由紧随其后的负控用例
177+
// 用**同一份检测结果**(仅去掉 memory_tier)独立钉住);后端记忆 = "rich"(用户上次选的)
178+
// ⇒ 「生效档(记忆 rich) ≠ 本地映射档(low)」,两者可区分。
179+
invokeMock.mockImplementation(async (cmd: string) => {
180+
if (cmd === "video_profiles") return [localStub];
181+
if (cmd === "detect_video_profile") return { ...detectResult, memory_tier: "rich" };
182+
if (cmd === "video_profile_for_spec") return specStub;
183+
return null;
184+
});
185+
render(<ProfileDetector windowTitle="某视频_哔哩哔哩_bilibili" />);
186+
const combos = await screen.findAllByRole("combobox");
187+
const tierSelect = combos[1] as HTMLSelectElement;
188+
// Assert:显示档 == 后端记忆档(§C11.4 后端为真源;新会话实际生效档同源:显式 > 记忆)
189+
await waitFor(() => expect(tierSelect.value).toBe("rich"));
190+
expect(tierSelect.value, "不得显示本地映射档(= 显示/生效不一致,U2 要修的正是这个)").not.toBe("low");
191+
});
192+
193+
it("批 7 T19 U2 负控:无记忆档 ⇒ 回落本地映射(兜底必须留)", async () => {
194+
// Arrange:detect 响应无 memory_tier(旧后端/无记忆)⇒ 走本地映射
195+
render(<ProfileDetector windowTitle="某视频_哔哩哔哩_bilibili" />);
196+
const combos = await screen.findAllByRole("combobox");
197+
const tierSelect = combos[1] as HTMLSelectElement;
198+
await waitFor(() => expect(tierSelect.value).toBe("low"));
199+
expect(tierSelect.value).not.toBe("rich");
200+
});
174201
});

‎app/src/components/ProfileDetector.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ export default function ProfileDetector({
153153
const top = r.candidates[0]?.kind ?? "unknown";
154154
const fromMemory = r.platform_form ?? r.memory_form ?? KIND_TO_FORM[top] ?? null;
155155
setForm(fromMemory);
156-
setTier(KIND_TO_TIER[top] ?? "medium");
156+
// 批 7 T19(U2):档位初值以后端**记忆档**为真源(与 start_live_session 的解析同源:
157+
// 显式 > 记忆)⇒ 显示档 == 新会话实际生效档;无记忆(旧后端/未选过)⇒ 回落本地映射。
158+
setTier(r.memory_tier ?? KIND_TO_TIER[top] ?? "medium");
157159
// 领域检测结果(detect_video_profile 内已含平台/标题领域检测)
158160
if (r.domain?.kind || r.domain?.fine_tags?.length || r.domain?.fine_ids?.length) {
159161
setDomain(r.domain);

‎app/src/types/live.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,8 @@ export interface DetectResult {
202202
memory_conflict?: ProfileKind | null;
203203
/** v0.13.6(REQ-221):平台分区映射表命中的形态(优先级高于记忆/标题候选;未命中缺省 null) */
204204
platform_form?: ContentForm | null;
205+
/** 批 7 T19(U2):命中标题的画面档记忆(检测卡档位下拉初值 ⇒ 显示 == 实际生效档;旧响应缺省 null) */
206+
memory_tier?: VisualTier | null;
205207
}
206208

207209
/** 领域检测结果(Rust DomainDetection) */

‎docs/standards/line-limit-exemptions.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
| app/src-tauri/src/live_session_loop.rs | 410 | v0.7.0 M0 拆分产物(音频编排循环):主循环 + 长静音/音量骤变/VAD 段事件写入 + drain/停止 flush;LiveSessionCtx 聚合上下文;A1 暂停边沿 + P1 停止 drain 重构;H1 修复(drain_deadline 改 Option,draining 置位时才计算) | 若再增长:事件写入块拆至 live_session_events.rs |
7171
| app/src/components/RichEditorView.tsx | 409 | v0.20.13 批 8(REQ-317)编辑态选区右键菜单接线(CM contextmenu extension + 动作分发,323→399;**前置 323 超 300 为 v0.20.8 偏差登记未登记债务**,本次实测纠偏并登记)——选区纯逻辑已拆 utils/noteSelectionMenu.ts 与 note-selection/SelectionActionMenu.tsx,本文件仅留宿主接线 | 若再增长:工具栏 action 分派拆至 commands/ 域(既有 toolbarCommands/headingCommand 范式) |
7272
| app/src-tauri/src/commands_live.rs | 471 | 实时采集命令域(启动/停止/档案热切换/剪辑监听);v0.13.6(REQ-220)update_live_profile 细目参数与校验 + 审查轮(clear sentinel 空串语义/fine 计入至少一项)再增 | 若再增长:档案覆写命令拆至 commands_live_profile.rs |
73-
| app/src/components/ProfileDetector.tsx | 446 | 档案检测组件:投票/确认流/记忆偏好 UI + v0.11.5 Task 5 冲突提示内聚 + v0.13.6(REQ-219~222)形态 10 下拉/领域 20 下拉/细目多选 chips/分区映射形态优先 + 审查轮(onProfileChange ref/独立 try/fine_ids 同步,实测 2026-08-24) | 若再增长:确认流与细目 chips 拆至 ProfileConfirmFlow.tsx |
73+
| app/src/components/ProfileDetector.tsx | 448 | 档案检测组件:投票/确认流/记忆偏好 UI + v0.11.5 Task 5 冲突提示内聚 + v0.13.6(REQ-219~222)形态 10 下拉/领域 20 下拉/细目多选 chips/分区映射形态优先 + 审查轮(onProfileChange ref/独立 try/fine_ids 同步,实测 2026-08-24) | 若再增长:确认流与细目 chips 拆至 ProfileConfirmFlow.tsx |
7474
| app/src-tauri/src/live_session_pause_tests.rs | 395 | plan_edge_observation 为纯函数(无 epoch/DB/emit 依赖——会话时刻由调用方以参数传入),可完整离线测真值表;事件对时刻单调性在此验证(DB 插入顺序 = events 顺序,单调即时间轴有序)。(自动摘取,待细化) | 若再增长:按职责拆分 |
7575
| app/src-tauri/src/live_session.rs | 394 | 会话装配/状态(LiveSessionParams 聚合 + run_session_after_engine 骨架 + ProfileOverride 细目字段);v0.13.6 +2 | 若再增长:ProfileOverride 与参数分拆至 live_session_params.rs |
7676
| app/src-tauri/src/fusion_tests.rs | 392 | 融合测试域(ADR-005 四规则 + REQ-062 概率加权 + REQ-103 音量透传 + REQ-111 切分对齐)单模块 #[path] 挂载 | 若再增长:REQ-111 切分对齐组拆至 fusion_split_tests.rs |

0 commit comments

Comments
 (0)