SPB Git

spb/admin-ka

Public
25.7 KB · 455 lines python
1#!/usr/bin/env python32"""Reel KA v4 — mini-clip vertical MULTI-SCÈNES (1080x1920) + musique procédurale.34Le reel n'est plus une seule carte animée de 10 s : c'est une séquence de5scènes (~20-32 s) qui couvre plusieurs facettes de l'insight :6  intro → stat géante (count-up) / donut % → tuiles chiffres → palmarès animé7  → photos réelles (Ken Burns) → « le saviez-vous » → outro CTA.8Les scènes disponibles dépendent des données (tiles/bars/list/product/fact).910Musique : music_engine.py (6 styles, mélodie/progression/tonalité par graine)11— deux reels n'ont jamais la même musique. Style en rotation via `variant`.1213Pipeline : HTML animé -> Playwright (Chromium, enregistrement vidéo) -> ffmpeg.14Sortie : chemin du mp4 sur stdout. Env : KA_REEL_H (défaut 1920).15"""16import json, sys, os, re, base64, datetime, urllib.request, ssl, subprocess, glob, hashlib1718sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))19import music_engine2021CTX = ssl.create_default_context(); CTX.check_hostname=False; CTX.verify_mode=ssl.CERT_NONE22FFMPEG = "/opt/homebrew/bin/ffmpeg"23MOIS=["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"]24FONT="'Avenir Next','SF Pro Display','Helvetica Neue',Helvetica,Arial,sans-serif"25H=int(os.environ.get("KA_REEL_H","1920"))26STYLES_ORDER=["house","synthwave","lofi","funk","epic","trap"]2728PALETTES={29 "lou-ka":{"a":"#ff6a00","b":"#ff9d4d","g1":"#2a1403","g2":"#0d0b09"},30 "immo-ka":{"a":"#e23744","b":"#ff6b76","g1":"#2a0c10","g2":"#0d0a0b"},31 "food-ka":{"a":"#1f9d55","b":"#5fd08a","g1":"#07230f","g2":"#0a0d0b"},32 "fabri-ka":{"a":"#c4532e","b":"#e8895f","g1":"#241009","g2":"#0d0a09"},33 "resto-ka":{"a":"#f08c00","b":"#ffb84d","g1":"#241804","g2":"#0d0b08"},34 "auto-ka":{"a":"#ff5a2a","b":"#ff8f6b","g1":"#26100a","g2":"#0d0a09"},35 "vrai-prix":{"a":"#d9f26b","b":"#b8e04a","g1":"#16230a","g2":"#0b0d08"},36 "trouve-ka":{"a":"#d9f26b","b":"#8fd3ff","g1":"#0a1a20","g2":"#080b0d"},37 "sorti-ka":{"a":"#d9f26b","b":"#ff8fd0","g1":"#210a1c","g2":"#0c090c"},38 "crea-ka":{"a":"#d9f26b","b":"#c08bff","g1":"#160a24","g2":"#0a090d"},39 "job-ka":{"a":"#d9f26b","b":"#7ab8ff","g1":"#0a1522","g2":"#080b0d"},40}41def pal(s): return PALETTES.get(s, 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: raw=r.read()47        mime="image/jpeg"48        if url.lower().endswith(".png"): mime="image/png"49        elif url.lower().endswith(".webp"): mime="image/webp"50        return "data:"+mime+";base64,"+base64.b64encode(raw).decode()51    except Exception: return None5253def esc(s): return str(s).replace("&","&amp;").replace("<","&lt;").replace(">","&gt;")54def today(): t=datetime.date.today(); return f"{t.day} {MOIS[t.month-1]} {t.year}"55def parse_pct(h):56    m=re.match(r'^\+?(\d+(?:[.,]\d+)?)\s*%$', str(h or "").strip())57    return float(m.group(1).replace(",",".")) if m else None58NUMRE = re.compile(r'\d[\d\u00a0\u202f ,\.]*%?\$?')5960# ---------- scènes ----------61# chaque constructeur renvoie (html, css, dur, hook_js_ou_None)6263def sc_intro(ins,p,i):64    css="""65    #S .mid{flex:1;display:flex;flex-direction:column;justify-content:center}66    #S .kick{font-size:30px;font-weight:800;letter-spacing:.22em;color:__A__;text-transform:uppercase;opacity:0}67    .scene.on #S_ .kick{animation:fu .6s ease .15s forwards}68    #S .big{font-size:150px;font-weight:900;letter-spacing:-.03em;line-height:1;margin-top:26px;opacity:0;transform:scale(.8)}69    .scene.on #S_ .big{animation:zoomin .8s cubic-bezier(.2,1.2,.3,1) .35s forwards}70    #S .bar{width:0;height:10px;border-radius:6px;background:linear-gradient(90deg,__A__,__B__);margin-top:34px}71    .scene.on #S_ .bar{animation:grow .7s ease .8s forwards}72    #S .tag{font-size:42px;font-weight:600;color:#ffffffc8;margin-top:30px;max-width:880px;line-height:1.2;opacity:0}73    .scene.on #S_ .tag{animation:fu .7s ease 1.05s forwards}74    @keyframes zoomin{to{opacity:1;transform:scale(1)}}75    @keyframes grow{to{width:280px}}76    """77    html=('<div class="mid"><div class="kick">Le pouls du jour</div>'78        '<div class="big">'+esc(ins["label"])+'</div><div class="bar"></div>'79        + (('<div class="tag">'+esc(ins.get("tagline",""))+'</div>') if ins.get("tagline") else '')80        + '</div>')81    return html, css, 2.8, None8283def _num_parts(headline):84    """-> (int_digits, dec, suffix) pour le count-up ; None si non numérique."""85    hl=str(headline).strip()86    m=re.match(r'^\+?([\d\s  ]+)(?:[.,](\d+))?\s*(.*)$', hl)87    if not m: return None88    digits=re.sub(r'\D','',m.group(1))89    if not digits: return None90    return digits, (m.group(2) or ""), m.group(3) or ""9192def sc_stat(ins,p,i):93    parts=_num_parts(ins["headline"]); hl=str(ins["headline"])94    L=len(hl); fs=170 if L>=9 else (215 if L>=7 else 260)95    css="""96    #S .mid{flex:1;display:flex;flex-direction:column;justify-content:center}97    #S .lab0{font-size:34px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:__A__;opacity:0}98    .scene.on #S_ .lab0{animation:fu .5s ease .1s forwards}99    #S .num{font-size:__FS__px;font-weight:900;line-height:.95;letter-spacing:-.03em;white-space:nowrap;margin-top:24px;100      background:linear-gradient(180deg,#fff,__A__ 135%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;101      filter:drop-shadow(0 18px 55px __A__55);opacity:0;transform:translateY(36px)}102    .scene.on #S_ .num{animation:fu .7s cubic-bezier(.2,1,.3,1) .3s forwards}103    #S .lab{font-size:56px;font-weight:700;margin-top:30px;max-width:900px;line-height:1.14;opacity:0}104    .scene.on #S_ .lab{animation:fu .7s ease .8s forwards}105    """.replace("__FS__",str(fs))106    hook=None107    if parts and len(parts[0])>=3 and not parts[1]:108        hook="cuInt('S_num',"+parts[0]+","+json.dumps(" "+parts[2] if parts[2] else "")+",1500,400);"109    html=('<div class="mid"><div class="lab0">'+esc(ins["label"])+' · en direct</div>'110        '<div class="num" id="S_num">'+esc(hl)+'</div>'111        '<div class="lab">'+esc(ins["headline_label"])+'</div></div>')112    return html, css, 4.2, hook113114def sc_donut(ins,p,i):115    pct=parse_pct(ins["headline"]) or 0.0116    C=2*3.14159*300; off=C*(1-min(100.0,pct)/100.0)117    dec=1 if re.search(r'[.,]\d',str(ins["headline"])) else 0118    css="""119    #S .mid{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center}120    #S .dw{position:relative;width:740px;height:740px;opacity:0;transform:scale(.88)}121    .scene.on #S_ .dw{animation:zoomin .7s cubic-bezier(.2,1.2,.4,1) .15s forwards}122    #S .arc{stroke-dasharray:__C__;stroke-dashoffset:__C__}123    .scene.on #S_ .arc{animation:draw 1.7s cubic-bezier(.3,1,.4,1) .5s forwards}124    @keyframes draw{to{stroke-dashoffset:__OFF__}}125    @keyframes zoomin{to{opacity:1;transform:scale(1)}}126    #S .dp{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:145px;font-weight:900;color:__A__;letter-spacing:-.02em;filter:drop-shadow(0 10px 34px __A__55)}127    #S .dl{font-size:48px;font-weight:700;line-height:1.16;text-align:center;max-width:900px;margin-top:34px;opacity:0}128    .scene.on #S_ .dl{animation:fu .7s ease 1.2s forwards}129    """.replace("__C__",str(round(C,1))).replace("__OFF__",str(round(off,1)))130    svg=('<svg width="740" height="740" viewBox="0 0 760 760">'131      '<circle cx="380" cy="380" r="300" fill="none" stroke="rgba(255,255,255,.10)" stroke-width="82"/>'132      '<circle class="arc" cx="380" cy="380" r="300" fill="none" stroke="url(#gd'+str(i)+')" stroke-width="82" stroke-linecap="round" transform="rotate(-90 380 380)"/>'133      '<defs><linearGradient id="gd'+str(i)+'" x1="0" y1="0" x2="1" y2="1">'134      '<stop offset="0" stop-color="__A__"/><stop offset="1" stop-color="__B__"/></linearGradient></defs></svg>')135    html=('<div class="mid"><div class="dw">'+svg+'<div class="dp" id="S_dp">0 %</div></div>'136        '<div class="dl">'+esc(ins["headline_label"])+'</div></div>')137    hook="cuPct('S_dp',"+str(pct)+","+str(dec)+",1700,500);"138    return html, css, 4.4, hook139140def sc_tiles(ins,p,i):141    tiles=(ins.get("tiles") or [])[:3]142    rows=""143    for k,(v,l) in enumerate(tiles):144        rows+=('<div class="trow" style="transition-delay:.0s"><div class="tv" data-d="'+str(k)+'">'+esc(v)+'</div>'145               '<div class="tl">'+esc(l)+'</div></div>')146    css="""147    #S .mid{flex:1;display:flex;flex-direction:column;justify-content:center;gap:26px}148    #S .h{font-size:40px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:__A__;opacity:0;margin-bottom:8px}149    .scene.on #S_ .h{animation:fu .5s ease .1s forwards}150    #S .trow{background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);border-radius:30px;151      padding:40px 44px;backdrop-filter:blur(6px);opacity:0;transform:translateX(-60px)}152    .scene.on #S_ .trow{animation:slidein .6s cubic-bezier(.2,.9,.3,1) forwards}153    .scene.on #S_ .trow:nth-child(2){animation-delay:.25s}154    .scene.on #S_ .trow:nth-child(3){animation-delay:.5s}155    .scene.on #S_ .trow:nth-child(4){animation-delay:.75s}156    @keyframes slidein{to{opacity:1;transform:none}}157    #S .tv{font-size:82px;font-weight:900;color:__A__;line-height:1;white-space:nowrap}158    #S .tl{font-size:34px;font-weight:600;color:#ffffffb0;margin-top:10px}159    """160    html='<div class="mid"><div class="h">En chiffres</div>'+rows+'</div>'161    return html, css, 3.6, None162163def sc_ranking(ins,p,i):164    bars=(ins.get("bars") or [])[:4]165    maxv=max([b[1] for b in bars] or [1])166    rows=""167    for k,b in enumerate(bars):168        w=max(10,int(100*b[1]/maxv))169        rows+=('<div class="rrow"><div class="rl"><span>'+esc(b[0])+'</span><span class="rv">'+esc(b[2])+'</span></div>'170            '<div class="tr2"><div class="fl" style="--w:'+str(w)+'%;animation-delay:'+str(round(0.35+k*0.28,2))+'s"></div></div></div>')171    css="""172    #S .mid{flex:1;display:flex;flex-direction:column;justify-content:center;gap:40px}173    #S .h{font-size:46px;font-weight:800;line-height:1.15;max-width:900px;opacity:0;margin-bottom:6px}174    .scene.on #S_ .h{animation:fu .5s ease .1s forwards}175    #S .rrow{opacity:0}176    .scene.on #S_ .rrow{animation:fu .5s ease forwards}177    .scene.on #S_ .rrow:nth-child(2){animation-delay:.2s}178    .scene.on #S_ .rrow:nth-child(3){animation-delay:.4s}179    .scene.on #S_ .rrow:nth-child(4){animation-delay:.6s}180    .scene.on #S_ .rrow:nth-child(5){animation-delay:.8s}181    #S .rl{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:14px;font-size:40px;font-weight:700}182    #S .rv{font-weight:900;color:__A__;font-size:44px}183    #S .tr2{height:26px;border-radius:14px;background:rgba(255,255,255,.09);overflow:hidden}184    #S .fl{height:100%;width:0;border-radius:14px;background:linear-gradient(90deg,__A__,__B__);box-shadow:0 4px 18px __A__55}185    .scene.on #S_ .fl{animation:growb 1s cubic-bezier(.2,.9,.3,1) forwards}186    @keyframes growb{to{width:var(--w)}}187    """188    html='<div class="mid"><div class="h">'+esc(ins["headline_label"])+'</div>'+rows+'</div>'189    return html, css, 4.6, None190191def sc_photo(item,p,i,idx,total):192    img=item.get("image_data") or ""193    kb = "kbA" if idx%2==0 else "kbB"194    css="""195    #S{padding:0}196    #S .ph{position:absolute;inset:0;background:url('__IMG__') center/cover}197    .scene.on #S_ .ph{animation:__KB__ 3.4s ease-out forwards}198    @keyframes kbA{from{transform:scale(1)}to{transform:scale(1.14) translate(-1.5%,-1%)}}199    @keyframes kbB{from{transform:scale(1.14)}to{transform:scale(1) translate(1%,1%)}}200    #S .sc2{position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,.25) 0%,transparent 30%,transparent 48%,__G2__e6 78%,__G2__ 92%)}201    #S .idx{position:absolute;top:200px;left:90px;background:__A__;color:#0d0d0d;font-weight:900;font-size:40px;202      padding:10px 26px;border-radius:16px;transform:rotate(-3deg) scale(.5);opacity:0}203    .scene.on #S_ .idx{animation:pop2 .5s cubic-bezier(.2,1.4,.4,1) .25s forwards}204    @keyframes pop2{to{opacity:1;transform:rotate(-3deg) scale(1)}}205    #S .inf{position:absolute;left:90px;right:90px;bottom:300px}206    #S .pt{font-size:64px;font-weight:900;line-height:1.05;letter-spacing:-.01em;text-shadow:0 4px 24px #000c;opacity:0;transform:translateY(30px)}207    .scene.on #S_ .pt{animation:fu .6s ease .35s forwards}208    #S .ps{font-size:34px;font-weight:600;color:#ffffffd0;margin-top:14px;opacity:0}209    .scene.on #S_ .ps{animation:fu .6s ease .55s forwards}210    #S .pp{font-size:84px;font-weight:900;color:__A__;margin-top:20px;letter-spacing:-.02em;filter:drop-shadow(0 8px 26px __A__66);opacity:0;transform:scale(.9)}211    .scene.on #S_ .pp{animation:popc2 .6s cubic-bezier(.2,1.3,.4,1) .75s forwards}212    @keyframes popc2{to{opacity:1;transform:scale(1)}}213    """.replace("__IMG__",img).replace("__KB__",kb)214    html=('<div class="ph"></div><div class="sc2"></div>'215        '<div class="idx">'+str(idx+1)+' / '+str(total)+'</div>'216        '<div class="inf"><div class="pt">'+esc(item.get("title",""))+'</div>'217        + (('<div class="ps">'+esc(item.get("subtitle",""))+'</div>') if item.get("subtitle") else '')218        + (('<div class="pp">'+esc(item.get("price",""))+'</div>') if item.get("price") else '')219        + '</div>')220    return html, css, 3.0, None221222def sc_quote(ins,p,i):223    fact=str(ins.get("fact") or "")224    words=fact.split()225    delay=min(0.11, 4.6/max(1,len(words)))226    spans=""227    for k,w in enumerate(words):228        cls="w hl" if NUMRE.search(w) else "w"229        spans+='<span class="'+cls+'" style="animation-delay:'+str(round(0.35+k*delay,2))+'s">'+esc(w)+'</span> '230    L=len(fact); fs=60 if L<170 else (53 if L<240 else 46)231    dur=min(6.0, 1.6+len(words)*delay+2.2)232    css="""233    #S .mid{flex:1;display:flex;flex-direction:column;justify-content:center}234    #S .qm{font-size:260px;line-height:.5;color:__A__;font-weight:900;font-family:Georgia,serif;height:140px;opacity:0}235    .scene.on #S_ .qm{animation:fu .5s ease .1s forwards}236    #S .qt{font-size:__FS__px;font-weight:800;line-height:1.35;letter-spacing:-.01em;max-width:920px}237    #S .w{display:inline-block;opacity:0;transform:translateY(24px)}238    .scene.on #S_ .w{animation:fu .45s ease forwards}239    #S .hl{color:__A__;filter:drop-shadow(0 6px 20px __A__44)}240    """.replace("__FS__",str(fs))241    html='<div class="mid"><div class="qm">“</div><div class="qt">'+spans+'</div></div>'242    return html, css, round(dur,1), None243244def sc_outro(ins,p,i):245    css="""246    #S .mid{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center}247    #S .kb2{background:__A__;color:#0d0d0d;font-weight:900;font-size:96px;padding:18px 46px;border-radius:32px;248      transform:rotate(-4deg) scale(.4);opacity:0;box-shadow:0 20px 60px __A__66}249    .scene.on #S_ .kb2{animation:pop3 .7s cubic-bezier(.2,1.4,.4,1) .15s forwards}250    @keyframes pop3{to{opacity:1;transform:rotate(-4deg) scale(1)}}251    #S .go{font-size:62px;font-weight:900;color:__A__;margin-top:60px;opacity:0}252    .scene.on #S_ .go{animation:fu .6s ease .7s forwards}253    #S .br{font-size:34px;font-weight:700;color:#ffffff8c;letter-spacing:.14em;margin-top:16px;opacity:0}254    .scene.on #S_ .br{animation:fu .6s ease .95s forwards}255    #S .sg{font-size:26px;color:#ffffff6a;font-weight:600;margin-top:56px;opacity:0}256    .scene.on #S_ .sg{animation:fu .6s ease 1.2s forwards}257    """258    html=('<div class="mid"><div class="kb2">KA</div>'259        '<div class="go">\U0001f449 www.'+ins["site"]+'.com</div>'260        '<div class="br">GROUPE ·KA</div>'261        '<div class="sg">✦ Rédigé et publié par l\'Agent KA</div></div>')262    return html, css, 3.2, None263264# ---------- plan des scènes ----------265def plan_scenes(ins,p):266    photos=[it for it in (ins.get("list") or []) if it.get("image_data")]267    if not photos and (ins.get("product") or {}).get("image_data"):268        pr=ins["product"]269        photos=[{"title":pr.get("title",""),"subtitle":pr.get("subtitle",""),270                 "price":pr.get("price",""),"image_data":pr["image_data"]}]271    has_pct=parse_pct(ins.get("headline")) is not None272    genre=ins.get("genre","hero")273    photo_led=genre in ("spotlight","list","grid") and photos274275    def stat_block():276        out=[]277        if has_pct: out.append(("donut",sc_donut))278        else: out.append(("stat",sc_stat))279        if ins.get("tiles"): out.append(("tiles",sc_tiles))280        if ins.get("bars") and len(ins["bars"])>=3: out.append(("ranking",sc_ranking))281        return out282    has_quote=len(str(ins.get("fact") or ""))>=40283284    # budget photos AVANT construction (les numéros « k / n » restent exacts) :285    # base ≈ intro 2.8 + stat/donut 4.3 + tuiles 3.6 + palmarès 4.6 + quote 4.5 + outro 3.2286    base_est=2.8+4.4+ (3.6 if ins.get("tiles") else 0) + (4.6 if (ins.get("bars") and len(ins["bars"])>=3) else 0) \287             + (4.5 if has_quote else 0) + 3.2288    maxp=4 if photo_led else 3289    nphotos=max(0,min(len(photos),maxp,int((34.0-base_est)//3.0)))290    tot_p=nphotos291    photo_scenes=[("photo%d"%k,(lambda it,k2: (lambda ins2,p2,i: sc_photo(it,p2,i,k2,tot_p)))(it,k))292                  for k,it in enumerate(photos[:nphotos])]293294    builders=[("intro",sc_intro)]295    if photo_led:296        builders+=photo_scenes+stat_block()297    else:298        builders+=stat_block()+photo_scenes299    if has_quote: builders.append(("quote",sc_quote))300    builders.append(("outro",sc_outro))301302    scenes=[]303    for i,(name,fn) in enumerate(builders):304        html,css,dur,hook=fn(ins,p,i)305        scenes.append(dict(name=name,html=html,css=css,dur=dur,hook=hook,i=i))306    return scenes307308# ---------- assemblage HTML ----------309def build_html(ins):310    p=pal(ins["site"])311    scenes=plan_scenes(ins,p)312    tot=round(sum(s["dur"] for s in scenes),2)313    shell="""314    *{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased}315    html,body{width:1080px;height:__H__px;overflow:hidden;font-family:__FONT__;color:#fff;background:__G2__}316    .stage{position:relative;width:1080px;height:__H__px;overflow:hidden;317      background:radial-gradient(1300px 900px at 78% -5%, __A__50,transparent 55%),318                 radial-gradient(1100px 1000px at -12% 105%, __B__36,transparent 55%),319                 linear-gradient(160deg,__G1__,__G2__);background-size:150% 150%;animation:drift 18s ease-in-out infinite alternate}320    @keyframes drift{0%{background-position:0% 0%}100%{background-position:100% 100%}}321    .vign{position:absolute;inset:0;pointer-events:none;background:radial-gradient(120% 90% at 50% 45%, transparent 55%, rgba(0,0,0,.5) 100%);z-index:3}322    .eyeb{position:absolute;top:104px;left:90px;right:250px;display:flex;align-items:center;gap:14px;font-weight:800;323      letter-spacing:.16em;font-size:25px;text-transform:uppercase;color:__A__;z-index:8;opacity:0;animation:fu .7s ease .2s forwards}324    .eyeb .dot{width:46px;height:6px;border-radius:3px;background:__A__;flex:0 0 46px}325    .kabadge{position:absolute;top:92px;right:90px;background:__A__;color:#0d0d0d;font-weight:900;font-size:48px;326      padding:10px 24px;border-radius:20px;transform:rotate(-4deg) scale(.4);opacity:0;z-index:8;327      animation:pop .7s cubic-bezier(.2,1.4,.4,1) .35s forwards;box-shadow:0 16px 46px __A__66}328    .wm{position:absolute;left:90px;bottom:96px;font-size:27px;font-weight:800;color:#ffffff9a;z-index:8}329    .wm b{color:__A__}330    .prog{position:absolute;left:0;bottom:0;height:9px;background:linear-gradient(90deg,__A__,__B__);width:0;z-index:9;331      animation:prog __TOT__s linear .1s forwards;box-shadow:0 0 20px __A__}332    @keyframes prog{to{width:100%}}333    @keyframes fu{to{opacity:1;transform:translateY(0)}}334    @keyframes pop{to{opacity:1;transform:rotate(-4deg) scale(1)}}335    .scene{position:absolute;inset:0;padding:220px 90px 210px;display:flex;flex-direction:column;z-index:4;336      opacity:0;pointer-events:none;transform:translateY(46px) scale(.985);337      transition:opacity .5s ease,transform .62s cubic-bezier(.2,.8,.2,1)}338    .scene.on{opacity:1;transform:none}339    .scene.off{opacity:0;transform:translateY(-46px) scale(1.012)}340    .fadeout{position:absolute;inset:0;background:#000;opacity:0;z-index:20;pointer-events:none;341      animation:fo .7s ease __FOT__s forwards}342    @keyframes fo{to{opacity:1}}343    """344    shell=(shell.replace("__H__",str(H)).replace("__FONT__",FONT).replace("__A__",p["a"])345        .replace("__B__",p["b"]).replace("__G1__",p["g1"]).replace("__G2__",p["g2"])346        .replace("__TOT__",str(tot)).replace("__FOT__",str(round(tot-0.7,2))))347348    css_all=[shell]; body=[]; sc_list=[]; hooks={}349    for s in scenes:350        sid="sc"+str(s["i"])351        css=(s["css"].replace(".scene.on #S_","#"+sid+".on").replace("#S_","#"+sid).replace("#S","#"+sid)352             .replace("__A__",p["a"]).replace("__B__",p["b"]).replace("__G2__",p["g2"]))353        # les hooks référencent des ids préfixés S_ -> remplacer par l'id de scène354        html=s["html"].replace('id="S_','id="'+sid+'_')355        hook=(s["hook"] or "").replace("'S_","'"+sid+"_")356        css_all.append(css)357        body.append('<div class="scene" id="'+sid+'">'+html+'</div>')358        sc_list.append([sid, s["dur"]])359        if hook: hooks[sid]=hook360361    hooks_js="{"+",".join('"'+k+'":function(){'+v+'}' for k,v in hooks.items())+"}"362    script="""<script>363    function fmtInt(n){return n.toLocaleString('fr-CA').replace(/[\\u00a0\\u202f,]/g,' ')}364    function cuInt(id,target,suffix,dur,begin){var el=document.getElementById(id);if(!el)return;var s=null;365      function st(ts){if(!s)s=ts;var t=ts-s;if(t<begin){requestAnimationFrame(st);return}366        var k=Math.min(1,(t-begin)/dur);var e=1-Math.pow(1-k,3);367        el.textContent=fmtInt(Math.round(target*e))+suffix;if(k<1)requestAnimationFrame(st)}368      el.textContent=fmtInt(0)+suffix;requestAnimationFrame(st)}369    function cuPct(id,pct,dec,dur,begin){var el=document.getElementById(id);if(!el)return;var s=null;370      function f(n){return n.toFixed(dec).replace('.',',')+' %'}371      function st(ts){if(!s)s=ts;var t=ts-s;if(t<begin){requestAnimationFrame(st);return}372        var k=Math.min(1,(t-begin)/dur);var e=1-Math.pow(1-k,3);373        el.textContent=f(pct*e);if(k<1)requestAnimationFrame(st)}374      requestAnimationFrame(st)}375    var SCENES=__SC__;376    var HOOKS=__HOOKS__;377    (function(){var t=200;378      SCENES.forEach(function(s){379        setTimeout(function(){380          document.querySelectorAll('.scene.on').forEach(function(el){el.classList.remove('on');el.classList.add('off')});381          var el=document.getElementById(s[0]);el.classList.remove('off');el.classList.add('on');382          if(HOOKS[s[0]])try{HOOKS[s[0]]()}catch(e){}383        },t);384        t+=s[1]*1000;385      });386    })();387    </script>""".replace("__SC__",json.dumps(sc_list)).replace("__HOOKS__",hooks_js)388389    eyeb='<div class="eyeb"><span class="dot"></span>'+esc(ins["label"].upper())+' · '+today().upper()+'</div>'390    wm='<div class="wm"><b>www.'+ins["site"]+'.com</b> · Agent KA</div>'391    html=("<!doctype html><html><head><meta charset=\"utf-8\"><style>"+"\n".join(css_all)+"</style></head><body>"392        '<div class="stage">'+eyeb+'<div class="kabadge">KA</div>'+"".join(body)393        +wm+'<div class="prog"></div><div class="vign"></div><div class="fadeout"></div></div>'+script+"</body></html>")394    return html, tot395396# ---------- enregistrement + musique + mux ----------397def record_webm(html_path, out_webm, tmpdir, dur):398    from playwright.sync_api import sync_playwright399    with sync_playwright() as pw:400        b=pw.chromium.launch(args=["--no-sandbox"])401        ctx=b.new_context(viewport={"width":1080,"height":H}, device_scale_factor=2,402                          record_video_dir=tmpdir, record_video_size={"width":1080,"height":H})403        pg=ctx.new_page()404        pg.goto("file://"+html_path)405        pg.wait_for_timeout(int((dur+0.3)*1000))406        vid=pg.video407        ctx.close(); b.close()408        path=vid.path()409    os.replace(path, out_webm)410411def render(ins, out_dir):412    os.makedirs(out_dir,exist_ok=True)413    prod=ins.get('product')414    if prod and prod.get('image') and not prod.get('image_data'):415        du=data_uri(prod['image'])416        if du: prod['image_data']=du417    for it in (ins.get('list') or []):418        if it.get('image') and not it.get('image_data'):419            du=data_uri(it['image'])420            if du: it['image_data']=du421422    key=hashlib.md5((ins['site']+str(ins.get('insight_id',''))+str(ins.get('genre',''))+'reelv4').encode()).hexdigest()[:8]423    tmp=os.path.join(out_dir,"_reeltmp_"+key); os.makedirs(tmp,exist_ok=True)424    html,dur=build_html(ins)425    html_path=os.path.join(tmp,"reel.html"); open(html_path,"w",encoding="utf-8").write(html)426    webm=os.path.join(tmp,"reel.webm"); record_webm(html_path,webm,tmp,dur)427428    # musique : style en rotation via variant, contenu par graine (unique)429    variant=ins.get('variant')430    seed=ins['site']+"|"+str(ins.get('insight_id',''))+"|"+str(variant)+"|"+datetime.date.today().isoformat()431    style=STYLES_ORDER[int(variant)%len(STYLES_ORDER)] if variant is not None else None432    wav=os.path.join(tmp,"bed.wav")433    used=music_engine.compose(dur+0.2, seed, wav, style)434    sys.stderr.write("musique: style=%s seed=%s dur=%.1fs\n"%(used,seed,dur))435436    out=os.path.join(out_dir,f"reel-{ins['site']}-{ins.get('insight_id','x')}.mp4")437    subprocess.run([FFMPEG,"-y","-i",webm,"-i",wav,438        "-filter_complex","[0:v]scale=1080:"+str(H)+":force_original_aspect_ratio=increase,crop=1080:"+str(H)+",fps=30,format=yuv420p[v];"439        "[1:a]acompressor=threshold=-16dB:ratio=3:attack=8:release=180,alimiter=limit=0.95[a]",440        "-map","[v]","-map","[a]","-c:v","libx264","-preset","slow","-crf","18",441        "-c:a","aac","-b:a","192k","-pix_fmt","yuv420p","-movflags","+faststart","-shortest",out],442        capture_output=True)443    for f in glob.glob(os.path.join(tmp,"*")):444        try: os.remove(f)445        except: pass446    try: os.rmdir(tmp)447    except: pass448    if not os.path.exists(out): raise SystemExit("mp4 non produit")449    return out450451if __name__=="__main__":452    ins=json.load(sys.stdin)453    out_dir=sys.argv[1] if len(sys.argv)>1 else "/tmp/ka-reels"454    print(render(ins,out_dir))455
Files · ee1f78227355f7efe4a3ebffbf93acfb5e6404a1
Menu

Theme