Files
Video-Tree-TRM5/research-wiki/plans/2026-07-14-action-recognition-strategy.md
T
2026-07-14 06:34:40 -04:00

775 lines
30 KiB
Markdown
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.
# ActionRecognitionStrategy 特化实现计划 (Plan B)
> **For agentic workers:** REQUIRED SUB-SKILL: Use subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 实现 ActionRecognitionStrategy 特化策略(6 个 SubPattern + AR 专属 prompt),替换 Plan A 中 AR 的临时 VISUAL_FAMILY 绑定,使 AR 出题靶向 22 道错题的 6 种失败子模式。
**Architecture:** 新建 `strategy_action_recognition.py`ActionRecognitionStrategy 类 + 6 个 SubPattern 定义),新建 `action_recognition.md` prompt 模板,在 `strategy.py` 的模块初始化中注册。Pipeline 无需改动 —— Plan A 已接好全部 sub_pattern 接口(`instruction``sampling_level_override``constraint_override`)。
**Tech Stack:** Python 3.11, pytest, Protocol (typing)
**关联设计:** `research-wiki/designs/2026-07-14-task-type-strategy-design.md` §4
**范围:** 仅 Plan BActionRecognitionStrategy 特化)。不改 pipeline、sampler、generator、gates、store。
---
### Task 1: AR 专属 prompt 模板
**Files:**
- Create: `store/prompts/question_gen/action_recognition.md`
- [ ] **Step 1: 创建 AR 专属 prompt 模板**
```markdown
You are a question generator for video understanding benchmarks, specializing in **Action Recognition**.
Your task: Generate a multiple-choice question that tests whether the answerer can accurately recognize, distinguish, and reason about **actions and behaviors** observed across multiple segments of the video.
## Action Recognition Guidelines
- The question MUST require watching multiple segments or the full video — single-frame-answerable questions are failures.
- Focus on **dynamic actions**: what someone does, how they do it, the sequence of actions, or which action is absent.
- The correct answer must be grounded in observable behavior (body movements, interactions, operations), NOT in static visual attributes or text/OCR.
- Questions should target action details that require temporal tracking: order of events, manner of execution, repetition counts, or cross-segment entity behavior.
## Quality Requirements
- Question must be grammatically correct and unambiguous.
- All four options must be parallel in structure and length.
- The correct answer must not be identifiable from linguistic cues alone.
- Each option must begin with "A. ", "B. ", "C. ", or "D. ".
## Prohibited Patterns
- Do NOT generate questions answerable from a single frame or screenshot — if pausing the video at one moment gives the answer, the question is too easy.
- Do NOT generate OCR/text-reading questions disguised as action recognition — reading jersey numbers, scoreboards, or on-screen text is NOT action recognition.
- Do NOT fabricate actions not observable in the provided material.
- Do NOT construct options where the correct answer is obvious from common sense or world knowledge alone.
- Do NOT write questions where multiple options could reasonably be correct.
## Output
Respond with ONLY a valid JSON object. No additional text.
```
- [ ] **Step 2: 验证模板可加载**
```bash
conda run -n Video-Tree-TRM python -c "
from app.question_gen.generator_v2 import _load_prompt_template
content = _load_prompt_template('action_recognition.md')
assert 'Action Recognition' in content
print('OK: action_recognition.md loaded successfully')
"
```
预期:打印 OK 消息,无异常。
- [ ] **Step 3: 提交**
```bash
git add store/prompts/question_gen/action_recognition.md
git commit -m "feat(question_gen): add Action Recognition specialized prompt template"
```
---
### Task 2: ActionRecognitionStrategy 类 + 6 个 SubPattern
**Files:**
- Create: `app/question_gen/strategy_action_recognition.py`
- Test: `tests/unit/test_strategy_action_recognition.py`
- [ ] **Step 1: 写失败测试**
```python
# tests/unit/test_strategy_action_recognition.py
"""ActionRecognitionStrategy 单元测试。"""
from __future__ import annotations
import random
import pytest
from app.question_gen.strategy_action_recognition import (
AR_SUB_PATTERNS,
ActionRecognitionStrategy,
)
class TestActionRecognitionStrategy:
"""ActionRecognitionStrategy 属性和行为。"""
@pytest.fixture
def strategy(self) -> ActionRecognitionStrategy:
"""创建策略实例。"""
return ActionRecognitionStrategy()
def test_task_type(self, strategy: ActionRecognitionStrategy) -> None:
"""task_type 固定为 Action Recognition。"""
assert strategy.task_type == "Action Recognition"
def test_strategy_name(self, strategy: ActionRecognitionStrategy) -> None:
"""strategy_name 为 ACTION_RECOGNITION。"""
assert strategy.strategy_name == "ACTION_RECOGNITION"
def test_skill_target(self, strategy: ActionRecognitionStrategy) -> None:
"""skill_target 为 M1_AR。"""
assert strategy.skill_target == "M1_AR"
def test_sampling_level(self, strategy: ActionRecognitionStrategy) -> None:
"""默认采样层级为 L2。"""
assert strategy.sampling_level == 2
def test_sampling_constraint(self, strategy: ActionRecognitionStrategy) -> None:
"""采样约束加强:min_subtitles=3, min_l3_nodes=5, require_frames=True, cross_l2_span=True。"""
c = strategy.sampling_constraint
assert c.min_subtitles == 3
assert c.min_l3_nodes == 5
assert c.require_frames is True
assert c.cross_l2_span is True
def test_prompt_template(self, strategy: ActionRecognitionStrategy) -> None:
"""使用 AR 专属 prompt 模板。"""
assert strategy.prompt_template == "action_recognition.md"
def test_leak_probe_template(self, strategy: ActionRecognitionStrategy) -> None:
"""复用 RETRIEVAL 的泄漏检测模板。"""
assert strategy.leak_probe_template == "gate_leak_retrieval.md"
def test_extra_gates_empty(self, strategy: ActionRecognitionStrategy) -> None:
"""当前版本无额外 gate。"""
assert strategy.extra_gates(None) == []
class TestSubPatternSelection:
"""select_sub_pattern 行为。"""
@pytest.fixture
def strategy(self) -> ActionRecognitionStrategy:
return ActionRecognitionStrategy()
def test_returns_sub_pattern(self, strategy: ActionRecognitionStrategy) -> None:
"""select_sub_pattern 返回 SubPattern 而非 None。"""
rng = random.Random(42)
sp = strategy.select_sub_pattern(rng)
assert sp is not None
assert sp.name in [p.name for p in AR_SUB_PATTERNS]
def test_deterministic_with_same_seed(self, strategy: ActionRecognitionStrategy) -> None:
"""相同 seed 返回相同 SubPattern。"""
sp1 = strategy.select_sub_pattern(random.Random(42))
sp2 = strategy.select_sub_pattern(random.Random(42))
assert sp1.name == sp2.name
def test_distribution_covers_all_patterns(self, strategy: ActionRecognitionStrategy) -> None:
"""足够多次采样应覆盖全部 6 个子模式。"""
rng = random.Random(123)
names = {strategy.select_sub_pattern(rng).name for _ in range(200)}
assert len(names) == 6
def test_sub_pattern_has_instruction(self, strategy: ActionRecognitionStrategy) -> None:
"""每个 SubPattern 都有非空 instruction。"""
for sp in AR_SUB_PATTERNS:
assert sp.instruction.strip(), f"{sp.name} 的 instruction 为空"
def test_sub_pattern_has_distractor_rules(self, strategy: ActionRecognitionStrategy) -> None:
"""每个 SubPattern 都有非空 distractor_rules。"""
for sp in AR_SUB_PATTERNS:
assert sp.distractor_rules.strip(), f"{sp.name} 的 distractor_rules 为空"
class TestSubPatternOverrides:
"""SubPattern 的 sampling 覆盖。"""
def test_l1_patterns_override_level(self) -> None:
"""L1 子模式覆盖默认的 L2 采样层级。"""
l1_names = {"premature_evidence_anchoring", "temporal_reasoning_failure", "cross_segment_entity_tracking"}
for sp in AR_SUB_PATTERNS:
if sp.name in l1_names:
assert sp.sampling_level_override == 1, f"{sp.name} 应覆盖为 L1"
else:
assert sp.sampling_level_override is None, f"{sp.name} 不应覆盖采样层级"
def test_weights_sum_to_one(self) -> None:
"""权重总和为 1.0。"""
total = sum(sp.weight for sp in AR_SUB_PATTERNS)
assert abs(total - 1.0) < 1e-9
def test_individual_weights_match_design(self) -> None:
"""每个子模式权重与设计文档一致。"""
expected = {
"premature_evidence_anchoring": 0.20,
"temporal_reasoning_failure": 0.20,
"semantic_rigidity": 0.15,
"fine_grained_visual_action": 0.15,
"cross_segment_entity_tracking": 0.15,
"evidence_gap_confabulation": 0.15,
}
for sp in AR_SUB_PATTERNS:
assert abs(sp.weight - expected[sp.name]) < 1e-9, f"{sp.name} 权重不匹配"
def test_all_six_patterns_defined(self) -> None:
"""定义了 6 个子模式。"""
assert len(AR_SUB_PATTERNS) == 6
expected_names = {
"premature_evidence_anchoring",
"temporal_reasoning_failure",
"semantic_rigidity",
"fine_grained_visual_action",
"cross_segment_entity_tracking",
"evidence_gap_confabulation",
}
actual_names = {sp.name for sp in AR_SUB_PATTERNS}
assert actual_names == expected_names
```
- [ ] **Step 2: 运行测试验证失败**
```bash
conda run -n Video-Tree-TRM pytest tests/unit/test_strategy_action_recognition.py -v
```
预期:ImportError — `app.question_gen.strategy_action_recognition` 不存在。
- [ ] **Step 3: 实现 strategy_action_recognition.py**
```python
# app/question_gen/strategy_action_recognition.py
"""Action Recognition 特化出题策略 — 靶向 6 种失败子模式。
来源:22 道 VME AR 错题双分类器仲裁分析。
每个 SubPattern 定义独立的 instruction、采样覆盖、干扰项构造规则,
由 pipeline 在出题时注入到 prompt 的 Special Focus 区段。
设计文档: research-wiki/designs/2026-07-14-task-type-strategy-design.md §4
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from app.question_gen.families import SamplingConstraint
from app.question_gen.strategy import SubPattern
if TYPE_CHECKING:
import random
# ---------------------------------------------------------------------------
# 默认采样约束(设计文档 §4 定义)
# ---------------------------------------------------------------------------
_AR_DEFAULT_CONSTRAINT = SamplingConstraint(
min_subtitles=3,
min_l3_nodes=5,
require_frames=True,
cross_l2_span=True,
)
# ---------------------------------------------------------------------------
# 6 个 SubPattern 定义
# ---------------------------------------------------------------------------
_SP_PREMATURE_EVIDENCE_ANCHORING = SubPattern(
name="premature_evidence_anchoring",
weight=0.20,
sampling_level_override=1,
constraint_override=None,
instruction=(
"Generate a question where the correct answer requires verifying evidence across "
"ALL options before committing — not just finding one matching piece of evidence. "
"The video must contain a plausible-looking but incorrect match that appears early "
"or prominently, while the true answer is confirmed only by cross-referencing "
"multiple segments. The question should punish an agent that stops searching after "
"the first evidence match."
),
positive_examples=[
{
"question": "Which of the following tasks did the heroine not complete while her baby was sleeping?",
"answer": "D. Doing laundry",
"why": "Requires checking all 4 options against all segments; stopping at first match misses the negation.",
},
{
"question": "Which acrobatic skill is absent from this video?",
"answer": "D. Somersault",
"why": "Must verify every option against every segment to confirm absence.",
},
],
negative_examples=[
{
"question": "What color jersey does the player wear?",
"why": "Single-frame answerable, no need to verify across segments.",
},
],
distractor_rules=(
"Place the most visually salient or early-appearing action as a distractor (not the answer). "
"Make one distractor partially correct (happens in a different segment or by a different person). "
"The correct answer should require exhaustive verification across segments."
),
)
_SP_TEMPORAL_REASONING_FAILURE = SubPattern(
name="temporal_reasoning_failure",
weight=0.20,
sampling_level_override=1,
constraint_override=None,
instruction=(
"Generate a question that requires precise temporal ordering or locating the Nth "
"occurrence of an event. The video must contain the same or similar action happening "
"multiple times, and the question must specify a temporal anchor (e.g., 'after X happens', "
"'the second time', 'at the beginning'). The correct answer depends on getting the "
"sequence order right."
),
positive_examples=[
{
"question": "In the video after feeding the ducks, what did the male protagonist do after riding his bike?",
"answer": "A. Went jogging in the park",
"why": "Requires precise temporal chain: feeding → biking → next action.",
},
{
"question": "What happened to the team on the counterattack after Sabonis' first steal?",
"answer": "D. They scored a three-pointer",
"why": "Must locate the FIRST steal (not second) and track what follows.",
},
],
negative_examples=[
{
"question": "What does the person do in the video?",
"why": "No temporal anchor, any observation suffices.",
},
],
distractor_rules=(
"Include actions that genuinely occur in the video but at a different time point. "
"One distractor should be what happens before the anchored moment. "
"Another should be what happens after the Nth+1 occurrence (off-by-one trap)."
),
)
_SP_SEMANTIC_RIGIDITY = SubPattern(
name="semantic_rigidity",
weight=0.15,
sampling_level_override=None,
constraint_override=None,
instruction=(
"Generate a question where the correct answer option uses a synonym, paraphrase, or "
"semantic equivalent of what is shown in the video — NOT the exact words from subtitles. "
"The agent must recognize that a rephrased description matches the observed action. "
"Include a distractor that uses near-verbatim subtitle wording but describes a "
"different or incorrect action."
),
positive_examples=[
{
"question": "What are the magic tricks about?",
"answer": "B. Sleight of hand with everyday objects",
"why": "Video shows card and coin manipulation; correct answer paraphrases rather than quoting subtitles.",
},
],
negative_examples=[
{
"question": "According to the narrator, what is the main topic?",
"why": "Invites verbatim subtitle matching, not semantic understanding.",
},
],
distractor_rules=(
"One distractor must reuse exact subtitle phrasing but apply it to the wrong action/context. "
"Another distractor should use a semantically related but distinct action verb "
"(e.g., 'cutting' vs 'slicing' vs 'chopping' when only one is correct). "
"The correct answer must be a valid semantic equivalent, not a stretch."
),
)
_SP_FINE_GRAINED_VISUAL_ACTION = SubPattern(
name="fine_grained_visual_action",
weight=0.15,
sampling_level_override=None,
constraint_override=None,
instruction=(
"Generate a question that distinguishes between visually similar actions — "
"the MANNER of how something is done, not just WHAT is done. "
"The video must show a specific technique, method, or style of performing an action, "
"and the question must test whether the agent can differentiate it from similar alternatives. "
"Frames are essential — the answer cannot come from subtitles alone."
),
positive_examples=[
{
"question": "How does the chef prepare the garlic in this recipe?",
"answer": "C. Crushes it with the flat side of a knife",
"why": "All options are valid garlic preparations; only visual observation distinguishes.",
},
{
"question": "What does the man with a laughing face do at the beginning of the video?",
"answer": "C. Clasps his hands together and bows",
"why": "Specific gesture detail requires frame-level observation.",
},
],
negative_examples=[
{
"question": "Does the person cook in the video?",
"why": "Binary yes/no, no manner distinction needed.",
},
],
distractor_rules=(
"All four options must describe the same general category of action "
"(e.g., all are ways of cutting, all are types of greetings). "
"Distractors must be visually plausible alternatives that could occur in the same context. "
"The distinction must be observable only from frames, not from subtitles."
),
)
_SP_CROSS_SEGMENT_ENTITY_TRACKING = SubPattern(
name="cross_segment_entity_tracking",
weight=0.15,
sampling_level_override=1,
constraint_override=None,
instruction=(
"Generate a question that requires tracking a specific entity (person, object, or group) "
"across multiple video segments and merging observations. The correct answer depends on "
"information from at least two separate segments — a single segment gives only a partial "
"or misleading picture. The entity must appear in different contexts or states across segments."
),
positive_examples=[
{
"question": "In the video, what happened in the car when the heroine came home from shopping?",
"answer": "D. The car wouldn't start and she had to call for help",
"why": "Must track heroine across shopping segment → car segment → resolution.",
},
],
negative_examples=[
{
"question": "What is the person wearing?",
"why": "Single-segment observation, no cross-segment tracking needed.",
},
],
distractor_rules=(
"One distractor should be correct for the entity in a DIFFERENT segment (right entity, wrong time). "
"Another distractor should be correct for a DIFFERENT entity in the same segment (right time, wrong entity). "
"The correct answer must require merging observations from multiple segments."
),
)
_SP_EVIDENCE_GAP_CONFABULATION = SubPattern(
name="evidence_gap_confabulation",
weight=0.15,
sampling_level_override=None,
constraint_override=None,
instruction=(
"Generate a question about an action where the video evidence is INCOMPLETE — "
"the full causal chain is not directly shown. The correct answer is the one that "
"stays faithful to what IS observable, while distractors fill in the gap with "
"plausible but unsupported causal narratives. The agent must resist inventing "
"explanations for unobserved transitions."
),
positive_examples=[
{
"question": "How were the Sawtooth ranges formed?",
"answer": "D. The video describes geological uplift but does not show the formation process",
"why": "Video describes result but not process; agent must not confabulate mechanism.",
},
],
negative_examples=[
{
"question": "Why did the person leave the room?",
"why": "If the reason is explicitly stated in dialogue, no evidence gap exists.",
},
],
distractor_rules=(
"Distractors must be plausible causal narratives that COULD explain the outcome but "
"are NOT supported by the video evidence. Each distractor should fill the evidence gap "
"with a different invented mechanism. The correct answer must be the one that "
"either: (a) states only what is directly observable, or (b) acknowledges the limitation."
),
)
# 导出 tuple(不可变,按名称排序供测试)
AR_SUB_PATTERNS: tuple[SubPattern, ...] = (
_SP_PREMATURE_EVIDENCE_ANCHORING,
_SP_TEMPORAL_REASONING_FAILURE,
_SP_SEMANTIC_RIGIDITY,
_SP_FINE_GRAINED_VISUAL_ACTION,
_SP_CROSS_SEGMENT_ENTITY_TRACKING,
_SP_EVIDENCE_GAP_CONFABULATION,
)
# ---------------------------------------------------------------------------
# Strategy 实现
# ---------------------------------------------------------------------------
class ActionRecognitionStrategy:
"""Action Recognition 特化出题策略。
自包含 —— 不依赖 QuestionFamilySpec,直接定义采样约束、prompt、gate 合约。
6 个 SubPattern 按权重随机选择,各自可覆盖默认采样层级。
设计文档: research-wiki/designs/2026-07-14-task-type-strategy-design.md §4
"""
@property
def task_type(self) -> str:
"""固定为 Action Recognition。"""
return "Action Recognition"
@property
def sampling_level(self) -> int:
"""默认 L2 事件级(从 L3 提升,有意变更)。"""
return 2
@property
def sampling_constraint(self) -> SamplingConstraint:
"""加强约束:要求字幕、帧、跨段。"""
return _AR_DEFAULT_CONSTRAINT
@property
def prompt_template(self) -> str:
"""AR 专属 prompt 模板。"""
return "action_recognition.md"
@property
def strategy_name(self) -> str:
"""store 的 family 字段。"""
return "ACTION_RECOGNITION"
@property
def skill_target(self) -> str:
"""M1_AR — 继承 RETRIEVAL 的 M1 + AR 后缀区分。"""
return "M1_AR"
@property
def leak_probe_template(self) -> str:
"""复用 RETRIEVAL 的泄漏检测模板。"""
return "gate_leak_retrieval.md"
def select_sub_pattern(self, rng: random.Random) -> SubPattern:
"""按权重随机选择一个子模式。
参数:
rng: 可控随机数生成器。
返回:
选中的 SubPattern 实例。
"""
names = [sp.name for sp in AR_SUB_PATTERNS]
weights = [sp.weight for sp in AR_SUB_PATTERNS]
chosen_name = rng.choices(names, weights=weights, k=1)[0]
return next(sp for sp in AR_SUB_PATTERNS if sp.name == chosen_name)
def build_prompt_context(self, material: Any, sub_pattern: SubPattern | None) -> dict:
"""返回 AR 的 prompt 上下文。
参数:
material: 采样素材上下文。
sub_pattern: 选中的子模式(AR 策略下始终非 None)。
返回:
上下文字典。
"""
return {
"family_name": "ACTION_RECOGNITION",
"prompt_template": "action_recognition.md",
"sub_pattern": sub_pattern.name if sub_pattern else None,
}
def extra_gates(self, candidate: Any) -> list:
"""当前版本无额外 gate,预留接口。
参数:
candidate: 候选题目。
返回:
空列表。
"""
return []
```
- [ ] **Step 4: 运行测试验证通过**
```bash
conda run -n Video-Tree-TRM pytest tests/unit/test_strategy_action_recognition.py -v
```
预期:全部 PASS
- [ ] **Step 5: 格式和 lint 检查**
```bash
conda run -n Video-Tree-TRM ruff format app/question_gen/strategy_action_recognition.py tests/unit/test_strategy_action_recognition.py
conda run -n Video-Tree-TRM ruff check app/question_gen/strategy_action_recognition.py tests/unit/test_strategy_action_recognition.py
conda run -n Video-Tree-TRM radon cc app/question_gen/strategy_action_recognition.py -n C -s
```
预期:零 error,无 C 级以上复杂度。
- [ ] **Step 6: 提交**
```bash
git add app/question_gen/strategy_action_recognition.py tests/unit/test_strategy_action_recognition.py
git commit -m "feat(question_gen): add ActionRecognitionStrategy with 6 SubPatterns"
```
---
### Task 3: 注册 AR 策略 + 移除临时绑定
**Files:**
- Modify: `app/question_gen/strategy.py:107`
- Test: `tests/unit/test_strategy.py`(追加测试)
- [ ] **Step 1: 写测试 — AR 注册后 get_strategy 返回特化策略**
`tests/unit/test_strategy.py` 末尾追加:
```python
class TestActionRecognitionRegistration:
"""AR 策略注册后 get_strategy 返回特化实例。"""
def test_get_strategy_returns_ar_strategy(self):
"""get_strategy('Action Recognition') 返回 ActionRecognitionStrategy。"""
from app.question_gen.strategy_action_recognition import ActionRecognitionStrategy
s = get_strategy("Action Recognition")
assert isinstance(s, ActionRecognitionStrategy)
assert s.task_type == "Action Recognition"
assert s.strategy_name == "ACTION_RECOGNITION"
def test_ar_not_base_strategy(self):
"""get_strategy('Action Recognition') 不再返回 BaseTaskTypeStrategy。"""
s = get_strategy("Action Recognition")
assert not isinstance(s, BaseTaskTypeStrategy)
def test_other_types_still_base(self):
"""其他题型仍返回 BaseTaskTypeStrategy。"""
for tt in ("Object Recognition", "Temporal Reasoning", "Spatial Reasoning"):
s = get_strategy(tt)
assert isinstance(s, BaseTaskTypeStrategy), f"{tt} 应该是 BaseTaskTypeStrategy"
```
- [ ] **Step 2: 运行测试验证失败**
```bash
conda run -n Video-Tree-TRM pytest tests/unit/test_strategy.py::TestActionRecognitionRegistration -v
```
预期:`test_get_strategy_returns_ar_strategy` FAIL(返回 BaseTaskTypeStrategy)。
- [ ] **Step 3: 在 get_strategy 中添加延迟注册**
修改 `app/question_gen/strategy.py` 中的 `get_strategy` 函数,在首次调用时触发特化策略注册(避免循环导入):
```python
_BUILTIN_REGISTERED = False
def get_strategy(task_type: str) -> TaskTypeStrategy:
"""获取题型策略。未注册的自动创建 BaseTaskTypeStrategy。
首次调用时延迟注册内建特化策略(避免循环导入)。
参数:
task_type: 题型名。
返回:
TaskTypeStrategy 实例。
异常:
KeyError: task_type 不在消歧绑定表和注册表中。
"""
global _BUILTIN_REGISTERED # noqa: PLW0603
if not _BUILTIN_REGISTERED:
_BUILTIN_REGISTERED = True
_register_builtin_strategies()
if task_type in _STRATEGY_REGISTRY:
return _STRATEGY_REGISTRY[task_type]
return _build_default_strategy(task_type)
def _register_builtin_strategies() -> None:
"""注册内建的特化策略。由 get_strategy 首次调用时延迟执行。"""
from app.question_gen.strategy_action_recognition import ActionRecognitionStrategy
register_strategy(ActionRecognitionStrategy())
```
同时更新 `_TASK_TYPE_TO_FAMILY` 中 AR 的注释:
```python
# 修改前:
"Action Recognition": VISUAL_FAMILY, # Plan A 临时绑定;Plan B 替换为特化策略
# 修改后:
"Action Recognition": VISUAL_FAMILY, # fallback — 注册表中已被 ActionRecognitionStrategy 替换
```
- [ ] **Step 4: 运行测试验证通过**
```bash
conda run -n Video-Tree-TRM pytest tests/unit/test_strategy.py tests/unit/test_strategy_action_recognition.py -v
```
预期:全部 PASS。
- [ ] **Step 5: 提交**
```bash
git add app/question_gen/strategy.py tests/unit/test_strategy.py
git commit -m "feat(question_gen): register ActionRecognitionStrategy, replace temp VISUAL binding"
```
---
### Task 4: 全量回归测试 + lint
- [ ] **Step 1: lint**
```bash
conda run -n Video-Tree-TRM ruff format app/question_gen/ tests/ && conda run -n Video-Tree-TRM ruff check app/question_gen/ --fix
```
- [ ] **Step 2: 全量测试**
```bash
conda run -n Video-Tree-TRM pytest tests/unit/ tests/integration/ -v --tb=short
```
预期:1173+ 全部 PASS
- [ ] **Step 3: 提交(如有 lint 修复)**
```bash
git add -A && git commit -m "chore: lint and format Plan B changes"
```
---
## 行为保真检查清单
| # | 行为 | 状态 | 说明 |
|---|------|------|------|
| 1 | 11 个非 AR 题型行为不变 | 保留 | get_strategy 对非 AR 类型仍返回 BaseTaskTypeStrategy |
| 2 | AR 从 VISUAL_FAMILY(L3) 变为 AR 特化(L2) | **有意变更** | 设计文档 §4 明确标注 |
| 3 | AR 采样约束加强 | **有意变更** | 设计文档 §4: min_subtitles=3, cross_l2_span=True |
| 4 | AR prompt 从 visual.md 变为 action_recognition.md | **有意变更** | 专属 prompt 靶向动作识别 |
| 5 | AR strategy_name 从 "VISUAL" 变为 "ACTION_RECOGNITION" | **有意变更** | store 中新记录可区分 |
| 6 | AR skill_target 从 "M4" 变为 "M1_AR" | **有意变更** | 设计文档 §4 |
| 7 | select_sub_pattern 返回 SubPattern(非 None | **有意变更** | AR 始终有 sub_pattern |
| 8 | sub_pattern.instruction 注入到 prompt | 保留 | Plan A 已接线(generator_v2.py:176-177 |
| 9 | sub_pattern.sampling_level_override 覆盖 level | 保留 | Plan A 已接线(pipeline_v2.py:359-360 |
| 10 | sub_pattern.name 写入 store | 保留 | Plan A 已接线(pipeline_v2.py:425 |
| 11 | pipeline 重出循环/后处理/四门 gate 不变 | 保留 | Plan B 不改 pipeline |
| 12 | 断点续跑 | 保留 | pipeline 的 progress 机制不变 |
## 核心算法保真校验
本计划不涉及核心算法迁移,保真校验不适用。
## 非功能性需求
| 维度 | 设计 |
|------|------|
| 持久化 | 不变 — on_accept 逐题回调。sub_pattern 名已在 Plan A 中接入 store |
| 幂等性 | 不变 — strategy 查找确定性,sub_pattern 选择由 rng 控制 |
| 断点续跑 | 不变 — progress 机制在 pipeline 层,strategy 无状态 |
| 原子性 | 不变 — 逐题落库 |