aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/build_video.py
blob: 572e3b58536d3a90df6eda672d7defe2c9b9b140 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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)