fix(api): 一键生成自动选素材全链路修复,4个核心根因一次性解决 #491
@@ -132,17 +132,21 @@ def _auto_fallback_auto_material_mode(
|
||||
asset_library_repo: Any,
|
||||
asset_repo: Any,
|
||||
) -> None:
|
||||
"""自动兜底 4: 自动素材模式 → 从项目默认视频素材库选取"""
|
||||
"""自动兜底 4: 项目有视频素材库时,自动选取 ready 视频素材分配给无素材片段
|
||||
|
||||
注:原先需要 material_mode=="auto" 才触发,但全代码库没有任何地方设置为 auto,
|
||||
导致这道兜底防线永远不生效。现改为:只要有 project_id 且存在无素材片段,
|
||||
就自动从项目视频素材库选取素材兜底,确保一键生成等场景能正常出片。
|
||||
"""
|
||||
if not clips_without_asset:
|
||||
return
|
||||
material_mode = (plan_check.config or {}).get("material_mode", "manual")
|
||||
if material_mode != "auto" or not plan_check.project_id:
|
||||
if not plan_check.project_id:
|
||||
return
|
||||
|
||||
import random
|
||||
|
||||
logger.info(
|
||||
"自动兜底4: plan=%s 自动素材模式,从项目素材库选取素材 (%d 个片段需要)",
|
||||
"自动兜底4: plan=%s 自动选素材分配给 %d 个无素材片段",
|
||||
plan_id,
|
||||
len(clips_without_asset),
|
||||
)
|
||||
|
||||
Executable → Regular
+13
-1
@@ -214,8 +214,20 @@ def generate_from_template(
|
||||
plan = result["plan"]
|
||||
clips = result["clips"]
|
||||
|
||||
# 把 asset_ids 写入 plan.config,供生成时兜底分配使用
|
||||
if resolved_asset_ids:
|
||||
from app.services import EditPlanService
|
||||
|
||||
from packages.domain.config_schemas import normalize_plan_config
|
||||
|
||||
svc = EditPlanService(db)
|
||||
current_config = plan.config or {}
|
||||
if current_config.get("asset_ids") != resolved_asset_ids:
|
||||
current_config["asset_ids"] = resolved_asset_ids
|
||||
plan = svc.update_plan(plan.id, config=normalize_plan_config(current_config))
|
||||
|
||||
logger.info(
|
||||
"基于模板生成剪辑计划: plan_id=%s template_id=%s clips=%d by user=%s",
|
||||
"基于模板生成剪辑计划: plan_id=%s template_id=%s clips=%d by user=%d",
|
||||
plan.id,
|
||||
body.template_id,
|
||||
len(clips),
|
||||
|
||||
Regular → Executable
+40
@@ -99,6 +99,11 @@ class PlanGeneratorService:
|
||||
# 3. 生成片段列表
|
||||
if clip_configs:
|
||||
clips = self._create_clips_from_configs(plan.id, clip_configs)
|
||||
# 模板 clip_config 的 clip_type 是 ClipType 枚举(main/intro/outro 等),
|
||||
# 但 PIP / VOICE_PIP 模式需要特定的 clip_type(overlay/background/corner_voice/b_roll)
|
||||
# 才能让素材分配和渲染分层正确工作。
|
||||
# 这里将 MAIN 类型的片段按顺序映射为对应模式的角色类型。
|
||||
self._map_clip_types_for_mode(clips, editing_mode)
|
||||
else:
|
||||
clips = self._generate_default_clips(plan.id, editing_mode, len(asset_ids))
|
||||
|
||||
@@ -195,6 +200,41 @@ class PlanGeneratorService:
|
||||
|
||||
return clips
|
||||
|
||||
def _map_clip_types_for_mode(self, clips: List[EditPlanClip], editing_mode: str) -> None:
|
||||
"""将模板 clip_config 生成的 MAIN 类型片段,按 editing_mode 映射为对应角色类型。
|
||||
|
||||
模板的 clip_config 使用 ClipType 枚举(main/intro/outro 等),
|
||||
但 PIP / VOICE_PIP 模式的素材分配和渲染分层依赖特定的 clip_type 命名
|
||||
(overlay / background / corner_voice / b_roll)。
|
||||
|
||||
映射规则(仅修改 MAIN 类型片段,非 MAIN 片段保持原类型):
|
||||
- PIP: 第1个 MAIN → main(背景),其余 MAIN → overlay(画中画)
|
||||
- VOICE_PIP: 第1个 → background,第2个 → corner_voice,第3+个 → b_roll
|
||||
- ONE_TAKE / VOICE_OVER: 保持 main 不变
|
||||
"""
|
||||
from packages.domain.template_clip_config import ClipType
|
||||
|
||||
main_clips = [c for c in clips if c.clip_type == ClipType.MAIN.value]
|
||||
if not main_clips:
|
||||
return
|
||||
|
||||
if editing_mode == EditingMode.PIP.value:
|
||||
# 第1个 main 保持(背景层),其余改为 overlay(画中画层)
|
||||
for i, clip in enumerate(main_clips):
|
||||
if i > 0:
|
||||
clip.clip_type = "overlay"
|
||||
|
||||
elif editing_mode == EditingMode.VOICE_PIP.value:
|
||||
for i, clip in enumerate(main_clips):
|
||||
if i == 0:
|
||||
clip.clip_type = "background"
|
||||
elif i == 1:
|
||||
clip.clip_type = "corner_voice"
|
||||
else:
|
||||
clip.clip_type = "b_roll"
|
||||
|
||||
# ONE_TAKE / VOICE_OVER: 保持 main 不变,无需处理
|
||||
|
||||
def _generate_default_clips(
|
||||
self,
|
||||
plan_id: str,
|
||||
|
||||
Regular → Executable
+87
@@ -334,6 +334,93 @@ class TestGeneratePipPlan:
|
||||
assert overlay_clips[1].asset_id == "overlay-2"
|
||||
assert overlay_clips[2].asset_id == "overlay-3"
|
||||
|
||||
def test_pip_with_clip_configs(self):
|
||||
"""PIP + 有 clip_configs(都是 MAIN 类型)→ 自动映射为 main + overlay,素材正确分配"""
|
||||
svc, _, _ = _make_generator()
|
||||
template = _make_template(editing_mode=EditingMode.PIP.value)
|
||||
|
||||
# 4 个 MAIN 类型的 clip_config(模拟 PIP 模板配置)
|
||||
clip_configs = [
|
||||
TemplateClipConfig.create(
|
||||
template_id=template.id,
|
||||
clip_type=ClipType.MAIN,
|
||||
order=i,
|
||||
min_duration=3.0,
|
||||
max_duration=5.0,
|
||||
)
|
||||
for i in range(4)
|
||||
]
|
||||
asset_ids = ["bg-asset", "overlay-1", "overlay-2", "overlay-3"]
|
||||
|
||||
result = svc.generate_from_template(
|
||||
template=template,
|
||||
clip_configs=clip_configs,
|
||||
asset_ids=asset_ids,
|
||||
)
|
||||
|
||||
plan = result["plan"]
|
||||
clips = result["clips"]
|
||||
|
||||
assert plan.config["editing_mode"] == "pip"
|
||||
assert len(clips) == 4
|
||||
|
||||
# 第1个 MAIN → main(背景),其余 MAIN → overlay(画中画)
|
||||
main_clips = [c for c in clips if c.clip_type == ClipType.MAIN.value]
|
||||
overlay_clips = [c for c in clips if c.clip_type == "overlay"]
|
||||
|
||||
assert len(main_clips) == 1
|
||||
assert len(overlay_clips) == 3
|
||||
|
||||
# 素材分配正确
|
||||
assert main_clips[0].asset_id == "bg-asset"
|
||||
assert overlay_clips[0].asset_id == "overlay-1"
|
||||
assert overlay_clips[1].asset_id == "overlay-2"
|
||||
assert overlay_clips[2].asset_id == "overlay-3"
|
||||
|
||||
def test_voice_pip_with_clip_configs(self):
|
||||
"""VOICE_PIP + 有 clip_configs(都是 MAIN 类型)→ 自动映射为 background + corner_voice + b_roll"""
|
||||
svc, _, _ = _make_generator()
|
||||
template = _make_template(editing_mode=EditingMode.VOICE_PIP.value)
|
||||
|
||||
# 4 个 MAIN 类型的 clip_config
|
||||
clip_configs = [
|
||||
TemplateClipConfig.create(
|
||||
template_id=template.id,
|
||||
clip_type=ClipType.MAIN,
|
||||
order=i,
|
||||
min_duration=3.0,
|
||||
max_duration=5.0,
|
||||
)
|
||||
for i in range(4)
|
||||
]
|
||||
asset_ids = ["bg-asset", "voice-asset", "broll-1", "broll-2"]
|
||||
|
||||
result = svc.generate_from_template(
|
||||
template=template,
|
||||
clip_configs=clip_configs,
|
||||
asset_ids=asset_ids,
|
||||
)
|
||||
|
||||
plan = result["plan"]
|
||||
clips = result["clips"]
|
||||
|
||||
assert plan.config["editing_mode"] == "voice_pip"
|
||||
assert len(clips) == 4
|
||||
|
||||
bg_clips = [c for c in clips if c.clip_type == "background"]
|
||||
corner_clips = [c for c in clips if c.clip_type == "corner_voice"]
|
||||
broll_clips = [c for c in clips if c.clip_type == "b_roll"]
|
||||
|
||||
assert len(bg_clips) == 1
|
||||
assert len(corner_clips) == 1
|
||||
assert len(broll_clips) == 2
|
||||
|
||||
# 素材分配正确
|
||||
assert bg_clips[0].asset_id == "bg-asset"
|
||||
assert corner_clips[0].asset_id == "voice-asset"
|
||||
assert broll_clips[0].asset_id == "broll-1"
|
||||
assert broll_clips[1].asset_id == "broll-2"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 测试:VOICE_OVER 模式
|
||||
|
||||
Reference in New Issue
Block a user