SPB Git forge

spb/admin-ka

Public
41commits 1branches 0releases
172.9 MBsize
maindefault branch
19 days agolast push
JavaScript 65.5% Python 17.8% CSS 13% HTML 3.7%

Fix noVNC (import module statique window.KA_RFB) + reels polyvalents

- noVNC: import ESM statique en tete -> window.KA_RFB (fin de undefined is not a constructor)
- Reels genre spotlight: vraie annonce/produit avec photo (Ken Burns), legende decrivant l item
- decorateForPrompt: une demande (ex. dernier logement Lou-Ka) sort le vrai contenu, pas la stat globale
- fix count-up decimales/% (48,2 % ne devient plus 482 %)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 23, 2026) parent bfff2ef

4 changed files +89 −6

modified public/app.js +1 −1
@@ -80,7 +80,7 @@
80 80 statusEl.textContent = 'connexion…';
81 81 try {
82 82 const pw = await (await fetch('/api/social/vncpw')).json();
83 const { default: RFB } = await import('/vendor/novnc/core/rfb.js');
83 + const RFB = window.KA_RFB; if (!RFB) { statusEl.textContent = 'noVNC pas encore chargé — rafraîchis la page'; return; }
84 84 const url = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/vncws';
85 85 panel.style.display = 'block';
86 86 _rfb = new RFB(panel, url, { credentials: { password: pw.password } });
modified public/index.html +1 −0
@@ -267,5 +267,6 @@
267 267 <script src="/vendor/marked.min.js"></script>
268 268 <script src="/vendor/highlight.min.js"></script>
269 269 <script src="/app.js"></script>
270 +<script type="module">import RFB from "/vendor/novnc/core/rfb.js?v=3"; window.KA_RFB=RFB;</script>
270 271 </body>
271 272 </html>
modified server/social.js +28 −5
@@ -485,6 +485,15 @@ async function fetchFeatured(site) {
485 485 return null;
486 486 }
487 487
488 +async function decorateForPrompt(insight) {
489 + if (SPOT[insight.site]) {
490 + const feat = await fetchFeatured(insight.site);
491 + if (feat) { insight.product = feat; insight.genre = 'spotlight'; return insight; }
492 + }
493 + insight.genre = (insight.bars && insight.bars.length >= 4) ? 'ranking' : 'hero';
494 + return insight;
495 +}
496 +
488 497 // choisit le genre visuel (rotation) et attache le produit vedette si spotlight
489 498 async function decorateCreative(insight) {
490 499 const n = state.postCount || 0;
@@ -522,10 +531,21 @@ Règles STRICTES :
522 531 export async function writeCaption(insight, extraPrompt = '') {
523 532 const model = CFG.socialModel || CFG.upgraderModel || 'claude-opus-4-8';
524 533 const url = `https://www.${insight.site}.com`;
525 const user = `Fait statistique à mettre en valeur :\n${insight.fact}\n\n` +
526 `Site : ${insight.label} (${url})\nAngle/tagline : ${insight.tagline || ''}\n` +
527 (extraPrompt ? `\nConsigne supplémentaire de l'administrateur :\n${extraPrompt}\n` : '') +
528 `\nRédige la publication Facebook.`;
534 + let user;
535 + if (insight.product && insight.genre === 'spotlight') {
536 + const pr = insight.product;
537 + user = `Mets en vedette CETTE annonce/produit réel de ${insight.label} (ne cite que ces infos, n'invente rien) :\n` +
538 + `Titre : ${pr.title}\n` + (pr.subtitle ? `Détail : ${pr.subtitle}\n` : '') +
539 + (pr.price ? `Prix : ${pr.price}\n` : '') + (pr.meta && pr.meta.length ? `Infos : ${pr.meta.join(', ')}\n` : '') +
540 + `Site : ${insight.label} (${url})\nAngle : ${insight.tagline || ''}\n` +
541 + (extraPrompt ? `\nConsigne de l'administrateur : ${extraPrompt}\n` : '') +
542 + `\nRédige une publication Facebook accrocheuse qui donne envie de voir cette annonce.`;
543 + } else {
544 + user = `Fait statistique à mettre en valeur :\n${insight.fact}\n\n` +
545 + `Site : ${insight.label} (${url})\nAngle/tagline : ${insight.tagline || ''}\n` +
546 + (extraPrompt ? `\nConsigne supplémentaire de l'administrateur :\n${extraPrompt}\n` : '') +
547 + `\nRédige la publication Facebook.`;
548 + }
529 549 const txt = await anthropic(CAPTION_SYSTEM, user, model, 500);
530 550 return txt + "\n\n— ✶ Publié par l'Agent KA · administration-ka.com";
531 551 }
@@ -545,10 +565,11 @@ export async function generateDraft({ site = '', prompt = '' } = {}) {
545 565 CFG.socialModel || 'claude-opus-4-8', 30);
546 566 const pickSite = (chosen || '').trim().split(/\s/)[0];
547 567 insight = cands.find(c => c.site === pickSite) || cands[0];
568 + await decorateForPrompt(insight);
548 569 } else {
549 570 insight = await pickInsight({ site });
571 + await decorateCreative(insight);
550 572 }
551 await decorateCreative(insight);
552 573 const image = await renderCard(insight);
553 574 const caption = await writeCaption(insight, prompt);
554 575 return { insight, image, caption, imageUrl: '/api/social/card/' + path.basename(image) };
@@ -617,8 +638,10 @@ export async function generateReelDraft({ site = '', prompt = '' } = {}) {
617 638 "Tu choisis le site du Groupe KA le plus pertinent pour une demande. Réponds UNIQUEMENT par l'identifiant du site.",
618 639 `Demande : ${prompt}\n\nInsights :\n${list}`, CFG.socialModel || 'claude-opus-4-8', 30);
619 640 insight = cands.find(c => c.site === (chosen || '').trim().split(/\s/)[0]) || cands[0];
641 + await decorateForPrompt(insight);
620 642 } else {
621 643 insight = await pickInsight({ site });
644 + await decorateForPrompt(insight);
622 645 }
623 646 const video = await makeReel(insight);
624 647 const caption = await writeCaption(insight, prompt);
modified server/social/make_reel.py +59 −0
@@ -27,10 +27,65 @@ PALETTES={
27 27 "job-ka":{"a":"#d9f26b","b":"#7ab8ff","g1":"#0a1522","g2":"#080b0d"},
28 28 }
29 29 def pal(s): return PALETTES.get(s, PALETTES["vrai-prix"])
30 +def data_uri(url):
31 + import base64
32 + try:
33 + req=urllib.request.Request(url, headers={"User-Agent":"Mozilla/5.0 (Macintosh)"})
34 + with urllib.request.urlopen(req, timeout=15, context=CTX) as r: raw=r.read()
35 + mime="image/jpeg"
36 + if url.lower().endswith(".png"): mime="image/png"
37 + elif url.lower().endswith(".webp"): mime="image/webp"
38 + return "data:"+mime+";base64,"+base64.b64encode(raw).decode()
39 + except Exception: return None
30 40 def esc(s): return str(s).replace("&","&amp;").replace("<","&lt;").replace(">","&gt;")
31 41 def today(): t=datetime.date.today(); return f"{t.day} {MOIS[t.month-1]} {t.year}"
32 42
43 +def build_spotlight(ins):
44 + H=HREEL; p=pal(ins["site"]); prod=ins.get("product") or {}
45 + img=prod.get("image_data",""); chips="".join('<span class="chip">'+esc(c)+'</span>' for c in (prod.get("meta") or []))
46 + price=esc(prod.get("price","")); title=esc(prod.get("title","")); sub=esc(prod.get("subtitle",""))
47 + css = """
48 + *{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased}
49 + html,body{width:1080px;height:__H__px;overflow:hidden;font-family:__FONT__;color:#fff;background:__G2__}
50 + .photo{position:absolute;top:0;left:0;right:0;height:__PH__px;background:url('__IMG__') center/cover;animation:kb 10s ease-out forwards}
51 + @keyframes kb{from{transform:scale(1.0)}to{transform:scale(1.12)}}
52 + .scrim{position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,.15) 0%,rgba(0,0,0,0) 30%,__G2__cc 55%,__G2__ 72%)}
53 + .eyebrow{position:absolute;top:120px;left:90px;display:flex;align-items:center;gap:14px;font-weight:800;letter-spacing:.16em;font-size:26px;text-transform:uppercase;color:__A__;opacity:0;animation:fu .8s ease .2s forwards;text-shadow:0 2px 12px #000}
54 + .eyebrow .dot{width:46px;height:6px;border-radius:3px;background:__A__}
55 + .kabadge{position:absolute;top:110px;right:90px;background:__A__;color:#0d0d0d;font-weight:900;font-size:52px;padding:10px 24px;border-radius:20px;transform:rotate(-4deg) scale(.5);opacity:0;animation:pop .7s cubic-bezier(.2,1.4,.4,1) .5s forwards;box-shadow:0 16px 40px __A__66}
56 + .info{position:absolute;left:90px;right:90px;bottom:270px}
57 + .ptitle{font-size:74px;font-weight:900;line-height:1.02;letter-spacing:-.02em;text-shadow:0 4px 24px #000a;opacity:0;transform:translateY(30px);animation:fu .9s ease 1.0s forwards}
58 + .psub{font-size:36px;font-weight:600;color:#ffffffc8;margin-top:14px;opacity:0;animation:fu .9s ease 1.25s forwards}
59 + .chips{display:flex;gap:14px;flex-wrap:wrap;margin-top:26px}
60 + .chip{background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.22);border-radius:999px;padding:12px 24px;font-size:28px;font-weight:700;opacity:0;animation:fu .8s ease 1.45s forwards}
61 + .price{font-size:92px;font-weight:900;color:__A__;margin-top:26px;letter-spacing:-.02em;filter:drop-shadow(0 10px 30px __A__66);opacity:0;transform:scale(.9);animation:popc .8s cubic-bezier(.2,1.3,.4,1) 1.7s forwards}
62 + .cta{position:absolute;left:90px;right:90px;bottom:150px;opacity:0;animation:fu .8s ease 2.2s forwards}
63 + .cta .go{font-size:46px;font-weight:900;color:__A__}
64 + .cta .brand{font-size:30px;font-weight:700;color:#ffffff88;letter-spacing:.12em;margin-top:6px}
65 + .sig{position:absolute;left:90px;bottom:88px;font-size:26px;color:#ffffff66;font-weight:600;opacity:0;animation:fu .8s ease 2.5s forwards}
66 + @keyframes fu{to{opacity:1;transform:translateY(0)}}
67 + @keyframes pop{to{opacity:1;transform:rotate(-4deg) scale(1)}}
68 + @keyframes popc{to{opacity:1;transform:scale(1)}}
69 + """
70 + css = (css.replace("__H__",str(H)).replace("__FONT__",FONT).replace("__G2__",p["g2"])
71 + .replace("__PH__",str(int(H*0.62))).replace("__IMG__",img).replace("__A__",p["a"]))
72 + body = (
73 + '<div class="photo"></div><div class="scrim"></div>'
74 + '<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · À DÉCOUVRIR</div>'
75 + '<div class="kabadge">KA</div>'
76 + '<div class="info"><div class="ptitle">'+title+'</div>'
77 + + (('<div class="psub">'+sub+'</div>') if sub else '')
78 + + '<div class="chips">'+chips+'</div>'
79 + + (('<div class="price">'+price+'</div>') if price else '')
80 + + '</div>'
81 + '<div class="cta"><div class="go">\U0001f449 www.'+ins["site"]+'.com</div><div class="brand">GROUPE ·KA</div></div>'
82 + '<div class="sig">✦ Publié par l\'Agent KA</div>'
83 + )
84 + return "<!doctype html><html><head><meta charset=\"utf-8\"><style>"+css+"</style></head><body>"+body+"</body></html>"
85 +
33 86 def build_html(ins):
87 + if ins.get('genre')=='spotlight' and (ins.get('product') or {}).get('image_data'):
88 + return build_spotlight(ins)
34 89 H=HREEL
35 90 pt=130 if H>=1600 else 80
36 91 pb=400 if H>=1600 else 220
@@ -168,6 +223,10 @@ def make_music(dur, out_wav):
168 223
169 224 def render(ins, out_dir):
170 225 os.makedirs(out_dir,exist_ok=True)
226 + prod=ins.get('product')
227 + if prod and prod.get('image') and not prod.get('image_data'):
228 + du=data_uri(prod['image'])
229 + if du: prod['image_data']=du
171 230 key=hashlib.md5((ins['site']+ins.get('insight_id','')+'reel').encode()).hexdigest()[:8]
172 231 tmp=os.path.join(out_dir,"_reeltmp_"+key); os.makedirs(tmp,exist_ok=True)
173 232 html_path=os.path.join(tmp,"reel.html"); open(html_path,"w",encoding="utf-8").write(build_html(ins))
174 233