6bdb802f01
- Add all claude skills (brainstorming, commit, debugging, TDD, etc.) - Add claude hooks (pre-commit-guard, post-edit-quality) - Add research templates (experiment plan, research brief, etc.) - Add claude tools (arxiv/semantic_scholar/openalex fetch, wiki, exa) - Add TRM4 reference implementation as algorithm fidelity baseline - Add research-wiki content (plans, index, graph, query_pack) - Update .gitignore to exclude .graphify_version runtime state
194 lines
6.6 KiB
Python
194 lines
6.6 KiB
Python
"""
|
||
Embedding 持久化单元测试
|
||
=======================
|
||
测试 TreeIndex 的 embedding 序列化/反序列化功能。
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
import numpy as np
|
||
import pytest
|
||
|
||
from video_tree_trm.tree_index import (
|
||
L1Node,
|
||
L2Node,
|
||
L3Node,
|
||
IndexMeta,
|
||
TreeIndex,
|
||
_embed_to_str,
|
||
_embed_from_str,
|
||
)
|
||
|
||
|
||
class TestEmbeddingSerialization:
|
||
"""测试 embedding 序列化辅助函数。"""
|
||
|
||
def test_embed_to_str_and_back(self):
|
||
"""测试 base64 序列化/反序列化往返正确。"""
|
||
arr = np.random.randn(768).astype(np.float32)
|
||
s = _embed_to_str(arr)
|
||
recovered = _embed_from_str(s)
|
||
|
||
assert recovered is not None
|
||
assert recovered.dtype == np.float32
|
||
assert recovered.shape == (768,)
|
||
np.testing.assert_array_almost_equal(arr, recovered, decimal=6)
|
||
|
||
def test_embed_to_str_handles_none(self):
|
||
"""测试 None 输入返回 None。"""
|
||
assert _embed_to_str(None) is None
|
||
assert _embed_from_str(None) is None
|
||
assert _embed_from_str("") is None
|
||
|
||
|
||
class TestL3NodeEmbedding:
|
||
"""测试 L3Node embedding 序列化。"""
|
||
|
||
def test_l3_to_dict_without_embedding(self):
|
||
"""测试不带 embedding 序列化。"""
|
||
node = L3Node(
|
||
id="l3_0",
|
||
description="测试描述",
|
||
timestamp=1.0,
|
||
frame_path="frame.jpg",
|
||
raw_content="原始内容",
|
||
)
|
||
d = {
|
||
"id": node.id,
|
||
"description": node.description,
|
||
"timestamp": node.timestamp,
|
||
"frame_path": node.frame_path,
|
||
"raw_content": node.raw_content,
|
||
}
|
||
# 无 embedding 字段
|
||
assert "embedding" not in d
|
||
|
||
def test_l3_embedding_roundtrip(self):
|
||
"""测试 L3 embedding 序列化往返。"""
|
||
embed = np.random.randn(768).astype(np.float32)
|
||
s = _embed_to_str(embed)
|
||
recovered = _embed_from_str(s)
|
||
np.testing.assert_array_almost_equal(embed, recovered, decimal=6)
|
||
|
||
|
||
class TestTreeIndexEmbedding:
|
||
"""测试 TreeIndex 完整序列化/反序列化。"""
|
||
|
||
def test_save_load_without_embedding(self):
|
||
"""测试不带 embedding 保存/加载(向后兼容)。"""
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
path = os.path.join(tmpdir, "test.json")
|
||
|
||
# 创建简单树
|
||
l3 = L3Node(id="l3_0", description="L3 描述")
|
||
l2 = L2Node(id="l2_0", description="L2 描述", children=[l3])
|
||
l1 = L1Node(id="l1_0", summary="L1 摘要", children=[l2])
|
||
meta = IndexMeta(source_path="test.mp4", modality="video")
|
||
tree = TreeIndex(metadata=meta, roots=[l1])
|
||
|
||
# 保存(不含 embedding)
|
||
tree.save_json(path, include_embedding=False)
|
||
|
||
# 加载
|
||
loaded = TreeIndex.load_json(path)
|
||
|
||
assert len(loaded.roots) == 1
|
||
assert loaded.roots[0].summary == "L1 摘要"
|
||
assert not loaded.is_embedded # 无 embedding
|
||
|
||
def test_save_load_with_embedding(self):
|
||
"""测试带 embedding 保存/加载。"""
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
path = os.path.join(tmpdir, "test_embed.json")
|
||
|
||
# 创建带 embedding 的树
|
||
embed_l1 = np.random.randn(768).astype(np.float32)
|
||
embed_l2 = np.random.randn(768).astype(np.float32)
|
||
embed_l3 = np.random.randn(768).astype(np.float32)
|
||
|
||
l3 = L3Node(id="l3_0", description="L3 描述", embedding=embed_l3)
|
||
l2 = L2Node(id="l2_0", description="L2 描述", embedding=embed_l2, children=[l3])
|
||
l1 = L1Node(id="l1_0", summary="L1 摘要", embedding=embed_l1, children=[l2])
|
||
meta = IndexMeta(
|
||
source_path="test.mp4",
|
||
modality="video",
|
||
embed_model="test-model",
|
||
embed_dim=768,
|
||
)
|
||
tree = TreeIndex(metadata=meta, roots=[l1])
|
||
|
||
# 保存(含 embedding)
|
||
tree.save_json(path, include_embedding=True)
|
||
|
||
# 验证 JSON 中有 embedding 字段
|
||
with open(path, encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
assert "embedding" in data["roots"][0]
|
||
assert data["metadata"]["embed_model"] == "test-model"
|
||
|
||
# 加载
|
||
loaded = TreeIndex.load_json(path)
|
||
|
||
assert loaded.is_embedded
|
||
np.testing.assert_array_almost_equal(
|
||
loaded.roots[0].embedding, embed_l1, decimal=6
|
||
)
|
||
np.testing.assert_array_almost_equal(
|
||
loaded.roots[0].children[0].embedding, embed_l2, decimal=6
|
||
)
|
||
np.testing.assert_array_almost_equal(
|
||
loaded.roots[0].children[0].children[0].embedding, embed_l3, decimal=6
|
||
)
|
||
|
||
def test_load_old_format_compatible(self):
|
||
"""测试加载旧格式(无 embedding 字段)JSON 兼容。"""
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
path = os.path.join(tmpdir, "old_format.json")
|
||
|
||
# 手动创建旧格式 JSON(无 embedding 字段)
|
||
old_data = {
|
||
"metadata": {
|
||
"source_path": "test.mp4",
|
||
"modality": "video",
|
||
"created_at": "2024-01-01T00:00:00",
|
||
},
|
||
"roots": [
|
||
{
|
||
"id": "l1_0",
|
||
"summary": "L1 摘要",
|
||
"time_range": None,
|
||
"children": [
|
||
{
|
||
"id": "l2_0",
|
||
"description": "L2 描述",
|
||
"time_range": None,
|
||
"children": [
|
||
{
|
||
"id": "l3_0",
|
||
"description": "L3 描述",
|
||
"timestamp": 1.0,
|
||
"frame_path": None,
|
||
"raw_content": None,
|
||
}
|
||
],
|
||
}
|
||
],
|
||
}
|
||
],
|
||
}
|
||
with open(path, "w", encoding="utf-8") as f:
|
||
json.dump(old_data, f)
|
||
|
||
# 加载
|
||
loaded = TreeIndex.load_json(path)
|
||
|
||
assert len(loaded.roots) == 1
|
||
assert not loaded.is_embedded # 无 embedding,向后兼容
|
||
|
||
|
||
if __name__ == "__main__":
|
||
pytest.main([__file__, "-v"])
|