← back to Beverlyhillsvideos
filmgen/store_card.py
102 lines
#!/usr/bin/env python3
"""Render a text-forward LUXE store title card (1080x1920 vertical / IG Reel) — $0, local.
No brand logos or generated b-roll: store name in Didot + editorial tagline on a deep-green
ground with a gold hairline frame, BHV wordmark, and the on-screen no-affiliation disclaimer
baked in. Later animated by store_card.sh (ffmpeg zoompan) + narrated with `say`.
Usage: python3 filmgen/store_card.py "<Store Name>" "<one-line tagline>" out.png
"""
import sys
from PIL import Image, ImageDraw, ImageFont, ImageFilter
W, H = 1080, 1920
GREEN = (18, 40, 31) # deep luxe green ground
GREEN2 = (9, 22, 17) # darker for vignette
CREAM = (246, 243, 236)
GOLD = (184, 147, 90)
MUTED = (176, 170, 158)
FAINT = (120, 128, 118)
SERIF = "/System/Library/Fonts/Supplemental/Didot.ttc"
SANS = "filmgen/fonts/Jost.ttf"
sf = lambda s, i=0: ImageFont.truetype(SERIF, s, index=i)
jf = lambda s: ImageFont.truetype(SANS, s)
def center(d, cx, y, txt, fnt, fill):
w = d.textlength(txt, font=fnt)
d.text((cx - w / 2, y), txt, font=fnt, fill=fill)
def tracked(d, cx, y, txt, fnt, fill, tr):
ws = [d.textlength(c, font=fnt) for c in txt]
tot = sum(ws) + tr * (len(txt) - 1)
x = cx - tot / 2
for c, w in zip(txt, ws):
d.text((x, y), c, font=fnt, fill=fill)
x += w + tr
def wrap(d, txt, fnt, maxw):
words, lines, cur = txt.split(), [], ""
for w in words:
t = (cur + " " + w).strip()
if d.textlength(t, font=fnt) <= maxw:
cur = t
else:
lines.append(cur); cur = w
if cur:
lines.append(cur)
return lines
def render(name, tagline, out):
img = Image.new("RGB", (W, H), GREEN)
# radial vignette toward the edges
vig = Image.new("L", (W, H), 0)
ImageDraw.Draw(vig).ellipse([-W * 0.25, -H * 0.15, W * 1.25, H * 1.15], fill=120)
vig = vig.filter(ImageFilter.GaussianBlur(260))
img = Image.composite(img, Image.new("RGB", (W, H), GREEN2), vig)
d = ImageDraw.Draw(img)
# gold hairline inner frame
m = 54
d.rectangle([m, m, W - m, H - m], outline=GOLD, width=2)
cx = W / 2
# eyebrow
tracked(d, cx, H * 0.30, "RODEO DRIVE · BEVERLY HILLS", jf(30), GOLD, 8)
# store name (Didot, wrap long names)
lines = wrap(d, name, sf(150), W - 2 * m - 120) or [name]
tsize = 150 if len(lines) == 1 and len(name) <= 12 else (120 if len(lines) == 1 else 104)
tf = sf(tsize)
ty = H * 0.40
for ln in lines:
center(d, cx, ty, ln, tf, CREAM)
ty += tsize * 1.02
# gold hairline rule
ry = ty + 30
d.line([(cx - 80, ry), (cx + 80, ry)], fill=GOLD, width=2)
# tagline (muted serif italic-ish via Didot regular, wrapped)
tgf = sf(46)
for i, ln in enumerate(wrap(d, tagline, tgf, W - 2 * m - 160)):
center(d, cx, ry + 44 + i * 60, ln, tgf, MUTED)
# BHV wordmark
center(d, cx, H * 0.82, "BEVERLY HILLS VIDEOS", sf(56), GOLD)
tracked(d, cx, H * 0.82 + 74, "THE INSIDER FILM GUIDE", jf(24), MUTED, 8)
# on-screen no-affiliation disclaimer (compliance guardrail C, baked in)
tracked(d, cx, H - m - 40,
"INDEPENDENT EDITORIAL GUIDE · NOT AFFILIATED WITH THE BRANDS SHOWN",
jf(18), FAINT, 2)
img.save(out)
print("wrote", out)
if __name__ == "__main__":
render(sys.argv[1], sys.argv[2], sys.argv[3])