Studio: onglet generation-only (post + reel) avec galerie visuelle (download + copier texte), bouton 10 reels ; plus de publication FB ni de tache horaire
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,'&').replace(/</g,'<').replace(/>/g,'>'); } | |
| 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 | |