1import { VideoPlayerUI } from './video-player-ui.js'; 2import { TTSPlayer } from './video-player-tts.js'; 3import { 4 generateVideoScript, 5 generateElaborationScript, 6 generateComparisonVideoScript, 7 generateThematicVideoScript 8} from './video-script-generator.js'; 9import { fetchClosestSynonyms } from './api.js'; 10 11const videoBtn = document.querySelector('.video-btn'); 12const searchInput = document.querySelector('.search-input'); 13 14const ui = new VideoPlayerUI(); 15const tts = new TTSPlayer(); 16 17let currentScript = { 18 rawScript: null, 19 title: '', 20 chapters: [], 21 sentencesForDisplay: [], 22 sentencesForTTS: [], 23 sentenceToChapterMap: [], 24 exploreNext: [], // Add exploreNext to script data 25}; 26 27let generationState = { 28 word: '', 29 type: '', // 'explanation', 'comparison', 'thematic' 30 comparisonWords: [] 31}; 32 33// --- Link UI events to TTS player --- 34ui.on('play', () => { 35 if (tts.isPaused()) { 36 tts.resume(); 37 } else { 38 ui.hideEndScreen(); // Hide end screen on replay 39 tts.play(currentScript.sentencesForTTS); 40 } 41}); 42 43ui.on('pause', () => tts.pause()); 44ui.on('stop', () => tts.stop()); 45ui.on('close', () => { 46 tts.stop(); 47 ui.closeModal(); 48}); 49ui.on('replay', () => { 50 ui.hideEndScreen(); 51 tts.play(currentScript.sentencesForTTS); 52}); 53 54ui.on('scriptSelect', async ({ type }) => { 55 generationState.type = type; 56 57 if (type === 'comparison') { 58 ui.showComparisonChoice(generationState.word); 59 } else { 60 runScriptGeneration(); 61 } 62}); 63 64ui.on('comparisonChoice', async ({ userSelectsWords }) => { 65 if (userSelectsWords) { 66 const synonyms = await fetchClosestSynonyms(generationState.word, 5); 67 // Filter out the original word from suggestions, case-insensitive 68 const filteredSynonyms = synonyms.filter(s => s.toLowerCase() !== generationState.word.toLowerCase()); 69 ui.showComparisonInput(filteredSynonyms); 70 } else { 71 // User wants AI to choose, so we run generation with empty comparison words. 72 generationState.comparisonWords = []; 73 runScriptGeneration(); 74 } 75}); 76 77ui.on('comparisonSubmit', ({ words }) => { 78 generationState.comparisonWords = words; 79 runScriptGeneration(); 80}); 81 82ui.on('seek', (percentage) => { 83 if (currentScript.sentencesForTTS.length > 0) { 84 const totalSentences = currentScript.sentencesForTTS.length; 85 // Ensure index is within bounds 86 let index = Math.floor(percentage * totalSentences); 87 index = Math.max(0, Math.min(index, totalSentences - 1)); 88 89 tts.jumpTo(index); 90 ui.hideEndScreen(); 91 } 92}); 93 94ui.on('exploreNext', (word) => { 95 ui.closeModal(); 96 tts.stop(); 97 searchInput.value = word; 98 searchInput.focus(); 99 // Optional: could trigger a search here, but for now just populates the input 100 // fetchWord(word); 101}); 102 103ui.on('copy', () => { 104 const fullScriptText = currentScript.sentencesForTTS.join('\n\n'); 105 navigator.clipboard.writeText(fullScriptText).catch(err => { 106 console.error("No se pudo copiar el guion:", err); 107 // Optionally show an error to the user 108 }); 109}); 110 111ui.on('jumpToSentence', (index) => { 112 tts.jumpTo(index); 113 ui.hideEndScreen(); // If user jumps from end screen, hide it. 114}); 115 116ui.on('jumpToChapter', (chapterIndex) => { 117 if (currentScript.sentenceToChapterMap) { 118 const sentenceIndex = currentScript.sentenceToChapterMap.findIndex( 119 ci => ci === chapterIndex 120 ); 121 if (sentenceIndex !== -1) { 122 tts.jumpTo(sentenceIndex); 123 ui.hideEndScreen(); // If user jumps from end screen, hide it. 124 } 125 } 126}); 127 128ui.on('elaborate', async () => { 129 if (!currentScript.rawScript) { 130 ui.showError("No se puede elaborar, faltan los datos del guion original."); 131 return; 132 } 133 134 ui.hideEndScreen(); 135 ui.showLoading(true, "Profundizando... Elaborando el guion..."); 136 137 try { 138 const newRawScript = await generateElaborationScript(currentScript.rawScript); 139 processAndPlayScript(newRawScript); 140 } catch (error) { 141 console.error("Failed to generate elaborated script:", error); 142 ui.showLoading(false); 143 ui.showError("La musa de la IA está durmiendo. No se pudo elaborar."); 144 } 145}); 146 147// --- Link TTS events to UI updates --- 148tts.on('sentencechange', ({ sentence, index }) => { 149 const displaySentence = currentScript.sentencesForDisplay[index]; 150 ui.updateTeleprompter(displaySentence); 151 ui.highlightSentenceInPanel(index); 152 153 const chapterIndex = currentScript.sentenceToChapterMap[index]; 154 if (chapterIndex !== undefined) { 155 ui.highlightChapter(chapterIndex); 156 } 157 158 ui.updateProgress(index + 1, currentScript.sentencesForTTS.length); 159}); 160 161tts.on('statechange', (state) => { 162 ui.setPlayerState(state); // 'playing', 'paused', 'stopped' 163}); 164 165tts.on('error', (errorMessage) => { 166 ui.showError(errorMessage); 167}); 168 169tts.on('ended', () => { 170 ui.showEndScreen(); 171}); 172 173// --- Sentence Splitter Helper --- 174/** 175 * A more robust sentence splitter. 176 * It splits based on periods, question marks, exclamation marks, and newlines. 177 * It handles cases where multiple punctuation marks appear together and avoids creating empty sentences. 178 * It also tries to keep sentences with abbreviations (like "e.g.") together. 179 * @param {string} text - The text content to split. 180 * @returns {string[]} An array of sentences. 181 */ 182function splitIntoSentences(text) { 183 if (!text || typeof text !== 'string') return []; 184 185 // Normalize newlines and clean up whitespace 186 text = text.replace(/(\r\n|\n|\r)/gm, ". ").replace(/\s+/g, ' '); 187 188 // This regex looks for sentence-ending punctuation (.!?) followed by a space and an uppercase letter, or the end of the string. 189 // It's designed to be conservative to avoid splitting in the middle of sentences. 190 // Updated for Spanish uppercase letters after punctuation. 191 const sentences = text.match(/[^.!?]+[.!?]+(\s(?=[A-ZÃÃÃÃÃÃÃ])|$|(?=\"))/g); 192 193 if (!sentences) { 194 // If the regex fails (e.g., single sentence with no punctuation), return the whole text if it's not empty. 195 const trimmedText = text.trim();
196 return trimmedText ? [trimmedText] : []; 197 } 198 199 return sentences 200 .map(s => s.trim()) 201 .filter(s => s && s.length > 1 && /[a-zA-Z]/.test(s)); // Filter out empty strings, single characters, and non-alphabetic fragments. 202} 203 204async function runScriptGeneration() { 205 ui.showLoading(true, "Invocando al genio creativo..."); 206 let rawScript; 207 208 try { 209 switch (generationState.type) { 210 case 'explanation': 211 rawScript = await generateVideoScript(generationState.word); 212 break; 213 case 'comparison': 214 rawScript = await generateComparisonVideoScript(generationState.word, generationState.comparisonWords); 215 break; 216 case 'thematic': 217 rawScript = await generateThematicVideoScript(generationState.word); 218 break; 219 default: 220 throw new Error("Invalid script type selected."); 221 } 222 223 processAndPlayScript(rawScript); 224 225 } catch (error) { 226 console.error("Failed to generate or display video script:", error); 227 ui.showLoading(false); 228 ui.showError("¡Ups! El guionista de IA está en su descanso para el café. Por favor, inténtalo de nuevo."); 229 } 230} 231 232function processAndPlayScript(rawScript) { 233 if (!rawScript || !rawScript.chapters || !rawScript.chapters.length) { 234 throw new Error("El guion generado estaba vacÃo o no era válido."); 235 } 236 237 // Process script: flatten sentences, map to chapters, handle formatting 238 let flatSentencesForDisplay = []; 239 let flatSentencesForTTS = []; 240 let sentenceToChapterMap = []; 241 242 rawScript.chapters.forEach((chapter, chapterIndex) => { 243 // Use the new, more robust sentence splitter 244 const chapterSentences = splitIntoSentences(chapter.content); 245 246 chapterSentences.forEach(sentence => { 247 // For display: convert asterisks to <em> tags 248 const displaySentence = sentence.replace(/\*(.*?)\*/g, '<em>$1</em>'); 249 // For TTS: strip asterisks completely 250 const ttsSentence = sentence.replace(/\*/g, ''); 251 252 flatSentencesForDisplay.push(displaySentence); 253 flatSentencesForTTS.push(ttsSentence); 254 sentenceToChapterMap.push(chapterIndex); 255 }); 256 }); 257 258 if (flatSentencesForTTS.length === 0) { 259 throw new Error("El procesamiento del guion no resultó en frases válidas."); 260 } 261 262 currentScript = { 263 rawScript: rawScript, 264 title: rawScript.title, 265 chapters: rawScript.chapters.map(c => c.title), 266 sentencesForDisplay: flatSentencesForDisplay, 267 sentencesForTTS: flatSentencesForTTS, 268 sentenceToChapterMap: sentenceToChapterMap, 269 exploreNext: rawScript.exploreNext || [], 270 }; 271 272 ui.showLoading(false); 273 ui.setScript(currentScript.title, currentScript.sentencesForDisplay, currentScript.chapters, currentScript.exploreNext); 274 275 // Add to history after successful generation and processing 276 window.addToHistory(generationState.word, generationState.type); 277 278 // Auto-play after loading, only if the script is valid. 279 if (currentScript.sentencesForTTS && currentScript.sentencesForTTS.length > 0) { 280 setTimeout(() => { 281 if (tts.isStopped()) { 282 tts.play(currentScript.sentencesForTTS); 283 } 284 }, 500); 285 } else { 286 // This is a fallback case, though the earlier check should catch it. 287 throw new Error("No se puede reproducir el video: El guion no tiene frases que se puedan verbalizar."); 288 } 289} 290 291// --- Main Button Action --- 292videoBtn.addEventListener('click', async () => { 293 const query = searchInput.value.trim(); 294 if (!query) { 295 searchInput.placeholder = "¡Por favor, introduce una palabra primero!"; 296 searchInput.focus(); 297 setTimeout(() => { 298 searchInput.placeholder = "Escribe una palabra..."; 299 }, 2000); 300 return; 301 } 302 303 generationState.word = query; 304 ui.openModal(); 305 ui.showScriptSelection(query); 306});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.