9f8ff6a57e
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 41s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m29s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m37s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m48s
AI Code Review / AI Code Review (pull_request) Failing after 1m49s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m9s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m45s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m44s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m47s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m37s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m57s
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 Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m38s
CI/CD Pipeline / CI Gate (pull_request) Failing after 5s
根因: plan.config 中某 key 的值可能为 bool 而非 dict,
当 .get() 或 from_dict() 被调用时崩溃。
'or {}' 只能防 None/空值,防不了 True/False。
修复策略(双层防护):
1. 治本: 所有 from_dict 方法入口加 isinstance(data, dict) 检查
- chroma_key_config.py / color_grade_config.py / noise_reduction_config.py
- watermark_config.py / pip_config.py / intro_outro_config.py
- reverse_engine.py
2. 治标: 调用方直接 .get() 处加 isinstance 防护
- unified_render_service.py: tts/bgm/title/subtitle/export/bgm
- render_adapter.py: bgm/export/title
- render_audio.py: bgm/audio_tracks
- generation.py: title/subtitle/bgm/export
- edit_plan_generation.py: title
共修复 12 个文件,覆盖渲染管线全部 unprotected 调用点
117 lines
3.9 KiB
Python
Executable File
117 lines
3.9 KiB
Python
Executable File
"""视频倒放引擎 — 基于 FFmpeg reverse + areverse 滤镜实现视频/音频倒放.
|
|
|
|
支持能力:
|
|
- 视频倒放(reverse 滤镜)
|
|
- 音频倒放(areverse 滤镜)
|
|
- 按 clip 分段倒放,每个 clip 独立配置
|
|
- 降级策略:不支持时跳过,不阻断渲染
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ── 数据模型 ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@dataclass
|
|
class ReverseConfig:
|
|
"""视频倒放配置.
|
|
|
|
从 clip.config.reverse 读取,零侵入数据模型.
|
|
"""
|
|
|
|
enabled: bool = False
|
|
reverse_video: bool = True # 是否倒放视频
|
|
reverse_audio: bool = True # 是否倒放音频
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any] | None) -> "ReverseConfig":
|
|
"""从字典解析配置."""
|
|
if not isinstance(data, dict):
|
|
return cls(enabled=False)
|
|
try:
|
|
if not data.get("enabled", False):
|
|
return cls(enabled=False)
|
|
return cls(
|
|
enabled=True,
|
|
reverse_video=bool(data.get("reverse_video", True)),
|
|
reverse_audio=bool(data.get("reverse_audio", True)),
|
|
)
|
|
except (AttributeError, TypeError) as e:
|
|
logger.warning("倒放配置解析失败: %s,使用默认配置", e)
|
|
return cls(enabled=False)
|
|
|
|
|
|
# ── 倒放引擎 ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class ReverseEngine:
|
|
"""视频倒放引擎 — 生成 FFmpeg 倒放滤镜.
|
|
|
|
视频倒放:reverse 滤镜
|
|
音频倒放:areverse 滤镜
|
|
|
|
注意事项:
|
|
- reverse 滤镜需要将整个视频帧加载到内存,长视频可能占用大量内存
|
|
- 建议对单 clip 时长做限制(如 < 60s),超长视频建议降级
|
|
"""
|
|
|
|
# 安全限制:单 clip 超过此时长不启用倒放(防止内存溢出)
|
|
MAX_SAFE_DURATION = 120.0 # 秒
|
|
|
|
@staticmethod
|
|
def build_video_filter(config: ReverseConfig, duration: float = 0.0) -> str:
|
|
"""构建视频倒放滤镜字符串.
|
|
|
|
Args:
|
|
config: 倒放配置
|
|
duration: clip 时长(秒),用于安全检查
|
|
|
|
Returns:
|
|
FFmpeg 滤镜字符串,如 "reverse";无效果返回空字符串
|
|
"""
|
|
if not config.enabled or not config.reverse_video:
|
|
return ""
|
|
|
|
# 安全检查:超长视频不启用倒放
|
|
if duration > ReverseEngine.MAX_SAFE_DURATION:
|
|
logger.warning(
|
|
"视频倒放安全限制:clip 时长 %.1fs 超过上限 %.1fs,跳过倒放",
|
|
duration,
|
|
ReverseEngine.MAX_SAFE_DURATION,
|
|
)
|
|
return ""
|
|
|
|
return "reverse"
|
|
|
|
@staticmethod
|
|
def build_audio_filter(config: ReverseConfig, duration: float = 0.0) -> str:
|
|
"""构建音频倒放滤镜字符串.
|
|
|
|
Args:
|
|
config: 倒放配置
|
|
duration: clip 时长(秒),用于安全检查
|
|
|
|
Returns:
|
|
FFmpeg 音频滤镜字符串,如 "areverse";无效果返回空字符串
|
|
"""
|
|
if not config.enabled or not config.reverse_audio:
|
|
return ""
|
|
|
|
# 安全检查:超长音频不启用倒放
|
|
if duration > ReverseEngine.MAX_SAFE_DURATION:
|
|
logger.warning(
|
|
"音频倒放安全限制:clip 时长 %.1fs 超过上限 %.1fs,跳过倒放",
|
|
duration,
|
|
ReverseEngine.MAX_SAFE_DURATION,
|
|
)
|
|
return ""
|
|
|
|
return "areverse"
|