← back to Consulting Rentv Com

data/audit-video/compose_noavatar.py

67 lines

#!/usr/bin/env python3
# Same as compose.py but WITHOUT the Steve avatar PIP. Keeps chips + title + outro.
import json, subprocess, os
from PIL import Image, ImageDraw, ImageFont
HERE = os.path.dirname(os.path.abspath(__file__)); os.chdir(HERE)
nar = json.load(open("narration.json")); tim = json.load(open("timing.json"))
starts = tim["starts"]; audio_end = tim["audio_end"]; segs = nar["segments"]
ids = [s["id"] for s in segs]
ARIAL = "/System/Library/Fonts/Supplemental/Arial Bold.ttf"
GOLD = (200,161,90); BG = (12,16,22); INK = (238,241,246)
labels = {
 "summary":"EXECUTIVE SUMMARY","swot":"SWOT ANALYSIS","reputation":"REPUTATION & AUTHORITY",
 "empire":"THE EMPIRE & SUBLEASE.COM","competitors":"COMPETITORS","advertising":"WHERE TO ADVERTISE",
 "timing":"TIMING & CADENCE","audit":"WEBSITE AUDIT","teardown":"PAGE-BY-PAGE TEARDOWN",
 "social":"SOCIAL MEDIA AUDIT","suggested":"SUGGESTED NEW WEBSITES","partners":"UNLIKELY PARTNERS",
 "revenue":"UNLIKELY REVENUE","videos":"VIDEO & BRAND FILMS","dealdesk":"LIVE DEAL DESK",
 "roadmap":"90-DAY ROADMAP",
}
os.makedirs("chips", exist_ok=True)
fnum = ImageFont.truetype(ARIAL, 30); ftxt = ImageFont.truetype(ARIAL, 30)
padx, pady, gap = 26, 16, 16
chip_paths = []
tmp = ImageDraw.Draw(Image.new("RGBA",(10,10)))
for i, id in enumerate(ids):
    num = f"{i+1:02d}"; title = labels[id]
    wn = tmp.textlength(num, font=fnum); wt = tmp.textlength(title, font=ftxt)
    W = int(padx + wn + gap + wt + padx); H = 62
    im = Image.new("RGBA", (W, H), (0,0,0,0)); d = ImageDraw.Draw(im)
    d.rounded_rectangle([0,0,W-1,H-1], radius=8, fill=BG+(210,), outline=GOLD+(120,), width=1)
    ty = (H-30)//2 - 4
    d.text((padx, ty), num, font=fnum, fill=GOLD+(255,))
    d.text((padx+wn+gap, ty), title, font=ftxt, fill=INK+(255,))
    p = f"chips/{i:02d}.png"; im.save(p); chip_paths.append(p)
print("chips:", len(chip_paths))

# main body: NO avatar. input 1 kept only so 1:a preserves original length/parity.
inputs = ["-i","rec/portal-walk.webm","-i","steve-narration.mp4"]
for p in chip_paths: inputs += ["-i", p]
fc = ["[0:v]scale=1920:1080,setsar=1,fps=25[bg]"]
prev = "bg"
for i, id in enumerate(ids):
    s = 0.0 if i == 0 else starts[id]
    e = starts[ids[i+1]] if i+1 < len(ids) else audio_end
    inp = 2 + i
    out = f"o{i}"
    fc.append(f"[{prev}][{inp}:v]overlay=48:H-h-44:enable='between(t,{s:.2f},{e:.2f})'[{out}]")
    prev = out
fc_str = ";".join(fc)
cmd = ["ffmpeg","-y",*inputs,"-filter_complex",fc_str,
       "-map",f"[{prev}]","-map","1:a",
       "-c:v","libx264","-pix_fmt","yuv420p","-crf","19","-preset","medium","-r","25",
       "-c:a","aac","-b:a","160k","-ar","48000","-ac","2","-shortest","main_clean.mp4"]
print("== main body (no avatar) =="); subprocess.run(cmd, check=True)

def card(png, out, dur):
    subprocess.run(["ffmpeg","-y","-loop","1","-i",png,"-f","lavfi","-i","anullsrc=r=48000:cl=stereo",
        "-t",str(dur),"-vf",f"scale=1920:1080,fps=25,fade=in:0:15,fade=out:st={dur-0.6:.2f}:d=0.6",
        "-c:v","libx264","-pix_fmt","yuv420p","-r","25","-c:a","aac","-b:a","160k","-shortest",out], check=True)
print("== title / outro =="); card("title.png","title.mp4",4.0); card("outro.png","outro.mp4",4.0)

open("concat_clean.txt","w").write("file 'title.mp4'\nfile 'main_clean.mp4'\nfile 'outro.mp4'\n")
r = subprocess.run(["ffmpeg","-y","-f","concat","-safe","0","-i","concat_clean.txt","-c","copy","clean_final.mp4"])
if r.returncode != 0:
    subprocess.run(["ffmpeg","-y","-f","concat","-safe","0","-i","concat_clean.txt",
        "-c:v","libx264","-pix_fmt","yuv420p","-crf","19","-r","25","-c:a","aac","-b:a","160k","clean_final.mp4"], check=True)
print("DONE -> clean_final.mp4")