salomonsky commited on
Commit
df80175
·
verified ·
1 Parent(s): eb00a38

Delete index.html

Browse files
Files changed (1) hide show
  1. index.html +0 -664
index.html DELETED
@@ -1,664 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="es">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Generador de Noticias en Video</title>
7
- <!-- Tailwind CSS CDN -->
8
- <script src="https://cdn.tailwindcss.com"></script>
9
- <!-- Google Fonts - Inter -->
10
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
11
- <style>
12
- body {
13
- font-family: 'Inter', sans-serif;
14
- background-color: #f0f4f8;
15
- display: flex;
16
- justify-content: center;
17
- align-items: center;
18
- min-height: 100vh;
19
- padding: 20px;
20
- }
21
- .container {
22
- background-color: #ffffff;
23
- border-radius: 1rem; /* rounded-xl */
24
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* shadow-lg */
25
- padding: 2.5rem; /* p-10 */
26
- width: 100%;
27
- max-width: 960px;
28
- display: flex;
29
- flex-direction: column;
30
- gap: 2rem;
31
- }
32
- .section-title {
33
- font-size: 1.5rem; /* text-2xl */
34
- font-weight: 600; /* font-semibold */
35
- color: #1a202c; /* text-gray-900 */
36
- margin-bottom: 1rem;
37
- border-bottom: 2px solid #edf2f7; /* border-b-2 border-gray-200 */
38
- padding-bottom: 0.5rem;
39
- }
40
- .button-primary {
41
- background-color: #4f46e5; /* indigo-600 */
42
- color: white;
43
- padding: 0.75rem 1.5rem;
44
- border-radius: 0.5rem; /* rounded-lg */
45
- font-weight: 500; /* font-medium */
46
- transition: background-color 0.3s ease;
47
- cursor: pointer;
48
- border: none;
49
- }
50
- .button-primary:hover {
51
- background-color: #4338ca; /* indigo-700 */
52
- }
53
- .button-secondary {
54
- background-color: #e2e8f0; /* gray-200 */
55
- color: #4a5568; /* gray-700 */
56
- padding: 0.75rem 1.5rem;
57
- border-radius: 0.5rem; /* rounded-lg */
58
- font-weight: 500; /* font-medium */
59
- transition: background-color 0.3s ease;
60
- cursor: pointer;
61
- border: none;
62
- }
63
- .button-secondary:hover {
64
- background-color: #cbd5e0; /* gray-300 */
65
- }
66
- .button-danger {
67
- background-color: #ef4444; /* red-500 */
68
- color: white;
69
- padding: 0.75rem 1.5rem;
70
- border-radius: 0.5rem;
71
- font-weight: 500;
72
- transition: background-color 0.3s ease;
73
- cursor: pointer;
74
- border: none;
75
- }
76
- .button-danger:hover {
77
- background-color: #dc2626; /* red-600 */
78
- }
79
- .input-field {
80
- border: 1px solid #cbd5e0; /* border-gray-300 */
81
- border-radius: 0.5rem; /* rounded-lg */
82
- padding: 0.75rem 1rem;
83
- width: 100%;
84
- transition: border-color 0.3s ease, box-shadow 0.3s ease;
85
- }
86
- .input-field:focus {
87
- outline: none;
88
- border-color: #6366f1; /* indigo-500 */
89
- box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2); /* ring-indigo-200 */
90
- }
91
- .loading-spinner {
92
- border: 4px solid rgba(0, 0, 0, 0.1);
93
- border-left-color: #4f46e5;
94
- border-radius: 50%;
95
- width: 24px;
96
- height: 24px;
97
- animation: spin 1s linear infinite;
98
- display: inline-block;
99
- vertical-align: middle;
100
- margin-left: 10px;
101
- }
102
- @keyframes spin {
103
- 0% { transform: rotate(0deg); }
104
- 100% { transform: rotate(360deg); }
105
- }
106
- /* Message box styles */
107
- .message-box {
108
- padding: 1rem;
109
- border-radius: 0.5rem;
110
- margin-top: 1rem;
111
- font-weight: 500;
112
- }
113
- .message-box.info {
114
- background-color: #feebc8; /* yellow-100 */
115
- border: 1px solid #fbd38d; /* yellow-400 */
116
- color: #975a16; /* yellow-800 */
117
- }
118
- .message-box.success {
119
- background-color: #d1fae5; /* green-100 */
120
- border: 1px solid #34d399; /* green-400 */
121
- color: #065f46; /* green-800 */
122
- }
123
- .message-box.error {
124
- background-color: #fee2e2; /* red-100 */
125
- border: 1px solid #f87171; /* red-400 */
126
- color: #991b1b; /* red-800 */
127
- }
128
- canvas {
129
- border: 1px solid #cbd5e0;
130
- border-radius: 0.5rem;
131
- background-color: #f7fafc;
132
- max-width: 100%;
133
- height: auto;
134
- }
135
-
136
- /* Video container aspect ratio styling */
137
- .video-container-16-9 {
138
- width: 100%;
139
- padding-bottom: 56.25%; /* 9 / 16 * 100% */
140
- position: relative;
141
- }
142
- .video-container-9-16 {
143
- width: 100%;
144
- max-width: 300px; /* Limit width for portrait */
145
- padding-bottom: 177.77%; /* 16 / 9 * 100% */
146
- position: relative;
147
- margin-left: auto;
148
- margin-right: auto;
149
- }
150
- .video-container-inner {
151
- position: absolute;
152
- top: 0;
153
- left: 0;
154
- width: 100%;
155
- height: 100%;
156
- display: flex;
157
- align-items: center;
158
- justify-content: center;
159
- }
160
- </style>
161
- </head>
162
- <body class="bg-gray-100 flex items-center justify-center min-h-screen p-4">
163
- <div class="container" role="main">
164
- <h1 class="text-3xl font-bold text-center text-gray-800" aria-label="Generador de Noticias en Video">Generador de Noticias en Video</h1>
165
-
166
- <!-- Sección 1: Imagen (Cargar o Generar) -->
167
- <div class="flex flex-col gap-4">
168
- <h2 class="section-title">1. Imágenes para la Noticia</h2>
169
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
170
- <div>
171
- <label for="imageUpload" class="block text-gray-700 font-medium mb-2">Cargar Nueva Imagen:</label>
172
- <input type="file" id="imageUpload" accept="image/*" class="input-field py-2" aria-label="Cargar nueva imagen">
173
- </div>
174
- <div>
175
- <label for="imagePrompt" class="block text-gray-700 font-medium mb-2">O Generar con IA (Prompt):</label>
176
- <input type="text" id="imagePrompt" placeholder="Ej: Un coche volando por la ciudad" class="input-field" aria-label="Prompt para generar imagen">
177
- <button id="generateImageBtn" class="button-primary mt-3 w-full" aria-label="Generar imagen con inteligencia artificial">
178
- Generar Imagen
179
- <span id="imageLoading" class="loading-spinner hidden" role="status" aria-label="Cargando imagen"></span>
180
- </button>
181
- </div>
182
- </div>
183
-
184
- <div class="mt-4">
185
- <label class="block text-gray-700 font-medium mb-2">Proporción de Aspecto para Nueva Imagen:</label>
186
- <div class="flex gap-4" role="radiogroup" aria-labelledby="aspectRatioLabel">
187
- <div class="flex items-center">
188
- <input type="radio" id="aspectRatio16-9" name="aspectRatio" value="16:9" class="form-radio h-4 w-4 text-indigo-600" checked aria-label="Relación de aspecto 16:9">
189
- <label for="aspectRatio16-9" class="ml-2 text-gray-700">16:9 (Horizontal)</label>
190
- </div>
191
- <div class="flex items-center">
192
- <input type="radio" id="aspectRatio9-16" name="aspectRatio" value="9:16" class="form-radio h-4 w-4 text-indigo-600" aria-label="Relación de aspecto 9:16">
193
- <label for="aspectRatio9-16" class="ml-2 text-gray-700">9:16 (Vertical)</label>
194
- </div>
195
- </div>
196
- </div>
197
-
198
- <div class="mt-4 text-center border p-4 rounded-lg bg-gray-50">
199
- <p class="text-sm text-gray-600 mb-2">La imagen cargada/generada aquí será procesada y añadida a la secuencia.</p>
200
- <img id="currentImageEditorPreview" src="https://placehold.co/400x225/E2E8F0/4A5568?text=Imagen+Nueva" alt="Previsualización de imagen a añadir" class="mx-auto rounded-lg shadow-md max-w-full h-auto object-contain" style="max-height: 300px;">
201
- <canvas id="imageCanvas" class="hidden mt-4 mx-auto" aria-hidden="true"></canvas>
202
- <button id="addImageToSequenceBtn" class="button-secondary mt-4 w-full" aria-label="Añadir imagen a la secuencia de video">Añadir Imagen a Secuencia</button>
203
- </div>
204
-
205
- <div class="mt-6">
206
- <h3 class="font-semibold text-lg text-gray-800 mb-3">Imágenes en Secuencia para el Video:</h3>
207
- <div id="imageThumbnailsContainer" class="flex flex-wrap gap-3 p-3 border border-dashed border-gray-300 rounded-lg bg-gray-50 min-h-[100px] items-center justify-center">
208
- <p id="noImagesMessage" class="text-gray-500 text-sm">No hay imágenes en la secuencia aún.</p>
209
- </div>
210
- <p class="text-sm text-gray-500 mt-2">Haz clic en una imagen para removerla de la secuencia.</p>
211
- <button id="clearImagesBtn" class="button-danger mt-4 w-full" aria-label="Limpiar todas las imágenes de la secuencia">Limpiar Imágenes</button>
212
- </div>
213
- </div>
214
-
215
- <!-- Sección 2: Texto de la Noticia -->
216
- <div class="flex flex-col gap-4">
217
- <h2 class="section-title">2. Texto de la Noticia</h2>
218
- <div>
219
- <label for="newsText" class="block text-gray-700 font-medium mb-2">Escribe tu noticia o genera una:</label>
220
- <textarea id="newsText" rows="6" placeholder="Escribe aquí el contenido de tu noticia..." class="input-field resize-y" aria-label="Contenido de la noticia"></textarea>
221
- </div>
222
- <button id="generateNewsTextBtn" class="button-primary w-full" aria-label="Generar texto de noticia con inteligencia artificial">
223
- Generar Texto de Noticia con IA
224
- <span id="textLoading" class="loading-spinner hidden" role="status" aria-label="Cargando texto"></span>
225
- </button>
226
- </div>
227
-
228
- <!-- Sección 3: Generar y Previsualizar Audio (TTS) -->
229
- <div class="flex flex-col gap-4">
230
- <h2 class="section-title">3. Audio de la Noticia (TTS)</h2>
231
- <button id="playAudioBtn" class="button-primary w-full" aria-label="Reproducir audio de la noticia">
232
- Reproducir Audio de Noticia
233
- </button>
234
- <div id="audioWarning" class="message-box info">
235
- <strong>Nota:</strong> La generación de un archivo MP3 directamente en el navegador no es posible sin un servicio externo. Esta función reproducirá el texto usando la voz del navegador.
236
- </div>
237
- </div>
238
-
239
- <!-- Sección 4: Crear y Previsualizar Video (Simulado) -->
240
- <div class="flex flex-col gap-4">
241
- <h2 class="section-title">4. Previsualizar Noticia en Video</h2>
242
- <button id="createVideoBtn" class="button-primary w-full" aria-label="Previsualizar noticia con imagen y audio">
243
- Previsualizar Noticia (Imagen + Audio)
244
- </button>
245
- <div id="videoContainerWrapper" class="mt-4 relative" style="width: 100%; max-width: 640px; margin-left: auto; margin-right: auto;">
246
- <div id="videoContainer" class="relative bg-black rounded-lg overflow-hidden flex items-center justify-center">
247
- <div id="videoContainerInner" class="video-container-inner">
248
- <img id="videoImageDisplay" class="w-full h-full object-contain" src="" alt="Noticia en Video" style="display: none;">
249
- <p id="videoPlaceholder" class="text-gray-400 text-center text-lg">Haz clic en 'Previsualizar Noticia'</p>
250
- <div id="videoPlayingIndicator" class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-75 text-white text-xl hidden">
251
- <span class="loading-spinner mr-3"></span> Reproduciendo Noticia...
252
- </div>
253
- </div>
254
- </div>
255
- </div>
256
- <audio id="videoAudioPlayback" src="" style="display: none;"></audio>
257
- <div id="videoWarning" class="message-box info">
258
- <strong>Importante:</strong> La creación de un archivo de video real (.mp4) a partir de una imagen y audio es una tarea compleja que normalmente requiere procesamiento en el lado del servidor (por ejemplo, con FFmpeg) o librerías muy grandes y específicas para el navegador (como ffmpeg.wasm). Esta previsualización simula la noticia mostrando la imagen y reproduciendo el audio.
259
- </div>
260
- </div>
261
- </div>
262
-
263
- <script type="module">
264
- const imageUpload = document.getElementById('imageUpload');
265
- const imagePromptInput = document.getElementById('imagePrompt');
266
- const generateImageBtn = document.getElementById('generateImageBtn');
267
- const currentImageEditorPreview = document.getElementById('currentImageEditorPreview');
268
- const imageLoading = document.getElementById('imageLoading');
269
- const addImageToSequenceBtn = document.getElementById('addImageToSequenceBtn');
270
- const imageThumbnailsContainer = document.getElementById('imageThumbnailsContainer');
271
- const noImagesMessage = document.getElementById('noImagesMessage');
272
- const clearImagesBtn = document.getElementById('clearImagesBtn'); // New button
273
-
274
- const newsTextarea = document.getElementById('newsText');
275
- const generateNewsTextBtn = document.getElementById('generateNewsTextBtn');
276
- const textLoading = document.getElementById('textLoading');
277
- const playAudioBtn = document.getElementById('playAudioBtn');
278
- const createVideoBtn = document.getElementById('createVideoBtn');
279
- const videoImageDisplay = document.getElementById('videoImageDisplay');
280
- const videoAudioPlayback = document.getElementById('videoAudioPlayback');
281
- const videoPlaceholder = document.getElementById('videoPlaceholder');
282
- const videoPlayingIndicator = document.getElementById('videoPlayingIndicator'); // New indicator
283
- const videoContainer = document.getElementById('videoContainer'); // Main video display div
284
- const videoContainerWrapper = document.getElementById('videoContainerWrapper'); // Wrapper for aspect ratio
285
- const imageCanvas = document.getElementById('imageCanvas');
286
- const ctx = imageCanvas.getContext('2d');
287
- const aspectRatioRadios = document.querySelectorAll('input[name="aspectRatio"]');
288
-
289
- let newsImages = []; // Array to store base64 images for the video sequence
290
- let currentProcessedImageBase64 = null; // Stores the base64 of the image currently in the editor preview
291
- let currentAudioSynthesizer = null; // Will store the SpeechSynthesisUtterance
292
- let imageInterval = null; // To store the interval ID for image cycling
293
-
294
- // Function to show a temporary message
295
- function showMessage(element, message, type = 'info', duration = 3000) {
296
- let messageDiv = document.createElement('div');
297
- messageDiv.className = `message-box ${type}`; // Use type for class
298
- messageDiv.textContent = message;
299
-
300
- // Remove existing messages of the same type under the same parent for cleanliness
301
- const existingMessage = element.parentNode.querySelector(`.message-box.${type}`);
302
- if (existingMessage) {
303
- existingMessage.remove();
304
- }
305
-
306
- element.parentNode.insertBefore(messageDiv, element.nextSibling);
307
- setTimeout(() => {
308
- messageDiv.remove();
309
- }, duration);
310
- }
311
-
312
- // Function to update the images display
313
- function updateImageThumbnails() {
314
- imageThumbnailsContainer.innerHTML = ''; // Clear existing thumbnails
315
- if (newsImages.length === 0) {
316
- noImagesMessage.style.display = 'block';
317
- clearImagesBtn.classList.add('hidden'); // Hide clear button if no images
318
- } else {
319
- noImagesMessage.style.display = 'none';
320
- clearImagesBtn.classList.remove('hidden'); // Show clear button if images exist
321
- newsImages.forEach((imgBase64, index) => {
322
- const thumbDiv = document.createElement('div');
323
- thumbDiv.className = 'relative group w-24 h-24 rounded-lg overflow-hidden shadow-md cursor-pointer';
324
- thumbDiv.setAttribute('role', 'button');
325
- thumbDiv.setAttribute('aria-label', `Imagen ${index + 1} en secuencia, clic para remover.`);
326
- thumbDiv.innerHTML = `
327
- <img src="${imgBase64}" alt="Imagen ${index + 1}" class="w-full h-full object-cover">
328
- <button class="absolute top-0 right-0 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs font-bold opacity-0 group-hover:opacity-100 transition-opacity duration-200" data-index="${index}" aria-label="Remover imagen ${index + 1}"></button>
329
- `;
330
- thumbDiv.querySelector('button').addEventListener('click', (e) => {
331
- e.stopPropagation(); // Prevent click from bubbling to thumbDiv itself
332
- const idx = parseInt(e.target.dataset.index);
333
- newsImages.splice(idx, 1); // Remove image from array
334
- updateImageThumbnails(); // Redraw thumbnails
335
- showMessage(thumbDiv.parentNode, "Imagen removida.", 'info');
336
- });
337
- imageThumbnailsContainer.appendChild(thumbDiv);
338
- });
339
- }
340
- }
341
-
342
- // --- Image Handling ---
343
-
344
- // Handle image upload
345
- imageUpload.addEventListener('change', (event) => {
346
- const file = event.target.files[0];
347
- if (file) {
348
- const reader = new FileReader();
349
- reader.onload = (e) => {
350
- loadImageToCanvas(e.target.result); // Load to canvas first for cropping
351
- };
352
- reader.onerror = () => {
353
- showMessage(imageUpload, "Error al leer el archivo de imagen.", 'error');
354
- };
355
- reader.readAsDataURL(file);
356
- }
357
- });
358
-
359
- // Event listener for aspect ratio changes
360
- aspectRatioRadios.forEach(radio => {
361
- radio.addEventListener('change', () => {
362
- if (currentProcessedImageBase64) {
363
- loadImageToCanvas(currentProcessedImageBase64); // Reload image to apply new aspect ratio
364
- }
365
- // Update video container aspect ratio class
366
- setVideoContainerAspectRatio();
367
- });
368
- });
369
-
370
- // Function to set the video container aspect ratio class dynamically
371
- function setVideoContainerAspectRatio() {
372
- const selectedRatio = document.querySelector('input[name="aspectRatio"]:checked').value;
373
- if (selectedRatio === '16:9') {
374
- videoContainerWrapper.classList.remove('video-container-9-16');
375
- videoContainerWrapper.classList.add('video-container-16-9');
376
- } else { // 9:16
377
- videoContainerWrapper.classList.remove('video-container-16-9');
378
- videoContainerWrapper.classList.add('video-container-9-16');
379
- }
380
- }
381
-
382
- // Function to load image onto canvas and apply selected aspect ratio
383
- function loadImageToCanvas(imageUrl) {
384
- const img = new Image();
385
- img.onload = () => {
386
- const selectedRatio = document.querySelector('input[name="aspectRatio"]:checked').value;
387
- let targetWidth, targetHeight;
388
-
389
- if (selectedRatio === '16:9') {
390
- targetWidth = 640;
391
- targetHeight = targetWidth / (16 / 9);
392
- } else { // 9:16
393
- targetHeight = 640; // Max height for portrait
394
- targetWidth = targetHeight * (9 / 16);
395
- }
396
-
397
- const imgAspectRatio = img.width / img.height;
398
- const targetAspectRatio = targetWidth / targetHeight;
399
-
400
- let sx, sy, sWidth, sHeight;
401
- let dx, dy, dWidth, dHeight;
402
-
403
- dx = 0; dy = 0; dWidth = targetWidth; dHeight = targetHeight;
404
-
405
- if (imgAspectRatio > targetAspectRatio) {
406
- sHeight = img.height;
407
- sWidth = img.height * targetAspectRatio;
408
- sx = (img.width - sWidth) / 2;
409
- sy = 0;
410
- } else {
411
- sWidth = img.width;
412
- sHeight = img.width / targetAspectRatio;
413
- sx = 0;
414
- sy = (img.height - sHeight) / 2;
415
- }
416
-
417
- imageCanvas.width = targetWidth;
418
- imageCanvas.height = targetHeight;
419
- ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
420
- ctx.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
421
-
422
- currentProcessedImageBase64 = imageCanvas.toDataURL('image/png');
423
- currentImageEditorPreview.src = currentProcessedImageBase64;
424
- addImageToSequenceBtn.disabled = false;
425
- };
426
- img.onerror = () => {
427
- showMessage(currentImageEditorPreview, "Error al cargar la imagen. Asegúrate de que es un formato válido.", 'error');
428
- currentProcessedImageBase64 = null;
429
- currentImageEditorPreview.src = "https://placehold.co/400x225/E2E8F0/4A5568?text=Imagen+Nueva";
430
- addImageToSequenceBtn.disabled = true;
431
- };
432
- img.src = imageUrl;
433
- }
434
-
435
-
436
- // Generate image using Imagen API via Flask backend
437
- generateImageBtn.addEventListener('click', async () => {
438
- const prompt = imagePromptInput.value.trim();
439
- if (!prompt) {
440
- showMessage(generateImageBtn, "Por favor, introduce un prompt para generar la imagen.", 'error');
441
- return;
442
- }
443
-
444
- imageLoading.classList.remove('hidden');
445
- generateImageBtn.disabled = true;
446
-
447
- try {
448
- const response = await fetch('/generate_image', {
449
- method: 'POST',
450
- headers: { 'Content-Type': 'application/json' },
451
- body: JSON.stringify({ prompt: prompt })
452
- });
453
-
454
- if (!response.ok) {
455
- const errorData = await response.json(); // Assume JSON error from backend
456
- throw new Error(`Error del backend: ${errorData.error || response.statusText}`);
457
- }
458
-
459
- const result = await response.json();
460
- if (result.imageUrl) {
461
- loadImageToCanvas(result.imageUrl);
462
- showMessage(generateImageBtn, "Imagen generada con éxito.", 'success');
463
- } else {
464
- showMessage(generateImageBtn, "No se recibió una imagen válida del backend.", 'error');
465
- }
466
- } catch (error) {
467
- console.error('Error generating image:', error);
468
- showMessage(generateImageBtn, `Error al generar la imagen: ${error.message}`, 'error');
469
- } finally {
470
- imageLoading.classList.add('hidden');
471
- generateImageBtn.disabled = false;
472
- }
473
- });
474
-
475
- // Add processed image to the sequence
476
- addImageToSequenceBtn.addEventListener('click', () => {
477
- if (currentProcessedImageBase64) {
478
- newsImages.push(currentProcessedImageBase64);
479
- updateImageThumbnails();
480
- showMessage(addImageToSequenceBtn, "Imagen añadida a la secuencia.", 'success');
481
- currentProcessedImageBase64 = null; // Clear processed image for next one
482
- currentImageEditorPreview.src = "https://placehold.co/400x225/E2E8F0/4A5568?text=Imagen+Nueva"; // Reset preview
483
- addImageToSequenceBtn.disabled = true;
484
- } else {
485
- showMessage(addImageToSequenceBtn, "No hay imagen para añadir. Carga o genera una primero.", 'error');
486
- }
487
- });
488
-
489
- // Clear all images from the sequence
490
- clearImagesBtn.addEventListener('click', () => {
491
- if (newsImages.length > 0) {
492
- newsImages = []; // Clear array
493
- updateImageThumbnails(); // Redraw
494
- showMessage(clearImagesBtn, "Todas las imágenes han sido eliminadas de la secuencia.", 'info');
495
- } else {
496
- showMessage(clearImagesBtn, "No hay imágenes para eliminar.", 'info');
497
- }
498
- });
499
-
500
- // --- Text Handling ---
501
-
502
- // Generate news text using Gemini-Flash via Flask backend
503
- generateNewsTextBtn.addEventListener('click', async () => {
504
- const userPrompt = "Escribe una noticia corta y concisa, ideal para un reportaje de video. Céntrate en un evento reciente o un tema interesante. El texto debe ser natural y apto para ser leído. No incluyas títulos, solo el cuerpo de la noticia. Aquí tienes una idea de la noticia: " + newsTextarea.value.trim();
505
-
506
- if (!newsTextarea.value.trim()) {
507
- showMessage(generateNewsTextBtn, "Por favor, introduce una idea para la noticia en el cuadro de texto.", 'info');
508
- return;
509
- }
510
-
511
- textLoading.classList.remove('hidden');
512
- generateNewsTextBtn.disabled = true;
513
-
514
- try {
515
- const response = await fetch('/generate_text', {
516
- method: 'POST',
517
- headers: { 'Content-Type': 'application/json' },
518
- body: JSON.stringify({ prompt: userPrompt })
519
- });
520
-
521
- if (!response.ok) {
522
- const errorData = await response.json(); // Assume JSON error from backend
523
- throw new Error(`Error del backend: ${errorData.error || response.statusText}`);
524
- }
525
-
526
- const result = await response.json();
527
- if (result.text) {
528
- newsTextarea.value = result.text; // Set the generated text
529
- showMessage(generateNewsTextBtn, "Texto de noticia generado con éxito.", 'success');
530
- } else {
531
- showMessage(generateNewsTextBtn, "No se recibió texto válido del backend.", 'error');
532
- }
533
- } catch (error) {
534
- console.error('Error generating text:', error);
535
- showMessage(generateNewsTextBtn, `Error al generar el texto: ${error.message}`, 'error');
536
- } finally {
537
- textLoading.classList.add('hidden');
538
- generateNewsTextBtn.disabled = false;
539
- }
540
- });
541
-
542
- // --- Audio (TTS) Handling ---
543
-
544
- playAudioBtn.addEventListener('click', () => {
545
- const textToSpeak = newsTextarea.value.trim();
546
- if (!textToSpeak) {
547
- showMessage(playAudioBtn, "Por favor, introduce texto para reproducir el audio.", 'error');
548
- return;
549
- }
550
-
551
- // Stop any ongoing speech
552
- if (speechSynthesis.speaking) {
553
- speechSynthesis.cancel();
554
- }
555
-
556
- currentAudioSynthesizer = new SpeechSynthesisUtterance(textToSpeak);
557
- currentAudioSynthesizer.lang = 'es-ES'; // Set language to Spanish
558
-
559
- currentAudioSynthesizer.onstart = () => {
560
- showMessage(playAudioBtn, "Reproduciendo audio...", 'info', 5000);
561
- playAudioBtn.disabled = true;
562
- };
563
- currentAudioSynthesizer.onend = () => {
564
- showMessage(playAudioBtn, "Reproducción de audio finalizada.", 'success');
565
- playAudioBtn.disabled = false;
566
- };
567
- currentAudioSynthesizer.onerror = (event) => {
568
- console.error('SpeechSynthesisUtterance.onerror', event);
569
- showMessage(playAudioBtn, `Error al reproducir audio: ${event.error}`, 'error');
570
- playAudioBtn.disabled = false;
571
- };
572
-
573
- speechSynthesis.speak(currentAudioSynthesizer);
574
- });
575
-
576
- // --- Video Simulation (Updated) ---
577
-
578
- createVideoBtn.addEventListener('click', () => {
579
- if (newsImages.length === 0) {
580
- showMessage(createVideoBtn, "Por favor, añade al menos una imagen a la secuencia.", 'error');
581
- return;
582
- }
583
- const textToSpeak = newsTextarea.value.trim();
584
- if (!textToSpeak) {
585
- showMessage(createVideoBtn, "Por favor, genera o escribe el texto de la noticia primero.", 'error');
586
- return;
587
- }
588
-
589
- // Stop any existing audio playback or image intervals
590
- if (speechSynthesis.speaking) {
591
- speechSynthesis.cancel();
592
- }
593
- if (imageInterval) {
594
- clearInterval(imageInterval);
595
- imageInterval = null;
596
- }
597
-
598
- // Show video elements
599
- videoPlaceholder.style.display = 'none';
600
- videoImageDisplay.style.display = 'block';
601
- videoPlayingIndicator.classList.remove('hidden'); // Show playing indicator
602
-
603
- // Set the video container's aspect ratio based on the selected radio button
604
- setVideoContainerAspectRatio();
605
-
606
- // Start audio playback
607
- const videoUtterance = new SpeechSynthesisUtterance(textToSpeak);
608
- videoUtterance.lang = 'es-ES';
609
-
610
- let currentImageIndex = 0;
611
- videoImageDisplay.src = newsImages[currentImageIndex]; // Display first image
612
-
613
- videoUtterance.onstart = () => {
614
- showMessage(createVideoBtn, "Simulando noticia en video...", 'info', 5000);
615
- createVideoBtn.disabled = true;
616
-
617
- // Estimate duration and set interval for image switching
618
- const words = textToSpeak.split(/\s+/).filter(word => word.length > 0).length;
619
- const estimatedDurationSeconds = Math.max(1, words / 2.67);
620
- const imageDisplayDuration = estimatedDurationSeconds / newsImages.length;
621
-
622
- console.log(`Text: ${textToSpeak.length} characters, ${words} words`);
623
- console.log(`Estimated audio duration: ${estimatedDurationSeconds.toFixed(2)} seconds`);
624
- console.log(`Duration per image: ${imageDisplayDuration.toFixed(2)} seconds`);
625
-
626
- if (newsImages.length > 1) {
627
- imageInterval = setInterval(() => {
628
- currentImageIndex = (currentImageIndex + 1) % newsImages.length;
629
- videoImageDisplay.src = newsImages[currentImageIndex];
630
- }, imageDisplayDuration * 1000);
631
- }
632
- };
633
-
634
- videoUtterance.onend = () => {
635
- showMessage(createVideoBtn, "Simulación de noticia en video finalizada.", 'success');
636
- createVideoBtn.disabled = false;
637
- videoPlayingIndicator.classList.add('hidden'); // Hide playing indicator
638
- if (imageInterval) {
639
- clearInterval(imageInterval);
640
- imageInterval = null;
641
- }
642
- };
643
-
644
- videoUtterance.onerror = (event) => {
645
- console.error('Video Utterance onerror', event);
646
- showMessage(createVideoBtn, `Error en la simulación de video: ${event.error}`, 'error');
647
- createVideoBtn.disabled = false;
648
- videoPlayingIndicator.classList.add('hidden'); // Hide playing indicator
649
- if (imageInterval) {
650
- clearInterval(imageInterval);
651
- imageInterval = null;
652
- }
653
- };
654
-
655
- speechSynthesis.speak(videoUtterance);
656
- });
657
-
658
- // Initial setup
659
- updateImageThumbnails(); // Display any pre-existing images (none initially)
660
- addImageToSequenceBtn.disabled = true; // Initially disable add button as no image is processed
661
- setVideoContainerAspectRatio(); // Set initial video container aspect ratio
662
- </script>
663
- </body>
664
- </html>