| 1 | #!/usr/bin/env python3 |
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 |
4 | +Le reel n'est plus une seule carte animée de 10 s : c'est une séquence de |
| 5 | −(mp4 h264 + lit musical synthétisé). Sortie : chemin du mp4 sur stdout. |
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 | CTX = ssl.create_default_context(); CTX.check_hostname=False; CTX.verify_mode=ssl.CERT_NONE |
21 | CTX = ssl.create_default_context(); CTX.check_hostname=False; CTX.verify_mode=ssl.CERT_NONE |
| 10 | FFMPEG = "/opt/homebrew/bin/ffmpeg" |
22 | FFMPEG = "/opt/homebrew/bin/ffmpeg" |
| 11 | MOIS=["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"] |
23 | MOIS=["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"] |
| 12 | FONT="'Avenir Next','SF Pro Display','Helvetica Neue',Helvetica,Arial,sans-serif" |
24 | FONT="'Avenir Next','SF Pro Display','Helvetica Neue',Helvetica,Arial,sans-serif" |
| 13 | −DUR=10.0 # secondes |
25 | +H=int(os.environ.get("KA_REEL_H","1920")) |
| 14 | −HREEL=int(os.environ.get("KA_REEL_H","1920")) |
26 | +STYLES_ORDER=["house","synthwave","lofi","funk","epic","trap"] |
| 15 | |
27 | |
| 16 | PALETTES={ |
28 | PALETTES={ |
| 17 | "lou-ka":{"a":"#ff6a00","b":"#ff9d4d","g1":"#2a1403","g2":"#0d0b09"}, |
29 | "lou-ka":{"a":"#ff6a00","b":"#ff9d4d","g1":"#2a1403","g2":"#0d0b09"}, |
| 27 | "job-ka":{"a":"#d9f26b","b":"#7ab8ff","g1":"#0a1522","g2":"#080b0d"}, |
39 | "job-ka":{"a":"#d9f26b","b":"#7ab8ff","g1":"#0a1522","g2":"#080b0d"}, |
| 28 | } |
40 | } |
| 29 | def pal(s): return PALETTES.get(s, PALETTES["vrai-prix"]) |
41 | def pal(s): return PALETTES.get(s, PALETTES["vrai-prix"]) |
| |
42 | + |
| 30 | def data_uri(url): |
43 | def data_uri(url): |
| 31 | − import base64 |
| |
| 32 | try: |
44 | try: |
| 33 | req=urllib.request.Request(url, headers={"User-Agent":"Mozilla/5.0 (Macintosh)"}) |
45 | 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() |
46 | with urllib.request.urlopen(req, timeout=15, context=CTX) as r: raw=r.read() |
| 37 | elif url.lower().endswith(".webp"): mime="image/webp" |
49 | elif url.lower().endswith(".webp"): mime="image/webp" |
| 38 | return "data:"+mime+";base64,"+base64.b64encode(raw).decode() |
50 | return "data:"+mime+";base64,"+base64.b64encode(raw).decode() |
| 39 | except Exception: return None |
51 | except Exception: return None |
| |
52 | + |
| 40 | def esc(s): return str(s).replace("&","&").replace("<","<").replace(">",">") |
53 | def esc(s): return str(s).replace("&","&").replace("<","<").replace(">",">") |
| 41 | def today(): t=datetime.date.today(); return f"{t.day} {MOIS[t.month-1]} {t.year}" |
54 | def today(): t=datetime.date.today(); return f"{t.day} {MOIS[t.month-1]} {t.year}" |
| 42 | −import re |
| |
| 43 | def parse_pct(h): |
55 | def parse_pct(h): |
| 44 | m=re.match(r'^\+?(\d+(?:[.,]\d+)?)\s*%$', str(h or "").strip()) |
56 | m=re.match(r'^\+?(\d+(?:[.,]\d+)?)\s*%$', str(h or "").strip()) |
| 45 | return float(m.group(1).replace(",",".")) if m else None |
57 | return float(m.group(1).replace(",",".")) if m else None |
| 46 | NUMRE = re.compile(r'\d[\d\u00a0\u202f ,\.]*%?\$?') |
58 | NUMRE = re.compile(r'\d[\d\u00a0\u202f ,\.]*%?\$?') |
| 47 | |
59 | |
| 48 | −def build_spotlight(ins): |
60 | +# ---------- scènes ---------- |
| 49 | − H=HREEL; p=pal(ins["site"]); prod=ins.get("product") or {} |
61 | +# chaque constructeur renvoie (html, css, dur, hook_js_ou_None) |
| 50 | − img=prod.get("image_data",""); chips="".join('<span class="chip">'+esc(c)+'</span>' for c in (prod.get("meta") or [])) |
62 | + |
| 51 | − price=esc(prod.get("price","")); title=esc(prod.get("title","")); sub=esc(prod.get("subtitle","")) |
63 | +def sc_intro(ins,p,i): |
| 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") |
| |
| 102 | css=""" |
64 | css=""" |
| 103 | − *{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased} |
65 | + #S .mid{flex:1;display:flex;flex-direction:column;justify-content:center} |
| 104 | − html,body{width:1080px;height:__H__px;overflow:hidden;font-family:__FONT__;color:#fff} |
66 | + #S .kick{font-size:30px;font-weight:800;letter-spacing:.22em;color:__A__;text-transform:uppercase;opacity:0} |
| 105 | − .stage{position:relative;width:1080px;height:__H__px;overflow:hidden; |
67 | + .scene.on #S_ .kick{animation:fu .6s ease .15s forwards} |
| 106 | − background:radial-gradient(1300px 900px at 78% -5%, __A__55,transparent 55%), |
68 | + #S .big{font-size:150px;font-weight:900;letter-spacing:-.03em;line-height:1;margin-top:26px;opacity:0;transform:scale(.8)} |
| 107 | − radial-gradient(1100px 1000px at -12% 105%, __B__3a,transparent 55%), |
69 | + .scene.on #S_ .big{animation:zoomin .8s cubic-bezier(.2,1.2,.3,1) .35s forwards} |
| 108 | − linear-gradient(160deg,__G1__,__G2__);background-size:150% 150%;animation:drift 14s ease-in-out infinite alternate} |
70 | + #S .bar{width:0;height:10px;border-radius:6px;background:linear-gradient(90deg,__A__,__B__);margin-top:34px} |
| 109 | − @keyframes drift{0%{background-position:0% 0%}100%{background-position:100% 100%}} |
71 | + .scene.on #S_ .bar{animation:grow .7s ease .8s forwards} |
| 110 | − .pad{position:absolute;inset:0;padding:120px 80px 210px;display:flex;flex-direction:column} |
72 | + #S .tag{font-size:42px;font-weight:600;color:#ffffffc8;margin-top:30px;max-width:880px;line-height:1.2;opacity:0} |
| 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} |
73 | + .scene.on #S_ .tag{animation:fu .7s ease 1.05s forwards} |
| 112 | − .eyebrow .dot{width:52px;height:6px;border-radius:3px;background:__A__} |
74 | + @keyframes zoomin{to{opacity:1;transform:scale(1)}} |
| 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} |
75 | + @keyframes grow{to{width:280px}} |
| 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)}} |
| |
| 131 | """ |
76 | """ |
| 132 | − css=(css.replace("__H__",str(H)).replace("__FONT__",FONT).replace("__A__",p["a"]) |
77 | + html=('<div class="mid"><div class="kick">Le pouls du jour</div>' |
| 133 | − .replace("__B__",p["b"]).replace("__G1__",p["g1"]).replace("__G2__",p["g2"]).replace("__DUR__",str(DUR))) |
78 | + '<div class="big">'+esc(ins["label"])+'</div><div class="bar"></div>' |
| 134 | − body=('<div class="stage"><div class="pad">' |
79 | + + (('<div class="tag">'+esc(ins.get("tagline",""))+'</div>') if ins.get("tagline") else '') |
| 135 | − + '<div class="eyebrow"><span class="dot"></span>' + esc(ins["label"].upper()) + ' · ' + today().upper() + '</div>' |
80 | + + '</div>') |
| 136 | − + '<div class="kabadge">KA</div>' |
81 | + return html, css, 2.8, None |
| 137 | − + '<div class="site">' + esc(ins["label"]) + '</div>' |
82 | + |
| 138 | − + '<div class="ltitle">' + heading + '</div>' |
83 | +def _num_parts(headline): |
| 139 | − + '<div class="lwrap">' + rows + '</div>' |
84 | + """-> (int_digits, dec, suffix) pour le count-up ; None si non numérique.""" |
| 140 | − + '<div class="cta"><div class="go">\U0001f449 www.' + ins["site"] + '.com</div><div class="brand">GROUPE ·KA</div></div>' |
85 | + hl=str(headline).strip() |
| 141 | − + '<div class="sig">✦ Rédigé et publié par l\'Agent KA</div><div class="prog"></div>' |
86 | + m=re.match(r'^\+?([\d\s ]+)(?:[.,](\d+))?\s*(.*)$', hl) |
| 142 | − + '</div></div>') |
87 | + if not m: return None |
| 143 | − return "<!doctype html><html><head><meta charset=\"utf-8\"><style>" + css + "</style></head><body>" + body + "</body></html>" |
88 | + digits=re.sub(r'\D','',m.group(1)) |
| 144 | − |
89 | + if not digits: return None |
| 145 | −REEL_SHELL_CSS = """ |
90 | + return digits, (m.group(2) or ""), m.group(3) or "" |
| 146 | − *{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased} |
91 | + |
| 147 | − html,body{width:1080px;height:__H__px;overflow:hidden;font-family:__FONT__;color:#fff} |
92 | +def sc_stat(ins,p,i): |
| 148 | − .stage{position:relative;width:1080px;height:__H__px;overflow:hidden; |
93 | + parts=_num_parts(ins["headline"]); hl=str(ins["headline"]) |
| 149 | − background:radial-gradient(1300px 900px at 78% -5%, __A__55,transparent 55%), |
94 | + L=len(hl); fs=170 if L>=9 else (215 if L>=7 else 260) |
| 150 | − radial-gradient(1100px 1000px at -12% 105%, __B__3a,transparent 55%), |
95 | + css=""" |
| 151 | − linear-gradient(160deg,__G1__,__G2__);background-size:150% 150%;animation:drift 14s ease-in-out infinite alternate} |
96 | + #S .mid{flex:1;display:flex;flex-direction:column;justify-content:center} |
| 152 | − @keyframes drift{0%{background-position:0% 0%}100%{background-position:100% 100%}} |
97 | + #S .lab0{font-size:34px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:__A__;opacity:0} |
| 153 | − .pad{position:absolute;inset:0;padding:120px 90px 250px;display:flex;flex-direction:column} |
98 | + .scene.on #S_ .lab0{animation:fu .5s ease .1s forwards} |
| 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} |
99 | + #S .num{font-size:__FS__px;font-weight:900;line-height:.95;letter-spacing:-.03em;white-space:nowrap;margin-top:24px; |
| 155 | − .eyebrow .dot{width:52px;height:6px;border-radius:3px;background:__A__;flex:0 0 52px} |
100 | + background:linear-gradient(180deg,#fff,__A__ 135%);-webkit-background-clip:text;-webkit-text-fill-color:transparent; |
| 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} |
101 | + filter:drop-shadow(0 18px 55px __A__55);opacity:0;transform:translateY(36px)} |
| 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} |
102 | + .scene.on #S_ .num{animation:fu .7s cubic-bezier(.2,1,.3,1) .3s forwards} |
| 158 | − .cta{position:absolute;left:90px;right:90px;bottom:150px;opacity:0;animation:fu .8s ease 2.5s forwards} |
103 | + #S .lab{font-size:56px;font-weight:700;margin-top:30px;max-width:900px;line-height:1.14;opacity:0} |
| 159 | − .cta .go{font-size:46px;font-weight:900;color:__A__} |
104 | + .scene.on #S_ .lab{animation:fu .7s ease .8s forwards} |
| 160 | − .cta .brand{font-size:30px;font-weight:700;color:#ffffff8c;letter-spacing:.12em;margin-top:6px} |
105 | + """.replace("__FS__",str(fs)) |
| 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} |
106 | + hook=None |
| 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__} |
107 | + if parts and len(parts[0])>=3 and not parts[1]: |
| 163 | − @keyframes prog{to{width:100%}} |
108 | + hook="cuInt('S_num',"+parts[0]+","+json.dumps(" "+parts[2] if parts[2] else "")+",1500,400);" |
| 164 | − @keyframes fu{to{opacity:1;transform:translateY(0)}} |
109 | + html=('<div class="mid"><div class="lab0">'+esc(ins["label"])+' · en direct</div>' |
| 165 | − @keyframes pop{to{opacity:1;transform:rotate(-4deg) scale(1)}} |
110 | + '<div class="num" id="S_num">'+esc(hl)+'</div>' |
| 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%)} |
111 | + '<div class="lab">'+esc(ins["headline_label"])+'</div></div>') |
| 167 | −""" |
112 | + return html, css, 4.2, hook |
| 168 | −def shell_css(p): |
113 | + |
| 169 | − return (REEL_SHELL_CSS.replace("__H__",str(HREEL)).replace("__FONT__",FONT) |
114 | +def sc_donut(ins,p,i): |
| 170 | − .replace("__A__",p["a"]).replace("__B__",p["b"]).replace("__G1__",p["g1"]) |
115 | + pct=parse_pct(ins["headline"]) or 0.0 |
| 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 |
| |
| 178 | C=2*3.14159*300; off=C*(1-min(100.0,pct)/100.0) |
116 | C=2*3.14159*300; off=C*(1-min(100.0,pct)/100.0) |
| 179 | − tiles=(ins.get("tiles") or [])[:3] |
117 | + dec=1 if re.search(r'[.,]\d',str(ins["headline"])) else 0 |
| 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)) |
118 | + css=""" |
| 181 | − dec=1 if (',' in str(ins["headline"]) or '.' in str(ins["headline"])) else 0 |
119 | + #S .mid{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center} |
| 182 | − svg=('<svg width="760" height="760" viewBox="0 0 760 760">' |
120 | + #S .dw{position:relative;width:740px;height:740px;opacity:0;transform:scale(.88)} |
| 183 | − '<circle cx="380" cy="380" r="300" fill="none" stroke="rgba(255,255,255,.10)" stroke-width="84"/>' |
121 | + .scene.on #S_ .dw{animation:zoomin .7s cubic-bezier(.2,1.2,.4,1) .15s forwards} |
| 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)"/>' |
122 | + #S .arc{stroke-dasharray:__C__;stroke-dashoffset:__C__} |
| 185 | − '<defs><linearGradient id="gd" x1="0" y1="0" x2="1" y2="1">' |
123 | + .scene.on #S_ .arc{animation:draw 1.7s cubic-bezier(.3,1,.4,1) .5s forwards} |
| 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} |
| |
| 192 | @keyframes draw{to{stroke-dashoffset:__OFF__}} |
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)} |
125 | + @keyframes zoomin{to{opacity:1;transform:scale(1)}} |
| 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} |
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)} |
| 195 | − .tiles{display:flex;gap:20px;margin-top:52px} |
127 | + #S .dl{font-size:48px;font-weight:700;line-height:1.16;text-align:center;max-width:900px;margin-top:34px;opacity:0} |
| 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} |
128 | + .scene.on #S_ .dl{animation:fu .7s ease 1.2s forwards} |
| 197 | − .t0{animation-delay:2.2s}.t1{animation-delay:2.35s}.t2{animation-delay:2.5s} |
129 | + """.replace("__C__",str(round(C,1))).replace("__OFF__",str(round(off,1))) |
| 198 | − .tv{font-size:50px;font-weight:900;color:__A__;white-space:nowrap;line-height:1} |
130 | + svg=('<svg width="740" height="740" viewBox="0 0 760 760">' |
| 199 | − .tl{font-size:26px;font-weight:600;color:#ffffffa8;margin-top:8px;line-height:1.15} |
131 | + '<circle cx="380" cy="380" r="300" fill="none" stroke="rgba(255,255,255,.10)" stroke-width="82"/>' |
| 200 | − """.replace("__C__",str(round(C,1))).replace("__OFF__",str(round(off,1))).replace("__A__",p["a"]) |
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)"/>' |
| 201 | − body=('<div class="stage"><div class="vign"></div><div class="pad">' |
133 | + '<defs><linearGradient id="gd'+str(i)+'" x1="0" y1="0" x2="1" y2="1">' |
| 202 | − '<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · '+today().upper()+'</div>' |
134 | + '<stop offset="0" stop-color="__A__"/><stop offset="1" stop-color="__B__"/></linearGradient></defs></svg>') |
| 203 | − '<div class="kabadge">KA</div>' |
135 | + html=('<div class="mid"><div class="dw">'+svg+'<div class="dp" id="S_dp">0 %</div></div>' |
| 204 | − '<div class="site">'+esc(ins["label"])+'</div>' |
136 | + '<div class="dl">'+esc(ins["headline_label"])+'</div></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>' |
137 | + hook="cuPct('S_dp',"+str(pct)+","+str(dec)+",1700,500);" |
| 206 | − '<div class="dlab">'+esc(ins["headline_label"])+'</div>' |
138 | + return html, css, 4.4, hook |
| 207 | − +(('<div class="tiles">'+tiles_html+'</div>') if tiles else '')+'</div>' |
139 | + |
| 208 | − +shell_tail(ins)+'</div></div>') |
140 | +def sc_tiles(ins,p,i): |
| 209 | − script="""<script>(function(){ |
141 | + tiles=(ins.get("tiles") or [])[:3] |
| 210 | − var el=document.getElementById('dperc'); var pct=parseFloat(el.getAttribute('data-pct')); |
142 | + rows="" |
| 211 | − var dec=parseInt(el.getAttribute('data-dec'),10); var start=null, dur=1800, begin=1100; |
143 | + for k,(v,l) in enumerate(tiles): |
| 212 | − function fmt(n){return n.toFixed(dec).replace('.',',')+' %'} |
144 | + rows+=('<div class="trow" style="transition-delay:.0s"><div class="tv" data-d="'+str(k)+'">'+esc(v)+'</div>' |
| 213 | − function step(ts){ if(!start)start=ts; var t=ts-start; |
145 | + '<div class="tl">'+esc(l)+'</div></div>') |
| 214 | − if(t<begin){requestAnimationFrame(step);return;} |
146 | + css=""" |
| 215 | − var k=Math.min(1,(t-begin)/dur); var e=1-Math.pow(1-k,3); |
147 | + #S .mid{flex:1;display:flex;flex-direction:column;justify-content:center;gap:26px} |
| 216 | − el.textContent=fmt(pct*e); |
148 | + #S .h{font-size:40px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:__A__;opacity:0;margin-bottom:8px} |
| 217 | − if(k<1)requestAnimationFrame(step);} |
149 | + .scene.on #S_ .h{animation:fu .5s ease .1s forwards} |
| 218 | − requestAnimationFrame(step); |
150 | + #S .trow{background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);border-radius:30px; |
| 219 | − })();</script>""" |
151 | + padding:40px 44px;backdrop-filter:blur(6px);opacity:0;transform:translateX(-60px)} |
| 220 | − return "<!doctype html><html><head><meta charset=\"utf-8\"><style>"+css+"</style></head><body>"+body+script+"</body></html>" |
152 | + .scene.on #S_ .trow{animation:slidein .6s cubic-bezier(.2,.9,.3,1) forwards} |
| 221 | − |
153 | + .scene.on #S_ .trow:nth-child(2){animation-delay:.25s} |
| 222 | −def build_quote(ins): |
154 | + .scene.on #S_ .trow:nth-child(3){animation-delay:.5s} |
| 223 | − p=pal(ins["site"]) |
155 | + .scene.on #S_ .trow:nth-child(4){animation-delay:.75s} |
| 224 | − fact=str(ins.get("fact") or (str(ins["headline"])+" "+ins.get("headline_label",""))) |
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 | words=fact.split() |
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 | spans="" |
226 | spans="" |
| 228 | − for i,w in enumerate(words): |
227 | + for k,w in enumerate(words): |
| 229 | cls="w hl" if NUMRE.search(w) else "w" |
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> ' |
229 | + spans+='<span class="'+cls+'" style="animation-delay:'+str(round(0.35+k*delay,2))+'s">'+esc(w)+'</span> ' |
| 231 | − L=len(fact); fs=62 if L<170 else (54 if L<240 else 47) |
230 | + L=len(fact); fs=60 if L<170 else (53 if L<240 else 46) |
| 232 | − css=shell_css(p)+""" |
231 | + dur=min(6.0, 1.6+len(words)*delay+2.2) |
| 233 | − .mid{flex:1;display:flex;flex-direction:column;justify-content:center;min-height:0} |
232 | + css=""" |
| 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} |
233 | + #S .mid{flex:1;display:flex;flex-direction:column;justify-content:center} |
| 235 | − .qtext{font-size:__FS__px;font-weight:800;line-height:1.35;letter-spacing:-.01em;max-width:920px} |
234 | + #S .qm{font-size:260px;line-height:.5;color:__A__;font-weight:900;font-family:Georgia,serif;height:140px;opacity:0} |
| 236 | − .w{display:inline-block;opacity:0;transform:translateY(26px);animation:fu .5s ease forwards} |
235 | + .scene.on #S_ .qm{animation:fu .5s ease .1s forwards} |
| 237 | − .hl{color:__A__;filter:drop-shadow(0 6px 20px __A__44)} |
236 | + #S .qt{font-size:__FS__px;font-weight:800;line-height:1.35;letter-spacing:-.01em;max-width:920px} |
| 238 | − """.replace("__A__",p["a"]).replace("__FS__",str(fs)) |
237 | + #S .w{display:inline-block;opacity:0;transform:translateY(24px)} |
| 239 | − body=('<div class="stage"><div class="vign"></div><div class="pad">' |
238 | + .scene.on #S_ .w{animation:fu .45s ease forwards} |
| 240 | − '<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · LE SAVIEZ-VOUS ? · '+today().upper()+'</div>' |
239 | + #S .hl{color:__A__;filter:drop-shadow(0 6px 20px __A__44)} |
| 241 | − '<div class="kabadge">KA</div>' |
240 | + """.replace("__FS__",str(fs)) |
| 242 | − '<div class="site">'+esc(ins["label"])+'</div>' |
241 | + html='<div class="mid"><div class="qm">“</div><div class="qt">'+spans+'</div></div>' |
| 243 | − '<div class="mid"><div class="qmark">“</div><div class="qtext">'+spans+'</div></div>' |
242 | + return html, css, round(dur,1), None |
| 244 | − +shell_tail(ins)+'</div></div>') |
243 | + |
| 245 | − return "<!doctype html><html><head><meta charset=\"utf-8\"><style>"+css+"</style></head><body>"+body+"</body></html>" |
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 | def build_html(ins): |
309 | def build_html(ins): |
| 248 | − g=ins.get('genre') |
310 | + p=pal(ins["site"]) |
| 249 | − if g in ('list','grid') and ins.get('list'): |
311 | + scenes=plan_scenes(ins,p) |
| 250 | − return build_list(ins) |
312 | + tot=round(sum(s["dur"] for s in scenes),2) |
| 251 | − if g=='spotlight' and (ins.get('product') or {}).get('image_data'): |
313 | + shell=""" |
| 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 = """ |
| |
| 263 | *{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased} |
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 | .stage{position:relative;width:1080px;height:__H__px;overflow:hidden; |
316 | .stage{position:relative;width:1080px;height:__H__px;overflow:hidden; |
| 266 | − background:radial-gradient(1300px 900px at 78% -5%, __A__55,transparent 55%), |
317 | + background:radial-gradient(1300px 900px at 78% -5%, __A__50,transparent 55%), |
| 267 | − radial-gradient(1100px 1000px at -12% 105%, __B__3a,transparent 55%), |
318 | + radial-gradient(1100px 1000px at -12% 105%, __B__36,transparent 55%), |
| 268 | − linear-gradient(160deg,__G1__,__G2__);background-size:150% 150%;animation:drift 14s ease-in-out infinite alternate} |
319 | + linear-gradient(160deg,__G1__,__G2__);background-size:150% 150%;animation:drift 18s ease-in-out infinite alternate} |
| 269 | @keyframes drift{0%{background-position:0% 0%}100%{background-position:100% 100%}} |
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%); |
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} |
| 271 | − filter:blur(30px);animation:float 16s ease-in-out infinite alternate;opacity:.7} |
322 | + .eyeb{position:absolute;top:104px;left:90px;right:250px;display:flex;align-items:center;gap:14px;font-weight:800; |
| 272 | − @keyframes float{0%{transform:translate(-6%, -4%) scale(1)}100%{transform:translate(8%, 6%) scale(1.15)}} |
323 | + letter-spacing:.16em;font-size:25px;text-transform:uppercase;color:__A__;z-index:8;opacity:0;animation:fu .7s ease .2s forwards} |
| 273 | − .pad{position:absolute;inset:0;padding:120px 90px 250px;display:flex;flex-direction:column} |
324 | + .eyeb .dot{width:46px;height:6px;border-radius:3px;background:__A__;flex:0 0 46px} |
| 274 | − .eyebrow{display:flex;align-items:center;gap:16px;font-weight:800;letter-spacing:.16em;font-size:27px; |
325 | + .kabadge{position:absolute;top:92px;right:90px;background:__A__;color:#0d0d0d;font-weight:900;font-size:48px; |
| 275 | − text-transform:uppercase;color:__A__;opacity:0;animation:fadeUp .8s ease .15s forwards} |
326 | + padding:10px 24px;border-radius:20px;transform:rotate(-4deg) scale(.4);opacity:0;z-index:8; |
| 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; |
| |
| 279 | animation:pop .7s cubic-bezier(.2,1.4,.4,1) .35s forwards;box-shadow:0 16px 46px __A__66} |
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} |
328 | + .wm{position:absolute;left:90px;bottom:96px;font-size:27px;font-weight:800;color:#ffffff9a;z-index:8} |
| 281 | − .tag{font-size:38px;font-weight:600;color:#ffffffb0;margin-top:12px;opacity:0;animation:fadeUp .8s ease .65s forwards} |
329 | + .wm b{color:__A__} |
| 282 | − .mid{flex:1;display:flex;flex-direction:column;justify-content:center;min-height:0} |
330 | + .prog{position:absolute;left:0;bottom:0;height:9px;background:linear-gradient(90deg,__A__,__B__);width:0;z-index:9; |
| 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} |
331 | + animation:prog __TOT__s linear .1s forwards;box-shadow:0 0 20px __A__} |
| 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__} |
| |
| 303 | @keyframes prog{to{width:100%}} |
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 | @keyframes pop{to{opacity:1;transform:rotate(-4deg) scale(1)}} |
334 | @keyframes pop{to{opacity:1;transform:rotate(-4deg) scale(1)}} |
| 306 | − @keyframes heroIn{to{opacity:1;transform:translateY(0) scale(1)}} |
335 | + .scene{position:absolute;inset:0;padding:220px 90px 210px;display:flex;flex-direction:column;z-index:4; |
| 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%)} |
336 | + opacity:0;pointer-events:none;transform:translateY(46px) scale(.985); |
| 308 | − .numwrap{animation:heroIn .95s cubic-bezier(.2,1,.3,1) .95s forwards, pulse 3s ease-in-out 2s infinite} |
337 | + transition:opacity .5s ease,transform .62s cubic-bezier(.2,.8,.2,1)} |
| 309 | − @keyframes pulse{0%,100%{filter:none}50%{filter:brightness(1.12)}} |
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"]) |
344 | + shell=(shell.replace("__H__",str(H)).replace("__FONT__",FONT).replace("__A__",p["a"]) |
| 312 | − .replace("__B__",p["b"]).replace("__G1__",p["g1"]).replace("__G2__",p["g2"]) |
345 | + .replace("__B__",p["b"]).replace("__G1__",p["g1"]).replace("__G2__",p["g2"]) |
| 313 | − .replace("__NUM__",str(numsize)).replace("__DUR__",str(DUR))) |
346 | + .replace("__TOT__",str(tot)).replace("__FOT__",str(round(tot-0.7,2)))) |
| 314 | − tag = ('<div class="tag">'+esc(ins.get("tagline",""))+'</div>') if ins.get("tagline") else "" |
347 | + |
| 315 | − tiles_block = ('<div class="tiles">'+tiles_html+'</div>') if tiles else "" |
348 | + css_all=[shell]; body=[]; sc_list=[]; hooks={} |
| 316 | − body = ('<div class="stage"><div class="mesh"></div><div class="vign"></div><div class="pad">' |
349 | + for s in scenes: |
| 317 | − '<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · '+today().upper()+'</div>' |
350 | + sid="sc"+str(s["i"]) |
| 318 | − '<div class="kabadge">KA</div>' |
351 | + css=(s["css"].replace(".scene.on #S_","#"+sid+".on").replace("#S_","#"+sid).replace("#S","#"+sid) |
| 319 | − '<div class="site">'+esc(ins["label"])+'</div>'+tag+ |
352 | + .replace("__A__",p["a"]).replace("__B__",p["b"]).replace("__G2__",p["g2"])) |
| 320 | − '<div class="mid">' |
353 | + # les hooks référencent des ids préfixés S_ -> remplacer par l'id de scène |
| 321 | − '<div class="numwrap"><div class="num" id="num" data-target="'+esc(ins["headline"])+'">'+esc(ins["headline"])+'</div></div>' |
354 | + html=s["html"].replace('id="S_','id="'+sid+'_') |
| 322 | − '<div class="lab">'+esc(ins["headline_label"])+'</div>'+tiles_block+ |
355 | + hook=(s["hook"] or "").replace("'S_","'"+sid+"_") |
| 323 | − '</div>' |
356 | + css_all.append(css) |
| 324 | − '<div class="cta"><div class="go">👉 www.'+ins["site"]+'.com</div><div class="brand">GROUPE ·KA</div></div>' |
357 | + body.append('<div class="scene" id="'+sid+'">'+html+'</div>') |
| 325 | − '<div class="sig">✦ Rédigé et publié par l\'Agent KA</div>' |
358 | + sc_list.append([sid, s["dur"]]) |
| 326 | − '<div class="prog"></div>' |
359 | + if hook: hooks[sid]=hook |
| 327 | − '</div></div>') |
360 | + |
| 328 | − script = """<script> |
361 | + hooks_js="{"+",".join('"'+k+'":function(){'+v+'}' for k,v in hooks.items())+"}" |
| 329 | − (function(){ |
362 | + script="""<script> |
| 330 | − // ajuste la taille des valeurs de tuiles pour tenir sur une ligne |
363 | + function fmtInt(n){return n.toLocaleString('fr-CA').replace(/[\\u00a0\\u202f,]/g,' ')} |
| 331 | − document.querySelectorAll('.tv[data-fit]').forEach(function(el){ |
364 | + function cuInt(id,target,suffix,dur,begin){var el=document.getElementById(id);if(!el)return;var s=null; |
| 332 | − var s=58; el.style.fontSize=s+'px'; |
365 | + function st(ts){if(!s)s=ts;var t=ts-s;if(t<begin){requestAnimationFrame(st);return} |
| 333 | − while(el.scrollWidth>el.clientWidth && s>26){ s-=3; el.style.fontSize=s+'px'; } |
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>""" |
387 | + </script>""".replace("__SC__",json.dumps(sc_list)).replace("__HOOKS__",hooks_js) |
| 351 | − return "<!doctype html><html><head><meta charset=\"utf-8\"><style>"+css+"</style></head><body>"+body+script+"</body></html>" |
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 | from playwright.sync_api import sync_playwright |
398 | from playwright.sync_api import sync_playwright |
| 355 | with sync_playwright() as pw: |
399 | with sync_playwright() as pw: |
| 356 | b=pw.chromium.launch(args=["--no-sandbox"]) |
400 | b=pw.chromium.launch(args=["--no-sandbox"]) |
| 357 | − ctx=b.new_context(viewport={"width":1080,"height":HREEL}, device_scale_factor=2, |
401 | + ctx=b.new_context(viewport={"width":1080,"height":H}, device_scale_factor=2, |
| 358 | − record_video_dir=tmpdir, record_video_size={"width":1080,"height":HREEL}) |
402 | + record_video_dir=tmpdir, record_video_size={"width":1080,"height":H}) |
| 359 | pg=ctx.new_page() |
403 | pg=ctx.new_page() |
| 360 | pg.goto("file://"+html_path) |
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 | vid=pg.video |
406 | vid=pg.video |
| 363 | ctx.close(); b.close() |
407 | ctx.close(); b.close() |
| 364 | path=vid.path() |
408 | path=vid.path() |
| 365 | os.replace(path, out_webm) |
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 | def render(ins, out_dir): |
411 | def render(ins, out_dir): |
| 430 | os.makedirs(out_dir,exist_ok=True) |
412 | os.makedirs(out_dir,exist_ok=True) |
| 431 | prod=ins.get('product') |
413 | prod=ins.get('product') |
| 436 | if it.get('image') and not it.get('image_data'): |
418 | if it.get('image') and not it.get('image_data'): |
| 437 | du=data_uri(it['image']) |
419 | du=data_uri(it['image']) |
| 438 | if du: it['image_data']=du |
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 | tmp=os.path.join(out_dir,"_reeltmp_"+key); os.makedirs(tmp,exist_ok=True) |
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)) |
424 | + html,dur=build_html(ins) |
| 442 | − webm=os.path.join(tmp,"reel.webm"); record_webm(html_path,webm,tmp) |
425 | + html_path=os.path.join(tmp,"reel.html"); open(html_path,"w",encoding="utf-8").write(html) |
| 443 | − mood=ins.get('variant') |
426 | + webm=os.path.join(tmp,"reel.webm"); record_webm(html_path,webm,tmp,dur) |
| 444 | − if mood is None: |
427 | + |
| 445 | − mood=int(hashlib.md5((ins['site']+str(ins.get('insight_id',''))).encode()).hexdigest(),16) |
428 | + # musique : style en rotation via variant, contenu par graine (unique) |
| 446 | − wav=os.path.join(tmp,"bed.wav"); make_music(DUR,wav,int(mood)) |
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 | out=os.path.join(out_dir,f"reel-{ins['site']}-{ins.get('insight_id','x')}.mp4") |
436 | out=os.path.join(out_dir,f"reel-{ins['site']}-{ins.get('insight_id','x')}.mp4") |
| 448 | subprocess.run([FFMPEG,"-y","-i",webm,"-i",wav, |
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]", |
438 | + "-filter_complex","[0:v]scale=1080:"+str(H)+":force_original_aspect_ratio=increase,crop=1080:"+str(H)+",fps=30,format=yuv420p[v];" |
| 450 | − "-map","[v]","-map","1:a","-c:v","libx264","-preset","slow","-crf","18", |
439 | + "[1:a]acompressor=threshold=-16dB:ratio=3:attack=8:release=180,alimiter=limit=0.95[a]", |
| 451 | − "-c:a","aac","-b:a","160k","-pix_fmt","yuv420p","-movflags","+faststart","-shortest",out], |
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 | capture_output=True) |
442 | capture_output=True) |
| 453 | − # nettoyage |
| |
| 454 | for f in glob.glob(os.path.join(tmp,"*")): |
443 | for f in glob.glob(os.path.join(tmp,"*")): |
| 455 | try: os.remove(f) |
444 | try: os.remove(f) |
| 456 | except: pass |
445 | except: pass |