SPB Git

spb/admin-ka

Public
23.5 KB · 339 lines python
1#!/usr/bin/env python32"""Génère un Reel vertical animé (1080x1920) + musique à partir d'un insight KA.34HTML animé (CSS/JS) -> enregistrement vidéo Playwright (Chromium) -> ffmpeg5(mp4 h264 + lit musical synthétisé). Sortie : chemin du mp4 sur stdout.6"""7import json, sys, os, base64, datetime, urllib.request, ssl, subprocess, glob, hashlib89CTX = ssl.create_default_context(); CTX.check_hostname=False; CTX.verify_mode=ssl.CERT_NONE10FFMPEG = "/opt/homebrew/bin/ffmpeg"11MOIS=["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"]12FONT="'Avenir Next','SF Pro Display','Helvetica Neue',Helvetica,Arial,sans-serif"13DUR=10.0  # secondes14HREEL=int(os.environ.get("KA_REEL_H","1920"))1516PALETTES={17 "lou-ka":{"a":"#ff6a00","b":"#ff9d4d","g1":"#2a1403","g2":"#0d0b09"},18 "immo-ka":{"a":"#e23744","b":"#ff6b76","g1":"#2a0c10","g2":"#0d0a0b"},19 "food-ka":{"a":"#1f9d55","b":"#5fd08a","g1":"#07230f","g2":"#0a0d0b"},20 "fabri-ka":{"a":"#c4532e","b":"#e8895f","g1":"#241009","g2":"#0d0a09"},21 "resto-ka":{"a":"#f08c00","b":"#ffb84d","g1":"#241804","g2":"#0d0b08"},22 "auto-ka":{"a":"#ff5a2a","b":"#ff8f6b","g1":"#26100a","g2":"#0d0a09"},23 "vrai-prix":{"a":"#d9f26b","b":"#b8e04a","g1":"#16230a","g2":"#0b0d08"},24 "trouve-ka":{"a":"#d9f26b","b":"#8fd3ff","g1":"#0a1a20","g2":"#080b0d"},25 "sorti-ka":{"a":"#d9f26b","b":"#ff8fd0","g1":"#210a1c","g2":"#0c090c"},26 "crea-ka":{"a":"#d9f26b","b":"#c08bff","g1":"#160a24","g2":"#0a090d"},27 "job-ka":{"a":"#d9f26b","b":"#7ab8ff","g1":"#0a1522","g2":"#080b0d"},28}29def pal(s): return PALETTES.get(s, PALETTES["vrai-prix"])30def data_uri(url):31    import base6432    try:33        req=urllib.request.Request(url, headers={"User-Agent":"Mozilla/5.0 (Macintosh)"})34        with urllib.request.urlopen(req, timeout=15, context=CTX) as r: raw=r.read()35        mime="image/jpeg"36        if url.lower().endswith(".png"): mime="image/png"37        elif url.lower().endswith(".webp"): mime="image/webp"38        return "data:"+mime+";base64,"+base64.b64encode(raw).decode()39    except Exception: return None40def esc(s): return str(s).replace("&","&amp;").replace("<","&lt;").replace(">","&gt;")41def today(): t=datetime.date.today(); return f"{t.day} {MOIS[t.month-1]} {t.year}"4243def build_spotlight(ins):44    H=HREEL; p=pal(ins["site"]); prod=ins.get("product") or {}45    img=prod.get("image_data",""); chips="".join('<span class="chip">'+esc(c)+'</span>' for c in (prod.get("meta") or []))46    price=esc(prod.get("price","")); title=esc(prod.get("title","")); sub=esc(prod.get("subtitle",""))47    css = """48    *{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased}49    html,body{width:1080px;height:__H__px;overflow:hidden;font-family:__FONT__;color:#fff;background:__G2__}50    .photo{position:absolute;top:0;left:0;right:0;height:__PH__px;background:url('__IMG__') center/cover;animation:kb 10s ease-out forwards}51    @keyframes kb{from{transform:scale(1.0)}to{transform:scale(1.12)}}52    .scrim{position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,.15) 0%,rgba(0,0,0,0) 30%,__G2__cc 55%,__G2__ 72%)}53    .eyebrow{position:absolute;top:120px;left:90px;display:flex;align-items:center;gap:14px;font-weight:800;letter-spacing:.16em;font-size:26px;text-transform:uppercase;color:__A__;opacity:0;animation:fu .8s ease .2s forwards;text-shadow:0 2px 12px #000}54    .eyebrow .dot{width:46px;height:6px;border-radius:3px;background:__A__}55    .kabadge{position:absolute;top:110px;right:90px;background:__A__;color:#0d0d0d;font-weight:900;font-size:52px;padding:10px 24px;border-radius:20px;transform:rotate(-4deg) scale(.5);opacity:0;animation:pop .7s cubic-bezier(.2,1.4,.4,1) .5s forwards;box-shadow:0 16px 40px __A__66}56    .info{position:absolute;left:90px;right:90px;bottom:270px}57    .ptitle{font-size:74px;font-weight:900;line-height:1.02;letter-spacing:-.02em;text-shadow:0 4px 24px #000a;opacity:0;transform:translateY(30px);animation:fu .9s ease 1.0s forwards}58    .psub{font-size:36px;font-weight:600;color:#ffffffc8;margin-top:14px;opacity:0;animation:fu .9s ease 1.25s forwards}59    .chips{display:flex;gap:14px;flex-wrap:wrap;margin-top:26px}60    .chip{background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.22);border-radius:999px;padding:12px 24px;font-size:28px;font-weight:700;opacity:0;animation:fu .8s ease 1.45s forwards}61    .price{font-size:92px;font-weight:900;color:__A__;margin-top:26px;letter-spacing:-.02em;filter:drop-shadow(0 10px 30px __A__66);opacity:0;transform:scale(.9);animation:popc .8s cubic-bezier(.2,1.3,.4,1) 1.7s forwards}62    .cta{position:absolute;left:90px;right:90px;bottom:150px;opacity:0;animation:fu .8s ease 2.2s forwards}63    .cta .go{font-size:46px;font-weight:900;color:__A__}64    .cta .brand{font-size:30px;font-weight:700;color:#ffffff88;letter-spacing:.12em;margin-top:6px}65    .sig{position:absolute;left:90px;bottom:88px;font-size:26px;color:#ffffff66;font-weight:600;opacity:0;animation:fu .8s ease 2.5s forwards}66    @keyframes fu{to{opacity:1;transform:translateY(0)}}67    @keyframes pop{to{opacity:1;transform:rotate(-4deg) scale(1)}}68    @keyframes popc{to{opacity:1;transform:scale(1)}}69    """70    css = (css.replace("__H__",str(H)).replace("__FONT__",FONT).replace("__G2__",p["g2"])71              .replace("__PH__",str(int(H*0.62))).replace("__IMG__",img).replace("__A__",p["a"]))72    body = (73      '<div class="photo"></div><div class="scrim"></div>'74      '<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · À DÉCOUVRIR</div>'75      '<div class="kabadge">KA</div>'76      '<div class="info"><div class="ptitle">'+title+'</div>'77      + (('<div class="psub">'+sub+'</div>') if sub else '')78      + '<div class="chips">'+chips+'</div>'79      + (('<div class="price">'+price+'</div>') if price else '')80      + '</div>'81      '<div class="cta"><div class="go">\U0001f449 www.'+ins["site"]+'.com</div><div class="brand">GROUPE ·KA</div></div>'82      '<div class="sig">✦ Publié par l\'Agent KA</div>'83    )84    return "<!doctype html><html><head><meta charset=\"utf-8\"><style>"+css+"</style></head><body>"+body+"</body></html>"8586def build_list(ins):87    H=HREEL; p=pal(ins["site"]); items=ins.get("list") or []88    rows=""89    for idx,it in enumerate(items[:5]):90        img=it.get("image_data") or ""91        thumb='<div class="lthumb" style="background-image:url(\'' + img + '\')"></div>' if img else '<div class="lthumb"></div>'92        price=esc(it.get("price","") or ""); pr=('<div class="lp">' + price + '</div>') if price else ''93        rows+=('<div class="lrow" style="animation-delay:' + str(round(1.0+idx*0.2,2)) + 's">' + thumb94               + '<div class="lmid"><div class="lt">' + esc(it.get("title","")) + '</div>'95               + '<div class="ls">' + esc(it.get("subtitle","") or "") + '</div></div>' + pr + '</div>')96    heading=esc(ins.get("list_title","") or ins.get("headline_label","") or "À la une")97    css="""98    *{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased}99    html,body{width:1080px;height:__H__px;overflow:hidden;font-family:__FONT__;color:#fff}100    .stage{position:relative;width:1080px;height:__H__px;overflow:hidden;101      background:radial-gradient(1300px 900px at 78% -5%, __A__55,transparent 55%),102                 radial-gradient(1100px 1000px at -12% 105%, __B__3a,transparent 55%),103                 linear-gradient(160deg,__G1__,__G2__);background-size:150% 150%;animation:drift 14s ease-in-out infinite alternate}104    @keyframes drift{0%{background-position:0% 0%}100%{background-position:100% 100%}}105    .pad{position:absolute;inset:0;padding:120px 80px 210px;display:flex;flex-direction:column}106    .eyebrow{display:flex;align-items:center;gap:16px;font-weight:800;letter-spacing:.16em;font-size:27px;text-transform:uppercase;color:__A__;opacity:0;animation:fu .8s ease .15s forwards}107    .eyebrow .dot{width:52px;height:6px;border-radius:3px;background:__A__}108    .kabadge{position:absolute;top:118px;right:80px;background:__A__;color:#0d0d0d;font-weight:900;font-size:52px;padding:10px 24px;border-radius:20px;transform:rotate(-4deg) scale(.4);opacity:0;animation:pop .7s cubic-bezier(.2,1.4,.4,1) .35s forwards}109    .site{font-size:82px;font-weight:900;letter-spacing:-.02em;margin-top:28px;line-height:1;opacity:0;animation:fu .8s ease .45s forwards}110    .ltitle{font-size:46px;font-weight:800;margin-top:10px;margin-bottom:26px;max-width:920px;line-height:1.1;opacity:0;animation:fu .8s ease .65s forwards}111    .lwrap{display:flex;flex-direction:column;gap:18px}112    .lrow{display:flex;align-items:center;gap:22px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);border-radius:24px;padding:18px 20px;opacity:0;transform:translateY(30px);animation:fu .7s ease forwards;backdrop-filter:blur(6px)}113    .lthumb{width:150px;height:150px;flex:0 0 150px;border-radius:18px;background-color:#222;background-size:cover;background-position:center}114    .lmid{flex:1;min-width:0}115    .lt{font-size:40px;font-weight:800;line-height:1.12;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}116    .ls{font-size:30px;font-weight:600;color:#ffffffa8;margin-top:8px}117    .lp{font-size:46px;font-weight:900;color:__A__;white-space:nowrap;text-align:right}118    .cta{position:absolute;left:80px;right:80px;bottom:150px;opacity:0;animation:fu .8s ease 2.4s forwards}119    .cta .go{font-size:46px;font-weight:900;color:__A__}120    .cta .brand{font-size:30px;font-weight:700;color:#ffffff8c;letter-spacing:.12em;margin-top:6px}121    .sig{position:absolute;left:80px;bottom:92px;font-size:26px;color:#ffffff6a;font-weight:600;opacity:0;animation:fu .8s ease 2.7s forwards}122    .prog{position:absolute;left:0;bottom:0;height:8px;background:__A__;width:0;animation:prog __DUR__s linear .2s forwards;box-shadow:0 0 20px __A__}123    @keyframes prog{to{width:100%}}124    @keyframes fu{to{opacity:1;transform:translateY(0)}}125    @keyframes pop{to{opacity:1;transform:rotate(-4deg) scale(1)}}126    """127    css=(css.replace("__H__",str(H)).replace("__FONT__",FONT).replace("__A__",p["a"])128            .replace("__B__",p["b"]).replace("__G1__",p["g1"]).replace("__G2__",p["g2"]).replace("__DUR__",str(DUR)))129    body=('<div class="stage"><div class="pad">'130        + '<div class="eyebrow"><span class="dot"></span>' + esc(ins["label"].upper()) + ' · ' + today().upper() + '</div>'131        + '<div class="kabadge">KA</div>'132        + '<div class="site">' + esc(ins["label"]) + '</div>'133        + '<div class="ltitle">' + heading + '</div>'134        + '<div class="lwrap">' + rows + '</div>'135        + '<div class="cta"><div class="go">\U0001f449 www.' + ins["site"] + '.com</div><div class="brand">GROUPE ·KA</div></div>'136        + '<div class="sig">✦ Rédigé et publié par l\'Agent KA</div><div class="prog"></div>'137        + '</div></div>')138    return "<!doctype html><html><head><meta charset=\"utf-8\"><style>" + css + "</style></head><body>" + body + "</body></html>"139140def build_html(ins):141    if ins.get('genre')=='list' and ins.get('list'):142        return build_list(ins)143    if ins.get('genre')=='spotlight' and (ins.get('product') or {}).get('image_data'):144        return build_spotlight(ins)145    H=HREEL; p=pal(ins["site"])146    tiles=ins.get("tiles") or []147    hl=str(ins["headline"]); L=len(hl)148    numsize = 150 if L>=9 else (185 if L>=7 else (235 if L>=5 else 300))149    tiles_html="".join('<div class="tile t'+str(i)+'"><div class="tv" data-fit>'+esc(v)+'</div><div class="tl">'+esc(l)+'</div></div>' for i,(v,l) in enumerate(tiles))150    css = """151    *{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased}152    html,body{width:1080px;height:__H__px;overflow:hidden;font-family:__FONT__;color:#fff}153    .stage{position:relative;width:1080px;height:__H__px;overflow:hidden;154      background:radial-gradient(1300px 900px at 78% -5%, __A__55,transparent 55%),155                 radial-gradient(1100px 1000px at -12% 105%, __B__3a,transparent 55%),156                 linear-gradient(160deg,__G1__,__G2__);background-size:150% 150%;animation:drift 14s ease-in-out infinite alternate}157    @keyframes drift{0%{background-position:0% 0%}100%{background-position:100% 100%}}158    .mesh{position:absolute;inset:-20%;background:radial-gradient(closest-side, __A__22, transparent 70%);159      filter:blur(30px);animation:float 16s ease-in-out infinite alternate;opacity:.7}160    @keyframes float{0%{transform:translate(-6%, -4%) scale(1)}100%{transform:translate(8%, 6%) scale(1.15)}}161    .pad{position:absolute;inset:0;padding:120px 90px 250px;display:flex;flex-direction:column}162    .eyebrow{display:flex;align-items:center;gap:16px;font-weight:800;letter-spacing:.16em;font-size:27px;163      text-transform:uppercase;color:__A__;opacity:0;animation:fadeUp .8s ease .15s forwards}164    .eyebrow .dot{width:52px;height:6px;border-radius:3px;background:__A__}165    .kabadge{position:absolute;top:118px;right:90px;background:__A__;color:#0d0d0d;font-weight:900;166      font-size:54px;padding:10px 26px;border-radius:20px;transform:rotate(-4deg) scale(.4);opacity:0;167      animation:pop .7s cubic-bezier(.2,1.4,.4,1) .35s forwards;box-shadow:0 16px 46px __A__66}168    .site{font-size:112px;font-weight:900;letter-spacing:-.02em;margin-top:34px;line-height:1;opacity:0;animation:fadeUp .8s ease .45s forwards}169    .tag{font-size:38px;font-weight:600;color:#ffffffb0;margin-top:12px;opacity:0;animation:fadeUp .8s ease .65s forwards}170    .mid{flex:1;display:flex;flex-direction:column;justify-content:center;min-height:0}171    .numwrap{position:relative;display:inline-block;align-self:flex-start;opacity:0;transform:translateY(40px) scale(.92);animation:heroIn .95s cubic-bezier(.2,1,.3,1) .95s forwards}172    .num{font-size:__NUM__px;font-weight:900;line-height:.92;letter-spacing:-.03em;white-space:nowrap;173      background:linear-gradient(180deg,#fff,__A__ 135%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;174      filter:drop-shadow(0 18px 55px __A__55)}175    .numwrap::after{content:"";position:absolute;top:0;left:-60%;width:45%;height:100%;176      background:linear-gradient(100deg,transparent,rgba(255,255,255,.55),transparent);transform:skewX(-18deg);177      animation:shine 2.6s ease-in-out 1.6s infinite}178    @keyframes shine{0%{left:-60%}55%{left:130%}100%{left:130%}}179    .lab{font-size:54px;font-weight:700;margin-top:26px;max-width:920px;line-height:1.12;opacity:0;animation:fadeUp .8s ease 1.35s forwards}180    .tiles{display:flex;gap:20px;margin-top:56px}181    .tile{flex:1 1 0;min-width:0;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);182      border-radius:26px;padding:28px 22px;opacity:0;transform:translateY(30px);animation:fadeUp .7s ease forwards;backdrop-filter:blur(6px)}183    .t0{animation-delay:1.7s}.t1{animation-delay:1.85s}.t2{animation-delay:2.0s}.t3{animation-delay:2.15s}184    .tv{font-size:58px;font-weight:900;color:__A__;white-space:nowrap;line-height:1}185    .tl{font-size:27px;font-weight:600;color:#ffffffa8;margin-top:10px;line-height:1.15}186    .cta{position:absolute;left:90px;right:90px;bottom:150px;opacity:0;animation:fadeUp .8s ease 2.5s forwards}187    .cta .go{font-size:46px;font-weight:900;color:__A__}188    .cta .brand{font-size:30px;font-weight:700;color:#ffffff8c;letter-spacing:.12em;margin-top:6px}189    .sig{position:absolute;left:90px;bottom:92px;font-size:26px;color:#ffffff6a;font-weight:600;opacity:0;animation:fadeUp .8s ease 2.8s forwards}190    .prog{position:absolute;left:0;bottom:0;height:8px;background:__A__;width:0;animation:prog __DUR__s linear .2s forwards;box-shadow:0 0 20px __A__}191    @keyframes prog{to{width:100%}}192    @keyframes fadeUp{to{opacity:1;transform:translateY(0)}}193    @keyframes pop{to{opacity:1;transform:rotate(-4deg) scale(1)}}194    @keyframes heroIn{to{opacity:1;transform:translateY(0) scale(1)}}195    .vign{position:absolute;inset:0;pointer-events:none;background:radial-gradient(120% 90% at 50% 42%, transparent 55%, rgba(0,0,0,.55) 100%)}196    .numwrap{animation:heroIn .95s cubic-bezier(.2,1,.3,1) .95s forwards, pulse 3s ease-in-out 2s infinite}197    @keyframes pulse{0%,100%{filter:none}50%{filter:brightness(1.12)}}198    """199    css=(css.replace("__H__",str(H)).replace("__FONT__",FONT).replace("__A__",p["a"])200            .replace("__B__",p["b"]).replace("__G1__",p["g1"]).replace("__G2__",p["g2"])201            .replace("__NUM__",str(numsize)).replace("__DUR__",str(DUR)))202    tag = ('<div class="tag">'+esc(ins.get("tagline",""))+'</div>') if ins.get("tagline") else ""203    tiles_block = ('<div class="tiles">'+tiles_html+'</div>') if tiles else ""204    body = ('<div class="stage"><div class="mesh"></div><div class="vign"></div><div class="pad">'205      '<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · '+today().upper()+'</div>'206      '<div class="kabadge">KA</div>'207      '<div class="site">'+esc(ins["label"])+'</div>'+tag+208      '<div class="mid">'209        '<div class="numwrap"><div class="num" id="num" data-target="'+esc(ins["headline"])+'">'+esc(ins["headline"])+'</div></div>'210        '<div class="lab">'+esc(ins["headline_label"])+'</div>'+tiles_block+211      '</div>'212      '<div class="cta"><div class="go">👉 www.'+ins["site"]+'.com</div><div class="brand">GROUPE ·KA</div></div>'213      '<div class="sig">✦ Rédigé et publié par l\'Agent KA</div>'214      '<div class="prog"></div>'215      '</div></div>')216    script = """<script>217    (function(){218      // ajuste la taille des valeurs de tuiles pour tenir sur une ligne219      document.querySelectorAll('.tv[data-fit]').forEach(function(el){220        var s=58; el.style.fontSize=s+'px';221        while(el.scrollWidth>el.clientWidth && s>26){ s-=3; el.style.fontSize=s+'px'; }222      });223      var el=document.getElementById('num'); if(!el) return; var raw=el.getAttribute('data-target');224      if(/[,%\/]/.test(raw)) return;225      var m=raw.match(/^[\d\u00a0\u202f ]+/); if(!m) return;226      var digits=m[0].replace(/[^\d]/g,''); if(digits.length<3) return;227      var target=parseInt(digits,10); var suffix=raw.slice(m[0].length);228      var start=null, dur=1400, begin=1000;229      function fmt(n){return n.toLocaleString('fr-CA').replace(/[\u00a0\u202f,]/g,' ')}230      function step(ts){ if(!start)start=ts; var t=ts-start;231        if(t<begin){requestAnimationFrame(step);return;}232        var k=Math.min(1,(t-begin)/dur); var e=1-Math.pow(1-k,3);233        el.textContent=fmt(Math.round(target*e))+suffix;234        if(k<1)requestAnimationFrame(step);235      }236      el.textContent=fmt(0)+suffix; requestAnimationFrame(step);237    })();238    </script>"""239    return "<!doctype html><html><head><meta charset=\"utf-8\"><style>"+css+"</style></head><body>"+body+script+"</body></html>"240241def record_webm(html_path, out_webm, tmpdir):242    from playwright.sync_api import sync_playwright243    with sync_playwright() as pw:244        b=pw.chromium.launch(args=["--no-sandbox"])245        ctx=b.new_context(viewport={"width":1080,"height":HREEL}, device_scale_factor=2,246                          record_video_dir=tmpdir, record_video_size={"width":1080,"height":HREEL})247        pg=ctx.new_page()248        pg.goto("file://"+html_path)249        pg.wait_for_timeout(int(DUR*1000))250        vid=pg.video251        ctx.close(); b.close()252        path=vid.path()253    os.replace(path, out_webm)254255def make_music(dur, out_wav):256    """Lit musical riche : progression Am-F-C-G (pad) + basse + kick 4-temps + shaker257    + arpège/pluck brillant, avec réverbe/compresseur/limiteur."""258    tmpdir=os.path.dirname(out_wav)259    chords=[(220.00,261.63,329.63),(174.61,220.00,261.63),(261.63,329.63,392.00),(196.00,246.94,293.66)]260    roots=[110.00,87.31,130.81,98.00]261    seg=round(dur/len(chords),3)262    def concat(paths,outp):263        lst=outp+".txt"; open(lst,"w").write("".join("file '"+x+"'\n" for x in paths))264        subprocess.run([FFMPEG,"-y","-f","concat","-safe","0","-i",lst,"-c","copy",outp],capture_output=True)265    # pad (accords)266    padparts=[]267    for i,(f1,f2,f3) in enumerate(chords):268        w=os.path.join(tmpdir,"ch%d.wav"%i)269        subprocess.run([FFMPEG,"-y","-f","lavfi","-i","sine=frequency=%s:duration=%s"%(f1,seg),270            "-f","lavfi","-i","sine=frequency=%s:duration=%s"%(f2,seg),271            "-f","lavfi","-i","sine=frequency=%s:duration=%s"%(f3,seg),272            "-filter_complex","[0][1][2]amix=inputs=3:normalize=0,volume=0.20,lowpass=f=1200,afade=t=in:st=0:d=0.3,afade=t=out:st=%s:d=0.4[a]"%(seg-0.4),273            "-map","[a]",w],capture_output=True); padparts.append(w)274    pad=os.path.join(tmpdir,"pad.wav"); concat(padparts,pad)275    # basse (fondamentale par accord)276    bassparts=[]277    for i,r in enumerate(roots):278        w=os.path.join(tmpdir,"b%d.wav"%i)279        subprocess.run([FFMPEG,"-y","-f","lavfi","-i","sine=frequency=%s:duration=%s"%(r,seg),280            "-af","volume=0.34,lowpass=f=320,afade=t=in:st=0:d=0.06,afade=t=out:st=%s:d=0.12"%(seg-0.15),281            w],capture_output=True); bassparts.append(w)282    bass=os.path.join(tmpdir,"bass.wav"); concat(bassparts,bass)283    # kick 4-temps284    kick=os.path.join(tmpdir,"kick.wav")285    subprocess.run([FFMPEG,"-y","-f","lavfi","-i","aevalsrc=0.75*sin(2*PI*55*t)*exp(-9*mod(t\,0.5)):d=%s:s=44100"%dur,kick],capture_output=True)286    # shaker/hat contretemps287    hat=os.path.join(tmpdir,"hat.wav")288    subprocess.run([FFMPEG,"-y","-f","lavfi","-i","aevalsrc=0.10*(random(0)*2-1)*exp(-45*mod(t+0.25\,0.5)):d=%s:s=44100"%dur,"-af","highpass=f=6500",hat],capture_output=True)289    # arpège/pluck brillant (croches, deux notes en alternance)290    arp=os.path.join(tmpdir,"arp.wav")291    subprocess.run([FFMPEG,"-y","-f","lavfi",292        "-i","aevalsrc=0.13*sin(2*PI*880*t)*exp(-13*mod(t\,0.25))+0.10*sin(2*PI*659.25*(t-0.125))*exp(-13*mod(t+0.125\,0.25)):d=%s:s=44100"%dur,293        "-af","highpass=f=350,lowpass=f=6000",arp],capture_output=True)294    # mix final295    subprocess.run([FFMPEG,"-y","-i",pad,"-i",bass,"-i",kick,"-i",hat,"-i",arp,296        "-filter_complex",297        "[0]volume=1.0[p];[1]volume=1.0[b];[2]volume=0.9[k];[3]volume=0.5[h];[4]volume=0.55[ar];"298        "[p][b][k][h][ar]amix=inputs=5:normalize=0,"299        "aecho=0.8:0.88:70:0.22,"300        "acompressor=threshold=-18dB:ratio=3.5:attack=6:release=160,"301        "highpass=f=32,alimiter=limit=0.94,"302        "afade=t=in:st=0:d=0.5,afade=t=out:st=%s:d=1.6[a]"%(dur-1.6),303        "-map","[a]","-ar","44100",out_wav],capture_output=True)304305def render(ins, out_dir):306    os.makedirs(out_dir,exist_ok=True)307    prod=ins.get('product')308    if prod and prod.get('image') and not prod.get('image_data'):309        du=data_uri(prod['image'])310        if du: prod['image_data']=du311    for it in (ins.get('list') or []):312        if it.get('image') and not it.get('image_data'):313            du=data_uri(it['image'])314            if du: it['image_data']=du315    key=hashlib.md5((ins['site']+ins.get('insight_id','')+'reel').encode()).hexdigest()[:8]316    tmp=os.path.join(out_dir,"_reeltmp_"+key); os.makedirs(tmp,exist_ok=True)317    html_path=os.path.join(tmp,"reel.html"); open(html_path,"w",encoding="utf-8").write(build_html(ins))318    webm=os.path.join(tmp,"reel.webm"); record_webm(html_path,webm,tmp)319    wav=os.path.join(tmp,"bed.wav"); make_music(DUR,wav)320    out=os.path.join(out_dir,f"reel-{ins['site']}-{ins.get('insight_id','x')}.mp4")321    subprocess.run([FFMPEG,"-y","-i",webm,"-i",wav,322        "-filter_complex","[0:v]scale=1080:"+str(HREEL)+":force_original_aspect_ratio=increase,crop=1080:"+str(HREEL)+",fps=30,format=yuv420p[v]",323        "-map","[v]","-map","1:a","-c:v","libx264","-preset","slow","-crf","18",324        "-c:a","aac","-b:a","160k","-pix_fmt","yuv420p","-movflags","+faststart","-shortest",out],325        capture_output=True)326    # nettoyage327    for f in glob.glob(os.path.join(tmp,"*")):328        try: os.remove(f)329        except: pass330    try: os.rmdir(tmp)331    except: pass332    if not os.path.exists(out): raise SystemExit("mp4 non produit")333    return out334335if __name__=="__main__":336    ins=json.load(sys.stdin)337    out_dir=sys.argv[1] if len(sys.argv)>1 else "/tmp/ka-reels"338    print(render(ins,out_dir))339
Files · 29e97596a60d41d7ca5ebbc1d45f784e244c0ab9
Menu

Theme