salomonsky commited on
Commit
3357bcb
·
verified ·
1 Parent(s): bf386f8

Create index.html

Browse files
Files changed (1) hide show
  1. index.html +664 -0
index.html ADDED
@@ -0,0 +1,664 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>