From eef372073a04ddaaf9b4d5d70ee18d4bca453844 Mon Sep 17 00:00:00 2001 From: saas-backend-agent Date: Mon, 7 Sep 2026 01:11:57 +0800 Subject: [PATCH] =?UTF-8?q?wip(#1749):=20render=5Faudio=20=E5=9B=9B?= =?UTF-8?q?=E8=B7=AF=20apad=20=E9=9D=99=E9=9F=B3=E8=A1=A5=E9=BD=90=20+=20s?= =?UTF-8?q?tream-copy=20tpad=20+=20worker=20=E9=87=8D=E6=B8=B2=E4=BC=A0?= =?UTF-8?q?=E9=85=8D=E9=9F=B3=E6=97=B6=E9=95=BF=EF=BC=88checkpoint?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/worker/video_processing/render_audio.py | 70 +++++++++++++++---- .../unified_render_service.py | 10 ++- apps/worker/worker_app/tasks/generation.py | 6 ++ 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/apps/worker/video_processing/render_audio.py b/apps/worker/video_processing/render_audio.py index 7dd31da46..3d569f7d7 100755 --- a/apps/worker/video_processing/render_audio.py +++ b/apps/worker/video_processing/render_audio.py @@ -47,13 +47,23 @@ class RenderContext: def clip_effective_duration(clip: ResolvedClip) -> float: - """计算 clip 的有效时长. + """计算 clip 的有效时长(#1749:目标段长始终为准)。 - 与 UnifiedRenderService._clip_effective_duration 逻辑一致。 + 与 UnifiedRenderService._clip_effective_duration 逻辑一致;素材短于段长的 + 部分由末帧冻结 tpad / 音频 apad 铺满,不在此处钳制。 """ if clip.duration > 0: - return min(clip.duration, clip.actual_duration) if clip.actual_duration > 0 else clip.duration - return clip.actual_duration if clip.actual_duration > 0 else 0.0 + return float(clip.duration) + return float(clip.actual_duration) if clip.actual_duration > 0 else 0.0 + + +def _clip_freeze_seconds(clip: ResolvedClip) -> float: + """读取 #1749 末帧冻结秒数(_resolve_clips 写入 config['_freeze_seconds'])。""" + cfg = getattr(clip, "config", None) or {} + try: + return max(0.0, float(cfg.get("_freeze_seconds", 0.0) or 0.0)) + except (TypeError, ValueError): + return 0.0 def clip_has_audio(ctx: RenderContext, clip: ResolvedClip) -> bool: @@ -273,6 +283,9 @@ def concat_main_audio( if video_duration > 0 and (final_duration <= 0 or final_duration > video_duration): final_duration = video_duration + # #1749 末帧冻结秒数(音频需 apad 补静音与视频等长) + freeze_seconds = _clip_freeze_seconds(clip) + # 音频倒放 reverse_config = ReverseConfig.from_dict(clip.config.get("reverse")) has_reverse = reverse_config.enabled and reverse_config.reverse_audio @@ -281,20 +294,30 @@ def concat_main_audio( if not has_speed and not has_reverse: # 无调速无倒放:根据是否需要裁剪/音量选择最高效的路径。 vol = _clip_volume(clip) - need_trim = trim_start > 0 or (effective_duration > 0 and final_duration < adjusted_duration) + need_trim = ( + trim_start > 0 + or freeze_seconds > 0 + or (effective_duration > 0 and final_duration < adjusted_duration) + ) need_volume = abs(vol - 1.0) >= 1e-6 if need_trim: # 需要裁剪:用 atrim 滤镜在滤镜链中精确裁剪(采样点级精度,不浪费解码)。 - # 滤镜顺序:atrim → asetpts → volume(先裁剪再调音量,避免处理被丢弃的数据)。 + # 滤镜顺序:atrim → asetpts → apad(冻结补静音) → volume。 af_parts: list[str] = [] - if trim_start > 0 and effective_duration > 0: - af_parts.append(f"atrim=start={trim_start:.3f}:duration={final_duration:.3f}") + # 冻结场景:素材内可用时长 = 目标段长 − freeze(截掉超出素材的部分后补静音) + atrim_dur = final_duration + if freeze_seconds > 0: + atrim_dur = max(0.0, final_duration - freeze_seconds) + if trim_start > 0 and atrim_dur > 0: + af_parts.append(f"atrim=start={trim_start:.3f}:duration={atrim_dur:.3f}") elif trim_start > 0: af_parts.append(f"atrim=start={trim_start:.3f}") - elif final_duration > 0: - af_parts.append(f"atrim=duration={final_duration:.3f}") + elif atrim_dur > 0: + af_parts.append(f"atrim=duration={atrim_dur:.3f}") af_parts.append("asetpts=PTS-STARTPTS") + if freeze_seconds > 0: + af_parts.append(f"apad=whole_dur={final_duration:.3f}") if need_volume: af_parts.append(f"volume={vol:.4f}") command = [ @@ -334,8 +357,13 @@ def concat_main_audio( "-ac", "2", ] + af_simple: list[str] = [] if need_volume: - command.extend(["-af", f"volume={vol:.4f}"]) + af_simple.append(f"volume={vol:.4f}") + if freeze_seconds > 0: + af_simple.append(f"apad=whole_dur={final_duration:.3f}") + if af_simple: + command.extend(["-af", ",".join(af_simple)]) if final_duration > 0: command.extend(["-t", f"{final_duration:.3f}"]) command.append(str(output_path)) @@ -369,6 +397,10 @@ def concat_main_audio( if abs(vol - 1.0) >= 1e-6: audio_filters.append(f"volume={vol:.4f}") + # #1749 冻结补静音(调速后时长已变,apad 补齐到最终时长) + if freeze_seconds > 0: + audio_filters.append(f"apad=whole_dur={final_duration:.3f}") + # aformat 归一化:统一输出格式为 48000Hz + stereo + fltp audio_filters.append("aformat=sample_rates=48000:channel_layouts=stereo:sample_fmts=fltp") @@ -410,10 +442,13 @@ def concat_main_audio( speed = getattr(clip, "playback_speed", 1.0) or 1.0 if not isinstance(speed, (int, float)) or speed <= 0: speed = 1.0 + # #1749 末帧冻结:素材内可用时长 = 目标段长 − freeze,apad 补静音 + freeze_seconds = _clip_freeze_seconds(clip) + atrim_dur = max(0.0, effective_duration - freeze_seconds) if freeze_seconds > 0 else effective_duration audio_filters: list[str] = [] if effective_duration > 0: - audio_filters.append(f"atrim=start={trim_start:.3f}:duration={effective_duration:.3f}") + audio_filters.append(f"atrim=start={trim_start:.3f}:duration={atrim_dur:.3f}") audio_filters.append("asetpts=PTS-STARTPTS") # 音频调速 — atempo 多级串联 @@ -435,6 +470,10 @@ def concat_main_audio( if reverse_filter: audio_filters.append(reverse_filter) + # #1749 冻结补静音:视频 tpad 延长后音频等长补齐(concat 时间轴对齐) + if freeze_seconds > 0: + audio_filters.append(f"apad=whole_dur={effective_duration:.3f}") + # 音量(0=静音,1=原声) vol = _clip_volume(clip) if abs(vol - 1.0) >= 1e-6: @@ -506,12 +545,17 @@ def mix_with_independent_audio( input_args.extend(["-i", str(clip.local_path)]) effective_duration = clip_effective_duration(clip) trim_start = getattr(clip, "start_time", 0) or 0 + freeze_seconds = _clip_freeze_seconds(clip) + atrim_dur = max(0.0, effective_duration - freeze_seconds) if freeze_seconds > 0 else effective_duration clip_filters = [] if effective_duration > 0: - clip_filters.append(f"atrim=start={trim_start:.3f}:duration={effective_duration:.3f}") + clip_filters.append(f"atrim=start={trim_start:.3f}:duration={atrim_dur:.3f}") clip_filters.append("asetpts=PTS-STARTPTS") else: clip_filters.append("asetpts=PTS-STARTPTS") + # #1749 冻结补静音(与视频 tpad 等长) + if freeze_seconds > 0: + clip_filters.append(f"apad=whole_dur={effective_duration:.3f}") vol = _clip_volume(clip) if abs(vol - 1.0) >= 1e-6: clip_filters.append(f"volume={vol:.4f}") diff --git a/apps/worker/video_processing/unified_render_service.py b/apps/worker/video_processing/unified_render_service.py index e13b3bb2a..61d86b54d 100755 --- a/apps/worker/video_processing/unified_render_service.py +++ b/apps/worker/video_processing/unified_render_service.py @@ -1297,10 +1297,16 @@ class UnifiedRenderService: # trim effective_duration = UnifiedRenderService._clip_effective_duration(clip) + freeze_seconds = float((clip.config or {}).get("_freeze_seconds", 0.0) or 0.0) + # #1749 冻结时素材内截取时长 = 目标段长 − freeze + trim_dur = max(0.0, effective_duration - freeze_seconds) if freeze_seconds > 0 else effective_duration - if effective_duration > 0: - filters.append(f"trim=duration={effective_duration}") + if trim_dur > 0: + filters.append(f"trim=duration={trim_dur}") filters.append("setpts=PTS-STARTPTS") + # #1749 末帧冻结(禁慢放) + if freeze_seconds > 0: + filters.append(f"tpad=stop_mode=clone:stop_duration={freeze_seconds:.3f}") # 调速 — 与 filter_complex 路径一致 speed = UnifiedRenderService._clip_speed(clip) diff --git a/apps/worker/worker_app/tasks/generation.py b/apps/worker/worker_app/tasks/generation.py index 357d5ab13..a429d7b47 100644 --- a/apps/worker/worker_app/tasks/generation.py +++ b/apps/worker/worker_app/tasks/generation.py @@ -535,11 +535,17 @@ def _reselect_plan_for_batch_retry(task_id: str, plan_id: str, task_info: dict) try: svc = EditPlanService(db) asset_pool = list(task_info.get("task_asset_ids") or []) + _voice_dur = 0.0 + try: + _voice_dur = float(task_info.get("voice_duration", 0.0) or 0.0) + except (TypeError, ValueError): + _voice_dur = 0.0 new_plan = svc.reselect_plan_for_variant( plan_id, asset_pool, created_by_user_id=task_info.get("user_id", ""), name_suffix="重渲变体", + voice_duration=_voice_dur, ) return new_plan.id finally: