| 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) |