diff options
Diffstat (limited to 'scripts/build_video.py')
| -rw-r--r-- | scripts/build_video.py | 93 |
1 files changed, 93 insertions, 0 deletions
diff --git a/scripts/build_video.py b/scripts/build_video.py new file mode 100644 index 0000000..572e3b5 --- /dev/null +++ b/scripts/build_video.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +"""Arma la pista final (voz + piano con ducking) siguiendo el timeline de Manim +y la pega al video renderizado.""" +import json, os, subprocess, sys, wave +import numpy as np + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(ROOT, "scripts")) +import music + +SR = 44100 +SCENE_DIR = {"montyhall": "v1_montyhall", "nueves": "v2_nueves", "cumple": "v3_cumple", + "luna": "v4_luna", "japones": "v5_japones", + "benford": "v6_benford", "hilbert": "v7_hilbert", "reuleaux": "v8_reuleaux", + "regla72": "v9_regla72", "buffon": "v10_buffon", + "collatz": "v11_collatz", "simpson": "v12_simpson", + "cuerda": "v13_cuerda", "gabriel": "v14_gabriel", + "mobius": "v15_mobius", "bayes": "v16_bayes", + "cicloide": "v17_cicloide", "dados": "v18_dados"} + + +def read_wav(p): + with wave.open(p) as w: + a = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16) + ch, sr = w.getnchannels(), w.getframerate() + a = a.astype(np.float32) / 32768.0 + if ch > 1: + a = a.reshape(-1, ch).mean(axis=1) + return a, sr + + +def to_sr(a, sr_in, sr_out=SR): + if sr_in == sr_out: + return a + n = int(round(len(a) * sr_out / sr_in)) + return np.interp(np.linspace(0, len(a) - 1, n), np.arange(len(a)), a).astype(np.float32) + + +def build_track(name, with_music=True): + tl = json.load(open(os.path.join(ROOT, "out", f"{name}_timeline.json"))) + segs = json.load(open(os.path.join(ROOT, "audio", name, "segments.json")))["segments"] + total = tl["total"] + 0.25 + speech = np.zeros(int(total * SR), dtype=np.float32) + + for beat in tl["beats"]: + a, sr = read_wav(segs[beat["i"]]["wav"]) + a = to_sr(a, sr) + off = int(beat["start"] * SR) + end = min(off + len(a), len(speech)) + speech[off:end] += a[:end - off] + + speech *= min(1.0, 0.90 / (np.abs(speech).max() + 1e-9)) + st = np.stack([speech, speech], axis=1) + + if with_music: + bed = music.bed(name, total, gain=0.32, seed=abs(hash(name)) % 9999) + if len(bed) < len(st): + bed = np.pad(bed, ((0, len(st) - len(bed)), (0, 0))) + bed = music.duck(bed[:len(st)], st) + mix = st * 0.96 + bed + else: + mix = st + + mix = np.clip(mix / max(1.0, np.abs(mix).max() / 0.97), -1, 1) + path = os.path.join(ROOT, "tmp", f"{name}_mix.wav") + with wave.open(path, "w") as w: + w.setnchannels(2); w.setsampwidth(2); w.setframerate(SR) + w.writeframes((mix * 32767).astype(np.int16).tobytes()) + return path + + +def mux(name, video=None, with_music=True): + if video is None: + video = os.path.join(ROOT, "media", "videos", SCENE_DIR[name], "1920p30", f"{name}.mp4") + wav = build_track(name, with_music) + out = os.path.join(ROOT, "out", f"{name}.mp4") + subprocess.run([ + "ffmpeg", "-v", "error", "-y", "-i", video, "-i", wav, + "-filter:a", "loudnorm=I=-14:TP=-1.5:LRA=11,aresample=48000", + "-map", "0:v:0", "-map", "1:a:0", + "-c:v", "libx264", "-profile:v", "high", "-pix_fmt", "yuv420p", + "-preset", "slow", "-crf", "20", "-r", "30", + "-c:a", "aac", "-b:a", "192k", "-ac", "2", + "-movflags", "+faststart", "-shortest", out], check=True) + d = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration", + "-of", "csv=p=0", out], capture_output=True, text=True).stdout.strip() + print(f"[{name}] {out} {float(d):.1f}s {os.path.getsize(out)/1e6:.1f} MB", flush=True) + return out + + +if __name__ == "__main__": + for n in (sys.argv[1:] or list(SCENE_DIR)): + mux(n) |