JavaScript 65.5%
Python 17.8%
CSS 13%
HTML 3.7%
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.56Genres : hero (nombre géant), ranking (palmarès), spotlight (produit vedette photo),7 list (sélection d'items photo), grid (mosaïque 2x2 de photos),8 donut (part en % en anneau SVG), quote (« le saviez-vous » typographique).9Thèmes (rotation via `variant` ou hash) : nuit (sombre), creme (clair néo-brutaliste Ka),10 cadre (sombre encadré). Les genres photo (spotlight/list/grid) restent en nuit.11Sortie : chemin du PNG sur stdout.12"""13import json, sys, os, re, base64, datetime, urllib.request, ssl, hashlib, subprocess, glob1415CTX = ssl.create_default_context(); CTX.check_hostname = False; CTX.verify_mode = ssl.CERT_NONE1617def find_chrome():18 cands = glob.glob(os.path.expanduser(19 "~/Library/Caches/ms-playwright/chromium_headless_shell-*/chrome-headless-shell-mac-arm64/chrome-headless-shell"))20 if cands: return sorted(cands)[-1]21 for p in ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"]:22 if os.path.exists(p): return p23 raise SystemExit("chrome-headless-shell introuvable")2425MOIS = ["janvier","février","mars","avril","mai","juin","juillet","août",26 "septembre","octobre","novembre","décembre"]2728# palettes par site : accent vif (a), accent secondaire (b), dégradé sombre (g1,g2),29# accent assombri lisible sur fond clair (ad)30PALETTES = {31 "lou-ka": {"a":"#ff6a00","b":"#ff9d4d","g1":"#2a1403","g2":"#0d0b09","ad":"#c24f00"},32 "immo-ka": {"a":"#e23744","b":"#ff6b76","g1":"#2a0c10","g2":"#0d0a0b","ad":"#b02532"},33 "food-ka": {"a":"#1f9d55","b":"#5fd08a","g1":"#07230f","g2":"#0a0d0b","ad":"#157a41"},34 "fabri-ka": {"a":"#c4532e","b":"#e8895f","g1":"#241009","g2":"#0d0a09","ad":"#9c3f21"},35 "auto-ka": {"a":"#ff5a2a","b":"#ff8f6b","g1":"#26100a","g2":"#0d0a09","ad":"#cc3f14"},36 "vrai-prix":{"a":"#d9f26b","b":"#b8e04a","g1":"#16230a","g2":"#0b0d08","ad":"#6b7f1d"},37 "sorti-ka": {"a":"#d9f26b","b":"#ff8fd0","g1":"#210a1c","g2":"#0c090c","ad":"#6b7f1d"},38 "job-ka": {"a":"#d9f26b","b":"#7ab8ff","g1":"#0a1522","g2":"#080b0d","ad":"#6b7f1d"},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("&","&").replace("<","<").replace(">",">"))5657def today_str():58 t = datetime.date.today()59 return f"{t.day} {MOIS[t.month-1]} {t.year}"6061def parse_pct(h):62 m = re.match(r'^\+?(\d+(?:[.,]\d+)?)\s*%$', str(h or "").strip())63 return float(m.group(1).replace(",", ".")) if m else None6465FONT = "'Avenir Next','SF Pro Display','Helvetica Neue',Helvetica,Arial,sans-serif"66THEMES = ["nuit","creme","cadre"]6768def theme_of(ins):69 t = ins.get("theme")70 if t in THEMES: return t71 v = ins.get("variant")72 if v is None:73 v = int(hashlib.md5((ins["site"]+str(ins.get("insight_id",""))).encode()).hexdigest(), 16)74 return THEMES[int(v) % len(THEMES)]7576def tokens(p, theme):77 if theme == "creme":78 return {79 "fg":"#141814","muted":"#141814b3","muted2":"#14181487","sig":"#14181466",80 "acc":p["ad"],81 "glass":"background:#ffffff;border:3px solid #141814;box-shadow:9px 9px 0 #141814;",82 "badge_extra":"border:3px solid #141814;box-shadow:7px 7px 0 #141814;",83 "numgrad":"background:linear-gradient(180deg,#141814 0%,"+p["ad"]+" 150%);",84 "numglow":"filter:drop-shadow(0 8px 22px "+p["a"]+"40);",85 "track":"#1418141f",86 "grain_op":".05",87 "stage":("background:radial-gradient(1100px 680px at 85% -8%, "+p["a"]+"30 0%, transparent 55%),"88 "radial-gradient(900px 900px at -10% 112%, "+p["b"]+"26 0%, transparent 55%),"89 "linear-gradient(168deg,#f7f6ef 0%,#edece4 100%);"),90 "frame":"",91 }92 t = {93 "fg":"#ffffff","muted":"#ffffffb0","muted2":"#ffffff8c","sig":"#ffffff66",94 "acc":p["a"],95 "glass":"background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);backdrop-filter:blur(8px);",96 "badge_extra":"box-shadow:0 10px 30px "+p["a"]+"55;",97 "numgrad":"background:linear-gradient(180deg,#fff 0%,"+p["a"]+" 130%);",98 "numglow":"filter:drop-shadow(0 12px 40px "+p["a"]+"44);",99 "track":"rgba(255,255,255,.09)",100 "grain_op":".045",101 "stage":("background:radial-gradient(1200px 700px at 82% -6%, "+p["a"]+"44 0%, transparent 55%),"102 "radial-gradient(900px 900px at -12% 108%, "+p["b"]+"33 0%, transparent 55%),"103 "linear-gradient(160deg, "+p["g1"]+" 0%, "+p["g2"]+" 100%);"),104 "frame":"",105 }106 if theme == "cadre":107 t["stage"] = ("background:radial-gradient(1100px 800px at 50% -12%, "+p["a"]+"3a 0%, transparent 60%),"108 "radial-gradient(800px 800px at 100% 110%, "+p["b"]+"2a 0%, transparent 55%),"109 "linear-gradient(200deg, "+p["g1"]+" 0%, "+p["g2"]+" 100%);")110 t["frame"] = (".stage::before{content:'';position:absolute;inset:26px;border:2px solid "+p["a"]+"66;"111 "border-radius:36px;pointer-events:none;z-index:5}"112 ".stage::after{content:'';position:absolute;inset:38px;border:1px solid #ffffff1c;"113 "border-radius:28px;pointer-events:none;z-index:5}")114 return t115116def base_css(p, T):117 css = """118 *{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased;}119 html,body{width:1080px;height:1350px;overflow:hidden;font-family:__FONT__;color:__FG__;}120 .stage{position:relative;width:1080px;height:1350px;__STAGE__}121 __FRAME__122 .grain{position:absolute;inset:0;opacity:__GOP__;mix-blend-mode:overlay;123 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>");}124 .pad{position:absolute;inset:0;padding:70px 72px 250px;display:flex;flex-direction:column;z-index:2;}125 .eyebrow{display:flex;align-items:center;gap:14px;font-weight:800;letter-spacing:.16em;126 font-size:22px;text-transform:uppercase;color:__ACC__;}127 .eyebrow .dot{width:40px;height:5px;border-radius:3px;background:__ACC__;}128 .kabadge{position:absolute;top:60px;right:72px;background:__A__;color:#0d0d0d;129 font-weight:900;font-size:40px;padding:8px 20px;border-radius:16px;transform:rotate(-4deg);z-index:6;__BADGE__}130 .site{font-size:88px;font-weight:900;letter-spacing:-.02em;margin-top:26px;}131 .tag{font-size:31px;font-weight:600;color:__MUTED__;margin-top:6px;}132 .foot{position:absolute;left:72px;right:72px;bottom:66px;display:flex;justify-content:space-between;133 align-items:center;font-weight:800;z-index:6;}134 .foot .url{font-size:34px;color:__ACC__;letter-spacing:.01em;}135 .foot .brand{font-size:26px;color:__MUTED2__;letter-spacing:.12em;}136 .sig{position:absolute;left:72px;bottom:150px;font-size:20px;color:__SIG__;font-weight:600;137 display:flex;align-items:center;gap:8px;z-index:6;}138 .glass{border-radius:22px;__GLASS__}139 """140 return (css.replace("__FONT__",FONT).replace("__FG__",T["fg"]).replace("__STAGE__",T["stage"])141 .replace("__FRAME__",T["frame"]).replace("__GOP__",T["grain_op"])142 .replace("__MUTED2__",T["muted2"]).replace("__MUTED__",T["muted"]).replace("__SIG__",T["sig"])143 .replace("__GLASS__",T["glass"]).replace("__BADGE__",T["badge_extra"])144 .replace("__ACC__",T["acc"]).replace("__A__",p["a"]))145146def hdr(ins, p, kicker="STAT DU JOUR"):147 return ('<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · '148 + esc(kicker)+' · '+today_str().upper()+'</div>'149 + '<div class="kabadge">KA</div>'150 + '<div class="site">'+esc(ins["label"])+'</div>'151 + (('<div class="tag">'+esc(ins.get("tagline",""))+'</div>') if ins.get("tagline") else ''))152153def footer(ins, p):154 url = "www." + ins["site"] + ".com"155 return ('<div class="sig">✶ Rédigé et publié par l\'Agent KA</div>'156 + '<div class="foot"><div class="url">'+url+'</div><div class="brand">GROUPE ·KA</div></div>')157158def tiles_html(tiles, p, T):159 cells = ""160 for v, l in tiles:161 cells += '<div class="glass tile"><div class="tv" data-fit>'+esc(v)+'</div><div class="tl">'+esc(l)+'</div></div>'162 css = """163 .tiles{display:grid;grid-template-columns:repeat(__N__,1fr);gap:18px;margin-top:auto;margin-bottom:0;}164 .tile{padding:22px 24px;}165 .tv{font-size:46px;font-weight:900;color:__ACC__;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;}166 .tl{font-size:23px;font-weight:600;color:__MUTED__;margin-top:6px;line-height:1.2;}167 """.replace("__N__",str(len(tiles))).replace("__ACC__",T["acc"]).replace("__MUTED__",T["muted"])168 return '<div class="tiles">'+cells+'</div><style>'+css+'</style>'169170def genre_hero(ins, p, T):171 tiles = ins.get("tiles") or []172 hl = str(ins["headline"]); L = len(hl)173 numsize = 150 if L >= 10 else (190 if L >= 8 else 230)174 css = """175 .hero{margin-top:70px;}176 .num{font-size:__NS__px;font-weight:900;line-height:.9;letter-spacing:-.03em;display:block;177 max-width:100%;white-space:nowrap;overflow:hidden;178 __NUMGRAD__-webkit-background-clip:text;-webkit-text-fill-color:transparent;__NUMGLOW__}179 .lab{font-size:44px;font-weight:700;margin-top:20px;max-width:820px;line-height:1.12;}180 """.replace("__NS__",str(numsize)).replace("__NUMGRAD__",T["numgrad"]).replace("__NUMGLOW__",T["numglow"])181 return ('<div class="pad">'+hdr(ins,p)182 + '<div class="hero"><div class="num" data-fit>'+esc(hl)+'</div>'183 + '<div class="lab">'+esc(ins["headline_label"])+'</div></div>'184 + (tiles_html(tiles,p,T) if tiles else '<div style="margin-top:auto"></div>')185 + footer(ins,p)+'</div><style>'+css+'</style>')186187def genre_ranking(ins, p, T):188 bars = ins.get("bars") or []189 # avec sous-textes, 6 rangées débordent sur le pied de page -> max 5190 has_sub = any(len(b) > 3 and b[3] for b in bars)191 bars = bars[:5] if has_sub else bars[:6]192 maxv = max([b[1] for b in bars] or [1])193 rows = ""194 for b in bars:195 label, val, vtxt = b[0], b[1], b[2]196 sub = b[3] if len(b) > 3 else ""197 w = max(8, int(100*val/maxv))198 rows += ('<div class="row"><div class="rl"><span class="rlab">'+esc(label)+'</span>'199 + '<span class="rval">'+esc(vtxt)+'</span></div>'200 + '<div class="track"><div class="fill" style="width:'+str(w)+'%"></div></div>'201 + (('<div class="rsub">'+esc(sub)+'</div>') if sub else '')+'</div>')202 css = """203 .rank-title{font-size:40px;font-weight:800;margin-top:50px;margin-bottom:8px;}204 .rows{margin-top:20px;display:flex;flex-direction:column;gap:30px;}205 .rl{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px;}206 .rlab{font-size:34px;font-weight:700;}207 .rval{font-size:36px;font-weight:900;color:__ACC__;}208 .track{height:20px;border-radius:12px;background:__TRACK__;overflow:hidden;}209 .fill{height:100%;border-radius:12px;background:linear-gradient(90deg,__ACC__,__B__);210 box-shadow:0 4px 18px __A__55;}211 .rsub{font-size:22px;color:__MUTED2__;margin-top:8px;font-weight:600;}212 """.replace("__ACC__",T["acc"]).replace("__TRACK__",T["track"]).replace("__B__",p["b"]) \213 .replace("__A__",p["a"]).replace("__MUTED2__",T["muted2"])214 return ('<div class="pad">'+hdr(ins,p)215 + '<div class="rank-title">'+esc(ins["headline_label"])+'</div>'216 + '<div class="rows">'+rows+'</div>'+footer(ins,p)+'</div><style>'+css+'</style>')217218def genre_spotlight(ins, p, T):219 prod = ins.get("product") or {}220 img = prod.get("image_data") or ""221 chips = "".join('<span class="chip">'+esc(c)+'</span>' for c in (prod.get("meta") or []))222 price = prod.get("price","")223 css = """224 .stage-photo{position:absolute;top:0;left:0;right:0;height:760px;background-size:cover;background-position:center;}225 .scrim{position:absolute;inset:0;background:226 linear-gradient(180deg, rgba(0,0,0,.15) 0%, rgba(0,0,0,0) 34%, __G2__cc 62%, __G2__ 82%);}227 .spot{margin-top:auto;}228 .ptitle{font-size:62px;font-weight:900;line-height:1.02;letter-spacing:-.02em;max-width:900px;229 text-shadow:0 4px 24px #000a;}230 .psub{font-size:30px;font-weight:600;color:#ffffffc0;margin-top:12px;}231 .chips{display:flex;gap:12px;flex-wrap:wrap;margin-top:22px;}232 .chip{background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.2);border-radius:999px;233 padding:10px 20px;font-size:24px;font-weight:700;}234 .price{font-size:80px;font-weight:900;color:__A__;margin-top:24px;letter-spacing:-.02em;235 filter:drop-shadow(0 8px 24px __A__55);}236 .tagline2{font-size:28px;font-weight:600;color:#ffffffa8;margin-top:18px;}237 """.replace("__G2__",p["g2"]).replace("__A__",p["a"])238 return ('<div class="stage-photo" style="background-image:url(\''+img+'\')"></div><div class="scrim"></div>'239 + '<div class="pad">'240 + '<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · À DÉCOUVRIR · '+today_str().upper()+'</div>'241 + '<div class="kabadge">KA</div>'242 + '<div class="spot"><div class="ptitle">'+esc(prod.get("title",""))+'</div>'243 + '<div class="psub">'+esc(prod.get("subtitle",""))+'</div>'244 + '<div class="chips">'+chips+'</div>'245 + (('<div class="price">'+esc(price)+'</div>') if price else '')246 + '<div class="tagline2">'+esc(ins.get("tagline",""))+'</div></div>'247 + footer(ins,p)+'</div><style>'+css+'</style>')248249def genre_list(ins, p, T):250 items = ins.get("list") or []251 rows = ""252 for it in items[:5]:253 img = it.get("image_data") or ""254 thumb = '<div class="lthumb" style="background-image:url(\'' + img + '\')"></div>' if img else '<div class="lthumb"></div>'255 price = esc(it.get("price", "") or "")256 pr = ('<div class="lp">' + price + '</div>') if price else ''257 rows += ('<div class="lrow">' + thumb258 + '<div class="lmid"><div class="lt">' + esc(it.get("title", "")) + '</div>'259 + '<div class="ls">' + esc(it.get("subtitle", "") or "") + '</div></div>'260 + pr + '</div>')261 heading = esc(ins.get("list_title", "") or ins.get("headline_label", "") or "À la une")262 css = """263 .ltitle{font-size:40px;font-weight:800;color:#fff;margin-top:8px;margin-bottom:18px;max-width:920px;line-height:1.1}264 .lwrap{display:flex;flex-direction:column;gap:15px}265 .lrow{display:flex;align-items:center;gap:20px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);border-radius:20px;padding:15px 18px}266 .lthumb{width:118px;height:118px;flex:0 0 118px;border-radius:14px;background-color:#222;background-size:cover;background-position:center;box-shadow:inset 0 0 0 1px rgba(255,255,255,.08)}267 .lmid{flex:1;min-width:0}268 .lt{font-size:32px;font-weight:800;line-height:1.12;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}269 .ls{font-size:25px;font-weight:600;color:#ffffffa0;margin-top:6px}270 .lp{font-size:36px;font-weight:900;color:__A__;white-space:nowrap;text-align:right}271 """.replace("__A__", p["a"])272 return ('<div class="pad">'273 + '<div class="eyebrow"><span class="dot"></span>' + esc(ins["label"].upper()) + ' · ' + today_str().upper() + '</div>'274 + '<div class="kabadge">KA</div>'275 + '<div class="site" style="font-size:68px;margin-top:14px">' + esc(ins["label"]) + '</div>'276 + '<div class="ltitle">' + heading + '</div>'277 + '<div class="lwrap">' + rows + '</div>'278 + footer(ins, p)279 + '</div><style>' + css + '</style>')280281def genre_grid(ins, p, T):282 items = [it for it in (ins.get("list") or []) if it.get("image_data")][:4]283 cells = ""284 for it in items:285 cells += ('<div class="cell"><div class="cph" style="background-image:url(\''+it["image_data"]+'\')"></div>'286 + '<div class="cscrim"></div><div class="cbot"><div class="ct">'+esc(it.get("title",""))+'</div>'287 + (('<div class="cp">'+esc(it["price"])+'</div>') if it.get("price") else '')288 + '</div></div>')289 heading = esc(ins.get("list_title","") or ins.get("headline_label","") or "À la une")290 css = """291 .gtitle{font-size:42px;font-weight:800;color:#fff;margin-top:10px;margin-bottom:22px;max-width:920px;line-height:1.1}292 .gwrap{display:grid;grid-template-columns:1fr 1fr;gap:18px}293 .cell{position:relative;height:350px;border-radius:22px;overflow:hidden;border:1px solid rgba(255,255,255,.16)}294 .cph{position:absolute;inset:0;background-size:cover;background-position:center}295 .cscrim{position:absolute;inset:0;background:linear-gradient(180deg,transparent 42%,#000d 100%)}296 .cbot{position:absolute;left:18px;right:18px;bottom:14px}297 .ct{font-size:26px;font-weight:800;line-height:1.15;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;text-shadow:0 2px 10px #000}298 .cp{font-size:28px;font-weight:900;color:__A__;margin-top:4px;text-shadow:0 2px 10px #000}299 """.replace("__A__", p["a"])300 return ('<div class="pad">'301 + '<div class="eyebrow"><span class="dot"></span>' + esc(ins["label"].upper()) + ' · EN IMAGES · ' + today_str().upper() + '</div>'302 + '<div class="kabadge">KA</div>'303 + '<div class="site" style="font-size:68px;margin-top:14px">' + esc(ins["label"]) + '</div>'304 + '<div class="gtitle">' + heading + '</div>'305 + '<div class="gwrap">' + cells + '</div>'306 + footer(ins, p)307 + '</div><style>' + css + '</style>')308309NUMRE = re.compile(r'(\d[\d\u00a0\u202f ,\.]*\d|\d)(\s?(%|\$|G\$|M\$|milliards?|millions?))?')310311def genre_quote(ins, p, T):312 fact = str(ins.get("fact") or (str(ins["headline"])+" "+ins.get("headline_label","")))313 txt = NUMRE.sub(lambda m: '<em>'+m.group(0)+'</em>', esc(fact))314 L = len(fact)315 fs = 64 if L < 170 else (56 if L < 240 else 48)316 tiles = (ins.get("tiles") or [])[:3]317 css = """318 .qmark{font-size:230px;line-height:.55;color:__ACC__;font-weight:900;margin-top:70px;height:118px;319 font-family:Georgia,serif;opacity:.9}320 .qtext{font-size:__FS__px;font-weight:800;line-height:1.3;letter-spacing:-.01em;max-width:930px;}321 .qtext em{font-style:normal;color:__ACC__;}322 """.replace("__ACC__",T["acc"]).replace("__FS__",str(fs))323 return ('<div class="pad">'+hdr(ins,p,"LE SAVIEZ-VOUS ?")324 + '<div class="qmark">“</div>'325 + '<div class="qtext">'+txt+'</div>'326 + (tiles_html(tiles,p,T) if tiles else '<div style="margin-top:auto"></div>')327 + footer(ins,p)+'</div><style>'+css+'</style>')328329def genre_donut(ins, p, T):330 pct = parse_pct(ins["headline"]) or 0.0331 C = 2*3.14159*210332 filled = C*min(100.0, pct)/100.0333 svg = ('<svg width="540" height="540" viewBox="0 0 540 540">'334 + '<circle cx="270" cy="270" r="210" fill="none" stroke="'+T["track"]+'" stroke-width="60"/>'335 + '<circle cx="270" cy="270" r="210" fill="none" stroke="url(#gd)" stroke-width="60" stroke-linecap="round" '336 + 'stroke-dasharray="'+str(round(filled,1))+' '+str(round(C,1))+'" transform="rotate(-90 270 270)"/>'337 + '<defs><linearGradient id="gd" x1="0" y1="0" x2="1" y2="1">'338 + '<stop offset="0" stop-color="'+T["acc"]+'"/><stop offset="1" stop-color="'+p["b"]+'"/></linearGradient></defs></svg>')339 tiles = (ins.get("tiles") or [])[:3]340 css = """341 .drow{display:flex;align-items:center;gap:44px;margin-top:56px}342 .dwrap{position:relative;width:540px;height:540px;flex:0 0 540px}343 .dperc{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;344 font-size:104px;font-weight:900;color:__ACC__;letter-spacing:-.02em}345 .dlab{flex:1;font-size:41px;font-weight:700;line-height:1.18}346 """.replace("__ACC__",T["acc"])347 return ('<div class="pad">'+hdr(ins,p)348 + '<div class="drow"><div class="dwrap">'+svg+'<div class="dperc">'+esc(ins["headline"])+'</div></div>'349 + '<div class="dlab">'+esc(ins["headline_label"])+'</div></div>'350 + (tiles_html(tiles,p,T) if tiles else '<div style="margin-top:auto"></div>')351 + footer(ins,p)+'</div><style>'+css+'</style>')352353GENRES = {"hero":genre_hero, "ranking":genre_ranking, "spotlight":genre_spotlight,354 "list":genre_list, "grid":genre_grid, "quote":genre_quote, "donut":genre_donut,355 "deal":genre_hero, "pulse":genre_hero}356357FIT_JS = """<script>358document.querySelectorAll('[data-fit]').forEach(function(el){359 var s=parseFloat(getComputedStyle(el).fontSize);360 while(el.scrollWidth>el.clientWidth && s>24){ s-=4; el.style.fontSize=s+'px'; }361});362</script>"""363364def build_html(ins):365 p = pal(ins["site"])366 genre = ins.get("genre","hero")367 if genre == "ranking" and not ins.get("bars"): genre = "hero"368 if genre == "spotlight" and not (ins.get("product") or {}).get("image_data"): genre = "hero"369 if genre == "grid":370 n_img = len([i for i in (ins.get("list") or []) if i.get("image_data")])371 if n_img < 4: genre = "list" if n_img >= 2 else "hero"372 if genre == "list" and not ins.get("list"): genre = "hero"373 if genre == "donut" and parse_pct(ins.get("headline")) is None: genre = "hero"374 if genre == "quote" and len(str(ins.get("fact") or "")) < 40: genre = "hero"375 # les genres photo restent sur le thème sombre (les photos y ressortent mieux)376 theme = "nuit" if genre in ("spotlight","list","grid") else theme_of(ins)377 T = tokens(p, theme)378 body = GENRES.get(genre, genre_hero)(ins, p, T)379 html = ("<!doctype html><html><head><meta charset=\"utf-8\"><style>"+base_css(p,T)+"</style></head>"380 + "<body><div class=\"stage\"><div class=\"grain\"></div>"+body+"</div>"+FIT_JS+"</body></html>")381 return html, genre, theme382383def render(ins, out_dir):384 os.makedirs(out_dir, exist_ok=True)385 # télécharger les photos en data-uri (robuste, pas de hotlink)386 prod = ins.get("product")387 if prod and prod.get("image") and not prod.get("image_data"):388 du = data_uri(prod["image"])389 if du: prod["image_data"] = du390 for it in (ins.get("list") or []):391 if it.get("image") and not it.get("image_data"):392 du = data_uri(it["image"])393 if du: it["image_data"] = du394 html, genre, theme = build_html(ins)395 key = hashlib.md5((ins["site"]+str(ins.get("insight_id",""))+genre+theme).encode()).hexdigest()[:8]396 html_path = os.path.join(out_dir, f"card-{ins['site']}-{key}.html")397 big = os.path.join(out_dir, f"_big-{key}.png")398 out = os.path.join(out_dir, f"card-{ins['site']}-{ins.get('insight_id','x')}-{genre}-{theme}.png")399 open(html_path,"w",encoding="utf-8").write(html)400 chrome = find_chrome()401 subprocess.run([chrome,"--headless","--disable-gpu","--hide-scrollbars",402 "--force-device-scale-factor=2","--window-size=1080,1350",403 f"--screenshot={big}","--virtual-time-budget=9000","--run-all-compositor-stages-before-draw",404 f"file://{html_path}"], capture_output=True, timeout=60)405 if not os.path.exists(big):406 raise SystemExit("chrome screenshot a échoué")407 # downscale 2x → 1080x1350 (texte net) via sips408 subprocess.run(["/usr/bin/sips","-z","1350","1080",big,"--out",out],capture_output=True)409 try: os.remove(big); os.remove(html_path)410 except: pass411 return out412413if __name__ == "__main__":414 ins = json.load(sys.stdin)415 out_dir = sys.argv[1] if len(sys.argv)>1 else "/tmp/ka-social-cards"416 print(render(ins, out_dir))417