refactor(tree): subtitle 迁入 L3Card/L2Card + 建树管线修正

- L3Card/L2Card 新增 subtitle: str 字段(L1Card 不加)
- L3Node 移除 subtitle 字段(数据迁入 Card)
- assign_subtitles_voronoi 改写 Card.subtitle + L2 聚合
- _collect_card_strings 增加 skip_fields 排除 subtitle
- _node_full_text/_node_anchored_text 保持 字幕:/[sN] 语义
- get_subtitle 读 Card.subtitle(L2/L3)
- verify.py/synthesizer.py: l3.subtitle → l3.card.subtitle
- 迁移脚本 tools/migrate_subtitle_to_card.py(幂等,300 棵树已迁移)
- 9→6 个测试文件适配(3 个无需改动)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 11:57:41 -04:00
parent c72b55508a
commit d3be9b1322
13 changed files with 253 additions and 46 deletions
+10 -4
View File
@@ -82,6 +82,7 @@ class L3Card:
visible_text: 画面中可见的文字列表。
spatial_layout: 空间布局描述。
visual_attributes: 视觉属性字典(如光照、色调等)。
subtitle: 字幕文本(Voronoi 分配后填充,默认空)。
"""
frame_summary: str
@@ -90,6 +91,7 @@ class L3Card:
visible_text: list[str]
spatial_layout: str
visual_attributes: dict[str, Any]
subtitle: str = ""
@dataclass(frozen=True)
@@ -106,6 +108,7 @@ class L2Card:
visible_text: 片段中可见的文字列表。
spatial_relations: 空间关系描述。
state_changes: 状态变化描述(可选)。
subtitle: 子 L3 字幕聚合文本(Voronoi 分配后填充,默认空)。
"""
event_description: str
@@ -115,6 +118,7 @@ class L2Card:
visible_text: list[str]
spatial_relations: str
state_changes: str | None
subtitle: str = ""
@dataclass(frozen=True)
@@ -183,7 +187,6 @@ class L3Node:
embedding: 文本嵌入向量,形状 [D]float32。
timestamp: 对应的时间戳(秒,可选)。
frame_path: 关联的帧图像路径(可选,仅视频模态)。
subtitle: 该帧对应的字幕文本(可选)。
"""
id: str
@@ -191,7 +194,6 @@ class L3Node:
embedding: np.ndarray | None = None
timestamp: float | None = None
frame_path: str | None = None
subtitle: str | None = None
@property
def description(self) -> str:
@@ -274,10 +276,10 @@ class L1Node:
"visible_text": n.card.visible_text,
"spatial_layout": n.card.spatial_layout,
"visual_attributes": n.card.visual_attributes,
"subtitle": n.card.subtitle,
},
"timestamp": n.timestamp,
"frame_path": n.frame_path,
"subtitle": n.subtitle,
}
if include_embedding:
d["embedding"] = _embed_to_str(n.embedding)
@@ -294,6 +296,7 @@ class L1Node:
"visible_text": n.card.visible_text,
"spatial_relations": n.card.spatial_relations,
"state_changes": n.card.state_changes,
"subtitle": n.card.subtitle,
},
"time_range": list(n.time_range) if n.time_range else None,
"children": [l3_to_dict(c) for c in n.children],
@@ -334,6 +337,8 @@ class L1Node:
for l2d in d.get("children", []):
l3_nodes: list[L3Node] = []
for l3d in l2d.get("children", []):
# 向后兼容:旧格式 subtitle 在节点级,新格式在 card 内
l3_subtitle = l3d["card"].get("subtitle", "") or l3d.get("subtitle", "") or ""
l3_card = L3Card(
frame_summary=l3d["card"]["frame_summary"],
visible_entities=l3d["card"]["visible_entities"],
@@ -341,6 +346,7 @@ class L1Node:
visible_text=l3d["card"]["visible_text"],
spatial_layout=l3d["card"]["spatial_layout"],
visual_attributes=l3d["card"]["visual_attributes"],
subtitle=l3_subtitle,
)
l3_nodes.append(
L3Node(
@@ -349,7 +355,6 @@ class L1Node:
embedding=_embed_from_str(l3d.get("embedding")),
timestamp=l3d.get("timestamp"),
frame_path=l3d.get("frame_path"),
subtitle=l3d.get("subtitle"),
)
)
l2_card = L2Card(
@@ -360,6 +365,7 @@ class L1Node:
visible_text=l2d["card"]["visible_text"],
spatial_relations=l2d["card"]["spatial_relations"],
state_changes=l2d["card"]["state_changes"],
subtitle=l2d["card"].get("subtitle", ""),
)
tr2 = l2d.get("time_range")
l2_nodes.append(