Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 146de5f016 | |||
| 1ec090bcac |
@@ -298,11 +298,13 @@ def _semantic_match_fallback(
|
||||
score = min(1.0, base_score * 0.8 + name_bonus)
|
||||
score = round(score, 3)
|
||||
|
||||
results.append({
|
||||
**asset,
|
||||
"match_score": score,
|
||||
"match_reason": "fallback_keyword",
|
||||
})
|
||||
results.append(
|
||||
{
|
||||
**asset,
|
||||
"match_score": score,
|
||||
"match_reason": "fallback_keyword",
|
||||
}
|
||||
)
|
||||
|
||||
# 按匹配度降序
|
||||
results.sort(key=lambda x: x["match_score"], reverse=True)
|
||||
@@ -414,9 +416,7 @@ def semantic_match_assets(
|
||||
tags = asset.get("tags", [])
|
||||
tags_str = ",".join(str(t) for t in tags[:5])
|
||||
desc = str(asset.get("description", ""))[:80]
|
||||
asset_summaries.append(
|
||||
f"ID:{aid} | 名称:{name} | 标签:[{tags_str}] | 描述:{desc}"
|
||||
)
|
||||
asset_summaries.append(f"ID:{aid} | 名称:{name} | 标签:[{tags_str}] | 描述:{desc}")
|
||||
|
||||
asset_ids = [str(a.get("id", "")) for a in assets[:50]]
|
||||
|
||||
@@ -434,8 +434,7 @@ def semantic_match_assets(
|
||||
|
||||
user_prompt = (
|
||||
f"目标视频描述:{description}\n\n"
|
||||
f"素材列表:\n" + "\n".join(asset_summaries) +
|
||||
f"\n\n请返回每个素材的匹配分数JSON:"
|
||||
f"素材列表:\n" + "\n".join(asset_summaries) + "\n\n请返回每个素材的匹配分数JSON:"
|
||||
)
|
||||
|
||||
messages = [
|
||||
@@ -457,11 +456,13 @@ def semantic_match_assets(
|
||||
for asset in assets:
|
||||
aid = str(asset.get("id", ""))
|
||||
score = scores.get(aid, 0.3) # 没评分的给默认偏低分
|
||||
matched.append({
|
||||
**asset,
|
||||
"match_score": round(score, 3),
|
||||
"match_reason": "doubao_semantic",
|
||||
})
|
||||
matched.append(
|
||||
{
|
||||
**asset,
|
||||
"match_score": round(score, 3),
|
||||
"match_reason": "doubao_semantic",
|
||||
}
|
||||
)
|
||||
matched.sort(key=lambda x: x["match_score"], reverse=True)
|
||||
|
||||
logger.info(
|
||||
|
||||
@@ -142,23 +142,25 @@ def _parse_recommend_response(
|
||||
return None
|
||||
|
||||
clips: List[Dict[str, Any]] = []
|
||||
for i, clip in enumerate(clips_data):
|
||||
for _, clip in enumerate(clips_data):
|
||||
if not isinstance(clip, dict):
|
||||
continue
|
||||
asset_id = str(clip.get("asset_id", ""))
|
||||
# 校验 asset_id 是否在输入列表中
|
||||
if asset_id and asset_id not in asset_ids:
|
||||
asset_id = ""
|
||||
clips.append({
|
||||
"clip_type": clip.get("clip_type", "showcase"),
|
||||
"order": clip.get("order", len(clips)),
|
||||
"text_content": str(clip.get("text_content", "")),
|
||||
"duration": max(1.0, min(30.0, float(clip.get("duration", 3.0)))),
|
||||
"transition_effect": clip.get("transition_effect", "cut"),
|
||||
"asset_id": asset_id,
|
||||
"start_time": max(0.0, float(clip.get("start_time", 0.0))),
|
||||
"config": clip.get("config", {}) or {},
|
||||
})
|
||||
clips.append(
|
||||
{
|
||||
"clip_type": clip.get("clip_type", "showcase"),
|
||||
"order": clip.get("order", len(clips)),
|
||||
"text_content": str(clip.get("text_content", "")),
|
||||
"duration": max(1.0, min(30.0, float(clip.get("duration", 3.0)))),
|
||||
"transition_effect": clip.get("transition_effect", "cut"),
|
||||
"asset_id": asset_id,
|
||||
"start_time": max(0.0, float(clip.get("start_time", 0.0))),
|
||||
"config": clip.get("config", {}) or {},
|
||||
}
|
||||
)
|
||||
|
||||
if not clips:
|
||||
return None
|
||||
@@ -219,7 +221,7 @@ def _call_ai_recommend_service(
|
||||
"3. 总时长接近 target_duration,每个素材至少用一次\n"
|
||||
"4. 转场效果合理分配,不要全用cut\n"
|
||||
"5. 返回纯JSON,不要其他文字\n"
|
||||
"返回格式:{\"clips\": [...], \"title\": \"视频标题\", \"confidence\": 0.85}"
|
||||
'返回格式:{"clips": [...], "title": "视频标题", "confidence": 0.85}'
|
||||
)
|
||||
|
||||
assets_desc = "\n".join([f" - 素材ID: {aid}" for i, aid in enumerate(asset_ids[:30])])
|
||||
|
||||
+139
-61
@@ -121,22 +121,41 @@ class TestRecommendResponseParsing(unittest.TestCase):
|
||||
"""解析正常响应."""
|
||||
data = {
|
||||
"clips": [
|
||||
{"clip_type": "intro", "order": 0, "text_content": "开场",
|
||||
"duration": 3.0, "transition_effect": "fade",
|
||||
"asset_id": "a1", "start_time": 0.0, "config": {}},
|
||||
{"clip_type": "showcase", "order": 1, "text_content": "展示",
|
||||
"duration": 5.0, "transition_effect": "cut",
|
||||
"asset_id": "a2", "start_time": 1.0, "config": {}},
|
||||
{"clip_type": "outro", "order": 2, "text_content": "结尾",
|
||||
"duration": 2.0, "transition_effect": "fade",
|
||||
"asset_id": "", "start_time": 0.0, "config": {}},
|
||||
{
|
||||
"clip_type": "intro",
|
||||
"order": 0,
|
||||
"text_content": "开场",
|
||||
"duration": 3.0,
|
||||
"transition_effect": "fade",
|
||||
"asset_id": "a1",
|
||||
"start_time": 0.0,
|
||||
"config": {},
|
||||
},
|
||||
{
|
||||
"clip_type": "showcase",
|
||||
"order": 1,
|
||||
"text_content": "展示",
|
||||
"duration": 5.0,
|
||||
"transition_effect": "cut",
|
||||
"asset_id": "a2",
|
||||
"start_time": 1.0,
|
||||
"config": {},
|
||||
},
|
||||
{
|
||||
"clip_type": "outro",
|
||||
"order": 2,
|
||||
"text_content": "结尾",
|
||||
"duration": 2.0,
|
||||
"transition_effect": "fade",
|
||||
"asset_id": "",
|
||||
"start_time": 0.0,
|
||||
"config": {},
|
||||
},
|
||||
],
|
||||
"title": "精彩视频",
|
||||
"confidence": 0.85,
|
||||
}
|
||||
result = _parse_recommend_response(
|
||||
json.dumps(data), self._asset_ids(), 30.0
|
||||
)
|
||||
result = _parse_recommend_response(json.dumps(data), self._asset_ids(), 30.0)
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(len(result["clips"]), 3)
|
||||
self.assertEqual(result["clips"][0]["clip_type"], "intro")
|
||||
@@ -145,11 +164,21 @@ class TestRecommendResponseParsing(unittest.TestCase):
|
||||
|
||||
def test_parse_markdown_code_block(self):
|
||||
"""解析markdown代码块."""
|
||||
data = {"clips": [
|
||||
{"clip_type": "showcase", "order": 0, "text_content": "t",
|
||||
"duration": 3, "transition_effect": "cut",
|
||||
"asset_id": "a1", "start_time": 0, "config": {}}
|
||||
], "confidence": 0.7}
|
||||
data = {
|
||||
"clips": [
|
||||
{
|
||||
"clip_type": "showcase",
|
||||
"order": 0,
|
||||
"text_content": "t",
|
||||
"duration": 3,
|
||||
"transition_effect": "cut",
|
||||
"asset_id": "a1",
|
||||
"start_time": 0,
|
||||
"config": {},
|
||||
}
|
||||
],
|
||||
"confidence": 0.7,
|
||||
}
|
||||
content = "```json\n" + json.dumps(data) + "\n```"
|
||||
result = _parse_recommend_response(content, self._asset_ids(), 30.0)
|
||||
self.assertIsNotNone(result)
|
||||
@@ -167,51 +196,78 @@ class TestRecommendResponseParsing(unittest.TestCase):
|
||||
|
||||
def test_parse_no_clips(self):
|
||||
"""无clips字段返回None."""
|
||||
result = _parse_recommend_response(
|
||||
json.dumps({"title": "abc"}), self._asset_ids(), 30.0
|
||||
)
|
||||
result = _parse_recommend_response(json.dumps({"title": "abc"}), self._asset_ids(), 30.0)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_parse_filters_invalid_asset_ids(self):
|
||||
"""过滤不在输入列表中的asset_id."""
|
||||
data = {"clips": [
|
||||
{"clip_type": "showcase", "order": 0, "text_content": "t",
|
||||
"duration": 3, "transition_effect": "cut",
|
||||
"asset_id": "fake-id", "start_time": 0, "config": {}}
|
||||
], "confidence": 0.7}
|
||||
result = _parse_recommend_response(
|
||||
json.dumps(data), self._asset_ids(), 30.0
|
||||
)
|
||||
data = {
|
||||
"clips": [
|
||||
{
|
||||
"clip_type": "showcase",
|
||||
"order": 0,
|
||||
"text_content": "t",
|
||||
"duration": 3,
|
||||
"transition_effect": "cut",
|
||||
"asset_id": "fake-id",
|
||||
"start_time": 0,
|
||||
"config": {},
|
||||
}
|
||||
],
|
||||
"confidence": 0.7,
|
||||
}
|
||||
result = _parse_recommend_response(json.dumps(data), self._asset_ids(), 30.0)
|
||||
self.assertIsNotNone(result)
|
||||
# 非法asset_id被清空
|
||||
self.assertEqual(result["clips"][0]["asset_id"], "")
|
||||
|
||||
def test_parse_clamps_duration(self):
|
||||
"""时长被限制在合理范围."""
|
||||
data = {"clips": [
|
||||
{"clip_type": "showcase", "order": 0, "text_content": "t",
|
||||
"duration": 100, "transition_effect": "cut",
|
||||
"asset_id": "a1", "start_time": 0, "config": {}}
|
||||
]}
|
||||
result = _parse_recommend_response(
|
||||
json.dumps(data), self._asset_ids(), 30.0
|
||||
)
|
||||
data = {
|
||||
"clips": [
|
||||
{
|
||||
"clip_type": "showcase",
|
||||
"order": 0,
|
||||
"text_content": "t",
|
||||
"duration": 100,
|
||||
"transition_effect": "cut",
|
||||
"asset_id": "a1",
|
||||
"start_time": 0,
|
||||
"config": {},
|
||||
}
|
||||
]
|
||||
}
|
||||
result = _parse_recommend_response(json.dumps(data), self._asset_ids(), 30.0)
|
||||
self.assertIsNotNone(result)
|
||||
self.assertLessEqual(result["clips"][0]["duration"], 30.0)
|
||||
|
||||
def test_parse_reorders_clips(self):
|
||||
"""clips按order排序并重新编号."""
|
||||
data = {"clips": [
|
||||
{"clip_type": "showcase", "order": 5, "text_content": "b",
|
||||
"duration": 3, "transition_effect": "cut",
|
||||
"asset_id": "a2", "start_time": 0, "config": {}},
|
||||
{"clip_type": "intro", "order": 0, "text_content": "a",
|
||||
"duration": 3, "transition_effect": "fade",
|
||||
"asset_id": "a1", "start_time": 0, "config": {}},
|
||||
]}
|
||||
result = _parse_recommend_response(
|
||||
json.dumps(data), self._asset_ids(), 30.0
|
||||
)
|
||||
data = {
|
||||
"clips": [
|
||||
{
|
||||
"clip_type": "showcase",
|
||||
"order": 5,
|
||||
"text_content": "b",
|
||||
"duration": 3,
|
||||
"transition_effect": "cut",
|
||||
"asset_id": "a2",
|
||||
"start_time": 0,
|
||||
"config": {},
|
||||
},
|
||||
{
|
||||
"clip_type": "intro",
|
||||
"order": 0,
|
||||
"text_content": "a",
|
||||
"duration": 3,
|
||||
"transition_effect": "fade",
|
||||
"asset_id": "a1",
|
||||
"start_time": 0,
|
||||
"config": {},
|
||||
},
|
||||
]
|
||||
}
|
||||
result = _parse_recommend_response(json.dumps(data), self._asset_ids(), 30.0)
|
||||
self.assertIsNotNone(result)
|
||||
# 第一个应该是order=0的intro
|
||||
self.assertEqual(result["clips"][0]["clip_type"], "intro")
|
||||
@@ -221,14 +277,22 @@ class TestRecommendResponseParsing(unittest.TestCase):
|
||||
|
||||
def test_parse_confidence_clamped(self):
|
||||
"""confidence被限制在0-1."""
|
||||
data = {"clips": [
|
||||
{"clip_type": "showcase", "order": 0, "text_content": "t",
|
||||
"duration": 3, "transition_effect": "cut",
|
||||
"asset_id": "a1", "start_time": 0, "config": {}}
|
||||
], "confidence": 2.5}
|
||||
result = _parse_recommend_response(
|
||||
json.dumps(data), self._asset_ids(), 30.0
|
||||
)
|
||||
data = {
|
||||
"clips": [
|
||||
{
|
||||
"clip_type": "showcase",
|
||||
"order": 0,
|
||||
"text_content": "t",
|
||||
"duration": 3,
|
||||
"transition_effect": "cut",
|
||||
"asset_id": "a1",
|
||||
"start_time": 0,
|
||||
"config": {},
|
||||
}
|
||||
],
|
||||
"confidence": 2.5,
|
||||
}
|
||||
result = _parse_recommend_response(json.dumps(data), self._asset_ids(), 30.0)
|
||||
self.assertIsNotNone(result)
|
||||
self.assertLessEqual(result["confidence"], 1.0)
|
||||
|
||||
@@ -260,12 +324,26 @@ class TestRunAIRecommend(unittest.TestCase):
|
||||
mock_client.is_available = True
|
||||
mock_response = {
|
||||
"clips": [
|
||||
{"clip_type": "intro", "order": 0, "text_content": "开场",
|
||||
"duration": 3.0, "transition_effect": "fade",
|
||||
"asset_id": "a1", "start_time": 0.0, "config": {}},
|
||||
{"clip_type": "outro", "order": 1, "text_content": "结尾",
|
||||
"duration": 2.0, "transition_effect": "fade",
|
||||
"asset_id": "a2", "start_time": 0.0, "config": {}},
|
||||
{
|
||||
"clip_type": "intro",
|
||||
"order": 0,
|
||||
"text_content": "开场",
|
||||
"duration": 3.0,
|
||||
"transition_effect": "fade",
|
||||
"asset_id": "a1",
|
||||
"start_time": 0.0,
|
||||
"config": {},
|
||||
},
|
||||
{
|
||||
"clip_type": "outro",
|
||||
"order": 1,
|
||||
"text_content": "结尾",
|
||||
"duration": 2.0,
|
||||
"transition_effect": "fade",
|
||||
"asset_id": "a2",
|
||||
"start_time": 0.0,
|
||||
"config": {},
|
||||
},
|
||||
],
|
||||
"title": "AI生成标题",
|
||||
"confidence": 0.9,
|
||||
|
||||
Reference in New Issue
Block a user