Files
xiaoxia-saas/apps/worker/video_processing/transition_engine.py
CI Bot 254ad761b2
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 25s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m3s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m56s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 38s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 56s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 34s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 45s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m2s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 53s
AI Code Review / AI Code Review (pull_request) Successful in 2m10s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m0s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m29s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m20s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m48s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 19s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1237h15m19s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1237h15m40s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1237h15m42s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1237h15m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1237h20m20s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1237h20m22s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1237h20m24s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 1237h20m26s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1237h34m14s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1237h34m18s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1237h48m24s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1237h53m4s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1238h6m58s
refactor(wave113): 抽离transition_config领域模型 + 49单测
- 新建 packages/domain/transition_config.py:纯逻辑配置层
  + TransitionType 枚举(14种转场 + 别名映射)
  + TransitionConfig 数据类(parse / is_cut / ffmpeg_transition / validate)
  + MIN/MAX/DEFAULT 时长常量 + CUT_TRANSITION 常量
- transition_engine.py: 381→172行 (-209行, -55%)
  保留模块级导出,完全向后兼容
- 新增 49 个单测覆盖枚举 + 数据类 + 纯逻辑
2026-07-27 01:02:49 +08:00

173 lines
6.7 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""转场特效引擎 — Phase 8 智能增强.
基于 FFmpeg xfade 滤镜的统一转场抽象层,提供:
1. 转场类型枚举与预设管理
2. 转场配置解析与边界校验
3. 降级策略(不支持的转场自动 fallback 到硬切)
4. xfade 滤镜链构建(封装底层 ffmpeg_utils)
新增转场只需在 TransitionType 中加一项 + 在 XFADE_TRANSITION_MAP 中映射。
"""
from __future__ import annotations
import logging
from packages.domain.transition_config import ( # noqa: F401 — 向后兼容
CUT_TRANSITION,
DEFAULT_TRANSITION_DURATION,
MAX_TRANSITION_DURATION,
MIN_TRANSITION_DURATION,
TransitionConfig,
TransitionType,
)
from video_processing.ffmpeg_utils import build_xfade_filter_chain
logger = logging.getLogger(__name__)
# ── 转场引擎 ──────────────────────────────────────────────────────────────────
class TransitionEngine:
"""转场特效引擎.
封装转场配置验证、降级策略和 xfade 滤镜链构建,
供 UnifiedRenderService 等上层调用。
用法::
engine = TransitionEngine(default_duration=0.5)
config = engine.resolve_config("fade", 0.8)
filter_str, total_dur = engine.build_xfade_chain(
clip_durations=[3.0, 4.0, 5.0],
clip_video_labels=["v0", "v1", "v2"],
transitions=["cut", "fade", "dissolve"],
)
"""
def __init__(self, default_duration: float = DEFAULT_TRANSITION_DURATION) -> None:
"""初始化转场引擎.
Args:
default_duration: 默认转场时长(秒),用于未指定时长的 clip
"""
self._default_duration = default_duration
def resolve_config(
self,
effect: str | None = None,
duration: float | None = None,
) -> TransitionConfig:
"""解析单个转场配置,应用验证和降级.
Args:
effect: 转场效果名称
duration: 转场时长
Returns:
验证后的 TransitionConfig
"""
# 若未指定 duration,使用引擎默认值
dur = duration if duration is not None else self._default_duration
return TransitionConfig.parse(effect=effect, duration=dur)
def resolve_clip_transitions(
self,
clip_transitions: list[str],
clip_durations: list[float] | None = None,
) -> list[TransitionConfig]:
"""批量解析 clip 级别的转场配置.
Args:
clip_transitions: 每个 clip 的转场效果名称列表
clip_durations: 每个 clip 的时长列表(用于验证转场时长不超过片段时长)
Returns:
TransitionConfig 列表
"""
configs: list[TransitionConfig] = []
for i, effect in enumerate(clip_transitions):
cfg = self.resolve_config(effect=effect)
# 额外校验:转场时长不能超过对应 clip 时长的一半(保守限制)
if clip_durations and i < len(clip_durations) and not cfg.is_cut:
max_safe_duration = max(MIN_TRANSITION_DURATION, clip_durations[i] * 0.5)
if cfg.duration > max_safe_duration:
cfg = TransitionConfig(effect=cfg.effect, duration=max_safe_duration)
configs.append(cfg)
return configs
def build_xfade_chain(
self,
clip_durations: list[float],
clip_video_labels: list[str],
transitions: list[str],
*,
transition_duration: float | None = None,
output_label: str = "outv",
) -> tuple[str, float]:
"""构建 xfade 转场滤镜链.
对每步转场应用验证和降级,然后调用底层 ffmpeg_utils 构建。
Args:
clip_durations: 每个片段的时长
clip_video_labels: 每个片段的视频流标签
transitions: 每个片段对应的转场效果
transition_duration: 统一转场时长,None 则使用引擎默认值
output_label: 最终输出标签
Returns:
(filter_string, estimated_total_duration)
"""
if len(clip_durations) <= 1:
return build_xfade_filter_chain(
clip_durations=clip_durations,
clip_video_labels=clip_video_labels,
transitions=transitions,
transition_duration=transition_duration or self._default_duration,
output_label=output_label,
)
# 解析所有转场配置
resolved = self.resolve_clip_transitions(transitions, clip_durations)
resolved_effects = [c.effect for c in resolved]
# 使用统一的时长(取各转场中最大的时长作为基准,底层会做每步钳制)
dur = transition_duration or self._default_duration
if not dur:
dur = max(c.duration for c in resolved) if resolved else DEFAULT_TRANSITION_DURATION
# 调用底层构建
return build_xfade_filter_chain(
clip_durations=clip_durations,
clip_video_labels=clip_video_labels,
transitions=resolved_effects,
transition_duration=dur,
output_label=output_label,
)
@staticmethod
def supported_transitions() -> list[dict[str, str]]:
"""获取所有支持的转场效果列表(用于 API 返回给前端).
Returns:
[{name, display_name, category}, ...]
"""
return [
{"name": "cut", "display_name": "硬切", "category": "basic"},
{"name": "fade", "display_name": "淡入淡出", "category": "basic"},
{"name": "dissolve", "display_name": "溶解", "category": "basic"},
{"name": "slideleft", "display_name": "左滑入", "category": "slide"},
{"name": "slideright", "display_name": "右滑入", "category": "slide"},
{"name": "slideup", "display_name": "上滑入", "category": "slide"},
{"name": "slidedown", "display_name": "下滑入", "category": "slide"},
{"name": "zoom", "display_name": "缩放", "category": "zoom"},
{"name": "wipeleft", "display_name": "左擦除", "category": "wipe"},
{"name": "wiperight", "display_name": "右擦除", "category": "wipe"},
{"name": "wipeup", "display_name": "上擦除", "category": "wipe"},
{"name": "wipedown", "display_name": "下擦除", "category": "wipe"},
{"name": "circlecrop", "display_name": "圆形扩散", "category": "special"},
{"name": "rectcrop", "display_name": "矩形扩散", "category": "special"},
]