40 lines
1.8 KiB
Python
40 lines
1.8 KiB
Python
"""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}")
|