● Zion Boggan
repos/Stepping App/promo/build_video.py
zionboggan.com ↗
111 lines · python
History for this file →
1
import json, os, subprocess, shlex
2
 
3
ROOT = "/shared/projects/SteppingApp/promo"
4
SHOTS = f"{ROOT}/shots"
5
CLIPS = f"{ROOT}/build/clips"
6
os.makedirs(CLIPS, exist_ok=True)
7
FPS = 30
8
W, H = 1920, 1080
9
def pick(paths):
10
    for p in paths:
11
        if os.path.exists(p):
12
            return p
13
    return paths[-1]
14
MONO = pick([
15
    "/usr/share/fonts/truetype/liberation2/LiberationMono-Bold.ttf",
16
    "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf",
17
    "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
18
])
19
SANS = pick([
20
    "/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf",
21
    "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
22
])
23
GOLD = "0xC8A24C"
24
BONE = "0xF2ECE0"
25
STAGE = "0x0F0E0C"
26
 
27
meta = json.load(open(f"{ROOT}/voiceover/scene_times.json"))
28
ss = meta["scene_start"]
29
total = meta["total"]
30
TAIL = 1.6
31
 
32
order = ["s1","s2","s3","s4","s5","s6","s7","s8","s9","s10"]
33
cfg = {
34
  "s1": ("hero.png","wide","STEPPING  /  DIVINE NINE STEP CULTURE"),
35
  "s2": ("cities.png","wide","CHOOSE YOUR CITY"),
36
  "s3": ("city.png","wide","THE LINE-UP  /  UPCOMING SHOWS"),
37
  "s4": ("event.png","wide","EVERY SHOW, IN DETAIL"),
38
  "s5": ("yard.png","tall","THE YARD  /  THE COMMUNITY"),
39
  "s6": ("profile.png","wide","PROFILES  /  WHO'S WHO"),
40
  "s7": ("route.png","tall","THE ROUTE  /  PLAN THE TRIP"),
41
  "s8": ("mobile.png","phone","RIGHT IN YOUR PHONE"),
42
  "s9": ("hero.png","wide","MORE ON THE WAY"),
43
  "s10": (None,"end",""),
44
}
45
 
46
def dur(i):
47
    s = order[i]
48
    if i < len(order)-1:
49
        return round(ss[order[i+1]] - ss[s], 3)
50
    return round(total - ss[s] + TAIL, 3)
51
 
52
def esc(t):
53
    return t.replace(":", "\\:").replace("'", "’").replace(",", "\\,")
54
 
55
def caption(text):
56
    if not text:
57
        return ""
58
    t = esc(text)
59
    return (f",drawtext=fontfile={MONO}:text='{t}':fontcolor={GOLD}:fontsize=30"
60
            f":x=88:y=h-96:box=1:boxcolor={STAGE}@0.55:boxborderw=22")
61
 
62
def run(cmd):
63
    r = subprocess.run(cmd, shell=True, capture_output=True, text=True)
64
    if r.returncode != 0:
65
        print("FFMPEG ERR:\n", r.stderr[-1500:])
66
        raise SystemExit(1)
67
 
68
clip_files = []
69
for i, s in enumerate(order):
70
    img, kind, cap = cfg[s]
71
    d = dur(i)
72
    out = f"{CLIPS}/{s}.mp4"
73
    frames = int(round(d*FPS))
74
    fin = "fade=t=in:st=0:d=0.4,fade=t=out:st=%.3f:d=0.4" % max(d-0.4, 0)
75
    if kind == "wide":
76
        z = f"scale=2400:-1,zoompan=z='min(zoom+0.00035,1.06)':d={frames}:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s={W}x{H}:fps={FPS}"
77
        vf = f"{z},{fin}{caption(cap)}"
78
        run(f"ffmpeg -y -loop 1 -t {d} -i {shlex.quote(SHOTS+'/'+img)} -vf \"{vf}\" -r {FPS} -c:v libx264 -pix_fmt yuv420p -an {out}")
79
    elif kind == "tall":
80
        vf = f"scale={W}:-1,crop={W}:{H}:0:'(ih-{H})*t/{d}',{fin}{caption(cap)}"
81
        run(f"ffmpeg -y -loop 1 -t {d} -i {shlex.quote(SHOTS+'/'+img)} -vf \"{vf}\" -r {FPS} -c:v libx264 -pix_fmt yuv420p -an {out}")
82
    elif kind == "phone":
83
        fc = (f"color={STAGE}:s={W}x{H}:d={d}:r={FPS}[bg];"
84
              f"[0:v]scale=-1:1000[ph];"
85
              f"[bg][ph]overlay=(W-w)/2:(H-h)/2,{fin}{caption(cap)}")
86
        run(f"ffmpeg -y -loop 1 -t {d} -i {shlex.quote(SHOTS+'/'+img)} -filter_complex \"{fc}\" -r {FPS} -c:v libx264 -pix_fmt yuv420p -an {out}")
87
    else:
88
        vf = (f"drawtext=fontfile={SANS}:text='STEPPING':fontcolor={BONE}:fontsize=120:x=(w-text_w)/2:y=(h-text_h)/2-40,"
89
              f"drawtext=fontfile={MONO}:text='BUILT FOR THE CULTURE':fontcolor={GOLD}:fontsize=30:x=(w-text_w)/2:y=(h/2)+70,"
90
              f"fade=t=in:st=0:d=0.5")
91
        run(f"ffmpeg -y -f lavfi -i color={STAGE}:s={W}x{H}:d={d}:r={FPS} -vf \"{vf}\" -c:v libx264 -pix_fmt yuv420p -an {out}")
92
    clip_files.append(out)
93
    print(f"  {s}  {d}s  -> {out}")
94
 
95
with open(f"{CLIPS}/list.txt","w") as f:
96
    for c in clip_files:
97
        f.write(f"file '{c}'\n")
98
run(f"ffmpeg -y -f concat -safe 0 -i {CLIPS}/list.txt -c:v libx264 -pix_fmt yuv420p -r {FPS} {ROOT}/build/video_silent.mp4")
99
 
100
vlen = round(total + TAIL, 3)
101
fadest = round(total - 0.3, 3)
102
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];"
103
      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];"
104
      f"[m][key]sidechaincompress=threshold=0.035:ratio=9:attack=15:release=350:makeup=1[md];"
105
      f"[vo][md]amix=inputs=2:duration=longest:normalize=0,alimiter=limit=0.97[out]")
106
run(f"ffmpeg -y -i {ROOT}/voiceover/stepping_vo.wav -i {ROOT}/assets/music_inspired.mp3 "
107
    f"-filter_complex \"{af}\" -map \"[out]\" -ar 44100 -ac 2 -c:a aac -b:a 192k {ROOT}/build/audio_final.m4a")
108
 
109
run(f"ffmpeg -y -i {ROOT}/build/video_silent.mp4 -i {ROOT}/build/audio_final.m4a "
110
    f"-map 0:v -map 1:a -c:v copy -c:a copy -shortest {ROOT}/build/stepping_walkthrough.mp4")
111
print("DONE -> build/stepping_walkthrough.mp4  (", vlen, "s )")