fix: harden mirror material rebuild (empty/invalid frames, dedup, observability)
This commit is contained in:
@@ -244,10 +244,12 @@ def _rebuild_material(tree: TreeIndex, source_nodes: tuple[str, ...]) -> Materia
|
||||
_collect_subtitle_sentences,
|
||||
)
|
||||
|
||||
subtitles = _collect_subtitle_sentences(tree, source_nodes)
|
||||
# 保序去重:source_nodes 若同时含父子节点,两者子树重叠会重复采集,去冗余再喂 VLM。
|
||||
subtitles = list(dict.fromkeys(_collect_subtitle_sentences(tree, source_nodes)))
|
||||
frames: list[str] = []
|
||||
for nid in source_nodes:
|
||||
frames.extend(_collect_frame_paths(tree, nid))
|
||||
frames = list(dict.fromkeys(frames))
|
||||
return _MaterialContext(
|
||||
anchor=None, # 镜像 prompt 不用 anchor
|
||||
source_nodes=source_nodes,
|
||||
@@ -286,6 +288,34 @@ def _parse_mirror(raw: str) -> dict | None:
|
||||
return mirror if isinstance(mirror, dict) else None
|
||||
|
||||
|
||||
def _existing_frames(frame_paths: list[str]) -> list[str]:
|
||||
"""过滤掉磁盘上不存在的帧路径,保序返回有效帧列表(防失效帧喂给 VLM)。"""
|
||||
return [p for p in frame_paths if Path(p).exists()]
|
||||
|
||||
|
||||
def _build_mirror_question(
|
||||
question: GeneratedQuestion, mirror: dict
|
||||
) -> GeneratedQuestion | None:
|
||||
"""从解析出的 mirror dict 构造镜像题;字段缺失/类型错误 → None。"""
|
||||
try:
|
||||
options = tuple(str(o) for o in mirror["options"])
|
||||
answer = str(mirror["answer"]).strip().upper()
|
||||
m_question = str(mirror["question"])
|
||||
except (KeyError, TypeError):
|
||||
return None
|
||||
return GeneratedQuestion(
|
||||
question_id=f"{question.question_id}_mirror",
|
||||
video_id=question.video_id,
|
||||
task_type=question.task_type,
|
||||
question=m_question,
|
||||
options=options,
|
||||
answer=answer,
|
||||
source_nodes=question.source_nodes,
|
||||
difficulty=question.difficulty,
|
||||
sub_pattern=question.sub_pattern,
|
||||
)
|
||||
|
||||
|
||||
async def generate_mirror_question(
|
||||
question: GeneratedQuestion,
|
||||
*,
|
||||
@@ -305,8 +335,15 @@ async def generate_mirror_question(
|
||||
|
||||
返回:
|
||||
镜像 GeneratedQuestion(question_id 加 "_mirror" 后缀,不进题库);
|
||||
无法造出有效对(null / 正解相同 / 解析失败)返回 None。
|
||||
素材为空 / null / 正解相同 / 解析失败均返回 None(上层记 flip_skipped)。
|
||||
"""
|
||||
# 素材防御:无字幕 AND 无有效帧 → 不调 VLM,直接 flip_skipped(避免空素材空转 VLM)。
|
||||
valid_frames = _existing_frames(list(material.frame_paths))
|
||||
if not material.subtitle_sentences and not valid_frames:
|
||||
logger.warning(
|
||||
"镜像素材为空(source_nodes 可能缺失或帧失效),跳过: {}", question.question_id
|
||||
)
|
||||
return None
|
||||
system = (_PROMPTS_DIR / "ar_mirror_question.md").read_text(encoding="utf-8")
|
||||
subs = "\n".join(f" - {s}" for s in material.subtitle_sentences)
|
||||
user = (
|
||||
@@ -317,32 +354,20 @@ async def generate_mirror_question(
|
||||
f"## Subtitles\n{subs}\n"
|
||||
)
|
||||
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
||||
resp = await vlm.chat_with_images(
|
||||
messages, list(material.frame_paths), session_id=session_id
|
||||
)
|
||||
resp = await vlm.chat_with_images(messages, valid_frames, session_id=session_id)
|
||||
mirror = _parse_mirror(resp.content)
|
||||
if mirror is None:
|
||||
return None
|
||||
try:
|
||||
options = tuple(str(o) for o in mirror["options"])
|
||||
answer = str(mirror["answer"]).strip().upper()
|
||||
m_question = str(mirror["question"])
|
||||
except (KeyError, TypeError):
|
||||
mirror_q = _build_mirror_question(question, mirror)
|
||||
if mirror_q is None:
|
||||
return None
|
||||
mirror_q = GeneratedQuestion(
|
||||
question_id=f"{question.question_id}_mirror",
|
||||
video_id=question.video_id,
|
||||
task_type=question.task_type,
|
||||
question=m_question,
|
||||
options=options,
|
||||
answer=answer,
|
||||
source_nodes=question.source_nodes,
|
||||
difficulty=question.difficulty,
|
||||
sub_pattern=question.sub_pattern,
|
||||
)
|
||||
# 镜像正解字面校验:canonical(P) 必须 != canonical(Q)(按选项文本比较,非字母)
|
||||
p_text = canonical_answer_text(question.options, question.answer)
|
||||
q_text = canonical_answer_text(mirror_q.options, answer)
|
||||
q_text = canonical_answer_text(mirror_q.options, mirror_q.answer)
|
||||
if p_text is None or q_text is None or p_text.strip() == q_text.strip():
|
||||
logger.info(
|
||||
"镜像正解与原题正解无区分(或无效),跳过翻转门: question_id={}",
|
||||
question.question_id,
|
||||
)
|
||||
return None
|
||||
return mirror_q
|
||||
|
||||
Reference in New Issue
Block a user