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
+100 -19
View File
@@ -372,10 +372,11 @@ def cmd_import(project_path: Path, force: bool, verbose: bool) -> int:
shared_assets_dir = _find_shared_assets(project_path)
if shared_assets_dir:
_import_shared_assets(shared_assets_dir, verbose)
_import_shared_audio(shared_assets_dir, project_path, config, verbose)
_sync_shared_videos_to_local(project_path, config, shared_assets_dir, verbose)
# Probe and cache audio file durations into audio.json
_probe_audio_durations(project_path, config, force, verbose)
_probe_audio_durations(project_path, config, force, verbose, shared_assets_dir)
# Probe and cache video metadata (duration, has_audio) into videos.json
_probe_video_metadata(project_path, config, shared_assets_dir, force, verbose)
@@ -384,8 +385,71 @@ def cmd_import(project_path: Path, force: bool, verbose: bool) -> int:
return 0
def _import_shared_audio(
shared_assets_dir: Path,
project_path: Path,
config,
verbose: bool,
) -> None:
"""Import audio files from shared_assets/media/audio into the project's audio.json."""
audio_extensions = {".mp3", ".wav", ".aac", ".m4a", ".ogg", ".flac"}
shared_audio_dir = shared_assets_dir / "media" / "audio"
if not shared_audio_dir.exists():
if verbose:
print(f" No shared audio dir found at {shared_audio_dir}")
return
audio_files = sorted(
f
for f in shared_audio_dir.iterdir()
if f.is_file()
and f.suffix.lower() in audio_extensions
and not f.name.startswith(".")
)
if not audio_files:
if verbose:
print(f" No audio files found in {shared_audio_dir}")
return
# Resolve project audio.json path
if config and config.audio_path:
audio_json_path = project_path / config.audio_path
else:
audio_json_path = project_path / "media" / "audio" / "audio.json"
audio_json_path.parent.mkdir(parents=True, exist_ok=True)
existing: dict = _read_json(audio_json_path) if audio_json_path.exists() else {}
added = 0
for f in audio_files:
audio_id = f.stem
if audio_id in existing:
if verbose:
print(f" Skipping {audio_id} (already in audio.json)")
continue
existing[audio_id] = {
"file": f.name,
"is_shared": True,
"volume": 1.0,
}
added += 1
if verbose:
print(f" Added shared audio: {audio_id}")
if added > 0:
with open(audio_json_path, "w", encoding="utf-8") as fh:
json.dump(existing, fh, indent=2)
print(f" Updated {audio_json_path.relative_to(project_path)} (+{added} shared audio files)")
else:
if verbose:
print(f" No new shared audio files to add")
def _probe_audio_durations(
project_path: Path, config, force: bool, verbose: bool
project_path: Path, config, force: bool, verbose: bool,
shared_assets_dir: Optional[Path] = None,
) -> None:
"""Probe and cache audio file durations into audio.json.
@@ -413,7 +477,10 @@ def _probe_audio_durations(
if verbose:
print(f" Audio '{audio_id}': cached ({audio_data['duration']:.1f}s)")
continue
audio_path = audio_dir / audio_data["file"]
if audio_data.get("is_shared") and shared_assets_dir:
audio_path = shared_assets_dir / "media" / "audio" / audio_data["file"]
else:
audio_path = audio_dir / audio_data["file"]
if not audio_path.exists():
if verbose:
print(f" Audio '{audio_id}': file not found, skipping")
@@ -1060,8 +1127,16 @@ _TASKS_VIDEO_PREFIXES = {
"video:": 6,
"vft:": 4,
"vfb:": 4,
"vf2t:": 5,
"vf2b:": 5,
"vst:": 4,
"vsb:": 4,
"vftp:": 5,
"vfbp:": 5,
"vf2tp:": 6,
"vf2bp:": 6,
"vstp:": 5,
"vsbp:": 5,
"narration:": 10,
}
@@ -1993,6 +2068,14 @@ def cmd_stitch(
print(f"\n Combined narration exists: {stitch_output.name}")
print(" (use --force to regenerate)")
else:
# Extract loudnorm config from talkinghead filter so stitch uses
# per-project settings instead of hardcoded defaults.
_loudnorm_cfg = None
if config and config.default_filters:
for _f in (config.default_filters.get("talkinghead") or []):
if isinstance(_f, dict) and _f.get("type") == "audio_normalize":
_loudnorm_cfg = _f
break
stitch_narration_segments(
narration_dir,
segment_ids,
@@ -2000,6 +2083,7 @@ def cmd_stitch(
stitch_output,
verbose=verbose,
default_end_trim=config.default_end_trim if config else 0.0,
loudnorm_config=_loudnorm_cfg,
)
# Run import videos again, because at this point narration_combined might have been created.
_import_videos(videos_dir, config, verbose)
@@ -2127,14 +2211,10 @@ def _print_render_plan_details(plan, marker_timings, slides: dict) -> None:
marker_id.startswith(p)
for p in (
"video:",
"vft:",
"vfb:",
"vst:",
"vsb:",
"vft:",
"vfbp:",
"vstp:",
"vsbp:",
"vft:", "vfb:", "vf2t:", "vf2b:",
"vst:", "vsb:",
"vftp:", "vfbp:", "vf2tp:", "vf2bp:",
"vstp:", "vsbp:",
)
):
aligned_count += 1
@@ -2142,14 +2222,10 @@ def _print_render_plan_details(plan, marker_timings, slides: dict) -> None:
len(p)
for p in (
"video:",
"vft:",
"vfb:",
"vst:",
"vsb:",
"vft:",
"vfbp:",
"vstp:",
"vsbp:",
"vft:", "vfb:", "vf2t:", "vf2b:",
"vst:", "vsb:",
"vftp:", "vfbp:", "vf2tp:", "vf2bp:",
"vstp:", "vsbp:",
)
if marker_id.startswith(p)
)
@@ -3066,6 +3142,11 @@ _RSYNC_EXCLUDES = [
"media/narration/processed/",
"media/narration/processed/**",
"media/videos/narration_combined.mov",
# Low-res preview files (generated locally, not synced)
"media/narration/low/",
"media/narration/low/**",
"media/videos/low/",
"media/videos/low/**",
# Chunk scratch directories
"**/chunks/",
"**/chunks/**",