"""Build the narration track from recorded beat marks and mux it onto the video.""" import json, subprocess, sys from pathlib import Path SP = Path(__file__).parent VO = SP / "vo" take = sys.argv[1] if len(sys.argv) > 1 else "win1" video = SP / f"{take}.mp4" out = SP / f"{take}_narrated.mp4" marks = [json.loads(l) for l in (SP / "marks.jsonl").read_text().splitlines() if l.strip()] dur = json.loads((VO / "durations.json").read_text()) vlen = float(subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=nw=1:nk=1", str(video)], capture_output=True, text=True).stdout.strip()) inputs, filters, labels = [], [], [] for i, m in enumerate(marks): wav = VO / f"{m['key']}.wav" if not wav.exists(): print(f" missing {wav.name}, skipping"); continue delay_ms = int(m["t"] * 1000) inputs += ["-i", str(wav)] filters.append(f"[{i}:a]adelay={delay_ms}|{delay_ms},apad[a{i}]") labels.append(f"[a{i}]") end = m["t"] + dur.get(m["key"], 0) flag = "" if end <= vlen + 0.5 else " <-- OVERRUNS VIDEO" print(f" {m['key']:9} start {m['t']:7.2f}s len {dur.get(m['key'],0):5.2f}s end {end:7.2f}s{flag}") mix = "".join(labels) + f"amix=inputs={len(labels)}:normalize=0,atrim=0:{vlen},asetpts=N/SR/TB[a]" cmd = (["ffmpeg", "-y", "-loglevel", "error"] + inputs + ["-i", str(video), "-filter_complex", ";".join(filters) + ";" + mix, "-map", f"{len(labels)}:v", "-map", "[a]", "-c:v", "copy", "-c:a", "aac", "-b:a", "160k", "-shortest", str(out)]) r = subprocess.run(cmd, capture_output=True, text=True) if r.returncode != 0: print("ffmpeg failed:\n", r.stderr[-1500:]); sys.exit(1) print(f"\n video {vlen:.1f}s | narration ends {max(m['t']+dur.get(m['key'],0) for m in marks):.1f}s") print(f" -> {out}")