SPB Git

spb/admin-ka

Public

Studio: indicateur de chargement (spinner+shimmer) ; musique v3 (basse+arpege) ; vignette+pulse

SPB committed (Aug 23, 2026) parent fbbdcd8 Browse files

3 changed files +95 −65

modified public/app.js +40 −33
@@ -1435,10 +1435,23 @@
1435 })(); 1435 })();
1436 1436
1437 1437
1438−// ===== STUDIO_SOCIAL v2 (générateurs + galerie, download + copy) ===== 1438+
1439+
1440+
1441+// ===== STUDIO_SOCIAL v3 (générateurs + galerie + loading) =====
1439 (function(){ 1442 (function(){
1440 const $ = (id) => document.getElementById(id); 1443 const $ = (id) => document.getElementById(id);
1441 const SITE_LABEL = {"lou-ka":"Lou·Ka","immo-ka":"Immo·Ka","vrai-prix":"Vrai-Prix","auto-ka":"Auto·Ka","food-ka":"Food·Ka","fabri-ka":"Fabri·Ka","resto-ka":"Resto·Ka","sorti-ka":"Sorti·Ka","crea-ka":"Créa·Ka","job-ka":"Job·Ka","trouve-ka":"Trouve·Ka"}; 1444 const SITE_LABEL = {"lou-ka":"Lou·Ka","immo-ka":"Immo·Ka","vrai-prix":"Vrai-Prix","auto-ka":"Auto·Ka","food-ka":"Food·Ka","fabri-ka":"Fabri·Ka","resto-ka":"Resto·Ka","sorti-ka":"Sorti·Ka","crea-ka":"Créa·Ka","job-ka":"Job·Ka","trouve-ka":"Trouve·Ka"};
1445+ // overlay de chargement
1446+ let ov;
1447+ function showLoading(msg){
1448+ if(!ov){ ov=document.createElement('div'); ov.id='stLoad';
1449+ ov.innerHTML='<div class="stload-box"><div class="stload-spin"></div><div class="stload-msg"></div><div class="stload-sub">Ça peut prendre un moment…</div></div>';
1450+ document.body.appendChild(ov); }
1451+ ov.querySelector('.stload-msg').textContent=msg||'Génération…';
1452+ ov.classList.add('on');
1453+ }
1454+ function hideLoading(){ if(ov) ov.classList.remove('on'); }
1442 async function copyText(txt, btn){ 1455 async function copyText(txt, btn){
1443 try { await navigator.clipboard.writeText(txt); } 1456 try { await navigator.clipboard.writeText(txt); }
1444 catch(e){ const t=document.createElement('textarea'); t.value=txt; document.body.appendChild(t); t.select(); try{document.execCommand('copy');}catch(_){} 1457 catch(e){ const t=document.createElement('textarea'); t.value=txt; document.body.appendChild(t); t.select(); try{document.execCommand('copy');}catch(_){}
@@ -1446,55 +1459,49 @@
1446 if(btn){ const o=btn.innerHTML; btn.innerHTML='✓ Copié'; setTimeout(()=>btn.innerHTML=o,1400); } 1459 if(btn){ const o=btn.innerHTML; btn.innerHTML='✓ Copié'; setTimeout(()=>btn.innerHTML=o,1400); }
1447 } 1460 }
1448 function esc(s){ return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); } 1461 function esc(s){ return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
1449− function renderGallery(items){ 1462+ function renderGallery(items, generating){
1450 const el=$('stGallery'); if(!el) return; 1463 const el=$('stGallery'); if(!el) return;
1451 if($('stCount')) $('stCount').textContent = items.length ? (items.length+' visuel'+(items.length>1?'s':'')) : ''; 1464 if($('stCount')) $('stCount').textContent = items.length ? (items.length+' visuel'+(items.length>1?'s':'')) : '';
1452− if(!items.length){ el.innerHTML='<div class="soc-sub">Aucun visuel encore — génère ton premier post ou reel ci-dessus.</div>'; return; } 1465+ let html = '';
1453− el.innerHTML = items.map(function(it,idx){ 1466+ if(generating){ html += '<div class="st-item st-skel"><div class="st-media"><div class="st-shimmer"></div><span class="st-badge">⏳ en création…</span></div><div class="st-cap">L\'Agent KA génère un visuel…</div></div>'; }
1467+ if(!items.length && !generating){ el.innerHTML='<div class="soc-sub">Aucun visuel encore — génère ton premier post ou reel ci-dessus.</div>'; return; }
1468+ html += items.map(function(it,idx){
1454 const media = it.kind==='reel' 1469 const media = it.kind==='reel'
1455 ? '<video src="'+it.url+'" muted loop playsinline preload="metadata" onmouseover="this.play()" onmouseout="this.pause()"></video>' 1470 ? '<video src="'+it.url+'" muted loop playsinline preload="metadata" onmouseover="this.play()" onmouseout="this.pause()"></video>'
1456 : '<img src="'+it.url+'" loading="lazy">'; 1471 : '<img src="'+it.url+'" loading="lazy">';
1457 const badge = (SITE_LABEL[it.site]||it.site)+(it.kind==='reel'?' · Reel':' · Post'); 1472 const badge = (SITE_LABEL[it.site]||it.site)+(it.kind==='reel'?' · Reel':' · Post');
1458− return '<div class="st-item">' 1473+ return '<div class="st-item"><div class="st-media">'+media+'<span class="st-badge">'+esc(badge)+'</span></div>'
1459− + '<div class="st-media">'+media+'<span class="st-badge">'+esc(badge)+'</span></div>'
1460 + '<div class="st-cap" id="stcap'+idx+'">'+esc(it.caption||'')+'</div>' 1474 + '<div class="st-cap" id="stcap'+idx+'">'+esc(it.caption||'')+'</div>'
1461− + '<div class="st-item-actions">' 1475+ + '<div class="st-item-actions"><a class="btn-cta small" href="'+it.url+'" download="'+it.site+'-'+it.kind+(it.kind==='reel'?'.mp4':'.png')+'">⬇︎</a>'
1462− + '<a class="btn-cta small" href="'+it.url+'" download="'+it.site+'-'+it.kind+(it.kind==='reel'?'.mp4':'.png')+'">⬇︎</a>' 1476+ + '<button class="btn-ghost small" data-copy="'+idx+'">📋 Copier</button></div></div>';
1463− + '<button class="btn-ghost small" data-copy="'+idx+'">📋 Copier</button>'
1464− + '</div></div>';
1465 }).join(''); 1477 }).join('');
1478+ el.innerHTML = html;
1466 el.querySelectorAll('[data-copy]').forEach(function(b){ b.onclick=function(){ copyText(items[+b.dataset.copy].caption||'', b); }; }); 1479 el.querySelectorAll('[data-copy]').forEach(function(b){ b.onclick=function(){ copyText(items[+b.dataset.copy].caption||'', b); }; });
1467 } 1480 }
1481+ let polling=false;
1468 async function loadGallery(){ 1482 async function loadGallery(){
1469 try{ const d=await (await fetch('/api/social/gallery')).json(); 1483 try{ const d=await (await fetch('/api/social/gallery')).json();
1470− renderGallery(d.items||[]); 1484+ const gen = d.gen && d.gen.running;
1471− if(d.gen && d.gen.running){ if($('stGenStatus')) $('stGenStatus').textContent='⏳ génération des reels en cours…'; setTimeout(loadGallery, 12000); } 1485+ renderGallery(d.items||[], gen);
1472− else if($('stGenStatus')) $('stGenStatus').textContent=''; 1486+ if($('stGenStatus')) $('stGenStatus').textContent = gen ? '⏳ génération des reels en cours…' : '';
1487+ if(gen && !polling){ polling=true; setTimeout(function(){ polling=false; loadGallery(); }, 10000); }
1473 }catch(e){} 1488 }catch(e){}
1474 } 1489 }
1475− async function genPost(){ 1490+ async function genOne(url, body, msg){
1476− const g=$('stPostGen'); if(!g) return; const prev=g.textContent; g.disabled=true; g.textContent='Génération…'; 1491+ showLoading(msg);
1477− try{ const r=await fetch('/api/social/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({site:$('stPostSite').value,prompt:$('stPostPrompt').value})}); 1492+ try{ const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
1478− const d=await r.json(); if(!d.draft) alert('Échec : '+(d.error||'?')); else loadGallery(); 1493+ const d=await r.json(); if(!d.draft){ alert('Échec : '+(d.error||'?')); } else { await loadGallery(); }
1479− }catch(e){ alert('Échec : '+e.message); } g.disabled=false; g.textContent=prev;
1480− }
1481− async function genReel(){
1482− const g=$('stReelGen'); if(!g) return; const prev=g.textContent; g.disabled=true; g.textContent='Génération vidéo…';
1483− try{ const r=await fetch('/api/social/reel/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({site:$('stReelSite').value,prompt:$('stReelPrompt').value})});
1484− const d=await r.json(); if(!d.draft) alert('Échec : '+(d.error||'?')); else loadGallery();
1485− }catch(e){ alert('Échec : '+e.message); } g.disabled=false; g.textContent=prev;
1486− }
1487− async function gen10(){
1488− const b=$('stReel10'); if(!b) return;
1489− try{ const r=await fetch('/api/social/reel/genbatch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({})});
1490− const d=await r.json(); if($('stGenStatus')) $('stGenStatus').textContent = d.started? '⏳ 10 reels en génération…' : (d.reason||'déjà en cours');
1491− setTimeout(loadGallery, 8000);
1492 }catch(e){ alert('Échec : '+e.message); } 1494 }catch(e){ alert('Échec : '+e.message); }
1495+ hideLoading();
1493 } 1496 }
1494 function bind(){ 1497 function bind(){
1495− if($('stPostGen')) $('stPostGen').onclick=genPost; 1498+ if($('stPostGen')) $('stPostGen').onclick=()=>genOne('/api/social/generate',{site:$('stPostSite').value,prompt:$('stPostPrompt').value},'🖼️ Génération de l\'image…');
1496− if($('stReelGen')) $('stReelGen').onclick=genReel; 1499+ if($('stReelGen')) $('stReelGen').onclick=()=>genOne('/api/social/reel/generate',{site:$('stReelSite').value,prompt:$('stReelPrompt').value},'🎬 Génération du reel (vidéo + musique)…');
1497− if($('stReel10')) $('stReel10').onclick=gen10; 1500+ if($('stReel10')) $('stReel10').onclick=async ()=>{
1501+ try{ const r=await fetch('/api/social/reel/genbatch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({})});
1502+ const d=await r.json(); if($('stGenStatus')) $('stGenStatus').textContent=d.started?'⏳ 10 reels en génération…':(d.reason||'déjà en cours'); loadGallery();
1503+ }catch(e){ alert('Échec : '+e.message); }
1504+ };
1498 if($('stRefresh')) $('stRefresh').onclick=loadGallery; 1505 if($('stRefresh')) $('stRefresh').onclick=loadGallery;
1499 } 1506 }
1500 bind(); 1507 bind();
modified public/style.css +12 −0
@@ -677,3 +677,15 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p
677 .st-item-actions{display:flex;gap:6px;padding:0 10px 10px;margin-top:auto} 677 .st-item-actions{display:flex;gap:6px;padding:0 10px 10px;margin-top:auto}
678 .st-item-actions .btn-cta.small{flex:0 0 auto;text-decoration:none;text-align:center} 678 .st-item-actions .btn-cta.small{flex:0 0 auto;text-decoration:none;text-align:center}
679 .st-item-actions .btn-ghost.small{flex:1} 679 .st-item-actions .btn-ghost.small{flex:1}
680+
681+/* ---- Studio loading ---- */
682+#stLoad{position:fixed;inset:0;z-index:9999;display:none;align-items:center;justify-content:center;background:rgba(8,8,10,.72);backdrop-filter:blur(4px)}
683+#stLoad.on{display:flex}
684+.stload-box{background:var(--card,#141414);border:2px solid var(--ink,#1a1a1a);border-radius:18px;padding:26px 30px;text-align:center;box-shadow:4px 4px 0 var(--ink,#1a1a1a);max-width:80vw}
685+.stload-spin{width:44px;height:44px;margin:0 auto 14px;border:5px solid rgba(127,127,127,.25);border-top-color:var(--accent,#f97316);border-radius:50%;animation:stspin .8s linear infinite}
686+@keyframes stspin{to{transform:rotate(360deg)}}
687+.stload-msg{font-weight:800;font-size:15px}
688+.stload-sub{font-size:12px;color:var(--muted,#888);margin-top:6px}
689+.st-skel .st-media{position:relative}
690+.st-shimmer{position:absolute;inset:0;background:linear-gradient(100deg,#1a1a1a 30%,#2a2a2a 50%,#1a1a1a 70%);background-size:200% 100%;animation:stshim 1.3s linear infinite}
691+@keyframes stshim{to{background-position:-200% 0}}
modified server/social/make_reel.py +43 −32
@@ -136,13 +136,16 @@ def build_html(ins):
136 @keyframes fadeUp{to{opacity:1;transform:translateY(0)}} 136 @keyframes fadeUp{to{opacity:1;transform:translateY(0)}}
137 @keyframes pop{to{opacity:1;transform:rotate(-4deg) scale(1)}} 137 @keyframes pop{to{opacity:1;transform:rotate(-4deg) scale(1)}}
138 @keyframes heroIn{to{opacity:1;transform:translateY(0) scale(1)}} 138 @keyframes heroIn{to{opacity:1;transform:translateY(0) scale(1)}}
139+ .vign{position:absolute;inset:0;pointer-events:none;background:radial-gradient(120% 90% at 50% 42%, transparent 55%, rgba(0,0,0,.55) 100%)}
140+ .numwrap{animation:heroIn .95s cubic-bezier(.2,1,.3,1) .95s forwards, pulse 3s ease-in-out 2s infinite}
141+ @keyframes pulse{0%,100%{filter:none}50%{filter:brightness(1.12)}}
139 """ 142 """
140 css=(css.replace("__H__",str(H)).replace("__FONT__",FONT).replace("__A__",p["a"]) 143 css=(css.replace("__H__",str(H)).replace("__FONT__",FONT).replace("__A__",p["a"])
141 .replace("__B__",p["b"]).replace("__G1__",p["g1"]).replace("__G2__",p["g2"]) 144 .replace("__B__",p["b"]).replace("__G1__",p["g1"]).replace("__G2__",p["g2"])
142 .replace("__NUM__",str(numsize)).replace("__DUR__",str(DUR))) 145 .replace("__NUM__",str(numsize)).replace("__DUR__",str(DUR)))
143 tag = ('<div class="tag">'+esc(ins.get("tagline",""))+'</div>') if ins.get("tagline") else "" 146 tag = ('<div class="tag">'+esc(ins.get("tagline",""))+'</div>') if ins.get("tagline") else ""
144 tiles_block = ('<div class="tiles">'+tiles_html+'</div>') if tiles else "" 147 tiles_block = ('<div class="tiles">'+tiles_html+'</div>') if tiles else ""
145− body = ('<div class="stage"><div class="mesh"></div><div class="pad">' 148+ body = ('<div class="stage"><div class="mesh"></div><div class="vign"></div><div class="pad">'
146 '<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · '+today().upper()+'</div>' 149 '<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · '+today().upper()+'</div>'
147 '<div class="kabadge">KA</div>' 150 '<div class="kabadge">KA</div>'
148 '<div class="site">'+esc(ins["label"])+'</div>'+tag+ 151 '<div class="site">'+esc(ins["label"])+'</div>'+tag+
@@ -194,45 +197,53 @@ def record_webm(html_path, out_webm, tmpdir):
194 os.replace(path, out_webm) 197 os.replace(path, out_webm)
195 198
196 def make_music(dur, out_wav): 199 def make_music(dur, out_wav):
197− """Lit musical pro : progression Am-F-C-G (pad) + kick 4-temps + shaker + réverbe.""" 200+ """Lit musical riche : progression Am-F-C-G (pad) + basse + kick 4-temps + shaker
201+ + arpège/pluck brillant, avec réverbe/compresseur/limiteur."""
198 tmpdir=os.path.dirname(out_wav) 202 tmpdir=os.path.dirname(out_wav)
199 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)] 203 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)]
204+ roots=[110.00,87.31,130.81,98.00]
200 seg=round(dur/len(chords),3) 205 seg=round(dur/len(chords),3)
201− parts=[] 206+ def concat(paths,outp):
207+ lst=outp+".txt"; open(lst,"w").write("".join("file '"+x+"'\n" for x in paths))
208+ subprocess.run([FFMPEG,"-y","-f","concat","-safe","0","-i",lst,"-c","copy",outp],capture_output=True)
209+ # pad (accords)
210+ padparts=[]
202 for i,(f1,f2,f3) in enumerate(chords): 211 for i,(f1,f2,f3) in enumerate(chords):
203− w=os.path.join(tmpdir,f"ch{i}.wav") 212+ w=os.path.join(tmpdir,"ch%d.wav"%i)
204− subprocess.run([FFMPEG,"-y", 213+ subprocess.run([FFMPEG,"-y","-f","lavfi","-i","sine=frequency=%s:duration=%s"%(f1,seg),
205− "-f","lavfi","-i",f"sine=frequency={f1}:duration={seg}", 214+ "-f","lavfi","-i","sine=frequency=%s:duration=%s"%(f2,seg),
206− "-f","lavfi","-i",f"sine=frequency={f2}:duration={seg}", 215+ "-f","lavfi","-i","sine=frequency=%s:duration=%s"%(f3,seg),
207− "-f","lavfi","-i",f"sine=frequency={f3}:duration={seg}", 216+ "-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),
208− "-filter_complex", 217+ "-map","[a]",w],capture_output=True); padparts.append(w)
209− f"[0][1][2]amix=inputs=3:normalize=0,volume=0.22,lowpass=f=1300," 218+ pad=os.path.join(tmpdir,"pad.wav"); concat(padparts,pad)
210− f"afade=t=in:st=0:d=0.25,afade=t=out:st={seg-0.35}:d=0.35[a]", 219+ # basse (fondamentale par accord)
211− "-map","[a]",w],capture_output=True) 220+ bassparts=[]
212− parts.append(w) 221+ for i,r in enumerate(roots):
213− # concat des accords -> pad 222+ w=os.path.join(tmpdir,"b%d.wav"%i)
214− lst=os.path.join(tmpdir,"chords.txt") 223+ subprocess.run([FFMPEG,"-y","-f","lavfi","-i","sine=frequency=%s:duration=%s"%(r,seg),
215− open(lst,"w").write("".join(f"file '{w}'\n" for w in parts)) 224+ "-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),
216− pad=os.path.join(tmpdir,"pad.wav") 225+ w],capture_output=True); bassparts.append(w)
217− subprocess.run([FFMPEG,"-y","-f","concat","-safe","0","-i",lst,"-c","copy",pad],capture_output=True) 226+ bass=os.path.join(tmpdir,"bass.wav"); concat(bassparts,bass)
218− # kick 4-temps (120 BPM) + shaker offbeat, synthétisés 227+ # kick 4-temps
219 kick=os.path.join(tmpdir,"kick.wav") 228 kick=os.path.join(tmpdir,"kick.wav")
220− subprocess.run([FFMPEG,"-y","-f","lavfi", 229+ subprocess.run([FFMPEG,"-y","-f","lavfi","-i","aevalsrc=0.75*sin(2*PI*55*t)*exp(-9*mod(t\,0.5)):d=%s:s=44100"%dur,kick],capture_output=True)
221− "-i",f"aevalsrc=0.7*sin(2*PI*55*t)*exp(-9*mod(t\,0.5)):d={dur}:s=44100", 230+ # shaker/hat contretemps
222− kick],capture_output=True)
223 hat=os.path.join(tmpdir,"hat.wav") 231 hat=os.path.join(tmpdir,"hat.wav")
232+ subprocess.run([FFMPEG,"-y","-f","lavfi","-i","aevalsrc=0.10*(random(0)*2-1)*exp(-45*mod(t+0.25\,0.5)):d=%s:s=44100"%dur,"-af","highpass=f=6500",hat],capture_output=True)
233+ # arpège/pluck brillant (croches, deux notes en alternance)
234+ arp=os.path.join(tmpdir,"arp.wav")
224 subprocess.run([FFMPEG,"-y","-f","lavfi", 235 subprocess.run([FFMPEG,"-y","-f","lavfi",
225− "-i",f"aevalsrc=0.12*(random(0)*2-1)*exp(-45*mod(t+0.25\,0.5)):d={dur}:s=44100", 236+ "-i","aevalsrc=0.13*sin(2*PI*880*t)*exp(-13*mod(t\,0.25))+0.10*sin(2*PI*659.25*(t-0.125))*exp(-13*mod(t+0.125\,0.25)):d=%s:s=44100"%dur,
226− "-af","highpass=f=6000",hat],capture_output=True) 237+ "-af","highpass=f=350,lowpass=f=6000",arp],capture_output=True)
227− # mix final : pad + kick + hat, réverbe douce, master fades 238+ # mix final
228− subprocess.run([FFMPEG,"-y","-i",pad,"-i",kick,"-i",hat, 239+ subprocess.run([FFMPEG,"-y","-i",pad,"-i",bass,"-i",kick,"-i",hat,"-i",arp,
229 "-filter_complex", 240 "-filter_complex",
230− f"[0]volume=1.0[p];[1]volume=0.9[k];[2]volume=0.5[h];" 241+ "[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];"
231− f"[p][k][h]amix=inputs=3:normalize=0," 242+ "[p][b][k][h][ar]amix=inputs=5:normalize=0,"
232− f"aecho=0.8:0.85:60:0.25," 243+ "aecho=0.8:0.88:70:0.22,"
233− f"acompressor=threshold=-16dB:ratio=3:attack=8:release=180," 244+ "acompressor=threshold=-18dB:ratio=3.5:attack=6:release=160,"
234− f"alimiter=limit=0.95," 245+ "highpass=f=32,alimiter=limit=0.94,"
235− f"afade=t=in:st=0:d=0.5,afade=t=out:st={dur-1.6}:d=1.6[a]", 246+ "afade=t=in:st=0:d=0.5,afade=t=out:st=%s:d=1.6[a]"%(dur-1.6),
236 "-map","[a]","-ar","44100",out_wav],capture_output=True) 247 "-map","[a]","-ar","44100",out_wav],capture_output=True)
237 248
238 def render(ins, out_dir): 249 def render(ins, out_dir):
239 250
Menu

Theme