● Zion Boggan
repos/Stepping App/promo/build_video_win.py
zionboggan.com ↗
98 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
SHOTS = ROOT + r"\shots"
6
CLIPS = ROOT + r"\build\clips"
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 = 30
20
W, H = 1920, 1080
21
MONO = "fonts/mono.ttf"
22
SANS = "fonts/sans.ttf"
23
GOLD, BONE, STAGE = "0xC8A24C", "0xF2ECE0", "0x0F0E0C"
24
VCODEC = "-c:v h264_nvenc -pix_fmt yuv420p -preset p5 -rc vbr -cq 20 -b:v 0"
25
 
26
meta = json.load(open(ROOT + r"\voiceover\scene_times.json"))
27
ss, total, TAIL = meta["scene_start"], meta["total"], 1.6
28
order = ["s1","s2","s3","s4","s5","s6","s7","s8","s9","s10"]
29
cfg = {
30
  "s1": ("hero.png","wide","STEPPING  /  DIVINE NINE STEP CULTURE"),
31
  "s2": ("cities.png","wide","CHOOSE YOUR CITY"),
32
  "s3": ("city.png","wide","THE LINE-UP  /  UPCOMING SHOWS"),
33
  "s4": ("event.png","wide","EVERY SHOW, IN DETAIL"),
34
  "s5": ("yard.png","tall","THE YARD  /  THE COMMUNITY"),
35
  "s6": ("profile.png","wide","PROFILES  /  WHO'S WHO"),
36
  "s7": ("route.png","tall","THE ROUTE  /  PLAN THE TRIP"),
37
  "s8": ("mobile.png","phone","RIGHT IN YOUR PHONE"),
38
  "s9": ("hero.png","wide","MORE ON THE WAY"),
39
  "s10": (None,"end",""),
40
}
41
 
42
def dur(i):
43
    s = order[i]
44
    return round((ss[order[i+1]] - ss[s]) if i < len(order)-1 else (total - ss[s] + TAIL), 3)
45
 
46
def esc(t):
47
    return t.replace(":", "\\:").replace("'", "’").replace(",", "\\,")
48
 
49
def cap(text):
50
    if not text: return ""
51
    return (f",drawtext=fontfile={MONO}:text='{esc(text)}':fontcolor={GOLD}:fontsize=30"
52
            f":x=88:y=h-96:box=1:boxcolor={STAGE}@0.55:boxborderw=22")
53
 
54
def q(p): return '"' + p + '"'
55
 
56
def run(cmd):
57
    r = subprocess.run(cmd, shell=True, capture_output=True, text=True)
58
    if r.returncode != 0:
59
        print("ERR:", r.stderr[-1400:]); raise SystemExit(1)
60
 
61
clips = []
62
for i, s in enumerate(order):
63
    img, kind, c = cfg[s]
64
    d = dur(i); frames = int(round(d*FPS)); out = f"{CLIPS}\\{s}.mp4"
65
    fin = "fade=t=in:st=0:d=0.4,fade=t=out:st=%.3f:d=0.4" % max(d-0.4, 0)
66
    ip = q(f"{SHOTS}\\{img}") if img else ""
67
    if kind == "wide":
68
        vf = (f"scale=2112:-2,crop={W}:{H}:'(iw-{W})*t/{d}':(ih-{H})/2,{fin}{cap(c)}")
69
        run(f'{q(FF)} -y -loop 1 -t {d} -i {ip} -vf "{vf}" -r {FPS} {VCODEC} -an {q(out)}')
70
    elif kind == "tall":
71
        vf = f"scale={W}:-1,crop={W}:{H}:0:'(ih-{H})*t/{d}',{fin}{cap(c)}"
72
        run(f'{q(FF)} -y -loop 1 -t {d} -i {ip} -vf "{vf}" -r {FPS} {VCODEC} -an {q(out)}')
73
    elif kind == "phone":
74
        fc = (f"color={STAGE}:s={W}x{H}:d={d}:r={FPS}[bg];[0:v]scale=-1:1000[ph];"
75
              f"[bg][ph]overlay=(W-w)/2:(H-h)/2,{fin}{cap(c)}")
76
        run(f'{q(FF)} -y -loop 1 -t {d} -i {ip} -filter_complex "{fc}" -r {FPS} {VCODEC} -an {q(out)}')
77
    else:
78
        vf = (f"drawtext=fontfile={SANS}:text='STEPPING':fontcolor={BONE}:fontsize=120:x=(w-text_w)/2:y=(h-text_h)/2-40,"
79
              f"drawtext=fontfile={MONO}:text='BUILT FOR THE CULTURE':fontcolor={GOLD}:fontsize=30:x=(w-text_w)/2:y=(h/2)+70,"
80
              f"fade=t=in:st=0:d=0.5")
81
        run(f'{q(FF)} -y -f lavfi -i color={STAGE}:s={W}x{H}:d={d}:r={FPS} -vf "{vf}" {VCODEC} -an {q(out)}')
82
    clips.append(out); print(f"  {s} {d}s ok")
83
 
84
with open(f"{CLIPS}\\list.txt","w") as f:
85
    for c in clips: f.write("file '" + c.replace("\\","/") + "'\n")
86
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) + "video_silent.mp4")}')
87
 
88
vlen = round(total + TAIL, 3); fadest = round(total - 0.3, 3)
89
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];"
90
      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];"
91
      f"[m][key]sidechaincompress=threshold=0.035:ratio=9:attack=15:release=350:makeup=1[md];"
92
      f"[vo][md]amix=inputs=2:duration=longest:normalize=0,alimiter=limit=0.97[out]")
93
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")} '
94
    f'-filter_complex "{af}" -map "[out]" -ar 44100 -ac 2 -c:a aac -b:a 192k {q(ROOT + chr(92) + "build" + chr(92) + "audio_final.m4a")}')
95
 
96
run(f'{q(FF)} -y -i {q(ROOT + chr(92) + "build" + chr(92) + "video_silent.mp4")} -i {q(ROOT + chr(92) + "build" + chr(92) + "audio_final.m4a")} '
97
    f'-map 0:v -map 1:a -c:v copy -c:a copy -shortest {q(ROOT + chr(92) + "build" + chr(92) + "stepping_walkthrough.mp4")}')
98
print("DONE", vlen, "s")