// app.js — Orquesta el flujo: subida → parser → análisis → descargas. import { parseFdx } from './parser.js'; import { buildSceneMap, buildCharacterReport, buildScaleta } from './analyzers.js'; import { pdfScript, pdfSceneMap, pdfCharacters, pdfScaleta } from './pdf-generator.js'; const els = { fileInput: document.getElementById('file-input'), dropzone: document.getElementById('dropzone'), processing: document.getElementById('processing'), results: document.getElementById('results'), stats: document.getElementById('stats'), error: document.getElementById('error'), iaButton: document.getElementById('btn-scaleta-ia'), iaStatus: document.getElementById('ai-status') }; const state = { json: null, sceneMap: null, characters: null, scaleta: null, baseName: 'guion' }; // --- subida y arrastre ----------------------------------------------------- els.fileInput.addEventListener('change', (e) => { const file = e.target.files?.[0]; if (file) handleFile(file); }); ['dragenter', 'dragover'].forEach(ev => { els.dropzone.addEventListener(ev, (e) => { e.preventDefault(); e.stopPropagation(); els.dropzone.classList.add('is-dragover'); }); }); ['dragleave', 'drop'].forEach(ev => { els.dropzone.addEventListener(ev, (e) => { e.preventDefault(); e.stopPropagation(); els.dropzone.classList.remove('is-dragover'); }); }); els.dropzone.addEventListener('drop', (e) => { const file = e.dataTransfer?.files?.[0]; if (file) handleFile(file); }); // --- procesamiento --------------------------------------------------------- function handleFile(file) { hideError(); if (!file.name.toLowerCase().endsWith('.fdx')) { showError('Solo se aceptan archivos .fdx (Final Draft).'); return; } state.baseName = file.name.replace(/\.fdx$/i, '').trim() || 'guion'; showProcessing(true); hideResults(); const reader = new FileReader(); reader.onload = () => { try { const xmlText = reader.result; const json = parseFdx(xmlText); state.json = json; state.sceneMap = buildSceneMap(json); state.characters = buildCharacterReport(json); state.scaleta = buildScaleta(json); renderResults(); } catch (err) { console.error(err); showError(err.message || 'No se ha podido procesar el archivo.'); } finally { showProcessing(false); } }; reader.onerror = () => { showProcessing(false); showError('No se ha podido leer el archivo.'); }; reader.readAsText(file, 'UTF-8'); } // --- render ---------------------------------------------------------------- function renderResults() { const s = state.json.stats; const parts = [ `${s.scene_count} escenas`, `${s.character_count} personajes` ]; if (s.pages != null) parts.push(`${s.pages} páginas`); els.stats.textContent = parts.join(' · '); resetAIStatus(); els.results.hidden = false; } function resetAIStatus() { if (!els.iaButton || !els.iaStatus) return; els.iaButton.disabled = false; els.iaButton.textContent = 'Mejorar con IA'; els.iaStatus.textContent = ''; els.iaStatus.classList.remove('is-error'); } function hideResults() { els.results.hidden = true; els.stats.textContent = ''; } function showProcessing(on) { els.processing.hidden = !on; } function showError(msg) { els.error.textContent = msg; els.error.hidden = false; } function hideError() { els.error.hidden = true; els.error.textContent = ''; } // --- descargas ------------------------------------------------------------- document.querySelectorAll('[data-download]').forEach(btn => { btn.addEventListener('click', () => { if (!state.json) return; const kind = btn.getAttribute('data-download'); handleDownload(kind); }); }); function handleDownload(kind) { const base = state.baseName; switch (kind) { case 'json-script': downloadJSON(state.json, `${base}.tramoya.json`); break; case 'pdf-script': pdfScript(state.json).save(`${base}.tramoya.pdf`); break; case 'json-scenemap': downloadJSON(state.sceneMap, `${base}.mapa-escenas.json`); break; case 'pdf-scenemap': pdfSceneMap(state.sceneMap, scriptTitle()).save(`${base}.mapa-escenas.pdf`); break; case 'json-characters': downloadJSON(state.characters, `${base}.personajes.json`); break; case 'pdf-characters': pdfCharacters(state.characters, scriptTitle()).save(`${base}.personajes.pdf`); break; case 'json-scaleta': downloadJSON(state.scaleta, `${base}.escaleta.json`); break; case 'pdf-scaleta': pdfScaleta(state.scaleta, scriptTitle()).save(`${base}.escaleta.pdf`); break; } } function scriptTitle() { return state.json?.title_page?.title || state.baseName || 'Guión'; } function downloadJSON(obj, filename) { const blob = new Blob([JSON.stringify(obj, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1000); } // --- escaleta IA ----------------------------------------------------------- if (els.iaButton) { els.iaButton.addEventListener('click', () => generateAISummaries()); } async function generateAISummaries() { if (!state.scaleta || !state.json) return; els.iaButton.disabled = true; els.iaStatus.classList.remove('is-error'); els.iaStatus.textContent = 'Iniciando…'; const total = state.scaleta.length; let ok = 0; let failed = 0; for (let i = 0; i < total; i++) { const scene = state.json.scenes[i]; const item = state.scaleta[i]; const text = sceneToText(scene); if (!text || text.length < 30) { // Escena vacía o demasiado corta: dejamos el resumen determinista. continue; } els.iaStatus.textContent = `Procesando escena ${i + 1} / ${total}` + (failed ? ` · ${failed} fallidas` : ''); try { const summary = await summarizeScene(text, item.heading || ''); if (summary) { item.beat_summary = summary; ok += 1; } } catch (err) { console.warn(`Escena ${i + 1} falló:`, err); failed += 1; } } if (ok === 0) { els.iaStatus.classList.add('is-error'); els.iaStatus.textContent = 'No se ha podido generar ningún resumen. Comprueba que HF_TOKEN está configurado en el Space.'; } else { els.iaStatus.textContent = `Listo: ${ok} resúmenes IA${failed ? `, ${failed} fallidas (se conservó el resumen determinista)` : ''}. Las descargas usan los resúmenes IA.`; } els.iaButton.disabled = false; els.iaButton.textContent = 'Regenerar con IA'; } async function summarizeScene(text, heading, retries = 2) { try { const res = await fetch('/api/summarize', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, heading }) }); if (!res.ok) { if ((res.status === 502 || res.status === 503) && retries > 0) { await sleep(2500); return summarizeScene(text, heading, retries - 1); } throw new Error(`HTTP ${res.status}`); } const data = await res.json(); return (data.summary || '').trim(); } catch (err) { if (retries > 0) { await sleep(1500); return summarizeScene(text, heading, retries - 1); } throw err; } } function sceneToText(scene) { const parts = []; for (const el of scene.elements || []) { if (el.type === 'action' && el.text) { parts.push(el.text); } else if (el.type === 'character' && el.dialogue) { const name = el.name || '???'; const paren = el.parenthetical ? ` (${el.parenthetical})` : ''; parts.push(`${name}${paren}: ${el.dialogue}`); } else if (el.type === 'transition' && el.text) { parts.push(`[${el.text}]`); } } return parts.join('\n').slice(0, 5000); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }