Files
Video-Tree-TRM5/app/harness/question_units.py
T
iomgaa 5ef5f2b8b7 refactor: extract _assemble_pair to lower build_units complexity to B
将 pair 分组的数量/角色校验与组装下沉到 _assemble_pair 私有 helper,
build_units 主体只做分组+派发,radon 复杂度从 C(11) 降到 A(4)。
fail-fast 语义、公共函数签名、稳定顺序均不变。
2026-07-15 05:54:08 -04:00

114 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""QuestionUnit 组装/展开/校验/单元正确性——pair 契约的唯一入口。
pool 构建、批处理、推理、评测(Task 3+)均通过本模块聚合/展开孪生对,
保证 AR pair 的"两题作为整体调度"契约只在一处实现、fail-fast 暴露非法配对。
"""
from collections import defaultdict
from core.types import GeneratedQuestion, QuestionUnit
def _assemble_pair(pair_id: str, group: list[GeneratedQuestion]) -> QuestionUnit:
"""校验单个 pair 分组的数量/角色并组装为 pair 单元(fail-fast)。
参数:
pair_id: 该分组共享的孪生对标识。
group: 归属同一 pair_id 的题目列表。
返回:
kind="pair" 的 QuestionUnit。
关键实现:
- 分组必须恰好 2 条,否则视为孤儿/超员,raise ValueError。
- 显式检查 pair_original / pair_mirror 角色齐备且唯一,缺失或重复
直接 raise ValueError(防 next(...) 静默 StopIteration)。
- 合法孪生对交由 QuestionUnit.from_pair 做 video_id/task_type/flip_axis
一致性断言。
"""
if len(group) != 2:
raise ValueError(f"pair {pair_id} 数量={len(group)}≠2(孤儿或超员)")
originals = [q for q in group if q.question_role == "pair_original"]
mirrors = [q for q in group if q.question_role == "pair_mirror"]
if len(originals) != 1 or len(mirrors) != 1:
raise ValueError(
f"pair {pair_id} 角色非法:original={len(originals)} mirror={len(mirrors)}"
"需各恰好 1 条"
)
return QuestionUnit.from_pair(originals[0], mirrors[0])
def build_units(questions: list[GeneratedQuestion]) -> list[QuestionUnit]:
"""将扁平题目列表聚合为单元列表:single 单封、pair 按 pair_id 成对聚合。
参数:
questions: 待聚合的题目列表,可混含 single 与孪生对成员。
返回:
单元列表,先 single 后 pair,顺序稳定(single 保留输入顺序,
pair 按首次出现的 pair_id 顺序)。
关键实现:
pair_id 为空 → single 单元;非空 → 归入对应 pair 桶。各 pair 桶的
数量/角色校验与组装下沉到 _assemble_pairfail-fast),本体只做分组
与派发。
"""
by_pair: dict[str, list[GeneratedQuestion]] = defaultdict(list)
singles: list[QuestionUnit] = []
for q in questions:
if q.pair_id:
by_pair[q.pair_id].append(q)
else:
singles.append(QuestionUnit.from_single(q))
pairs = [_assemble_pair(pid, qs) for pid, qs in by_pair.items()]
return singles + pairs
def flatten_units(units: list[QuestionUnit]) -> list[GeneratedQuestion]:
"""将单元列表无损展开回扁平题目列表。
参数:
units: 单元列表。
返回:
展开后的题目列表,保持单元顺序及单元内题目顺序。
"""
return [q for u in units for q in u.questions]
def validate_units(units: list[QuestionUnit]) -> list[QuestionUnit]:
"""校验单元列表结构合法性,通过则原样返回(便于链式调用)。
参数:
units: 待校验单元列表。
返回:
校验通过的原单元列表。
关键实现:
pair 单元必须恰好含 2 题,否则 raise ValueErrorsingle 单元无需额外
校验(构造时即为 1 题)。用于消费方在使用前做一道防御闸门。
"""
for u in units:
if u.kind == "pair" and u.size != 2:
raise ValueError(f"unit {u.unit_id} pair 不成对(size={u.size}")
return units
def unit_correctness(unit: QuestionUnit, per_q: dict[str, bool]) -> bool:
"""计算单元级正确性:AR pair 走双向 AND,single 即单题正确性。
参数:
unit: 目标单元。
per_q: 题目 question_id → 该题是否作答正确的映射。
返回:
单元内所有题目均正确时为 True,否则 False。
关键实现:
直接以 per_q[q.question_id] 取值,缺任一题触发 KeyError(防静默兜底),
强制上游先补齐全部单题结果再计单元正确性。
"""
return all(per_q[q.question_id] for q in unit.questions)