fix(worker): render_plan 保留视频原声并尊重每clip音量 (#1474)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m12s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m40s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m4s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m8s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m9s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m10s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m28s
AI Code Review / AI Code Review (pull_request) Failing after 2m17s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m34s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 38s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m31s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m11s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m47s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m15s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m47s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m38s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m41s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m46s
CI/CD Pipeline / Integration Tests (push) Successful in 3m4s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m12s
CI/CD Pipeline / Unit Tests (push) Failing after 13m33s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 11m30s
CI/CD Pipeline / CI Gate (pull_request) Failing after 10s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 565h55m23s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 565h55m24s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 565h55m26s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 565h55m26s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 565h55m24s
CI/CD Pipeline / Canary Release to Production (push) Failing after 565h55m58s
CI/CD Pipeline / Production Browser E2E (push) Failing after 565h55m58s
CI/CD Pipeline / Deploy Production (push) Failing after 565h55m59s
CI/CD Pipeline / Build Production Web Image (push) Failing after 565h56m0s
CI/CD Pipeline / Build Production API Image (push) Failing after 565h56m1s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 566h5m57s
CI/CD Pipeline / CI Gate (push) Failing after 565h56m2s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 566h6m1s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 566h6m3s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 566h6m56s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 566h6m57s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 566h7m1s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 566h8m28s
CI/CD Pipeline / PR Build Web Image (push) Failing after 566h8m30s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 566h8m34s
CI/CD Pipeline / PR Build API Image (push) Failing after 566h8m32s
CI/CD Pipeline / Frontend Lint (push) Failing after 566h8m54s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 566h8m36s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 566h10m7s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 566h8m38s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 566h29m17s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 566h29m51s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 566h39m50s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 566h40m0s

Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
This commit was merged in pull request #1474.
This commit is contained in:
2026-08-24 01:16:00 +08:00
committed by auto-approve-bot
parent 024aca3557
commit 61770cd4e4
4 changed files with 443 additions and 118 deletions
@@ -36,6 +36,7 @@ from video_processing.ffmpeg_utils import (
DEFAULT_TRANSITION_DURATION,
FFMPEG_BIN,
probe_duration,
probe_has_audio,
probe_video_info,
run_ffmpeg,
)
@@ -1000,6 +1001,11 @@ class UnifiedRenderService:
if abs(speed - 1.0) >= 1e-6:
return False, f"有调速: speed={speed:.2f}x"
# 音量非默认(静音/放大)→ 需要音频滤镜重编码 → 不能 copy
vol = UnifiedRenderService._clip_volume(clip)
if abs(vol - 1.0) >= 1e-6:
return False, f"音量非默认: volume={vol:.2f}"
# 有倒放 → 需要重编码 → 不能 copy
reverse_config = ReverseConfig.from_dict(clip.config.get("reverse"))
if reverse_config.enabled and (reverse_config.reverse_video or reverse_config.reverse_audio):
@@ -1270,13 +1276,23 @@ class UnifiedRenderService:
"+faststart",
]
# 音频处理:background 通常是图片无音频,跳过;其他编码为 aac
# background 以外的视频素材,默认带音频
has_audio = role != "background"
# 音频处理:background 通常是图片无音频,跳过;其他角色先探测是否真有音频流。
# volume=0 不丢弃音频流,而是保留后通过 volume=0 滤镜静音,保持时间轴对齐。
clip_volume = UnifiedRenderService._clip_volume(clip)
if role != "background":
try:
has_audio = probe_has_audio(clip.local_path)
except Exception as e:
# probe_has_audio 内部已保守返回 True;只有极端错误才会到这里。
# 此时不静默丢音频,记录 error 并向上抛出,让任务失败而不是产出无声视频。
logger.error("[unified-render] 探测音频流发生致命错误,终止渲染: %s: %s", clip.local_path, e)
raise
else:
has_audio = False
if has_audio:
af_parts: list[str] = []
# 音频降噪
# 音频降噪(最先处理:在原始信号上降噪效果最好)
try:
from video_processing.noise_reduction_engine import NoiseReductionConfig, NoiseReductionEngine
@@ -1290,13 +1306,16 @@ class UnifiedRenderService:
except Exception as e:
logger.warning("[unified-render] 直通模式音频降噪应用失败,跳过: %s", e)
# 音频调速(与视频setpts对应,保持音画同步)
# 音频调速(在降噪之后、音量之前,与 render_audio.py concat 路径保持一致)
# SpeedEngine.build_audio_filter 内部已实现多级 atempo 串联,
# 自动处理超出 [0.5, 2.0] 范围的速度(如 0.25x → atempo=0.5,atempo=0.5)。
speed = UnifiedRenderService._clip_speed(clip)
if abs(speed - 1.0) >= 1e-6:
try:
from video_processing.speed_engine import SpeedConfig, SpeedEngine
speed_cfg = SpeedConfig(speed=speed)
speed_cfg.clamp()
speed_engine = SpeedEngine()
af_parts.append(speed_engine.build_audio_filter(speed_cfg))
except Exception as e:
@@ -1309,6 +1328,10 @@ class UnifiedRenderService:
if af_filter:
af_parts.append(af_filter)
# 片段音量(最后应用:确保调速/倒放后的最终输出音量准确,与 concat 路径一致)
if abs(clip_volume - 1.0) >= 1e-6:
af_parts.append(f"volume={clip_volume:.4f}")
if af_parts:
command.extend(["-af", ",".join(af_parts)])
@@ -1976,6 +1999,16 @@ class UnifiedRenderService:
"""
return _clip_playback_speed_pure(getattr(clip, "playback_speed", 1.0))
@staticmethod
def _clip_volume(clip: ResolvedClip) -> float:
"""获取 clip 的音量(config.volume)。缺省 1.0 原声,0.0 静音。"""
cfg = getattr(clip, "config", None) or {}
try:
vol = float(cfg.get("volume", 1.0))
except (TypeError, ValueError):
return 1.0
return max(0.0, vol)
@staticmethod
def _clip_adjusted_duration(clip: ResolvedClip) -> float:
"""计算调速后的 clip 实际时长(用于拼接计算)。