● Zion Boggan
repos/Stepping App/promo/build_video2_win.py
zionboggan.com ↗
88 lines · python
History for this file →
1
import json, os, shutil, subprocess
2
 
3
FF = r"C:\Users\homelab\ffbuild71\ffmpeg-n7.1-latest-win64-gpl-7.1\bin\ffmpeg.exe"
4
ROOT = r"C:\Users\homelab\stepping-promo"
5
REC = ROOT + r"\rec"
6
CLIPS = ROOT + r"\build\clips2"
7
os.makedirs(CLIPS, exist_ok=True)
8
os.makedirs(ROOT + r"\fonts", exist_ok=True)
9
os.chdir(ROOT)
10
 
11
def cpfont(cands, dst):
12
    for s in cands:
13
        if os.path.exists(s):
14
            shutil.copy(s, dst); return
15
    raise SystemExit("no font " + dst)
16
cpfont([r"C:\Windows\Fonts\consolab.ttf", r"C:\Windows\Fonts\consola.ttf"], ROOT + r"\fonts\mono.ttf")
17
cpfont([r"C:\Windows\Fonts\arialbd.ttf", r"C:\Windows\Fonts\arial.ttf"], ROOT + r"\fonts\sans.ttf")
18
 
19
FPS, W, H = 30, 1920, 1080
20
MONO, SANS = "fonts/mono.ttf", "fonts/sans.ttf"
21
GOLD, BONE, STAGE = "0xC8A24C", "0xF2ECE0", "0x0F0E0C"
22
VC = "-c:v h264_nvenc -pix_fmt yuv420p -preset p5 -rc vbr -cq 20 -b:v 0"
23
 
24
meta = json.load(open(ROOT + r"\voiceover\scene_times.json"))
25
ss, total, TAIL = meta["scene_start"], meta["total"], 1.6
26
order = ["s1","s2","s3","s4","s5","s6","s7","s8","s9","s10"]
27
cfg = {
28
  "s1": ("desk", "STEPPING  /  DIVINE NINE STEP CULTURE"),
29
  "s2": ("desk", "CHOOSE YOUR CITY"),
30
  "s3": ("desk", "THE LINE-UP  /  UPCOMING SHOWS"),
31
  "s4": ("desk", "EVERY SHOW, IN DETAIL"),
32
  "s5": ("desk", "THE YARD  /  THE COMMUNITY"),
33
  "s6": ("desk", "PROFILES  /  WHO'S WHO"),
34
  "s7": ("desk", "THE ROUTE  /  PLAN THE TRIP"),
35
  "s8": ("phone", "RIGHT IN YOUR PHONE"),
36
  "s9": ("desk", "MORE ON THE WAY"),
37
  "s10": ("end", ""),
38
}
39
 
40
def dur(i):
41
    s = order[i]
42
    return round((ss[order[i+1]] - ss[s]) if i < len(order)-1 else (total - ss[s] + TAIL), 3)
43
 
44
def esc(t): return t.replace(":", "\\:").replace("'", "’").replace(",", "\\,")
45
def cap(t):
46
    if not t: return ""
47
    return (f",drawtext=fontfile={MONO}:text='{esc(t)}':fontcolor={GOLD}:fontsize=30"
48
            f":x=88:y=h-96:box=1:boxcolor={STAGE}@0.6:boxborderw=22")
49
def q(p): return '"' + p + '"'
50
def run(cmd):
51
    r = subprocess.run(cmd, shell=True, capture_output=True, text=True)
52
    if r.returncode != 0:
53
        print("ERR:", r.stderr[-1400:]); raise SystemExit(1)
54
 
55
clips = []
56
for i, s in enumerate(order):
57
    kind, c = cfg[s]; d = dur(i); out = f"{CLIPS}\\{s}.mp4"
58
    fin = "fade=t=in:st=0:d=0.4,fade=t=out:st=%.3f:d=0.4" % max(d-0.4, 0)
59
    src = q(f"{REC}\\{s}.webm")
60
    if kind == "desk":
61
        vf = f"scale={W}:{H}:force_original_aspect_ratio=increase,crop={W}:{H},fps={FPS},{fin}{cap(c)}"
62
        run(f'{q(FF)} -y -sseof -{d} -t {d} -i {src} -vf "{vf}" -r {FPS} {VC} -an {q(out)}')
63
    elif kind == "phone":
64
        fc = (f"color={STAGE}:s={W}x{H}:d={d}:r={FPS}[bg];[0:v]scale=-1:1000,fps={FPS}[ph];"
65
              f"[bg][ph]overlay=(W-w)/2:(H-h)/2,{fin}{cap(c)}")
66
        run(f'{q(FF)} -y -sseof -{d} -t {d} -i {src} -filter_complex "{fc}" -r {FPS} {VC} -an {q(out)}')
67
    else:
68
        vf = (f"drawtext=fontfile={SANS}:text='STEPPING':fontcolor={BONE}:fontsize=120:x=(w-text_w)/2:y=(h-text_h)/2-40,"
69
              f"drawtext=fontfile={MONO}:text='BUILT FOR THE CULTURE':fontcolor={GOLD}:fontsize=30:x=(w-text_w)/2:y=(h/2)+70,"
70
              f"fade=t=in:st=0:d=0.5")
71
        run(f'{q(FF)} -y -f lavfi -i color={STAGE}:s={W}x{H}:d={d}:r={FPS} -vf "{vf}" {VC} -an {q(out)}')
72
    clips.append(out); print(f"  {s} {d}s ok")
73
 
74
with open(f"{CLIPS}\\list.txt", "w") as f:
75
    for c in clips: f.write("file '" + c.replace("\\", "/") + "'\n")
76
run(f'{q(FF)} -y -f concat -safe 0 -i {q(CLIPS + chr(92) + "list.txt")} -c copy {q(ROOT + chr(92) + "build" + chr(92) + "video2_silent.mp4")}')
77
 
78
vlen = round(total + TAIL, 3); fadest = round(total - 0.3, 3)
79
af = (f"[0:a]aresample=44100,aformat=channel_layouts=stereo,loudnorm=I=-16:TP=-1.5:LRA=11,apad=whole_dur={vlen},asplit=2[vo][key];"
80
      f"[1:a]atrim=0:{vlen},asetpts=N/SR/TB,aresample=44100,aformat=channel_layouts=stereo,volume=0.22,afade=t=in:st=0:d=2,afade=t=out:st={fadest}:d=2.0[m];"
81
      f"[m][key]sidechaincompress=threshold=0.035:ratio=9:attack=15:release=350:makeup=1[md];"
82
      f"[vo][md]amix=inputs=2:duration=longest:normalize=0,alimiter=limit=0.97[out]")
83
run(f'{q(FF)} -y -i {q(ROOT + chr(92) + "voiceover" + chr(92) + "stepping_vo.wav")} -i {q(ROOT + chr(92) + "assets" + chr(92) + "music_inspired.mp3")} '
84
    f'-filter_complex "{af}" -map "[out]" -ar 44100 -ac 2 -c:a aac -b:a 192k {q(ROOT + chr(92) + "build" + chr(92) + "audio2.m4a")}')
85
 
86
run(f'{q(FF)} -y -i {q(ROOT + chr(92) + "build" + chr(92) + "video2_silent.mp4")} -i {q(ROOT + chr(92) + "build" + chr(92) + "audio2.m4a")} '
87
    f'-map 0:v -map 1:a -c:v copy -c:a copy -shortest {q(ROOT + chr(92) + "build" + chr(92) + "stepping_walkthrough_v2.mp4")}')
88
print("DONE", vlen)