Compare commits

...

2 Commits

Author SHA1 Message Date
xiaoxia 146de5f016 fix(ci): 修复2个ruff错误 - F541多余f前缀 + B007未用循环变量
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 11s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m16s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 31s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m18s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m10s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m40s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m56s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 50s
AI Code Review / AI Code Review (pull_request) Successful in 2m23s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 11s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 8m31s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m0s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 4m22s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 6s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 55m0s
2026-07-23 19:40:50 +08:00
CI Bot 1ec090bcac style: black格式化3个AI相关文件
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 6s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m28s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m5s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 26s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m7s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 39s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 49s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 48s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 16s
AI Code Review / AI Code Review (pull_request) Successful in 2m45s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m54s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 11m33s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m42s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m12s
- ai_service.py
- ai_tasks.py
- test_ai_tasks.py
2026-07-23 18:54:26 +08:00
3 changed files with 169 additions and 88 deletions
+16 -15
View File
@@ -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(
+14 -12
View File
@@ -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
View File
@@ -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,