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%

Studio: onglet generation-only (post + reel) avec galerie visuelle (download + copier texte), bouton 10 reels ; plus de publication FB ni de tache horaire

Simon-Pierre Boucher committed 1 mo ago (Aug 23, 2026) parent 6e04c50

5 changed files +198 −77

modified public/app.js +69 −1
@@ -62,7 +62,7 @@
62 62 if (name === 'sessions') loadSessions();
63 63 if (name === 'prompts') loadPrompts();
64 64 if (name === 'eco') { loadEco(); send({ type: 'eco_sub' }); } else { send({ type: 'eco_unsub' }); }
65 if (name === 'social') loadSocial();
65 + if (name === 'social' && window.__loadGallery) window.__loadGallery();
66 66 if (name === 'settings') refreshSettingsView();
67 67 }
68 68 document.querySelectorAll('.nav-btn').forEach((b) => { b.onclick = () => switchView(b.dataset.view); });
@@ -1433,3 +1433,71 @@
1433 1433
1434 1434 init();
1435 1435 })();
1436 +
1437 +
1438 +// ===== STUDIO_SOCIAL v2 (générateurs + galerie, download + copy) =====
1439 +(function(){
1440 + 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"};
1442 + async function copyText(txt, btn){
1443 + 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(_){}
1445 + document.body.removeChild(t); }
1446 + if(btn){ const o=btn.innerHTML; btn.innerHTML='✓ Copié'; setTimeout(()=>btn.innerHTML=o,1400); }
1447 + }
1448 + function esc(s){ return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
1449 + function renderGallery(items){
1450 + const el=$('stGallery'); if(!el) return;
1451 + 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; }
1453 + el.innerHTML = items.map(function(it,idx){
1454 + const media = it.kind==='reel'
1455 + ? '<video src="'+it.url+'" muted loop playsinline preload="metadata" onmouseover="this.play()" onmouseout="this.pause()"></video>'
1456 + : '<img src="'+it.url+'" loading="lazy">';
1457 + const badge = (SITE_LABEL[it.site]||it.site)+(it.kind==='reel'?' · Reel':' · Post');
1458 + return '<div class="st-item">'
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>'
1461 + + '<div class="st-item-actions">'
1462 + + '<a class="btn-cta small" href="'+it.url+'" download="'+it.site+'-'+it.kind+(it.kind==='reel'?'.mp4':'.png')+'">⬇︎</a>'
1463 + + '<button class="btn-ghost small" data-copy="'+idx+'">📋 Copier</button>'
1464 + + '</div></div>';
1465 + }).join('');
1466 + el.querySelectorAll('[data-copy]').forEach(function(b){ b.onclick=function(){ copyText(items[+b.dataset.copy].caption||'', b); }; });
1467 + }
1468 + async function loadGallery(){
1469 + try{ const d=await (await fetch('/api/social/gallery')).json();
1470 + renderGallery(d.items||[]);
1471 + if(d.gen && d.gen.running){ if($('stGenStatus')) $('stGenStatus').textContent='⏳ génération des reels en cours…'; setTimeout(loadGallery, 12000); }
1472 + else if($('stGenStatus')) $('stGenStatus').textContent='';
1473 + }catch(e){}
1474 + }
1475 + async function genPost(){
1476 + const g=$('stPostGen'); if(!g) return; const prev=g.textContent; g.disabled=true; g.textContent='Génération…';
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})});
1478 + const d=await r.json(); if(!d.draft) alert('Échec : '+(d.error||'?')); else 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); }
1493 + }
1494 + function bind(){
1495 + if($('stPostGen')) $('stPostGen').onclick=genPost;
1496 + if($('stReelGen')) $('stReelGen').onclick=genReel;
1497 + if($('stReel10')) $('stReel10').onclick=gen10;
1498 + if($('stRefresh')) $('stRefresh').onclick=loadGallery;
1499 + }
1500 + bind();
1501 + window.__loadGallery = loadGallery;
1502 +})();
1503 +
modified public/index.html +20 −69
@@ -148,82 +148,34 @@
148 148
149 149 <!-- ======== Vue SOCIAL ======== -->
150 150 <section id="view-social" class="view hidden">
151 <div class="view-head"><h2>Social</h2></div>
151 + <div class="view-head"><h2>🎨 Studio</h2><button id="stRefresh" class="btn-ghost small">↻ Galerie</button></div>
152 + <p class="soc-sub" style="margin:-4px 0 14px">Génère des visuels prêts à publier — télécharge-les et copie le texte. Rien n'est publié automatiquement.</p>
152 153
153 <div class="soc-card">
154 <h3>Écran du nœud (M3U96a) — pour finir un Reel</h3>
155 <p class="soc-sub">Quand un Reel est prêt à publier, l'éditeur Facebook demande un clic « Suivant » manuel. Connecte-toi ici pour voir et cliquer directement dans l'app.</p>
156 <div class="soc-actions">
157 <button id="vncConnect" class="btn-cta small">🖥️ Connecter l'écran</button>
158 <button id="vncDisconnect" class="btn-ghost small">Déconnecter</button>
159 <span id="vncStatus" class="soc-sub"></span>
160 </div>
161 <div id="vncPanel" style="display:none;margin-top:12px;border:2px solid var(--ink,#1a1a1a);border-radius:12px;overflow:hidden;background:#0d0d0d;min-height:200px"></div>
162 </div>
163
164 <div class="soc-card">
165 <label class="soc-switch">
166 <input type="checkbox" id="socAutoToggle">
167 <span>Publication automatique sur la Page Facebook Groupe-KA</span>
168 </label>
169 <div class="soc-sub" id="socNext">—</div>
170 <div class="soc-actions">
171 <button id="socRunNow" class="btn-cta small">⚡ Publier maintenant</button>
172 <button id="socCheckLogin" class="btn-ghost small">Vérifier Facebook</button>
173 </div>
174 <div class="soc-sub" id="socLogin"></div>
175 </div>
176
177 <div class="soc-card">
178 <h3>Générer un post</h3>
179 <textarea id="socPrompt" rows="3" placeholder="Décris le post voulu (optionnel). Ex : mets en valeur les aubaines de logement sous le prix du marché."></textarea>
180 <div class="soc-actions">
181 <select id="socSite" class="soc-select">
182 <option value="">Choix auto (fait marquant)</option>
183 <option value="lou-ka">Lou·Ka</option><option value="immo-ka">Immo·Ka</option><option value="vrai-prix">Vrai-Prix</option><option value="auto-ka">Auto·Ka</option><option value="food-ka">Food·Ka</option><option value="fabri-ka">Fabri·Ka</option><option value="resto-ka">Resto·Ka</option><option value="sorti-ka">Sorti·Ka</option><option value="crea-ka">Créa·Ka</option><option value="job-ka">Job·Ka</option><option value="trouve-ka">Trouve·Ka</option>
184 </select>
185 <button id="socGenerate" class="btn-cta small">✨ Générer</button>
186 </div>
187 <div id="socDraft" class="soc-draft hidden">
188 <img id="socImg" alt="aperçu">
189 <textarea id="socCaption" rows="7"></textarea>
154 + <div class="st-grid">
155 + <div class="soc-card st-gen">
156 + <h3>🖼️ Publication</h3>
157 + <textarea id="stPostPrompt" rows="2" placeholder="Sujet (optionnel). Ex : dernière propriété à vendre sur Immo-Ka."></textarea>
190 158 <div class="soc-actions">
191 <button id="socPublish" class="btn-cta small">📣 Publier ce post</button>
192 <button id="socRegen" class="btn-ghost small">↻ Regénérer</button>
159 + <select id="stPostSite" class="soc-select"><option value="">Choix auto</option><option value="lou-ka">Lou·Ka</option><option value="immo-ka">Immo·Ka</option><option value="vrai-prix">Vrai-Prix</option><option value="auto-ka">Auto·Ka</option><option value="food-ka">Food·Ka</option><option value="fabri-ka">Fabri·Ka</option><option value="resto-ka">Resto·Ka</option><option value="sorti-ka">Sorti·Ka</option><option value="crea-ka">Créa·Ka</option><option value="job-ka">Job·Ka</option><option value="trouve-ka">Trouve·Ka</option></select>
160 + <button id="stPostGen" class="btn-cta small">✨ Générer une image</button>
193 161 </div>
194 162 </div>
195 </div>
196
197 <div class="soc-card">
198 <h3>Reel vidéo (animation + musique)</h3>
199 <p class="soc-sub">L'Agent KA génère un reel animé avec musique. La publication vidéo demande un clic « Suivant » manuel (limite du nœud sans écran) — utilise « Publier (assisté) » puis termine sur l'écran distant, ou télécharge pour publier depuis ton téléphone.</p>
200 <textarea id="socReelPrompt" rows="2" placeholder="Sujet du reel (optionnel)"></textarea>
201 <div class="soc-actions">
202 <select id="socReelSite" class="soc-select">
203 <option value="">Choix auto (fait marquant)</option>
204 <option value="lou-ka">Lou·Ka</option><option value="immo-ka">Immo·Ka</option>
205 <option value="vrai-prix">Vrai-Prix</option><option value="auto-ka">Auto·Ka</option>
206 <option value="food-ka">Food·Ka</option><option value="fabri-ka">Fabri·Ka</option>
207 <option value="resto-ka">Resto·Ka</option><option value="sorti-ka">Sorti·Ka</option>
208 <option value="crea-ka">Créa·Ka</option><option value="job-ka">Job·Ka</option>
209 <option value="trouve-ka">Trouve·Ka</option>
210 </select>
211 <button id="socReelGen" class="btn-cta small">🎬 Générer un reel</button>
212 </div>
213 <div id="socReelDraft" class="soc-draft hidden">
214 <video id="socReelVid" controls playsinline style="width:100%;max-width:300px;border-radius:12px;border:2px solid var(--ink,#1a1a1a);align-self:center"></video>
215 <textarea id="socReelCaption" rows="6"></textarea>
163 + <div class="soc-card st-gen">
164 + <h3>🎬 Reel vidéo</h3>
165 + <textarea id="stReelPrompt" rows="2" placeholder="Sujet (optionnel). Ex : le dernier logement en annonce sur Lou-Ka."></textarea>
216 166 <div class="soc-actions">
217 <button id="socReelPublish" class="btn-cta small">📣 Publier (assisté)</button>
218 <a id="socReelDl" class="btn-ghost small" download>⬇︎ Télécharger</a>
167 + <select id="stReelSite" class="soc-select"><option value="">Choix auto</option><option value="lou-ka">Lou·Ka</option><option value="immo-ka">Immo·Ka</option><option value="vrai-prix">Vrai-Prix</option><option value="auto-ka">Auto·Ka</option><option value="food-ka">Food·Ka</option><option value="fabri-ka">Fabri·Ka</option><option value="resto-ka">Resto·Ka</option><option value="sorti-ka">Sorti·Ka</option><option value="crea-ka">Créa·Ka</option><option value="job-ka">Job·Ka</option><option value="trouve-ka">Trouve·Ka</option></select>
168 + <button id="stReelGen" class="btn-cta small">🎬 Générer un reel</button>
169 + </div>
170 + <div class="soc-actions" style="margin-top:8px">
171 + <button id="stReel10" class="btn-ghost small">🔥 Générer 10 reels</button>
172 + <span id="stGenStatus" class="soc-sub"></span>
219 173 </div>
220 174 </div>
221 175 </div>
222 176
223 <div class="soc-card">
224 <h3>Journal</h3>
225 <div id="socLog" class="soc-log"></div>
226 </div>
177 + <div class="view-head" style="margin-top:18px"><h2 style="font-size:17px">Galerie</h2><span id="stCount" class="soc-sub"></span></div>
178 + <div id="stGallery" class="st-gallery"><div class="soc-sub">Aucun visuel encore — génère ton premier post ou reel ci-dessus.</div></div>
227 179 </section>
228 180
229 181 <!-- ======== Bottom nav ======== -->
@@ -244,7 +196,7 @@
244 196 <svg viewBox="0 0 24 24"><path d="M4 17l4-6 4 3 4-8 4 5" stroke="currentColor" stroke-width="1.9" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 21h18" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
245 197 <span>Écosystème</span><i class="badge hidden"></i>
246 198 </button>
247 <!-- social-hidden (désactivé temporairement) -->
199 + <button data-view="social" class="nav-btn"><svg viewBox="0 0 24 24"><path d="M4 5h16v11H4z M8 20h8" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 8l5 2.5-5 2.5z" fill="currentColor"/></svg><span>Studio</span><i class="badge hidden"></i></button>
248 200 <button data-view="settings" class="nav-btn">
249 201 <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2" stroke="currentColor" stroke-width="1.8" fill="none"/><path d="M12 2.8v3M12 18.2v3M2.8 12h3M18.2 12h3M5.5 5.5l2.1 2.1M16.4 16.4l2.1 2.1M18.5 5.5l-2.1 2.1M7.6 16.4l-2.1 2.1" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>
250 202 <span>Réglages</span><i class="badge hidden"></i>
@@ -264,6 +216,5 @@
264 216 <script src="/vendor/marked.min.js"></script>
265 217 <script src="/vendor/highlight.min.js"></script>
266 218 <script src="/app.js"></script>
267 <script type="module">import RFB from "/vendor/novnc/core/rfb.js?v=3"; window.KA_RFB=RFB;</script>
268 219 </body>
269 220 </html>
modified public/style.css +14 −0
@@ -663,3 +663,17 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p
663 663
664 664 #vncPanel canvas{max-width:100%;height:auto;display:block;margin:0 auto}
665 665 #vncPanel{max-height:70vh}
666 +
667 +/* ---- Studio ---- */
668 +.st-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
669 +@media(max-width:640px){.st-grid{grid-template-columns:1fr}}
670 +.st-gen h3{margin:0 0 8px}
671 +.st-gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px;margin-top:8px}
672 +.st-item{background:var(--card,#fff);border:2px solid var(--ink,#1a1a1a);border-radius:14px;overflow:hidden;box-shadow:3px 3px 0 var(--ink,#1a1a1a);display:flex;flex-direction:column}
673 +.st-media{position:relative;aspect-ratio:4/5;background:#0d0d0d;overflow:hidden}
674 +.st-media img,.st-media video{width:100%;height:100%;object-fit:cover;display:block}
675 +.st-badge{position:absolute;left:8px;top:8px;background:rgba(13,13,13,.82);color:#fff;font-size:11px;font-weight:800;padding:4px 9px;border-radius:999px;letter-spacing:.02em}
676 +.st-cap{font-size:11px;line-height:1.35;color:var(--muted,#555);padding:8px 10px;max-height:76px;overflow:auto;white-space:pre-wrap}
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}
679 +.st-item-actions .btn-ghost.small{flex:1}
modified server/server.js +17 −1
@@ -11,7 +11,7 @@ import { spawn } from 'node:child_process';
11 11 import { fileURLToPath } from 'node:url';
12 12 import { WebSocketServer } from 'ws';
13 13 import { startMonitor, ecoSummary, ecoSite, ecoIncidents, ecoNodes, getIcon, SITES } from './monitor.js';
14 import { initSocial, socialState, socialLog, setAuto, generateDraft, publishDraft, runAutoCycle, cardPath, fbLoginState, generateReelDraft, publishReelDraft, runReelCycle, reelPath } from './social.js';
14 +import { initSocial, socialState, socialLog, setAuto, generateDraft, publishDraft, runAutoCycle, cardPath, fbLoginState, generateReelDraft, publishReelDraft, runReelCycle, reelPath, startReelBatch, batchStatus, REEL_BATCH_DEFAULT, getGallery, startGenBatch, genBatchStatus } from './social.js';
15 15 import { execFile } from 'node:child_process';
16 16
17 17 const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -727,6 +727,22 @@ const server = http.createServer(async (req, res) => {
727 727 const r = await runReelCycle('reel-manuel');
728 728 return json(res, r.ok ? 200 : 502, r);
729 729 }
730 + if (p === '/api/social/gallery' && req.method === 'GET') return json(res, 200, { items: getGallery(), gen: genBatchStatus() });
731 + if (p === '/api/social/reel/genbatch' && req.method === 'POST') {
732 + let body; try { body = JSON.parse(await readBody(req)); } catch { body = {}; }
733 + const sites = (Array.isArray(body.sites) && body.sites.length) ? body.sites : REEL_BATCH_DEFAULT;
734 + audit({ kind: 'social_genbatch', n: sites.length, ip: clientIp(req) });
735 + const r = startGenBatch(sites);
736 + return json(res, r.started ? 200 : 409, r);
737 + }
738 + if (p === '/api/social/reel/batch' && req.method === 'POST') {
739 + let body; try { body = JSON.parse(await readBody(req)); } catch { body = {}; }
740 + const sites = (Array.isArray(body.sites) && body.sites.length) ? body.sites : REEL_BATCH_DEFAULT;
741 + audit({ kind: 'social_reel_batch', n: sites.length, ip: clientIp(req) });
742 + const r = startReelBatch(sites);
743 + return json(res, r.started ? 200 : 409, r);
744 + }
745 + if (p === '/api/social/reel/batchstatus' && req.method === 'GET') return json(res, 200, batchStatus());
730 746 if (p.startsWith('/api/social/reel/') && req.method === 'GET') {
731 747 const fp = reelPath(p.split('/')[4] || '');
732 748 if (!fp) { res.writeHead(404); return res.end(); }
modified server/social.js +78 −6
@@ -201,8 +201,9 @@ async function _fbLoginState() {
201 201
202 202 // ---------- publication (texte + 1 image) sur la Page ----------
203 203 export function publishToPage(caption, imagePath) { return withSafari(() => _publishMedia(caption, imagePath, false)); }
204 export function publishReelToPage(caption, videoPath) { return withSafari(() => _publishMedia(caption, videoPath, true)); }
205 async function _publishMedia(caption, mediaPath, isVideo) {
204 +export function publishReelToPage(caption, videoPath) { return withSafari(() => _publishMedia(caption, videoPath, true, false)); }
205 +export function publishReelAssisted(caption, videoPath) { return withSafari(() => _publishMedia(caption, videoPath, true, true)); }
206 +async function _publishMedia(caption, mediaPath, isVideo, assisted) {
206 207 await gotoPage();
207 208 await sleep(9000);
208 209
@@ -373,7 +374,10 @@ async function _publishMedia(caption, mediaPath, isVideo) {
373 374 return 'clicked';
374 375 })();`;
375 376 const canPublishJS = `(function(){var ok=false;document.querySelectorAll('div[role=dialog] [role=button]').forEach(function(b){var al=(b.getAttribute('aria-label')||b.innerText||'').trim();if((al==='Publier'||al==='Partager')&&b.getAttribute('aria-disabled')!=='true')ok=true;});return ok?'YES':'NO';})();`;
376 if (isVideo) {
377 + if (isVideo && assisted) {
378 + // mode assisté : l'utilisateur clique « Suivant »/« Publier » sur l'écran du nœud
379 + logEvent({ kind: 'reel_waiting_user' });
380 + } else if (isVideo) {
377 381 // Reel/vidéo : avancer les « Suivant » par focus + touche Entrée réelle
378 382 for (let i = 0; i < 6; i++) {
379 383 if ((await osaJS(canPublishJS)) === 'YES') break;
@@ -408,7 +412,7 @@ async function _publishMedia(caption, mediaPath, isVideo) {
408 412 return open ? 'OPEN' : 'GONE';
409 413 })();`;
410 414 let gone = 'OPEN';
411 const goneTries = isVideo ? 40 : 18;
415 + const goneTries = assisted ? 45 : (isVideo ? 40 : 18);
412 416 for (let i = 0; i < goneTries; i++) {
413 417 await sleep(8000);
414 418 await dismissModals(2); // ferme toute boîte non bloquante en route
@@ -580,7 +584,9 @@ export async function generateDraft({ site = '', prompt = '' } = {}) {
580 584 }
581 585 const image = await renderCard(insight);
582 586 const caption = await writeCaption(insight, prompt);
583 return { insight, image, caption, imageUrl: '/api/social/card/' + path.basename(image) };
587 + const imageUrl='/api/social/card/'+path.basename(image);
588 + addToGallery({ kind:'post', site:insight.site, genre:insight.genre, caption, file:path.basename(image), url:imageUrl, ts:Date.now() });
589 + return { insight, image, caption, imageUrl };
584 590 }
585 591
586 592 // cycle complet automatique : choisir -> rendre -> rédiger -> publier
@@ -653,7 +659,9 @@ export async function generateReelDraft({ site = '', prompt = '' } = {}) {
653 659 }
654 660 const video = await makeReel(insight);
655 661 const caption = await writeCaption(insight, prompt);
656 return { insight, video, caption, videoUrl: '/api/social/reel/' + path.basename(video) };
662 + const videoUrl='/api/social/reel/'+path.basename(video);
663 + addToGallery({ kind:'reel', site:insight.site, genre:insight.genre, caption, file:path.basename(video), url:videoUrl, ts:Date.now() });
664 + return { insight, video, caption, videoUrl };
657 665 }
658 666
659 667 export async function publishReelDraft({ caption, video }) {
@@ -686,6 +694,70 @@ export async function runReelCycle(trigger = 'reel') {
686 694 }
687 695 }
688 696
697 +// ---------- Galerie (tout le contenu généré : posts + reels) ----------
698 +const GALLERY_PATH = path.join(SOCIAL_DIR, 'gallery.json');
699 +function loadGallery(){ try { return JSON.parse(fs.readFileSync(GALLERY_PATH,'utf8')); } catch { return []; } }
700 +function saveGallery(g){ try { fs.writeFileSync(GALLERY_PATH, JSON.stringify(g.slice(0,200))); } catch {} }
701 +function addToGallery(item){ const g=loadGallery(); g.unshift(item); saveGallery(g); }
702 +export function getGallery(){ return loadGallery().slice(0,60); }
703 +
704 +let genBatchRunning = false;
705 +export function startGenBatch(sites){
706 + if (genBatchRunning) return { started:false, reason:'génération en cours' };
707 + genBatchRunning = true;
708 + (async () => {
709 + logEvent({ kind:'genbatch_start', total:sites.length });
710 + for (let i=0;i<sites.length;i++){
711 + const site=sites[i];
712 + try{
713 + logEvent({ kind:'genbatch_item', i:i+1, total:sites.length, site });
714 + const insight=await pickInsight({ site });
715 + await decorateForPrompt(insight);
716 + const video=await makeReel(insight);
717 + const caption=await writeCaption(insight);
718 + addToGallery({ kind:'reel', site, genre:insight.genre, caption, file:path.basename(video),
719 + url:'/api/social/reel/'+path.basename(video), ts:Date.now() });
720 + logEvent({ kind:'genbatch_done_item', i:i+1, site });
721 + }catch(e){ logEvent({ kind:'genbatch_error', i:i+1, site, error:e.message }); }
722 + }
723 + logEvent({ kind:'genbatch_done', total:sites.length });
724 + genBatchRunning=false;
725 + })();
726 + return { started:true, count:sites.length };
727 +}
728 +export function genBatchStatus(){ return { running: genBatchRunning }; }
729 +
730 +// ---------- Lot de reels (génération + publication assistée) ----------
731 +let batchRunning = false;
732 +export const REEL_BATCH_DEFAULT = ['lou-ka','immo-ka','fabri-ka','sorti-ka','food-ka','auto-ka','vrai-prix','crea-ka','trouve-ka','resto-ka'];
733 +export function startReelBatch(sites) {
734 + if (batchRunning) return { started: false, reason: 'lot déjà en cours' };
735 + batchRunning = true;
736 + (async () => {
737 + logEvent({ kind: 'batch_start', total: sites.length });
738 + for (let i = 0; i < sites.length; i++) {
739 + const site = sites[i];
740 + try {
741 + logEvent({ kind: 'batch_item', i: i + 1, total: sites.length, site, step: 'génération vidéo' });
742 + const insight = await pickInsight({ site });
743 + await decorateForPrompt(insight);
744 + const video = await makeReel(insight);
745 + const caption = await writeCaption(insight);
746 + logEvent({ kind: 'batch_ready', i: i + 1, total: sites.length, site, genre: insight.genre, step: '⏳ CLIQUE « Suivant » puis « Publier » sur l\'écran du nœud' });
747 + await publishReelAssisted(caption, video);
748 + state.lastPostAt = Date.now(); state.postCount = (state.postCount || 0) + 1; saveState();
749 + logEvent({ kind: 'published', trigger: 'batch', format: 'reel', site, headline: insight.headline, caption, video: video.split('/').pop() });
750 + } catch (e) {
751 + logEvent({ kind: 'batch_error', i: i + 1, site, error: e.message });
752 + }
753 + }
754 + logEvent({ kind: 'batch_done', total: sites.length });
755 + batchRunning = false;
756 + })();
757 + return { started: true, count: sites.length };
758 +}
759 +export function batchStatus() { return { running: batchRunning }; }
760 +
689 761 // ---------- planificateur horaire ----------
690 762 function scheduleNext() {
691 763 clearTimeout(timer);
692 764