test(wave91): add 59 unit tests for generation_plan_builder (#945)
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
This commit is contained in:
@@ -17,7 +17,6 @@ import logging
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -25,6 +24,15 @@ from worker_app.celery_app import celery_app
|
||||
from worker_app.db import SessionLocal
|
||||
|
||||
from packages.domain.bgm_utils import merge_bgm_config
|
||||
from video_processing.ffmpeg_utils import probe_duration
|
||||
from worker_app.tasks.generation_plan_builder import (
|
||||
VirtualPlan as _VirtualPlan,
|
||||
VirtualClip as _VirtualClip,
|
||||
build_error_info as _build_error_info,
|
||||
extract_intro_outro_from_clip_configs as _extract_intro_outro_from_clip_configs,
|
||||
apply_template_clip_effects as _apply_template_clip_effects,
|
||||
build_clips_by_mode,
|
||||
)
|
||||
|
||||
OUTPUT_WIDTH = 1280
|
||||
OUTPUT_HEIGHT = 720
|
||||
@@ -88,36 +96,6 @@ def _update_task_status(task_id: str, status_action: str, **kwargs) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _build_error_info(error: Exception, stage: str = "render") -> dict:
|
||||
"""构建结构化错误信息。
|
||||
|
||||
Args:
|
||||
error: 异常对象
|
||||
stage: 发生错误的阶段(download/render/merge/upload等)
|
||||
|
||||
Returns:
|
||||
包含 error_type, message, stack_trace, stage, failed_at 的字典
|
||||
"""
|
||||
import traceback
|
||||
from datetime import datetime, timezone
|
||||
|
||||
tb_str = traceback.format_exc()
|
||||
# 截取堆栈前20行,避免字段过大
|
||||
tb_lines = tb_str.strip().splitlines()
|
||||
if len(tb_lines) > 20:
|
||||
tb_summary = "\n".join(tb_lines[:20]) + f"\n... (truncated, total {len(tb_lines)} lines)"
|
||||
else:
|
||||
tb_summary = tb_str
|
||||
|
||||
return {
|
||||
"error_type": type(error).__name__,
|
||||
"message": str(error),
|
||||
"stack_trace": tb_summary,
|
||||
"stage": stage,
|
||||
"failed_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
# ── 日志持久化辅助 ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -148,36 +126,6 @@ from video_processing.oss_helpers import (
|
||||
upload_to_oss,
|
||||
)
|
||||
|
||||
# ── 虚拟 Plan / Clip(内存中构建,不写数据库) ────────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class _VirtualPlan:
|
||||
"""内存中的虚拟剪辑计划,供 UnifiedRenderService 使用。"""
|
||||
|
||||
id: str
|
||||
name: str = ""
|
||||
config: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _VirtualClip:
|
||||
"""内存中的虚拟剪辑片段,供 UnifiedRenderService 使用。"""
|
||||
|
||||
id: str
|
||||
plan_id: str = ""
|
||||
clip_type: str = "main"
|
||||
order: int = 0
|
||||
asset_id: str = ""
|
||||
text_content: str = ""
|
||||
start_time: float = 0.0
|
||||
duration: float = 0.0
|
||||
transition_effect: str = "cut"
|
||||
transition_duration: float = 0.0 # 0 表示使用全局默认值
|
||||
playback_speed: float = 1.0
|
||||
status: str = "ready"
|
||||
config: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
def _load_template_clip_configs(template_id: str) -> list:
|
||||
"""从数据库读取模板的片段配置列表。
|
||||
@@ -206,124 +154,6 @@ def _load_template_clip_configs(template_id: str) -> list:
|
||||
return []
|
||||
|
||||
|
||||
def _extract_intro_outro_from_clip_configs(clip_configs: list) -> dict[str, Any]:
|
||||
"""从模板的 intro/outro 类型 clip_config 中提取 plan 级 intro_outro 配置。
|
||||
|
||||
UnifiedRenderService 已支持 plan.config.intro_outro 路径,
|
||||
这里把 intro/outro 片段配置转为统一格式注入。
|
||||
"""
|
||||
intro_configs = [
|
||||
c for c in clip_configs if (c.clip_type.value if hasattr(c.clip_type, "value") else c.clip_type) == "intro"
|
||||
]
|
||||
outro_configs = [
|
||||
c for c in clip_configs if (c.clip_type.value if hasattr(c.clip_type, "value") else c.clip_type) == "outro"
|
||||
]
|
||||
|
||||
result: dict[str, Any] = {}
|
||||
|
||||
if intro_configs:
|
||||
intro = intro_configs[0]
|
||||
intro_cfg = intro.config or {}
|
||||
result["has_intro"] = True
|
||||
result["intro_type"] = intro_cfg.get("intro_type", "text")
|
||||
result["intro_duration"] = intro.default_duration or 3.0
|
||||
if intro.text_template:
|
||||
result["intro_text"] = intro.text_template
|
||||
# 透传额外配置
|
||||
for key in ("intro_text_color", "intro_bg_color", "intro_font_size", "intro_video_url", "intro_video_path"):
|
||||
if key in intro_cfg:
|
||||
result[key] = intro_cfg[key]
|
||||
|
||||
if outro_configs:
|
||||
outro = outro_configs[0]
|
||||
outro_cfg = outro.config or {}
|
||||
result["has_outro"] = True
|
||||
result["outro_type"] = outro_cfg.get("outro_type", "text")
|
||||
result["outro_duration"] = outro.default_duration or 3.0
|
||||
if outro.text_template:
|
||||
result["outro_text"] = outro.text_template
|
||||
for key in ("outro_text_color", "outro_bg_color", "outro_font_size", "outro_follow_text"):
|
||||
if key in outro_cfg:
|
||||
result[key] = outro_cfg[key]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _apply_template_clip_effects(
|
||||
clips: list[_VirtualClip],
|
||||
clip_configs: list,
|
||||
mode: str,
|
||||
) -> None:
|
||||
"""将模板的 clip 级效果层映射到素材 clips 上(就地修改)。
|
||||
|
||||
映射规则:
|
||||
- 只对素材主体 clips 做映射(ONE_TAKE: main, PIP: main+overlay, VOICE_OVER: main, VOICE_PIP: background+b_roll)
|
||||
- 从模板中筛选 main 类型的 clip_config 作为效果模板
|
||||
- 素材 clips 按顺序循环匹配模板 clip_config(素材多的话重复使用最后一个模板配置)
|
||||
- 映射字段:transition_effect, config.color_grade, config.speed
|
||||
"""
|
||||
if not clip_configs or not clips:
|
||||
return
|
||||
|
||||
# 筛选 main 类型的模板配置(作为效果模板池)
|
||||
main_configs = [
|
||||
c
|
||||
for c in clip_configs
|
||||
if (c.clip_type.value if hasattr(c.clip_type, "value") else c.clip_type) in ("main", "showcase", "b_roll")
|
||||
]
|
||||
if not main_configs:
|
||||
return
|
||||
|
||||
# 确定需要映射的素材 clips(排除 corner_voice 等特殊层)
|
||||
target_clips = [c for c in clips if c.clip_type not in ("corner_voice",)]
|
||||
|
||||
for i, clip in enumerate(target_clips):
|
||||
# 循环匹配:素材多了用最后一个模板配置
|
||||
cfg_idx = min(i, len(main_configs) - 1)
|
||||
template_cfg = main_configs[cfg_idx]
|
||||
|
||||
# 1. 转场效果 + 时长
|
||||
transition = (
|
||||
template_cfg.transition_effect.value
|
||||
if hasattr(template_cfg.transition_effect, "value")
|
||||
else template_cfg.transition_effect
|
||||
)
|
||||
if transition and transition != "cut":
|
||||
clip.transition_effect = transition
|
||||
# 同步转场时长(模板 clip_config 里的 transition_duration)
|
||||
tpl_cfg = template_cfg.config or {}
|
||||
tpl_duration = tpl_cfg.get("transition_duration")
|
||||
if tpl_duration:
|
||||
try:
|
||||
dur_val = float(tpl_duration)
|
||||
if dur_val > 0:
|
||||
clip.transition_duration = dur_val
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 2. clip 级效果配置(滤镜、调速等)
|
||||
template_clip_config = template_cfg.config or {}
|
||||
if template_clip_config:
|
||||
# 合并到 clip.config(保留已有配置如 role 等)
|
||||
existing_config = clip.config or {}
|
||||
# 需要从模板复制的效果层 key
|
||||
effect_keys = ("color_grade", "speed", "playback_speed", "reverse", "chroma_key", "filter")
|
||||
for key in effect_keys:
|
||||
if key in template_clip_config:
|
||||
existing_config[key] = template_clip_config[key]
|
||||
clip.config = existing_config
|
||||
|
||||
# 3. 调速:同步到 clip.playback_speed 顶级字段(渲染引擎读此字段)
|
||||
template_speed = template_clip_config.get("playback_speed") or template_clip_config.get("speed")
|
||||
if template_speed:
|
||||
try:
|
||||
speed_val = float(template_speed)
|
||||
if speed_val > 0:
|
||||
clip.playback_speed = speed_val
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
|
||||
def _build_plan_and_clips_from_task(
|
||||
task_id: str,
|
||||
downloaded_paths: list[Path],
|
||||
|
||||
Reference in New Issue
Block a user