feat(tree): 质量校验 — 交叉验证 entities/visible_text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
"""质量校验模块单元测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.tree.index import (
|
||||
IndexMeta,
|
||||
L1Card,
|
||||
L1Node,
|
||||
L2Card,
|
||||
L2Node,
|
||||
L3Card,
|
||||
L3Node,
|
||||
TreeIndex,
|
||||
)
|
||||
from app.tree.verify import VerifyStats, _normalize, fuzzy_match, verify_tree
|
||||
|
||||
|
||||
class TestNormalize:
|
||||
def test_lowercase(self):
|
||||
assert _normalize("Hello World") == "hello world"
|
||||
|
||||
def test_strip_punctuation(self):
|
||||
assert _normalize("Hello, World!") == "hello world"
|
||||
|
||||
def test_empty(self):
|
||||
assert _normalize("") == ""
|
||||
|
||||
|
||||
class TestFuzzyMatch:
|
||||
def test_exact_match(self):
|
||||
assert fuzzy_match("hello", "hello world")
|
||||
|
||||
def test_case_insensitive(self):
|
||||
assert fuzzy_match("Hello", "say hello world")
|
||||
|
||||
def test_no_match(self):
|
||||
assert not fuzzy_match("xyz", "hello world")
|
||||
|
||||
def test_none_entity(self):
|
||||
assert not fuzzy_match(None, "hello")
|
||||
|
||||
def test_none_corpus(self):
|
||||
assert not fuzzy_match("hello", None)
|
||||
|
||||
|
||||
class TestVerifyTree:
|
||||
def _make_tree(self):
|
||||
"""构造一棵树,L2 有混合实体(有出处/无出处)。"""
|
||||
l3_0 = L3Node(
|
||||
id="l1_0_l2_0_l3_0",
|
||||
card=L3Card(
|
||||
frame_summary="一个运动员在跑步",
|
||||
visible_entities=["运动员", "跑道"],
|
||||
ongoing_actions=["跑步"],
|
||||
visible_text=["Nike", "2024"],
|
||||
spatial_layout="居中",
|
||||
visual_attributes={},
|
||||
),
|
||||
timestamp=1.0,
|
||||
subtitle="the athlete is running fast",
|
||||
)
|
||||
l3_1 = L3Node(
|
||||
id="l1_0_l2_0_l3_1",
|
||||
card=L3Card(
|
||||
frame_summary="观众在欢呼",
|
||||
visible_entities=["观众"],
|
||||
ongoing_actions=["欢呼"],
|
||||
visible_text=["Stadium"],
|
||||
spatial_layout="广角",
|
||||
visual_attributes={},
|
||||
),
|
||||
timestamp=3.0,
|
||||
)
|
||||
l2 = L2Node(
|
||||
id="l1_0_l2_0",
|
||||
card=L2Card(
|
||||
event_description="比赛片段",
|
||||
entities=["运动员", "裁判", "幻觉实体"], # "裁判"和"幻觉实体"无 L3 出处
|
||||
actions=["跑步"],
|
||||
action_subjects=["运动员"],
|
||||
visible_text=["Nike", "不存在的文字"], # "不存在的文字"无 L3 出处
|
||||
spatial_relations="",
|
||||
state_changes=None,
|
||||
),
|
||||
time_range=(0.0, 10.0),
|
||||
children=[l3_0, l3_1],
|
||||
)
|
||||
l1 = L1Node(
|
||||
id="l1_0",
|
||||
card=L1Card(
|
||||
scene_summary="体育比赛",
|
||||
main_setting="体育场",
|
||||
key_entities=["运动员", "不存在的人"], # "不存在的人"无出处
|
||||
main_actions=["比赛"],
|
||||
topic_keywords=["体育"],
|
||||
visible_text=["Nike", "Ghost"], # "Ghost"无出处
|
||||
temporal_flow="从左到右",
|
||||
),
|
||||
time_range=(0.0, 10.0),
|
||||
children=[l2],
|
||||
)
|
||||
return TreeIndex(metadata=IndexMeta("/test.mp4", "video"), roots=[l1])
|
||||
|
||||
def test_removes_ungrounded_l2_entities(self):
|
||||
index = self._make_tree()
|
||||
stats = verify_tree(index)
|
||||
l2 = index.roots[0].children[0]
|
||||
assert "运动员" in l2.card.entities
|
||||
assert "幻觉实体" not in l2.card.entities
|
||||
assert stats.l2_entities_removed >= 1
|
||||
|
||||
def test_removes_ungrounded_l2_visible_text(self):
|
||||
index = self._make_tree()
|
||||
stats = verify_tree(index)
|
||||
l2 = index.roots[0].children[0]
|
||||
assert "Nike" in l2.card.visible_text
|
||||
assert "不存在的文字" not in l2.card.visible_text
|
||||
assert stats.l2_visible_text_removed >= 1
|
||||
|
||||
def test_removes_ungrounded_l1_visible_text(self):
|
||||
index = self._make_tree()
|
||||
stats = verify_tree(index)
|
||||
l1 = index.roots[0]
|
||||
assert "Nike" in l1.card.visible_text
|
||||
assert "Ghost" not in l1.card.visible_text
|
||||
assert stats.l1_visible_text_removed >= 1
|
||||
|
||||
def test_removes_ungrounded_l1_key_entities(self):
|
||||
index = self._make_tree()
|
||||
stats = verify_tree(index)
|
||||
l1 = index.roots[0]
|
||||
assert "运动员" in l1.card.key_entities
|
||||
assert "不存在的人" not in l1.card.key_entities
|
||||
assert stats.l1_key_entities_removed >= 1
|
||||
|
||||
def test_preserves_grounded_entities(self):
|
||||
index = self._make_tree()
|
||||
verify_tree(index)
|
||||
l2 = index.roots[0].children[0]
|
||||
assert "运动员" in l2.card.entities
|
||||
|
||||
def test_returns_verify_stats(self):
|
||||
index = self._make_tree()
|
||||
stats = verify_tree(index)
|
||||
assert isinstance(stats, VerifyStats)
|
||||
total_kept = stats.l2_entities_kept + stats.l1_key_entities_kept
|
||||
assert total_kept > 0
|
||||
|
||||
def test_frozen_card_replaced(self):
|
||||
"""验证 Card 被替换为新实例(frozen dataclass 不能原地修改)。"""
|
||||
index = self._make_tree()
|
||||
old_l2_card = index.roots[0].children[0].card
|
||||
verify_tree(index)
|
||||
new_l2_card = index.roots[0].children[0].card
|
||||
# Card should be a different object if anything was removed
|
||||
assert old_l2_card is not new_l2_card
|
||||
Reference in New Issue
Block a user