SPB Git

spb/admin-ka

Public
12.3 KB · 251 lines python
1#!/usr/bin/env python32"""Rendu d'images sociales KA haute qualité via Chrome headless (HTML/CSS).34Lit un JSON « creative » sur stdin et produit une image 1080x1350 spectaculaire.5Plusieurs « genres » de mise en page, dégradés par site, photos produit réelles.67Genres : hero (nombre géant), ranking (palmarès), spotlight (produit vedette photo),8         deal (aubaine prix), pulse (activité live).9Sortie : chemin du PNG sur stdout.10"""11import json, sys, os, base64, datetime, urllib.request, ssl, hashlib, subprocess, glob1213CTX = ssl.create_default_context(); CTX.check_hostname = False; CTX.verify_mode = ssl.CERT_NONE1415def find_chrome():16    cands = glob.glob(os.path.expanduser(17        "~/Library/Caches/ms-playwright/chromium_headless_shell-*/chrome-headless-shell-mac-arm64/chrome-headless-shell"))18    if cands: return sorted(cands)[-1]19    for p in ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"]:20        if os.path.exists(p): return p21    raise SystemExit("chrome-headless-shell introuvable")2223MOIS = ["janvier","février","mars","avril","mai","juin","juillet","août",24        "septembre","octobre","novembre","décembre"]2526# palettes de dégradé par site (2 arrêts sombres tintés + couleur d'accent vive)27PALETTES = {28    "lou-ka":   {"a":"#ff6a00","b":"#ff9d4d","g1":"#2a1403","g2":"#0d0b09"},29    "immo-ka":  {"a":"#e23744","b":"#ff6b76","g1":"#2a0c10","g2":"#0d0a0b"},30    "food-ka":  {"a":"#1f9d55","b":"#5fd08a","g1":"#07230f","g2":"#0a0d0b"},31    "fabri-ka": {"a":"#c4532e","b":"#e8895f","g1":"#241009","g2":"#0d0a09"},32    "resto-ka": {"a":"#f08c00","b":"#ffb84d","g1":"#241804","g2":"#0d0b08"},33    "auto-ka":  {"a":"#ff5a2a","b":"#ff8f6b","g1":"#26100a","g2":"#0d0a09"},34    "vrai-prix":{"a":"#d9f26b","b":"#b8e04a","g1":"#16230a","g2":"#0b0d08"},35    "trouve-ka":{"a":"#d9f26b","b":"#8fd3ff","g1":"#0a1a20","g2":"#080b0d"},36    "sorti-ka": {"a":"#d9f26b","b":"#ff8fd0","g1":"#210a1c","g2":"#0c090c"},37    "crea-ka":  {"a":"#d9f26b","b":"#c08bff","g1":"#160a24","g2":"#0a090d"},38    "job-ka":   {"a":"#d9f26b","b":"#7ab8ff","g1":"#0a1522","g2":"#080b0d"},39}4041def pal(site): return PALETTES.get(site, PALETTES["vrai-prix"])4243def data_uri(url):44    try:45        req = urllib.request.Request(url, headers={"User-Agent":"Mozilla/5.0 (Macintosh)"})46        with urllib.request.urlopen(req, timeout=15, context=CTX) as r:47            raw = r.read()48        mime = "image/jpeg"49        if url.lower().endswith(".png"): mime="image/png"50        elif url.lower().endswith(".webp"): mime="image/webp"51        return f"data:{mime};base64," + base64.b64encode(raw).decode()52    except Exception:53        return None5455def esc(s): return (str(s).replace("&","&amp;").replace("<","&lt;").replace(">","&gt;"))5657def today_str():58    t = datetime.date.today()59    return f"{t.day} {MOIS[t.month-1]} {t.year}"6061FONT = "'Avenir Next','SF Pro Display','Helvetica Neue',Helvetica,Arial,sans-serif"6263def base_css(p):64    return f"""65    *{{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased;}}66    html,body{{width:1080px;height:1350px;overflow:hidden;font-family:{FONT};color:#fff;}}67    .stage{{position:relative;width:1080px;height:1350px;68      background:69        radial-gradient(1200px 700px at 82% -6%, {p['a']}44 0%, transparent 55%),70        radial-gradient(900px 900px at -12% 108%, {p['b']}33 0%, transparent 55%),71        linear-gradient(160deg, {p['g1']} 0%, {p['g2']} 100%);}}72    .grain{{position:absolute;inset:0;opacity:.045;mix-blend-mode:overlay;73      background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");}}74    .pad{{position:absolute;inset:0;padding:70px 72px 250px;display:flex;flex-direction:column;}}75    .eyebrow{{display:flex;align-items:center;gap:14px;font-weight:800;letter-spacing:.16em;76      font-size:22px;text-transform:uppercase;color:{p['a']};}}77    .eyebrow .dot{{width:40px;height:5px;border-radius:3px;background:{p['a']};}}78    .kabadge{{position:absolute;top:60px;right:72px;background:{p['a']};color:#0d0d0d;79      font-weight:900;font-size:40px;padding:8px 20px;border-radius:16px;transform:rotate(-4deg);80      box-shadow:0 10px 30px {p['a']}55;}}81    .site{{font-size:88px;font-weight:900;letter-spacing:-.02em;margin-top:26px;}}82    .tag{{font-size:31px;font-weight:600;color:#ffffffb0;margin-top:6px;}}83    .foot{{position:absolute;left:72px;right:72px;bottom:66px;display:flex;justify-content:space-between;84      align-items:center;font-weight:800;}}85    .foot .url{{font-size:34px;color:{p['a']};letter-spacing:.01em;}}86    .foot .brand{{font-size:26px;color:#ffffff88;letter-spacing:.12em;}}87    .sig{{position:absolute;left:72px;bottom:150px;font-size:20px;color:#ffffff66;font-weight:600;88      display:flex;align-items:center;gap:8px;}}89    .glass{{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);90      border-radius:22px;backdrop-filter:blur(8px);}}91    """9293def hdr(ins, p):94    return f"""95    <div class="eyebrow"><span class="dot"></span>{esc(ins['label'].upper())} · STAT DU JOUR · {today_str().upper()}</div>96    <div class="kabadge">KA</div>97    <div class="site">{esc(ins['label'])}</div>98    {f'<div class="tag">{esc(ins.get("tagline",""))}</div>' if ins.get('tagline') else ''}99    """100101def footer(ins, p):102    url = "www." + ins["site"] + ".com"103    return f"""104    <div class="sig">✶ Rédigé et publié par l'Agent KA</div>105    <div class="foot"><div class="url">{url}</div><div class="brand">GROUPE ·KA</div></div>106    """107108def tiles_html(tiles, p):109    cells = ""110    for v, l in tiles:111        cells += f"""<div class="glass tile"><div class="tv">{esc(v)}</div><div class="tl">{esc(l)}</div></div>"""112    return f"""<div class="tiles">{cells}</div>113    <style>114    .tiles{{display:grid;grid-template-columns:repeat({len(tiles)},1fr);gap:18px;margin-top:auto;margin-bottom:0;}}115    .tile{{padding:22px 24px;}}116    .tv{{font-size:46px;font-weight:900;color:{p['a']};letter-spacing:-.01em;}}117    .tl{{font-size:23px;font-weight:600;color:#ffffffa0;margin-top:6px;line-height:1.2;}}118    </style>"""119120def genre_hero(ins, p):121    tiles = ins.get("tiles") or []122    return f"""123    <div class="pad">124      {hdr(ins,p)}125      <div class="hero">126        <div class="num">{esc(ins['headline'])}</div>127        <div class="lab">{esc(ins['headline_label'])}</div>128      </div>129      {tiles_html(tiles,p) if tiles else '<div style="margin-top:auto"></div>'}130      {footer(ins,p)}131    </div>132    <style>133    .hero{{margin-top:70px;}}134    .num{{font-size:230px;font-weight:900;line-height:.9;letter-spacing:-.03em;135      background:linear-gradient(180deg,#fff 0%,{p['a']} 130%);-webkit-background-clip:text;136      -webkit-text-fill-color:transparent;filter:drop-shadow(0 12px 40px {p['a']}44);}}137    .lab{{font-size:44px;font-weight:700;color:#fff;margin-top:20px;max-width:820px;line-height:1.12;}}138    </style>"""139140def genre_ranking(ins, p):141    bars = ins.get("bars") or []142    maxv = max([b[1] for b in bars] or [1])143    rows = ""144    for b in bars:145        label, val, vtxt = b[0], b[1], b[2]146        sub = b[3] if len(b) > 3 else ""147        w = max(8, int(100*val/maxv))148        rows += f"""149        <div class="row">150          <div class="rl"><span class="rlab">{esc(label)}</span><span class="rval">{esc(vtxt)}</span></div>151          <div class="track"><div class="fill" style="width:{w}%"></div></div>152          {f'<div class="rsub">{esc(sub)}</div>' if sub else ''}153        </div>"""154    return f"""155    <div class="pad">156      {hdr(ins,p)}157      <div class="rank-title">{esc(ins['headline_label'])}</div>158      <div class="rows">{rows}</div>159      {footer(ins,p)}160    </div>161    <style>162    .rank-title{{font-size:40px;font-weight:800;color:#fff;margin-top:50px;margin-bottom:8px;}}163    .rows{{margin-top:20px;display:flex;flex-direction:column;gap:30px;}}164    .rl{{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px;}}165    .rlab{{font-size:34px;font-weight:700;}}166    .rval{{font-size:36px;font-weight:900;color:{p['a']};}}167    .track{{height:20px;border-radius:12px;background:rgba(255,255,255,.09);overflow:hidden;}}168    .fill{{height:100%;border-radius:12px;background:linear-gradient(90deg,{p['a']},{p['b']});169      box-shadow:0 4px 18px {p['a']}55;}}170    .rsub{{font-size:22px;color:#ffffff8c;margin-top:8px;font-weight:600;}}171    </style>"""172173def genre_spotlight(ins, p):174    prod = ins.get("product") or {}175    img = prod.get("image_data") or ""176    chips = "".join(f'<span class="chip">{esc(c)}</span>' for c in (prod.get("meta") or []))177    price = prod.get("price","")178    return f"""179    <div class="stage-photo" style="background-image:url('{img}')"></div>180    <div class="scrim"></div>181    <div class="pad">182      <div class="eyebrow"><span class="dot"></span>{esc(ins['label'].upper())} · À DÉCOUVRIR · {today_str().upper()}</div>183      <div class="kabadge">KA</div>184      <div class="spot">185        <div class="ptitle">{esc(prod.get('title',''))}</div>186        <div class="psub">{esc(prod.get('subtitle',''))}</div>187        <div class="chips">{chips}</div>188        {f'<div class="price">{esc(price)}</div>' if price else ''}189        <div class="tagline2">{esc(ins.get('tagline',''))}</div>190      </div>191      {footer(ins,p)}192    </div>193    <style>194    .stage-photo{{position:absolute;top:0;left:0;right:0;height:760px;background-size:cover;background-position:center;}}195    .scrim{{position:absolute;inset:0;background:196      linear-gradient(180deg, rgba(0,0,0,.15) 0%, rgba(0,0,0,0) 34%, {p['g2']}cc 62%, {p['g2']} 82%);}}197    .spot{{margin-top:auto;}}198    .ptitle{{font-size:62px;font-weight:900;line-height:1.02;letter-spacing:-.02em;max-width:900px;199      text-shadow:0 4px 24px #000a;}}200    .psub{{font-size:30px;font-weight:600;color:#ffffffc0;margin-top:12px;}}201    .chips{{display:flex;gap:12px;flex-wrap:wrap;margin-top:22px;}}202    .chip{{background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.2);border-radius:999px;203      padding:10px 20px;font-size:24px;font-weight:700;}}204    .price{{font-size:80px;font-weight:900;color:{p['a']};margin-top:24px;letter-spacing:-.02em;205      filter:drop-shadow(0 8px 24px {p['a']}55);}}206    .tagline2{{font-size:28px;font-weight:600;color:#ffffffa8;margin-top:18px;}}207    </style>"""208209GENRES = {"hero":genre_hero, "ranking":genre_ranking, "spotlight":genre_spotlight,210          "deal":genre_hero, "pulse":genre_hero}211212def build_html(ins):213    p = pal(ins["site"])214    genre = ins.get("genre","hero")215    if genre == "ranking" and not ins.get("bars"): genre = "hero"216    if genre == "spotlight" and not (ins.get("product") or {}).get("image_data"): genre = "hero"217    body = GENRES.get(genre, genre_hero)(ins, p)218    return f"""<!doctype html><html><head><meta charset="utf-8"><style>{base_css(p)}</style></head>219    <body><div class="stage"><div class="grain"></div>{body}</div></body></html>""", genre220221def render(ins, out_dir):222    os.makedirs(out_dir, exist_ok=True)223    # télécharger la photo produit en data-uri (robuste, pas de hotlink)224    prod = ins.get("product")225    if prod and prod.get("image") and not prod.get("image_data"):226        du = data_uri(prod["image"])227        if du: prod["image_data"] = du228    html, genre = build_html(ins)229    key = hashlib.md5((ins["site"]+ins.get("insight_id","")+genre).encode()).hexdigest()[:8]230    html_path = os.path.join(out_dir, f"card-{ins['site']}-{key}.html")231    big = os.path.join(out_dir, f"_big-{key}.png")232    out = os.path.join(out_dir, f"card-{ins['site']}-{ins.get('insight_id','x')}-{genre}.png")233    open(html_path,"w",encoding="utf-8").write(html)234    chrome = find_chrome()235    subprocess.run([chrome,"--headless","--disable-gpu","--hide-scrollbars",236        "--force-device-scale-factor=2","--window-size=1080,1350",237        f"--screenshot={big}","--virtual-time-budget=9000","--run-all-compositor-stages-before-draw",238        f"file://{html_path}"], capture_output=True, timeout=60)239    if not os.path.exists(big):240        raise SystemExit("chrome screenshot a échoué")241    # downscale 2x → 1080x1350 (texte net) via sips242    subprocess.run(["/usr/bin/sips","-z","1350","1080",big,"--out",out],capture_output=True)243    try: os.remove(big); os.remove(html_path)244    except: pass245    return out246247if __name__ == "__main__":248    ins = json.load(sys.stdin)249    out_dir = sys.argv[1] if len(sys.argv)>1 else "/tmp/ka-social-cards"250    print(render(ins, out_dir))251
Files · 1c6b5394eddd35a085e520678959ffbb71307f7e
Menu

Theme