Fixing loudness issue

This commit is contained in:
2026-05-12 00:52:14 +02:00
parent feb4df0506
commit 994a2e0bb6
7 changed files with 191 additions and 76 deletions
+51 -46
View File
@@ -395,7 +395,7 @@ def build_ffmpeg_command(plan: RenderPlan, output_path: Path) -> list[str]:
video_path = _resolve_video_path(
videos_dir, event.video_source, shared_assets_dir, project_path
)
skip = event.video_source.skip
skip = event.video_source.skip or 0.0
if skip > 0:
cmd.extend(["-ss", f"{skip:.3f}"])
cmd.extend(["-analyzeduration", "0", "-probesize", "1000"])
@@ -425,7 +425,7 @@ def build_ffmpeg_command(plan: RenderPlan, output_path: Path) -> list[str]:
video_path = _resolve_video_path(
videos_dir, event.video_source, shared_assets_dir, project_path
)
skip = event.video_source.skip
skip = event.video_source.skip or 0.0
if skip > 0:
cmd.extend(["-ss", f"{skip:.3f}"])
cmd.extend(["-analyzeduration", "0", "-probesize", "1000"])
@@ -455,7 +455,10 @@ def build_ffmpeg_command(plan: RenderPlan, output_path: Path) -> list[str]:
for event in plan.audio_events:
if event.audio_id not in audio_inputs:
audio_path = audio_dir / event.audio_def.file
if event.audio_def.is_shared and plan.shared_assets_dir:
audio_path = plan.shared_assets_dir / "media" / "audio" / event.audio_def.file
else:
audio_path = audio_dir / event.audio_def.file
audio_path, _ = resolve_with_cache(audio_path, project_path)
# Use pre-probed duration from audio.json if available (set by import).
# For MP3 without Xing/VBRI headers this is critical — FFmpeg otherwise
@@ -802,13 +805,14 @@ def build_filter_complex(
"""
Build the filter_complex string for FFmpeg.
Layer structure:
Layer structure (bottom to top):
- Layer 1: Background (solid color, image, or video)
- Layer 2: Always visible videos (like talking head) in cutouts
- Layer 3: Slides (with time-based enable)
- Layer 4: Triggered videos in cutouts (with time-based enable)
- Layer 5: Camera transform
- Layer 6: Outro videos (fullscreen, after narration ends)
- Layer 2: "below" triggered videos (vfb/vsb) — behind talking head
- Layer 3: Always visible videos (like talking head) in cutouts
- Layer 4: Slides (with time-based enable)
- Layer 5: "above" triggered videos (vft/vst) — in front of slides
- Layer 6: Camera transform
- Layer 7: Outro videos (fullscreen, after narration ends)
- Audio: Main audio mixed with triggered sound effects and outro audio
"""
outro_inputs = outro_inputs or {}
@@ -835,6 +839,44 @@ def build_filter_complex(
current_label = "bg"
# Add "below" triggered video overlays (vfb/vsb) BEFORE the talking head
# so they sit behind it in the composite stack.
for i, event in enumerate(plan.video_events):
if event.layer != "below":
continue
video_idx = video_inputs[i]
cut_x, cut_y, cut_width, cut_height = _calculate_cutout_position(
event.cutout, width, height
)
duration = event.end_time - event.start_time
if event.video_source.take is not None:
duration = min(duration, event.video_source.take)
effective_end = event.start_time + duration
zoom = event.video_source.zoom
zoomed_width = int(cut_width * zoom)
zoomed_height = int(cut_height * zoom)
video_label = f"tvb{i}"
start_pts = event.start_time
filters.append(
f"[{video_idx}:v]format=yuva444p10le,"
f"setpts=PTS-STARTPTS+{start_pts:.3f}/TB,"
f"scale={zoomed_width}:{zoomed_height}:force_original_aspect_ratio=increase,"
f"crop={cut_width}:{cut_height}:(iw-{cut_width})/2:(ih-{cut_height})/2,"
f"format=rgba[{video_label}]"
)
next_label = f"tvbbase{i}"
enable_expr = f"between(t\\,{event.start_time:.3f}\\,{effective_end:.3f})"
filters.append(
f"[{current_label}][{video_label}]overlay="
f"x={cut_x}:y={cut_y}:enable={enable_expr}"
f"[{next_label}]"
)
current_label = next_label
# Overlay always_visible videos (like talking head)
# If there are narration pauses, we need to segment the video
for i, (video_id, video_source, cutout) in enumerate(plan.narration_videos):
@@ -898,43 +940,6 @@ def build_filter_complex(
)
current_label = next_label
# Add "below-slides" triggered video overlays (vfb/vsb or layer="below")
for i, event in enumerate(plan.video_events):
if event.layer != "below":
continue
video_idx = video_inputs[i]
cut_x, cut_y, cut_width, cut_height = _calculate_cutout_position(
event.cutout, width, height
)
duration = event.end_time - event.start_time
if event.video_source.take is not None:
duration = min(duration, event.video_source.take)
effective_end = event.start_time + duration
zoom = event.video_source.zoom
zoomed_width = int(cut_width * zoom)
zoomed_height = int(cut_height * zoom)
video_label = f"tvb{i}"
start_pts = event.start_time
filters.append(
f"[{video_idx}:v]format=yuva444p10le,"
f"setpts=PTS-STARTPTS+{start_pts:.3f}/TB,"
f"scale={zoomed_width}:{zoomed_height}:force_original_aspect_ratio=increase,"
f"crop={cut_width}:{cut_height}:(iw-{cut_width})/2:(ih-{cut_height})/2,"
f"format=rgba[{video_label}]"
)
next_label = f"tvbbase{i}"
enable_expr = f"between(t\\,{event.start_time:.3f}\\,{effective_end:.3f})"
filters.append(
f"[{current_label}][{video_label}]overlay="
f"x={cut_x}:y={cut_y}:enable={enable_expr}"
f"[{next_label}]"
)
current_label = next_label
# Add slide overlays with time-based enable
for i, event in enumerate(plan.slide_events):
slide_idx = slide_inputs[event.slide_id]