| 1 |
1 |
#!/usr/bin/env python3 |
| 2 |
|
−"""Génère un Reel vertical animé (1080x1920) + musique à partir d'un insight KA. |
|
2 |
+"""Reel KA v4 — mini-clip vertical MULTI-SCÈNES (1080x1920) + musique procédurale. |
| 3 |
3 |
|
| 4 |
|
−HTML animé (CSS/JS) -> enregistrement vidéo Playwright (Chromium) -> ffmpeg |
| 5 |
|
−(mp4 h264 + lit musical synthétisé). Sortie : chemin du mp4 sur stdout. |
|
4 |
+Le reel n'est plus une seule carte animée de 10 s : c'est une séquence de |
|
5 |
+scè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. |
|
8 |
+Les scènes disponibles dépendent des données (tiles/bars/list/product/fact). |
|
9 |
+ |
|
10 |
+Musique : 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`. |
|
12 |
+ |
|
13 |
+Pipeline : HTML animé -> Playwright (Chromium, enregistrement vidéo) -> ffmpeg. |
|
14 |
+Sortie : chemin du mp4 sur stdout. Env : KA_REEL_H (défaut 1920). |
| 6 |
15 |
""" |
| 7 |
|
−import json, sys, os, base64, datetime, urllib.request, ssl, subprocess, glob, hashlib |
|
16 |
+import json, sys, os, re, base64, datetime, urllib.request, ssl, subprocess, glob, hashlib |
|
17 |
+ |
|
18 |
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) |
|
19 |
+import music_engine |
| 8 |
20 |
|
| 9 |
21 |
CTX = ssl.create_default_context(); CTX.check_hostname=False; CTX.verify_mode=ssl.CERT_NONE |
| 10 |
22 |
FFMPEG = "/opt/homebrew/bin/ffmpeg" |
| 11 |
23 |
MOIS=["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"] |
| 12 |
24 |
FONT="'Avenir Next','SF Pro Display','Helvetica Neue',Helvetica,Arial,sans-serif" |
| 13 |
|
−DUR=10.0 # secondes |
| 14 |
|
−HREEL=int(os.environ.get("KA_REEL_H","1920")) |
|
25 |
+H=int(os.environ.get("KA_REEL_H","1920")) |
|
26 |
+STYLES_ORDER=["house","synthwave","lofi","funk","epic","trap"] |
| 15 |
27 |
|
| 16 |
28 |
PALETTES={ |
| 17 |
29 |
"lou-ka":{"a":"#ff6a00","b":"#ff9d4d","g1":"#2a1403","g2":"#0d0b09"}, |
| 27 |
39 |
"job-ka":{"a":"#d9f26b","b":"#7ab8ff","g1":"#0a1522","g2":"#080b0d"}, |
| 28 |
40 |
} |
| 29 |
41 |
def pal(s): return PALETTES.get(s, PALETTES["vrai-prix"]) |
|
42 |
+ |
| 30 |
43 |
def data_uri(url): |
| 31 |
|
− import base64 |
| 32 |
44 |
try: |
| 33 |
45 |
req=urllib.request.Request(url, headers={"User-Agent":"Mozilla/5.0 (Macintosh)"}) |
| 34 |
46 |
with urllib.request.urlopen(req, timeout=15, context=CTX) as r: raw=r.read() |
| 37 |
49 |
elif url.lower().endswith(".webp"): mime="image/webp" |
| 38 |
50 |
return "data:"+mime+";base64,"+base64.b64encode(raw).decode() |
| 39 |
51 |
except Exception: return None |
|
52 |
+ |
| 40 |
53 |
def esc(s): return str(s).replace("&","&").replace("<","<").replace(">",">") |
| 41 |
54 |
def today(): t=datetime.date.today(); return f"{t.day} {MOIS[t.month-1]} {t.year}" |
| 42 |
|
−import re |
| 43 |
55 |
def parse_pct(h): |
| 44 |
56 |
m=re.match(r'^\+?(\d+(?:[.,]\d+)?)\s*%$', str(h or "").strip()) |
| 45 |
57 |
return float(m.group(1).replace(",",".")) if m else None |
| 46 |
58 |
NUMRE = re.compile(r'\d[\d\u00a0\u202f ,\.]*%?\$?') |
| 47 |
59 |
|
| 48 |
|
−def build_spotlight(ins): |
| 49 |
|
− H=HREEL; p=pal(ins["site"]); prod=ins.get("product") or {} |
| 50 |
|
− img=prod.get("image_data",""); chips="".join('<span class="chip">'+esc(c)+'</span>' for c in (prod.get("meta") or [])) |
| 51 |
|
− price=esc(prod.get("price","")); title=esc(prod.get("title","")); sub=esc(prod.get("subtitle","")) |
| 52 |
|
− css = """ |
| 53 |
|
− *{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased} |
| 54 |
|
− html,body{width:1080px;height:__H__px;overflow:hidden;font-family:__FONT__;color:#fff;background:__G2__} |
| 55 |
|
− .photo{position:absolute;top:0;left:0;right:0;height:__PH__px;background:url('__IMG__') center/cover;animation:kb 10s ease-out forwards} |
| 56 |
|
− @keyframes kb{from{transform:scale(1.0)}to{transform:scale(1.12)}} |
| 57 |
|
− .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%)} |
| 58 |
|
− .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} |
| 59 |
|
− .eyebrow .dot{width:46px;height:6px;border-radius:3px;background:__A__} |
| 60 |
|
− .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} |
| 61 |
|
− .info{position:absolute;left:90px;right:90px;bottom:270px} |
| 62 |
|
− .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} |
| 63 |
|
− .psub{font-size:36px;font-weight:600;color:#ffffffc8;margin-top:14px;opacity:0;animation:fu .9s ease 1.25s forwards} |
| 64 |
|
− .chips{display:flex;gap:14px;flex-wrap:wrap;margin-top:26px} |
| 65 |
|
− .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} |
| 66 |
|
− .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} |
| 67 |
|
− .cta{position:absolute;left:90px;right:90px;bottom:150px;opacity:0;animation:fu .8s ease 2.2s forwards} |
| 68 |
|
− .cta .go{font-size:46px;font-weight:900;color:__A__} |
| 69 |
|
− .cta .brand{font-size:30px;font-weight:700;color:#ffffff88;letter-spacing:.12em;margin-top:6px} |
| 70 |
|
− .sig{position:absolute;left:90px;bottom:88px;font-size:26px;color:#ffffff66;font-weight:600;opacity:0;animation:fu .8s ease 2.5s forwards} |
| 71 |
|
− @keyframes fu{to{opacity:1;transform:translateY(0)}} |
| 72 |
|
− @keyframes pop{to{opacity:1;transform:rotate(-4deg) scale(1)}} |
| 73 |
|
− @keyframes popc{to{opacity:1;transform:scale(1)}} |
| 74 |
|
− """ |
| 75 |
|
− css = (css.replace("__H__",str(H)).replace("__FONT__",FONT).replace("__G2__",p["g2"]) |
| 76 |
|
− .replace("__PH__",str(int(H*0.62))).replace("__IMG__",img).replace("__A__",p["a"])) |
| 77 |
|
− body = ( |
| 78 |
|
− '<div class="photo"></div><div class="scrim"></div>' |
| 79 |
|
− '<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · À DÉCOUVRIR</div>' |
| 80 |
|
− '<div class="kabadge">KA</div>' |
| 81 |
|
− '<div class="info"><div class="ptitle">'+title+'</div>' |
| 82 |
|
− + (('<div class="psub">'+sub+'</div>') if sub else '') |
| 83 |
|
− + '<div class="chips">'+chips+'</div>' |
| 84 |
|
− + (('<div class="price">'+price+'</div>') if price else '') |
| 85 |
|
− + '</div>' |
| 86 |
|
− '<div class="cta"><div class="go">\U0001f449 www.'+ins["site"]+'.com</div><div class="brand">GROUPE ·KA</div></div>' |
| 87 |
|
− '<div class="sig">✦ Publié par l\'Agent KA</div>' |
| 88 |
|
− ) |
| 89 |
|
− return "<!doctype html><html><head><meta charset=\"utf-8\"><style>"+css+"</style></head><body>"+body+"</body></html>" |
| 90 |
|
− |
| 91 |
|
−def build_list(ins): |
| 92 |
|
− H=HREEL; p=pal(ins["site"]); items=ins.get("list") or [] |
| 93 |
|
− rows="" |
| 94 |
|
− for idx,it in enumerate(items[:5]): |
| 95 |
|
− img=it.get("image_data") or "" |
| 96 |
|
− thumb='<div class="lthumb" style="background-image:url(\'' + img + '\')"></div>' if img else '<div class="lthumb"></div>' |
| 97 |
|
− price=esc(it.get("price","") or ""); pr=('<div class="lp">' + price + '</div>') if price else '' |
| 98 |
|
− rows+=('<div class="lrow" style="animation-delay:' + str(round(1.0+idx*0.2,2)) + 's">' + thumb |
| 99 |
|
− + '<div class="lmid"><div class="lt">' + esc(it.get("title","")) + '</div>' |
| 100 |
|
− + '<div class="ls">' + esc(it.get("subtitle","") or "") + '</div></div>' + pr + '</div>') |
| 101 |
|
− heading=esc(ins.get("list_title","") or ins.get("headline_label","") or "À la une") |
|
60 |
+# ---------- scènes ---------- |
|
61 |
+# chaque constructeur renvoie (html, css, dur, hook_js_ou_None) |
|
62 |
+ |
|
63 |
+def sc_intro(ins,p,i): |
| 102 |
64 |
css=""" |
| 103 |
|
− *{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased} |
| 104 |
|
− html,body{width:1080px;height:__H__px;overflow:hidden;font-family:__FONT__;color:#fff} |
| 105 |
|
− .stage{position:relative;width:1080px;height:__H__px;overflow:hidden; |
| 106 |
|
− background:radial-gradient(1300px 900px at 78% -5%, __A__55,transparent 55%), |
| 107 |
|
− radial-gradient(1100px 1000px at -12% 105%, __B__3a,transparent 55%), |
| 108 |
|
− linear-gradient(160deg,__G1__,__G2__);background-size:150% 150%;animation:drift 14s ease-in-out infinite alternate} |
| 109 |
|
− @keyframes drift{0%{background-position:0% 0%}100%{background-position:100% 100%}} |
| 110 |
|
− .pad{position:absolute;inset:0;padding:120px 80px 210px;display:flex;flex-direction:column} |
| 111 |
|
− .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} |
| 112 |
|
− .eyebrow .dot{width:52px;height:6px;border-radius:3px;background:__A__} |
| 113 |
|
− .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} |
| 114 |
|
− .site{font-size:82px;font-weight:900;letter-spacing:-.02em;margin-top:28px;line-height:1;opacity:0;animation:fu .8s ease .45s forwards} |
| 115 |
|
− .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} |
| 116 |
|
− .lwrap{display:flex;flex-direction:column;gap:18px} |
| 117 |
|
− .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)} |
| 118 |
|
− .lthumb{width:150px;height:150px;flex:0 0 150px;border-radius:18px;background-color:#222;background-size:cover;background-position:center} |
| 119 |
|
− .lmid{flex:1;min-width:0} |
| 120 |
|
− .lt{font-size:40px;font-weight:800;line-height:1.12;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical} |
| 121 |
|
− .ls{font-size:30px;font-weight:600;color:#ffffffa8;margin-top:8px} |
| 122 |
|
− .lp{font-size:46px;font-weight:900;color:__A__;white-space:nowrap;text-align:right} |
| 123 |
|
− .cta{position:absolute;left:80px;right:80px;bottom:150px;opacity:0;animation:fu .8s ease 2.4s forwards} |
| 124 |
|
− .cta .go{font-size:46px;font-weight:900;color:__A__} |
| 125 |
|
− .cta .brand{font-size:30px;font-weight:700;color:#ffffff8c;letter-spacing:.12em;margin-top:6px} |
| 126 |
|
− .sig{position:absolute;left:80px;bottom:92px;font-size:26px;color:#ffffff6a;font-weight:600;opacity:0;animation:fu .8s ease 2.7s forwards} |
| 127 |
|
− .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__} |
| 128 |
|
− @keyframes prog{to{width:100%}} |
| 129 |
|
− @keyframes fu{to{opacity:1;transform:translateY(0)}} |
| 130 |
|
− @keyframes pop{to{opacity:1;transform:rotate(-4deg) scale(1)}} |
|
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}} |
| 131 |
76 |
""" |
| 132 |
|
− css=(css.replace("__H__",str(H)).replace("__FONT__",FONT).replace("__A__",p["a"]) |
| 133 |
|
− .replace("__B__",p["b"]).replace("__G1__",p["g1"]).replace("__G2__",p["g2"]).replace("__DUR__",str(DUR))) |
| 134 |
|
− body=('<div class="stage"><div class="pad">' |
| 135 |
|
− + '<div class="eyebrow"><span class="dot"></span>' + esc(ins["label"].upper()) + ' · ' + today().upper() + '</div>' |
| 136 |
|
− + '<div class="kabadge">KA</div>' |
| 137 |
|
− + '<div class="site">' + esc(ins["label"]) + '</div>' |
| 138 |
|
− + '<div class="ltitle">' + heading + '</div>' |
| 139 |
|
− + '<div class="lwrap">' + rows + '</div>' |
| 140 |
|
− + '<div class="cta"><div class="go">\U0001f449 www.' + ins["site"] + '.com</div><div class="brand">GROUPE ·KA</div></div>' |
| 141 |
|
− + '<div class="sig">✦ Rédigé et publié par l\'Agent KA</div><div class="prog"></div>' |
| 142 |
|
− + '</div></div>') |
| 143 |
|
− return "<!doctype html><html><head><meta charset=\"utf-8\"><style>" + css + "</style></head><body>" + body + "</body></html>" |
| 144 |
|
− |
| 145 |
|
−REEL_SHELL_CSS = """ |
| 146 |
|
− *{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased} |
| 147 |
|
− html,body{width:1080px;height:__H__px;overflow:hidden;font-family:__FONT__;color:#fff} |
| 148 |
|
− .stage{position:relative;width:1080px;height:__H__px;overflow:hidden; |
| 149 |
|
− background:radial-gradient(1300px 900px at 78% -5%, __A__55,transparent 55%), |
| 150 |
|
− radial-gradient(1100px 1000px at -12% 105%, __B__3a,transparent 55%), |
| 151 |
|
− linear-gradient(160deg,__G1__,__G2__);background-size:150% 150%;animation:drift 14s ease-in-out infinite alternate} |
| 152 |
|
− @keyframes drift{0%{background-position:0% 0%}100%{background-position:100% 100%}} |
| 153 |
|
− .pad{position:absolute;inset:0;padding:120px 90px 250px;display:flex;flex-direction:column} |
| 154 |
|
− .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;padding-right:230px;line-height:1.3} |
| 155 |
|
− .eyebrow .dot{width:52px;height:6px;border-radius:3px;background:__A__;flex:0 0 52px} |
| 156 |
|
− .kabadge{position:absolute;top:118px;right:90px;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;box-shadow:0 16px 46px __A__66} |
| 157 |
|
− .site{font-size:88px;font-weight:900;letter-spacing:-.02em;margin-top:30px;line-height:1;opacity:0;animation:fu .8s ease .45s forwards} |
| 158 |
|
− .cta{position:absolute;left:90px;right:90px;bottom:150px;opacity:0;animation:fu .8s ease 2.5s forwards} |
| 159 |
|
− .cta .go{font-size:46px;font-weight:900;color:__A__} |
| 160 |
|
− .cta .brand{font-size:30px;font-weight:700;color:#ffffff8c;letter-spacing:.12em;margin-top:6px} |
| 161 |
|
− .sig{position:absolute;left:90px;bottom:92px;font-size:26px;color:#ffffff6a;font-weight:600;opacity:0;animation:fu .8s ease 2.8s forwards} |
| 162 |
|
− .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__} |
| 163 |
|
− @keyframes prog{to{width:100%}} |
| 164 |
|
− @keyframes fu{to{opacity:1;transform:translateY(0)}} |
| 165 |
|
− @keyframes pop{to{opacity:1;transform:rotate(-4deg) scale(1)}} |
| 166 |
|
− .vign{position:absolute;inset:0;pointer-events:none;background:radial-gradient(120% 90% at 50% 42%, transparent 55%, rgba(0,0,0,.55) 100%)} |
| 167 |
|
−""" |
| 168 |
|
−def shell_css(p): |
| 169 |
|
− return (REEL_SHELL_CSS.replace("__H__",str(HREEL)).replace("__FONT__",FONT) |
| 170 |
|
− .replace("__A__",p["a"]).replace("__B__",p["b"]).replace("__G1__",p["g1"]) |
| 171 |
|
− .replace("__G2__",p["g2"]).replace("__DUR__",str(DUR))) |
| 172 |
|
−def shell_tail(ins): |
| 173 |
|
− return ('<div class="cta"><div class="go">\U0001f449 www.'+ins["site"]+'.com</div><div class="brand">GROUPE ·KA</div></div>' |
| 174 |
|
− '<div class="sig">✦ Rédigé et publié par l\'Agent KA</div><div class="prog"></div>') |
| 175 |
|
− |
| 176 |
|
−def build_donut(ins): |
| 177 |
|
− p=pal(ins["site"]); pct=parse_pct(ins["headline"]) or 0.0 |
|
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, None |
|
82 |
+ |
|
83 |
+def _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 None |
|
88 |
+ digits=re.sub(r'\D','',m.group(1)) |
|
89 |
+ if not digits: return None |
|
90 |
+ return digits, (m.group(2) or ""), m.group(3) or "" |
|
91 |
+ |
|
92 |
+def 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=None |
|
107 |
+ 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, hook |
|
113 |
+ |
|
114 |
+def sc_donut(ins,p,i): |
|
115 |
+ pct=parse_pct(ins["headline"]) or 0.0 |
| 178 |
116 |
C=2*3.14159*300; off=C*(1-min(100.0,pct)/100.0) |
| 179 |
|
− tiles=(ins.get("tiles") or [])[:3] |
| 180 |
|
− tiles_html="".join('<div class="tile t'+str(i)+'"><div class="tv">'+esc(v)+'</div><div class="tl">'+esc(l)+'</div></div>' for i,(v,l) in enumerate(tiles)) |
| 181 |
|
− dec=1 if (',' in str(ins["headline"]) or '.' in str(ins["headline"])) else 0 |
| 182 |
|
− svg=('<svg width="760" height="760" viewBox="0 0 760 760">' |
| 183 |
|
− '<circle cx="380" cy="380" r="300" fill="none" stroke="rgba(255,255,255,.10)" stroke-width="84"/>' |
| 184 |
|
− '<circle class="arc" cx="380" cy="380" r="300" fill="none" stroke="url(#gd)" stroke-width="84" stroke-linecap="round" transform="rotate(-90 380 380)"/>' |
| 185 |
|
− '<defs><linearGradient id="gd" x1="0" y1="0" x2="1" y2="1">' |
| 186 |
|
− '<stop offset="0" stop-color="'+p["a"]+'"/><stop offset="1" stop-color="'+p["b"]+'"/></linearGradient></defs></svg>') |
| 187 |
|
− css=shell_css(p)+""" |
| 188 |
|
− .mid{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:0} |
| 189 |
|
− .dwrap{position:relative;width:760px;height:760px;opacity:0;transform:scale(.9);animation:popc .8s cubic-bezier(.2,1.2,.4,1) .8s forwards} |
| 190 |
|
− @keyframes popc{to{opacity:1;transform:scale(1)}} |
| 191 |
|
− .arc{stroke-dasharray:__C__;stroke-dashoffset:__C__;animation:draw 1.8s cubic-bezier(.3,1,.4,1) 1.1s forwards} |
|
117 |
+ dec=1 if re.search(r'[.,]\d',str(ins["headline"])) else 0 |
|
118 |
+ 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} |
| 192 |
124 |
@keyframes draw{to{stroke-dashoffset:__OFF__}} |
| 193 |
|
− .dperc{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:150px;font-weight:900;color:__A__;letter-spacing:-.02em;filter:drop-shadow(0 10px 34px __A__55)} |
| 194 |
|
− .dlab{font-size:48px;font-weight:700;line-height:1.15;text-align:center;max-width:900px;margin-top:36px;opacity:0;animation:fu .8s ease 1.9s forwards} |
| 195 |
|
− .tiles{display:flex;gap:20px;margin-top:52px} |
| 196 |
|
− .tile{flex:1 1 0;min-width:0;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);border-radius:26px;padding:24px 20px;opacity:0;transform:translateY(30px);animation:fu .7s ease forwards;backdrop-filter:blur(6px);text-align:center} |
| 197 |
|
− .t0{animation-delay:2.2s}.t1{animation-delay:2.35s}.t2{animation-delay:2.5s} |
| 198 |
|
− .tv{font-size:50px;font-weight:900;color:__A__;white-space:nowrap;line-height:1} |
| 199 |
|
− .tl{font-size:26px;font-weight:600;color:#ffffffa8;margin-top:8px;line-height:1.15} |
| 200 |
|
− """.replace("__C__",str(round(C,1))).replace("__OFF__",str(round(off,1))).replace("__A__",p["a"]) |
| 201 |
|
− body=('<div class="stage"><div class="vign"></div><div class="pad">' |
| 202 |
|
− '<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · '+today().upper()+'</div>' |
| 203 |
|
− '<div class="kabadge">KA</div>' |
| 204 |
|
− '<div class="site">'+esc(ins["label"])+'</div>' |
| 205 |
|
− '<div class="mid"><div class="dwrap">'+svg+'<div class="dperc" id="dperc" data-pct="'+str(pct)+'" data-dec="'+str(dec)+'">0 %</div></div>' |
| 206 |
|
− '<div class="dlab">'+esc(ins["headline_label"])+'</div>' |
| 207 |
|
− +(('<div class="tiles">'+tiles_html+'</div>') if tiles else '')+'</div>' |
| 208 |
|
− +shell_tail(ins)+'</div></div>') |
| 209 |
|
− script="""<script>(function(){ |
| 210 |
|
− var el=document.getElementById('dperc'); var pct=parseFloat(el.getAttribute('data-pct')); |
| 211 |
|
− var dec=parseInt(el.getAttribute('data-dec'),10); var start=null, dur=1800, begin=1100; |
| 212 |
|
− function fmt(n){return n.toFixed(dec).replace('.',',')+' %'} |
| 213 |
|
− function step(ts){ if(!start)start=ts; var t=ts-start; |
| 214 |
|
− if(t<begin){requestAnimationFrame(step);return;} |
| 215 |
|
− var k=Math.min(1,(t-begin)/dur); var e=1-Math.pow(1-k,3); |
| 216 |
|
− el.textContent=fmt(pct*e); |
| 217 |
|
− if(k<1)requestAnimationFrame(step);} |
| 218 |
|
− requestAnimationFrame(step); |
| 219 |
|
− })();</script>""" |
| 220 |
|
− return "<!doctype html><html><head><meta charset=\"utf-8\"><style>"+css+"</style></head><body>"+body+script+"</body></html>" |
| 221 |
|
− |
| 222 |
|
−def build_quote(ins): |
| 223 |
|
− p=pal(ins["site"]) |
| 224 |
|
− fact=str(ins.get("fact") or (str(ins["headline"])+" "+ins.get("headline_label",""))) |
|
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, hook |
|
139 |
+ |
|
140 |
+def 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, None |
|
162 |
+ |
|
163 |
+def 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, None |
|
190 |
+ |
|
191 |
+def 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, None |
|
221 |
+ |
|
222 |
+def sc_quote(ins,p,i): |
|
223 |
+ fact=str(ins.get("fact") or "") |
| 225 |
224 |
words=fact.split() |
| 226 |
|
− delay=min(0.13, 5.5/max(1,len(words))) |
|
225 |
+ delay=min(0.11, 4.6/max(1,len(words))) |
| 227 |
226 |
spans="" |
| 228 |
|
− for i,w in enumerate(words): |
|
227 |
+ for k,w in enumerate(words): |
| 229 |
228 |
cls="w hl" if NUMRE.search(w) else "w" |
| 230 |
|
− spans+='<span class="'+cls+'" style="animation-delay:'+str(round(0.9+i*delay,2))+'s">'+esc(w)+'</span> ' |
| 231 |
|
− L=len(fact); fs=62 if L<170 else (54 if L<240 else 47) |
| 232 |
|
− css=shell_css(p)+""" |
| 233 |
|
− .mid{flex:1;display:flex;flex-direction:column;justify-content:center;min-height:0} |
| 234 |
|
− .qmark{font-size:280px;line-height:.5;color:__A__;font-weight:900;font-family:Georgia,serif;height:150px;opacity:0;animation:fu .7s ease .6s forwards} |
| 235 |
|
− .qtext{font-size:__FS__px;font-weight:800;line-height:1.35;letter-spacing:-.01em;max-width:920px} |
| 236 |
|
− .w{display:inline-block;opacity:0;transform:translateY(26px);animation:fu .5s ease forwards} |
| 237 |
|
− .hl{color:__A__;filter:drop-shadow(0 6px 20px __A__44)} |
| 238 |
|
− """.replace("__A__",p["a"]).replace("__FS__",str(fs)) |
| 239 |
|
− body=('<div class="stage"><div class="vign"></div><div class="pad">' |
| 240 |
|
− '<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · LE SAVIEZ-VOUS ? · '+today().upper()+'</div>' |
| 241 |
|
− '<div class="kabadge">KA</div>' |
| 242 |
|
− '<div class="site">'+esc(ins["label"])+'</div>' |
| 243 |
|
− '<div class="mid"><div class="qmark">“</div><div class="qtext">'+spans+'</div></div>' |
| 244 |
|
− +shell_tail(ins)+'</div></div>') |
| 245 |
|
− return "<!doctype html><html><head><meta charset=\"utf-8\"><style>"+css+"</style></head><body>"+body+"</body></html>" |
|
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), None |
|
243 |
+ |
|
244 |
+def 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, None |
| 246 |
263 |
|
|
264 |
+# ---------- plan des scènes ---------- |
|
265 |
+def 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 None |
|
272 |
+ genre=ins.get("genre","hero") |
|
273 |
+ photo_led=genre in ("spotlight","list","grid") and photos |
|
274 |
+ |
|
275 |
+ 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 out |
|
282 |
+ has_quote=len(str(ins.get("fact") or ""))>=40 |
|
283 |
+ |
|
284 |
+ # 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.2 |
|
286 |
+ 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.2 |
|
288 |
+ maxp=4 if photo_led else 3 |
|
289 |
+ nphotos=max(0,min(len(photos),maxp,int((34.0-base_est)//3.0))) |
|
290 |
+ tot_p=nphotos |
|
291 |
+ 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])] |
|
293 |
+ |
|
294 |
+ builders=[("intro",sc_intro)] |
|
295 |
+ if photo_led: |
|
296 |
+ builders+=photo_scenes+stat_block() |
|
297 |
+ else: |
|
298 |
+ builders+=stat_block()+photo_scenes |
|
299 |
+ if has_quote: builders.append(("quote",sc_quote)) |
|
300 |
+ builders.append(("outro",sc_outro)) |
|
301 |
+ |
|
302 |
+ 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 scenes |
|
307 |
+ |
|
308 |
+# ---------- assemblage HTML ---------- |
| 247 |
309 |
def build_html(ins): |
| 248 |
|
− g=ins.get('genre') |
| 249 |
|
− if g in ('list','grid') and ins.get('list'): |
| 250 |
|
− return build_list(ins) |
| 251 |
|
− if g=='spotlight' and (ins.get('product') or {}).get('image_data'): |
| 252 |
|
− return build_spotlight(ins) |
| 253 |
|
− if g=='donut' and parse_pct(ins.get('headline')) is not None: |
| 254 |
|
− return build_donut(ins) |
| 255 |
|
− if g=='quote' and len(str(ins.get('fact') or ''))>=40: |
| 256 |
|
− return build_quote(ins) |
| 257 |
|
− H=HREEL; p=pal(ins["site"]) |
| 258 |
|
− tiles=ins.get("tiles") or [] |
| 259 |
|
− hl=str(ins["headline"]); L=len(hl) |
| 260 |
|
− numsize = 150 if L>=9 else (185 if L>=7 else (235 if L>=5 else 300)) |
| 261 |
|
− 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)) |
| 262 |
|
− css = """ |
|
310 |
+ p=pal(ins["site"]) |
|
311 |
+ scenes=plan_scenes(ins,p) |
|
312 |
+ tot=round(sum(s["dur"] for s in scenes),2) |
|
313 |
+ shell=""" |
| 263 |
314 |
*{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased} |
| 264 |
|
− html,body{width:1080px;height:__H__px;overflow:hidden;font-family:__FONT__;color:#fff} |
|
315 |
+ html,body{width:1080px;height:__H__px;overflow:hidden;font-family:__FONT__;color:#fff;background:__G2__} |
| 265 |
316 |
.stage{position:relative;width:1080px;height:__H__px;overflow:hidden; |
| 266 |
|
− background:radial-gradient(1300px 900px at 78% -5%, __A__55,transparent 55%), |
| 267 |
|
− radial-gradient(1100px 1000px at -12% 105%, __B__3a,transparent 55%), |
| 268 |
|
− linear-gradient(160deg,__G1__,__G2__);background-size:150% 150%;animation:drift 14s ease-in-out infinite alternate} |
|
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} |
| 269 |
320 |
@keyframes drift{0%{background-position:0% 0%}100%{background-position:100% 100%}} |
| 270 |
|
− .mesh{position:absolute;inset:-20%;background:radial-gradient(closest-side, __A__22, transparent 70%); |
| 271 |
|
− filter:blur(30px);animation:float 16s ease-in-out infinite alternate;opacity:.7} |
| 272 |
|
− @keyframes float{0%{transform:translate(-6%, -4%) scale(1)}100%{transform:translate(8%, 6%) scale(1.15)}} |
| 273 |
|
− .pad{position:absolute;inset:0;padding:120px 90px 250px;display:flex;flex-direction:column} |
| 274 |
|
− .eyebrow{display:flex;align-items:center;gap:16px;font-weight:800;letter-spacing:.16em;font-size:27px; |
| 275 |
|
− text-transform:uppercase;color:__A__;opacity:0;animation:fadeUp .8s ease .15s forwards} |
| 276 |
|
− .eyebrow .dot{width:52px;height:6px;border-radius:3px;background:__A__} |
| 277 |
|
− .kabadge{position:absolute;top:118px;right:90px;background:__A__;color:#0d0d0d;font-weight:900; |
| 278 |
|
− font-size:54px;padding:10px 26px;border-radius:20px;transform:rotate(-4deg) scale(.4);opacity:0; |
|
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; |
| 279 |
327 |
animation:pop .7s cubic-bezier(.2,1.4,.4,1) .35s forwards;box-shadow:0 16px 46px __A__66} |
| 280 |
|
− .site{font-size:112px;font-weight:900;letter-spacing:-.02em;margin-top:34px;line-height:1;opacity:0;animation:fadeUp .8s ease .45s forwards} |
| 281 |
|
− .tag{font-size:38px;font-weight:600;color:#ffffffb0;margin-top:12px;opacity:0;animation:fadeUp .8s ease .65s forwards} |
| 282 |
|
− .mid{flex:1;display:flex;flex-direction:column;justify-content:center;min-height:0} |
| 283 |
|
− .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} |
| 284 |
|
− .num{font-size:__NUM__px;font-weight:900;line-height:.92;letter-spacing:-.03em;white-space:nowrap; |
| 285 |
|
− background:linear-gradient(180deg,#fff,__A__ 135%);-webkit-background-clip:text;-webkit-text-fill-color:transparent; |
| 286 |
|
− filter:drop-shadow(0 18px 55px __A__55)} |
| 287 |
|
− .numwrap::after{content:"";position:absolute;top:0;left:-60%;width:45%;height:100%; |
| 288 |
|
− background:linear-gradient(100deg,transparent,rgba(255,255,255,.55),transparent);transform:skewX(-18deg); |
| 289 |
|
− animation:shine 2.6s ease-in-out 1.6s infinite} |
| 290 |
|
− @keyframes shine{0%{left:-60%}55%{left:130%}100%{left:130%}} |
| 291 |
|
− .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} |
| 292 |
|
− .tiles{display:flex;gap:20px;margin-top:56px} |
| 293 |
|
− .tile{flex:1 1 0;min-width:0;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15); |
| 294 |
|
− border-radius:26px;padding:28px 22px;opacity:0;transform:translateY(30px);animation:fadeUp .7s ease forwards;backdrop-filter:blur(6px)} |
| 295 |
|
− .t0{animation-delay:1.7s}.t1{animation-delay:1.85s}.t2{animation-delay:2.0s}.t3{animation-delay:2.15s} |
| 296 |
|
− .tv{font-size:58px;font-weight:900;color:__A__;white-space:nowrap;line-height:1} |
| 297 |
|
− .tl{font-size:27px;font-weight:600;color:#ffffffa8;margin-top:10px;line-height:1.15} |
| 298 |
|
− .cta{position:absolute;left:90px;right:90px;bottom:150px;opacity:0;animation:fadeUp .8s ease 2.5s forwards} |
| 299 |
|
− .cta .go{font-size:46px;font-weight:900;color:__A__} |
| 300 |
|
− .cta .brand{font-size:30px;font-weight:700;color:#ffffff8c;letter-spacing:.12em;margin-top:6px} |
| 301 |
|
− .sig{position:absolute;left:90px;bottom:92px;font-size:26px;color:#ffffff6a;font-weight:600;opacity:0;animation:fadeUp .8s ease 2.8s forwards} |
| 302 |
|
− .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__} |
|
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__} |
| 303 |
332 |
@keyframes prog{to{width:100%}} |
| 304 |
|
− @keyframes fadeUp{to{opacity:1;transform:translateY(0)}} |
|
333 |
+ @keyframes fu{to{opacity:1;transform:translateY(0)}} |
| 305 |
334 |
@keyframes pop{to{opacity:1;transform:rotate(-4deg) scale(1)}} |
| 306 |
|
− @keyframes heroIn{to{opacity:1;transform:translateY(0) scale(1)}} |
| 307 |
|
− .vign{position:absolute;inset:0;pointer-events:none;background:radial-gradient(120% 90% at 50% 42%, transparent 55%, rgba(0,0,0,.55) 100%)} |
| 308 |
|
− .numwrap{animation:heroIn .95s cubic-bezier(.2,1,.3,1) .95s forwards, pulse 3s ease-in-out 2s infinite} |
| 309 |
|
− @keyframes pulse{0%,100%{filter:none}50%{filter:brightness(1.12)}} |
|
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}} |
| 310 |
343 |
""" |
| 311 |
|
− css=(css.replace("__H__",str(H)).replace("__FONT__",FONT).replace("__A__",p["a"]) |
| 312 |
|
− .replace("__B__",p["b"]).replace("__G1__",p["g1"]).replace("__G2__",p["g2"]) |
| 313 |
|
− .replace("__NUM__",str(numsize)).replace("__DUR__",str(DUR))) |
| 314 |
|
− tag = ('<div class="tag">'+esc(ins.get("tagline",""))+'</div>') if ins.get("tagline") else "" |
| 315 |
|
− tiles_block = ('<div class="tiles">'+tiles_html+'</div>') if tiles else "" |
| 316 |
|
− body = ('<div class="stage"><div class="mesh"></div><div class="vign"></div><div class="pad">' |
| 317 |
|
− '<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · '+today().upper()+'</div>' |
| 318 |
|
− '<div class="kabadge">KA</div>' |
| 319 |
|
− '<div class="site">'+esc(ins["label"])+'</div>'+tag+ |
| 320 |
|
− '<div class="mid">' |
| 321 |
|
− '<div class="numwrap"><div class="num" id="num" data-target="'+esc(ins["headline"])+'">'+esc(ins["headline"])+'</div></div>' |
| 322 |
|
− '<div class="lab">'+esc(ins["headline_label"])+'</div>'+tiles_block+ |
| 323 |
|
− '</div>' |
| 324 |
|
− '<div class="cta"><div class="go">👉 www.'+ins["site"]+'.com</div><div class="brand">GROUPE ·KA</div></div>' |
| 325 |
|
− '<div class="sig">✦ Rédigé et publié par l\'Agent KA</div>' |
| 326 |
|
− '<div class="prog"></div>' |
| 327 |
|
− '</div></div>') |
| 328 |
|
− script = """<script> |
| 329 |
|
− (function(){ |
| 330 |
|
− // ajuste la taille des valeurs de tuiles pour tenir sur une ligne |
| 331 |
|
− document.querySelectorAll('.tv[data-fit]').forEach(function(el){ |
| 332 |
|
− var s=58; el.style.fontSize=s+'px'; |
| 333 |
|
− while(el.scrollWidth>el.clientWidth && s>26){ s-=3; el.style.fontSize=s+'px'; } |
|
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)))) |
|
347 |
+ |
|
348 |
+ 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ène |
|
354 |
+ 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]=hook |
|
360 |
+ |
|
361 |
+ 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; |
| 334 |
385 |
}); |
| 335 |
|
− var el=document.getElementById('num'); if(!el) return; var raw=el.getAttribute('data-target'); |
| 336 |
|
− if(/[,%\/]/.test(raw)) return; |
| 337 |
|
− var m=raw.match(/^[\d\u00a0\u202f ]+/); if(!m) return; |
| 338 |
|
− var digits=m[0].replace(/[^\d]/g,''); if(digits.length<3) return; |
| 339 |
|
− var target=parseInt(digits,10); var suffix=raw.slice(m[0].length); |
| 340 |
|
− var start=null, dur=1400, begin=1000; |
| 341 |
|
− function fmt(n){return n.toLocaleString('fr-CA').replace(/[\u00a0\u202f,]/g,' ')} |
| 342 |
|
− function step(ts){ if(!start)start=ts; var t=ts-start; |
| 343 |
|
− if(t<begin){requestAnimationFrame(step);return;} |
| 344 |
|
− var k=Math.min(1,(t-begin)/dur); var e=1-Math.pow(1-k,3); |
| 345 |
|
− el.textContent=fmt(Math.round(target*e))+suffix; |
| 346 |
|
− if(k<1)requestAnimationFrame(step); |
| 347 |
|
− } |
| 348 |
|
− el.textContent=fmt(0)+suffix; requestAnimationFrame(step); |
| 349 |
386 |
})(); |
| 350 |
|
− </script>""" |
| 351 |
|
− return "<!doctype html><html><head><meta charset=\"utf-8\"><style>"+css+"</style></head><body>"+body+script+"</body></html>" |
|
387 |
+ </script>""".replace("__SC__",json.dumps(sc_list)).replace("__HOOKS__",hooks_js) |
|
388 |
+ |
|
389 |
+ 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, tot |
| 352 |
395 |
|
| 353 |
|
−def record_webm(html_path, out_webm, tmpdir): |
|
396 |
+# ---------- enregistrement + musique + mux ---------- |
|
397 |
+def record_webm(html_path, out_webm, tmpdir, dur): |
| 354 |
398 |
from playwright.sync_api import sync_playwright |
| 355 |
399 |
with sync_playwright() as pw: |
| 356 |
400 |
b=pw.chromium.launch(args=["--no-sandbox"]) |
| 357 |
|
− ctx=b.new_context(viewport={"width":1080,"height":HREEL}, device_scale_factor=2, |
| 358 |
|
− record_video_dir=tmpdir, record_video_size={"width":1080,"height":HREEL}) |
|
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}) |
| 359 |
403 |
pg=ctx.new_page() |
| 360 |
404 |
pg.goto("file://"+html_path) |
| 361 |
|
− pg.wait_for_timeout(int(DUR*1000)) |
|
405 |
+ pg.wait_for_timeout(int((dur+0.3)*1000)) |
| 362 |
406 |
vid=pg.video |
| 363 |
407 |
ctx.close(); b.close() |
| 364 |
408 |
path=vid.path() |
| 365 |
409 |
os.replace(path, out_webm) |
| 366 |
410 |
|
| 367 |
|
−# 4 ambiances musicales en rotation (progression, basse, tempo du kick, notes d'arpège) |
| 368 |
|
−MOODS=[ |
| 369 |
|
− {"name":"uplift","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)], |
| 370 |
|
− "roots":[110.00,87.31,130.81,98.00],"beat":0.5,"arp":(880.0,659.25)}, # Am F C G |
| 371 |
|
− {"name":"chill","chords":[(261.63,329.63,392.00),(220.00,261.63,329.63),(174.61,220.00,261.63),(196.00,246.94,293.66)], |
| 372 |
|
− "roots":[130.81,110.00,87.31,98.00],"beat":0.62,"arp":(783.99,659.25)}, # C Am F G |
| 373 |
|
− {"name":"energique","chords":[(164.81,196.00,246.94),(130.81,164.81,196.00),(196.00,246.94,293.66),(146.83,185.00,220.00)], |
| 374 |
|
− "roots":[82.41,65.41,98.00,73.42],"beat":0.4,"arp":(987.77,739.99)}, # Em C G D |
| 375 |
|
− {"name":"chaleureux","chords":[(174.61,220.00,261.63),(196.00,246.94,293.66),(220.00,261.63,329.63),(220.00,277.18,329.63)], |
| 376 |
|
− "roots":[87.31,98.00,110.00,110.00],"beat":0.55,"arp":(880.0,698.46)}, # F G Am Am |
| 377 |
|
−] |
| 378 |
|
− |
| 379 |
|
−def make_music(dur, out_wav, mood=0): |
| 380 |
|
− """Lit musical riche : progression d'accords (pad) + basse + kick + shaker |
| 381 |
|
− + arpège/pluck brillant, avec réverbe/compresseur/limiteur. `mood` choisit l'ambiance.""" |
| 382 |
|
− tmpdir=os.path.dirname(out_wav) |
| 383 |
|
− M=MOODS[int(mood)%len(MOODS)] |
| 384 |
|
− chords=M["chords"]; roots=M["roots"]; beat=M["beat"]; arpf=M["arp"] |
| 385 |
|
− seg=round(dur/len(chords),3) |
| 386 |
|
− def concat(paths,outp): |
| 387 |
|
− lst=outp+".txt"; open(lst,"w").write("".join("file '"+x+"'\n" for x in paths)) |
| 388 |
|
− subprocess.run([FFMPEG,"-y","-f","concat","-safe","0","-i",lst,"-c","copy",outp],capture_output=True) |
| 389 |
|
− # pad (accords) |
| 390 |
|
− padparts=[] |
| 391 |
|
− for i,(f1,f2,f3) in enumerate(chords): |
| 392 |
|
− w=os.path.join(tmpdir,"ch%d.wav"%i) |
| 393 |
|
− subprocess.run([FFMPEG,"-y","-f","lavfi","-i","sine=frequency=%s:duration=%s"%(f1,seg), |
| 394 |
|
− "-f","lavfi","-i","sine=frequency=%s:duration=%s"%(f2,seg), |
| 395 |
|
− "-f","lavfi","-i","sine=frequency=%s:duration=%s"%(f3,seg), |
| 396 |
|
− "-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), |
| 397 |
|
− "-map","[a]",w],capture_output=True); padparts.append(w) |
| 398 |
|
− pad=os.path.join(tmpdir,"pad.wav"); concat(padparts,pad) |
| 399 |
|
− # basse (fondamentale par accord) |
| 400 |
|
− bassparts=[] |
| 401 |
|
− for i,r in enumerate(roots): |
| 402 |
|
− w=os.path.join(tmpdir,"b%d.wav"%i) |
| 403 |
|
− subprocess.run([FFMPEG,"-y","-f","lavfi","-i","sine=frequency=%s:duration=%s"%(r,seg), |
| 404 |
|
− "-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), |
| 405 |
|
− w],capture_output=True); bassparts.append(w) |
| 406 |
|
− bass=os.path.join(tmpdir,"bass.wav"); concat(bassparts,bass) |
| 407 |
|
− # kick (tempo selon l'ambiance) |
| 408 |
|
− kick=os.path.join(tmpdir,"kick.wav") |
| 409 |
|
− subprocess.run([FFMPEG,"-y","-f","lavfi","-i","aevalsrc=0.75*sin(2*PI*55*t)*exp(-9*mod(t\,%s)):d=%s:s=44100"%(beat,dur),kick],capture_output=True) |
| 410 |
|
− # shaker/hat contretemps |
| 411 |
|
− hat=os.path.join(tmpdir,"hat.wav") |
| 412 |
|
− subprocess.run([FFMPEG,"-y","-f","lavfi","-i","aevalsrc=0.10*(random(0)*2-1)*exp(-45*mod(t+%s\,%s)):d=%s:s=44100"%(beat/2,beat,dur),"-af","highpass=f=6500",hat],capture_output=True) |
| 413 |
|
− # arpège/pluck brillant (croches, deux notes en alternance) |
| 414 |
|
− arp=os.path.join(tmpdir,"arp.wav"); e=beat/2; e2=beat/4 |
| 415 |
|
− subprocess.run([FFMPEG,"-y","-f","lavfi", |
| 416 |
|
− "-i","aevalsrc=0.13*sin(2*PI*%s*t)*exp(-13*mod(t\,%s))+0.10*sin(2*PI*%s*(t-%s))*exp(-13*mod(t+%s\,%s)):d=%s:s=44100"%(arpf[0],e,arpf[1],e2,e2,e,dur), |
| 417 |
|
− "-af","highpass=f=350,lowpass=f=6000",arp],capture_output=True) |
| 418 |
|
− # mix final |
| 419 |
|
− subprocess.run([FFMPEG,"-y","-i",pad,"-i",bass,"-i",kick,"-i",hat,"-i",arp, |
| 420 |
|
− "-filter_complex", |
| 421 |
|
− "[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];" |
| 422 |
|
− "[p][b][k][h][ar]amix=inputs=5:normalize=0," |
| 423 |
|
− "aecho=0.8:0.88:70:0.22," |
| 424 |
|
− "acompressor=threshold=-18dB:ratio=3.5:attack=6:release=160," |
| 425 |
|
− "highpass=f=32,alimiter=limit=0.94," |
| 426 |
|
− "afade=t=in:st=0:d=0.5,afade=t=out:st=%s:d=1.6[a]"%(dur-1.6), |
| 427 |
|
− "-map","[a]","-ar","44100",out_wav],capture_output=True) |
| 428 |
|
− |
| 429 |
411 |
def render(ins, out_dir): |
| 430 |
412 |
os.makedirs(out_dir,exist_ok=True) |
| 431 |
413 |
prod=ins.get('product') |
| 436 |
418 |
if it.get('image') and not it.get('image_data'): |
| 437 |
419 |
du=data_uri(it['image']) |
| 438 |
420 |
if du: it['image_data']=du |
| 439 |
|
− key=hashlib.md5((ins['site']+str(ins.get('insight_id',''))+str(ins.get('genre',''))+'reel').encode()).hexdigest()[:8] |
|
421 |
+ |
|
422 |
+ key=hashlib.md5((ins['site']+str(ins.get('insight_id',''))+str(ins.get('genre',''))+'reelv4').encode()).hexdigest()[:8] |
| 440 |
423 |
tmp=os.path.join(out_dir,"_reeltmp_"+key); os.makedirs(tmp,exist_ok=True) |
| 441 |
|
− html_path=os.path.join(tmp,"reel.html"); open(html_path,"w",encoding="utf-8").write(build_html(ins)) |
| 442 |
|
− webm=os.path.join(tmp,"reel.webm"); record_webm(html_path,webm,tmp) |
| 443 |
|
− mood=ins.get('variant') |
| 444 |
|
− if mood is None: |
| 445 |
|
− mood=int(hashlib.md5((ins['site']+str(ins.get('insight_id',''))).encode()).hexdigest(),16) |
| 446 |
|
− wav=os.path.join(tmp,"bed.wav"); make_music(DUR,wav,int(mood)) |
|
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) |
|
427 |
+ |
|
428 |
+ # 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 None |
|
432 |
+ 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)) |
|
435 |
+ |
| 447 |
436 |
out=os.path.join(out_dir,f"reel-{ins['site']}-{ins.get('insight_id','x')}.mp4") |
| 448 |
437 |
subprocess.run([FFMPEG,"-y","-i",webm,"-i",wav, |
| 449 |
|
− "-filter_complex","[0:v]scale=1080:"+str(HREEL)+":force_original_aspect_ratio=increase,crop=1080:"+str(HREEL)+",fps=30,format=yuv420p[v]", |
| 450 |
|
− "-map","[v]","-map","1:a","-c:v","libx264","-preset","slow","-crf","18", |
| 451 |
|
− "-c:a","aac","-b:a","160k","-pix_fmt","yuv420p","-movflags","+faststart","-shortest",out], |
|
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], |
| 452 |
442 |
capture_output=True) |
| 453 |
|
− # nettoyage |
| 454 |
443 |
for f in glob.glob(os.path.join(tmp,"*")): |
| 455 |
444 |
try: os.remove(f) |
| 456 |
445 |
except: pass |