salomonsky commited on
Commit
b656836
·
verified ·
1 Parent(s): 523bce6

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +170 -25
index.html CHANGED
@@ -304,11 +304,37 @@
304
  </head>
305
  <body>
306
  <div class="container">
307
- <h1 class="section-title">✨ Creador de Noticias en Video ✨</h1>
308
 
309
- <!-- Sección de Configuración de Opciones Generales (anteriormente Sección 0) -->
310
  <div class="mb-8 p-4 bg-gray-50 rounded-xl shadow-inner">
311
- <h2 class="text-2xl font-semibold mb-4 text-gray-700">1. Configuración de Opciones Generales</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
 
313
  <div class="input-group mb-4">
314
  <label class="block text-gray-700 text-sm font-bold mb-2">Relación de Aspecto del Video:</label>
@@ -330,7 +356,7 @@
330
  <label for="imageDuration" class="block text-gray-700 text-sm font-bold mb-2">Duración de cada imagen (segundos):</label>
331
  <input type="number" id="imageDuration" value="3" min="1" step="0.5" class="py-2 px-3 rounded-lg">
332
  <div id="imageDurationMessage" class="message-box mt-2"></div>
333
- <p class="text-sm text-gray-500 mt-1">Define cuánto tiempo cada imagen aparecerá en el video. La duración del audio se sincronizará con la imagen.</p>
334
  </div>
335
 
336
  <div class="input-group mb-4">
@@ -343,7 +369,36 @@
343
  </div>
344
  </div>
345
 
346
- <!-- Sección de Carga de Fotos (anteriormente Sección 2) -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  <div class="mb-8 p-4 bg-gray-50 rounded-xl shadow-inner">
348
  <h2 class="text-2xl font-semibold mb-4 text-gray-700">2. Sube tus Fotos (Máx. 10)</h2>
349
  <div class="input-group mb-4">
@@ -357,17 +412,21 @@
357
  <div id="imageErrorMessage" class="message-box error-message mt-4 rounded-lg"></div>
358
  </div>
359
 
360
- <!-- Sección de Escritura del Guion (anteriormente Sección 3) -->
361
  <div class="mb-8 p-4 bg-gray-50 rounded-xl shadow-inner">
362
  <h2 class="text-2xl font-semibold mb-4 text-gray-700">3. Escribe el Guion de tu Noticia</h2>
363
  <div class="input-group mb-4">
364
  <label for="scriptTextarea" class="block text-gray-700 text-sm font-bold mb-2">Tu guion aquí (un párrafo por cada foto):</label>
365
  <textarea id="scriptTextarea" rows="8" placeholder="Ej: Hola, hoy exploraremos el fascinante mundo de El Principito. En nuestra primera imagen, vemos al Principito en su pequeño asteroide, cuidando su rosa. Esta escena evoca la ternura y la responsabilidad..." class="rounded-lg"></textarea>
366
  </div>
 
 
 
 
367
  <div id="llmMessage" class="message-box mt-4 rounded-lg"></div>
368
  </div>
369
 
370
- <!-- Sección de Selección de Voz (anteriormente Sección 4) -->
371
  <div class="mb-8 p-4 bg-gray-50 rounded-xl shadow-inner">
372
  <h2 class="text-2xl font-semibold mb-4 text-gray-700">4. Elige la Voz de tu Narrador</h2>
373
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
@@ -389,7 +448,7 @@
389
  <div id="voiceTestMessage" class="message-box mt-4 rounded-lg"></div>
390
  </div>
391
 
392
- <!-- Sección de Generación de Video (anteriormente Sección 5) -->
393
  <div class="mb-8 text-center p-4 bg-gray-50 rounded-xl shadow-inner">
394
  <h2 class="text-2xl font-semibold mb-4 text-gray-700">5. Generar Video</h2>
395
  <button id="generateVideoBtn" class="btn-primary w-full max-w-xs rounded-xl">Crear Noticia en Video</button>
@@ -399,7 +458,7 @@
399
  <div id="generateVideoMessage" class="message-box mt-4 rounded-lg"></div>
400
  </div>
401
 
402
- <!-- Sección de Video Resultante (anteriormente Sección 6) -->
403
  <div class="p-4 bg-gray-50 rounded-xl shadow-inner">
404
  <h2 class="text-2xl font-semibold mb-4 text-gray-700">6. Tu Video está Listo</h2>
405
  <div id="videoOutput" class="video-placeholder">
@@ -420,20 +479,33 @@
420
  </div>
421
 
422
  <script>
423
- // Referencias a elementos del DOM (actualizadas)
 
 
 
 
424
  const aspectRatio16_9Radio = document.getElementById('aspectRatio16_9');
425
  const aspectRatio9_16Radio = document.getElementById('aspectRatio9_16');
426
  const imageDurationInput = document.getElementById('imageDuration');
427
  const imageDurationMessage = document.getElementById('imageDurationMessage');
428
  const voiceServiceSelect = document.getElementById('voiceService');
429
-
 
 
 
 
 
 
 
 
430
  const photoUpload = document.getElementById('photoUpload');
431
  const thumbnailsContainer = document.getElementById('thumbnails');
432
  const photoCountDisplay = document.getElementById('photoCount');
433
  const imageErrorMessage = document.getElementById('imageErrorMessage');
434
 
435
  const scriptTextarea = document.getElementById('scriptTextarea');
436
- const llmMessage = document.getElementById('llmMessage'); // Todavía usado para mensajes generales
 
437
 
438
  const voiceLanguageSelect = document.getElementById('voiceLanguage');
439
  const testVoiceBtn = document.getElementById('testVoiceBtn');
@@ -519,6 +591,39 @@
519
  }
520
  }
521
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
  // Validación en tiempo real para la duración de la imagen
523
  imageDurationInput.addEventListener('input', () => {
524
  const value = parseFloat(imageDurationInput.value);
@@ -649,51 +754,84 @@
649
  // Limpiar mensajes y resetear el área de video
650
  hideMessage(generateVideoMessage);
651
  hideMessage(imageErrorMessage);
652
- hideMessage(llmMessage); // Sigue siendo útil para mensajes generales
653
  videoOutput.innerHTML = '<p>El video generado aparecerá aquí.</p>';
654
  resultVideo.classList.add('hidden');
655
  resultVideo.src = '';
656
  downloadVideoLink.classList.add('hidden');
657
 
658
- // Recolectar valores de los inputs del usuario (simplificados)
 
 
 
659
  const selectedAspectRatio = document.querySelector('input[name="aspectRatio"]:checked').value;
660
  const imageDuration = parseFloat(imageDurationInput.value);
661
  const selectedVoiceService = voiceServiceSelect.value;
662
  const selectedLanguage = voiceLanguageSelect.value;
 
 
 
 
663
  const script = scriptTextarea.value.trim();
664
-
665
- // Validaciones de entrada del usuario con modal personalizado (simplificadas)
666
- if (uploadedFiles.length === 0) {
667
- showCustomModal('Faltan Imágenes', 'Por favor, sube al menos una foto.');
 
668
  return;
669
  }
670
-
671
- if (!script) {
672
- showCustomModal('Falta el Guion', 'Por favor, escribe un guion para tu noticia.');
 
 
 
 
 
 
 
673
  return;
674
  }
675
-
676
  if (isNaN(imageDuration) || imageDuration <= 0) {
677
  showCustomModal('Duración de Imagen Inválida', 'Por favor, introduce una duración de imagen válida (un número positivo) para cada segmento de video.');
678
  return;
679
  }
680
-
 
 
 
 
 
 
 
 
 
681
  // Mostrar spinner de carga y deshabilitar el botón
682
  loadingSpinner.style.display = 'block';
683
  loadingText.style.display = 'block';
684
  generateVideoBtn.disabled = true;
685
 
686
  try {
687
- loadingText.textContent = 'Generando tu video: combinando fotos, audio y componiendo... esto puede tardar unos minutos.';
688
 
689
  // Crear objeto FormData para enviar datos y archivos al backend
690
  const formData = new FormData();
691
  formData.append('script', script);
 
 
 
692
  formData.append('aspectRatio', selectedAspectRatio);
693
  formData.append('imageDuration', imageDuration);
 
 
694
  formData.append('voiceService', selectedVoiceService);
695
  formData.append('voiceLanguage', selectedLanguage);
696
-
 
 
 
 
 
697
  // Añadir archivos subidos al FormData
698
  uploadedFiles.forEach((file, index) => {
699
  formData.append(`photos`, file);
@@ -739,6 +877,13 @@
739
  thumbnailsContainer.innerHTML = 'Arrastra y suelta tus imágenes aquí o usa el botón de "Seleccionar imágenes".';
740
  thumbnailsContainer.classList.remove('has-files');
741
  photoCountDisplay.textContent = '0/10 fotos seleccionadas';
 
 
 
 
 
 
 
742
 
743
  } catch (error) {
744
  console.error('Error al generar el vídeo:', error);
 
304
  </head>
305
  <body>
306
  <div class="container">
307
+ <h1 class="section-title">✨ Creador de Noticias en Video con IA ✨</h1>
308
 
309
+ <!-- Sección 0: Configuración de API y Opciones Avanzadas -->
310
  <div class="mb-8 p-4 bg-gray-50 rounded-xl shadow-inner">
311
+ <h2 class="text-2xl font-semibold mb-4 text-gray-700">0. Configuración de API y Opciones Avanzadas</h2>
312
+
313
+ <div class="input-group mb-4">
314
+ <label for="llmService" class="block text-gray-700 text-sm font-bold mb-2">Servicio de LLM para Guion:</label>
315
+ <select id="llmService" class="rounded-lg">
316
+ <option value="gemini">Google Gemini</option>
317
+ <option value="gpt">OpenAI GPT</option>
318
+ <option value="anthropic">Anthropic Claude</option>
319
+ <option value="bing">Microsoft Bing (Copilot)</option>
320
+ <option value="custom">Mi LLM Personalizado</option>
321
+ <option value="none">No usar LLM (solo mi guion)</option>
322
+ </select>
323
+ </div>
324
+
325
+ <div id="customLlmEndpointGroup" class="input-group mb-4 hidden">
326
+ <label for="customLlmEndpoint" class="block text-gray-700 text-sm font-bold mb-2">URL del Endpoint de mi LLM Personalizado:</label>
327
+ <input type="text" id="customLlmEndpoint" placeholder="Ej: https://mi-modelo-personalizado.huggingface.app/generate" class="rounded-lg">
328
+ <p class="text-sm text-gray-500 mt-1">Tu backend será el encargado de comunicarse con esta URL.</p>
329
+ </div>
330
+
331
+ <div class="input-group mb-4">
332
+ <label for="genericApiKey" class="block text-gray-700 text-sm font-bold mb-2">Clave de API (Para pruebas locales):</label>
333
+ <input type="text" id="genericApiKey" placeholder="Ingresa tu clave de API aquí" class="rounded-lg">
334
+ <div id="apiKeyWarning" class="message-box warning-message mt-2 show">
335
+ <strong>Advertencia de Seguridad:</strong> Para despliegues (ej. Hugging Face), tu clave de API debe cargarse de forma segura desde variables de entorno/secretos del servidor (ej. `gemini_api`). Si no se introduce aquí, el backend intentará usar las variables de entorno de tu Space.
336
+ </div>
337
+ </div>
338
 
339
  <div class="input-group mb-4">
340
  <label class="block text-gray-700 text-sm font-bold mb-2">Relación de Aspecto del Video:</label>
 
356
  <label for="imageDuration" class="block text-gray-700 text-sm font-bold mb-2">Duración de cada imagen (segundos):</label>
357
  <input type="number" id="imageDuration" value="3" min="1" step="0.5" class="py-2 px-3 rounded-lg">
358
  <div id="imageDurationMessage" class="message-box mt-2"></div>
359
+ <p class="text-sm text-gray-500 mt-1">Define cuánto tiempo cada imagen aparecerá en el video. Si el guion tiene audio, la duración de la imagen se ajustará a la duración del audio para ese segmento.</p>
360
  </div>
361
 
362
  <div class="input-group mb-4">
 
369
  </div>
370
  </div>
371
 
372
+ <!-- Sección 1: Generación de Imágenes (si no se suben fotos) -->
373
+ <div class="mb-8 p-4 bg-gray-50 rounded-xl shadow-inner">
374
+ <h2 class="text-2xl font-semibold mb-4 text-gray-700">1. Generación de Imágenes (si no subes fotos)</h2>
375
+ <div class="flex items-center mb-4">
376
+ <input type="checkbox" id="generateImagesWithAI" class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mr-2">
377
+ <label for="generateImagesWithAI" class="text-gray-700 text-sm">Generar imágenes con IA si no subo fotos</label>
378
+ </div>
379
+ <div id="imageGenerationOptions" class="hidden">
380
+ <div class="input-group mb-4">
381
+ <label for="imageGenerationService" class="block text-gray-700 text-sm font-bold mb-2">Servicio de Generación de Imágenes:</label>
382
+ <select id="imageGenerationService" class="rounded-lg">
383
+ <option value="google_imagen" selected>Google Imagen (Imagen 3.0)</option>
384
+ <option value="dalle">OpenAI DALL-E (No implementado en backend)</option>
385
+ <option value="custom_image_gen">Mi Servicio Personalizado (No implementado en backend)</option>
386
+ </select>
387
+ </div>
388
+ <div id="customImageGenEndpointGroup" class="input-group mb-4 hidden">
389
+ <label for="customImageGenEndpoint" class="block text-gray-700 text-sm font-bold mb-2">URL del Endpoint de mi Servicio Personalizado:</label>
390
+ <input type="text" id="customImageGenEndpoint" placeholder="Ej: https://mi-generador-ia.huggingface.app/generate" class="rounded-lg">
391
+ </div>
392
+ <div class="input-group mb-4">
393
+ <label for="imageGenerationPrompt" class="block text-gray-700 text-sm font-bold mb-2">Prompt para generar imágenes (ej. "paisajes de El Principito, estilo acuarela"):</label>
394
+ <textarea id="imageGenerationPrompt" rows="3" placeholder="Describe las imágenes que quieres generar..." class="rounded-lg"></textarea>
395
+ <p class="text-sm text-gray-500 mt-1">Se generarán entre 1 y 10 imágenes (según tu guion) basadas en este prompt. Est. 1.5 minutos por imagen.</p>
396
+ </div>
397
+ </div>
398
+ <div id="imageGenMessage" class="message-box mt-4 rounded-lg"></div>
399
+ </div>
400
+
401
+ <!-- Sección 2: Carga de Fotos -->
402
  <div class="mb-8 p-4 bg-gray-50 rounded-xl shadow-inner">
403
  <h2 class="text-2xl font-semibold mb-4 text-gray-700">2. Sube tus Fotos (Máx. 10)</h2>
404
  <div class="input-group mb-4">
 
412
  <div id="imageErrorMessage" class="message-box error-message mt-4 rounded-lg"></div>
413
  </div>
414
 
415
+ <!-- Sección 3: Escritura del Guion -->
416
  <div class="mb-8 p-4 bg-gray-50 rounded-xl shadow-inner">
417
  <h2 class="text-2xl font-semibold mb-4 text-gray-700">3. Escribe el Guion de tu Noticia</h2>
418
  <div class="input-group mb-4">
419
  <label for="scriptTextarea" class="block text-gray-700 text-sm font-bold mb-2">Tu guion aquí (un párrafo por cada foto):</label>
420
  <textarea id="scriptTextarea" rows="8" placeholder="Ej: Hola, hoy exploraremos el fascinante mundo de El Principito. En nuestra primera imagen, vemos al Principito en su pequeño asteroide, cuidando su rosa. Esta escena evoca la ternura y la responsabilidad..." class="rounded-lg"></textarea>
421
  </div>
422
+ <div class="flex items-center mb-4">
423
+ <input type="checkbox" id="useLlmForScript" class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mr-2">
424
+ <label for="useLlmForScript" class="text-gray-700 text-sm">Usar LLM para generar/mejorar el guion (basado en fotos y en mi texto)</label>
425
+ </div>
426
  <div id="llmMessage" class="message-box mt-4 rounded-lg"></div>
427
  </div>
428
 
429
+ <!-- Sección 4: Selección de Voz -->
430
  <div class="mb-8 p-4 bg-gray-50 rounded-xl shadow-inner">
431
  <h2 class="text-2xl font-semibold mb-4 text-gray-700">4. Elige la Voz de tu Narrador</h2>
432
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
 
448
  <div id="voiceTestMessage" class="message-box mt-4 rounded-lg"></div>
449
  </div>
450
 
451
+ <!-- Sección 5: Generación de Video -->
452
  <div class="mb-8 text-center p-4 bg-gray-50 rounded-xl shadow-inner">
453
  <h2 class="text-2xl font-semibold mb-4 text-gray-700">5. Generar Video</h2>
454
  <button id="generateVideoBtn" class="btn-primary w-full max-w-xs rounded-xl">Crear Noticia en Video</button>
 
458
  <div id="generateVideoMessage" class="message-box mt-4 rounded-lg"></div>
459
  </div>
460
 
461
+ <!-- Sección 6: Video Resultante -->
462
  <div class="p-4 bg-gray-50 rounded-xl shadow-inner">
463
  <h2 class="text-2xl font-semibold mb-4 text-gray-700">6. Tu Video está Listo</h2>
464
  <div id="videoOutput" class="video-placeholder">
 
479
  </div>
480
 
481
  <script>
482
+ // Referencias a elementos del DOM
483
+ const llmServiceSelect = document.getElementById('llmService');
484
+ const genericApiKeyInput = document.getElementById('genericApiKey');
485
+ const customLlmEndpointGroup = document.getElementById('customLlmEndpointGroup');
486
+ const customLlmEndpointInput = document.getElementById('customLlmEndpoint');
487
  const aspectRatio16_9Radio = document.getElementById('aspectRatio16_9');
488
  const aspectRatio9_16Radio = document.getElementById('aspectRatio9_16');
489
  const imageDurationInput = document.getElementById('imageDuration');
490
  const imageDurationMessage = document.getElementById('imageDurationMessage');
491
  const voiceServiceSelect = document.getElementById('voiceService');
492
+
493
+ const generateImagesWithAICheckbox = document.getElementById('generateImagesWithAI');
494
+ const imageGenerationOptionsDiv = document.getElementById('imageGenerationOptions');
495
+ const imageGenerationServiceSelect = document.getElementById('imageGenerationService');
496
+ const customImageGenEndpointGroup = document.getElementById('customImageGenEndpointGroup');
497
+ const customImageGenEndpointInput = document.getElementById('customImageGenEndpoint');
498
+ const imageGenerationPromptTextarea = document.getElementById('imageGenerationPrompt');
499
+ const imageGenMessage = document.getElementById('imageGenMessage');
500
+
501
  const photoUpload = document.getElementById('photoUpload');
502
  const thumbnailsContainer = document.getElementById('thumbnails');
503
  const photoCountDisplay = document.getElementById('photoCount');
504
  const imageErrorMessage = document.getElementById('imageErrorMessage');
505
 
506
  const scriptTextarea = document.getElementById('scriptTextarea');
507
+ const useLlmForScriptCheckbox = document.getElementById('useLlmForScript');
508
+ const llmMessage = document.getElementById('llmMessage');
509
 
510
  const voiceLanguageSelect = document.getElementById('voiceLanguage');
511
  const testVoiceBtn = document.getElementById('testVoiceBtn');
 
591
  }
592
  }
593
 
594
+ // Event listener para mostrar/ocultar el campo de endpoint de LLM personalizado
595
+ llmServiceSelect.addEventListener('change', () => {
596
+ if (llmServiceSelect.value === 'custom') {
597
+ customLlmEndpointGroup.classList.remove('hidden');
598
+ } else {
599
+ customLlmEndpointGroup.classList.add('hidden');
600
+ customLlmEndpointInput.value = '';
601
+ }
602
+ });
603
+
604
+ // Event listener para mostrar/ocultar las opciones de generación de imagen por IA
605
+ generateImagesWithAICheckbox.addEventListener('change', () => {
606
+ if (generateImagesWithAICheckbox.checked) {
607
+ imageGenerationOptionsDiv.classList.remove('hidden');
608
+ } else {
609
+ imageGenerationOptionsDiv.classList.add('hidden');
610
+ imageGenerationPromptTextarea.value = '';
611
+ imageGenerationServiceSelect.value = 'google_imagen';
612
+ customImageGenEndpointGroup.classList.add('hidden');
613
+ customImageGenEndpointInput.value = '';
614
+ }
615
+ });
616
+
617
+ // Event listener para mostrar/ocultar el campo de endpoint de generación de imagen personalizado
618
+ imageGenerationServiceSelect.addEventListener('change', () => {
619
+ if (imageGenerationServiceSelect.value === 'custom_image_gen') {
620
+ customImageGenEndpointGroup.classList.remove('hidden');
621
+ } else {
622
+ customImageGenEndpointGroup.classList.add('hidden');
623
+ customImageGenEndpointInput.value = '';
624
+ }
625
+ });
626
+
627
  // Validación en tiempo real para la duración de la imagen
628
  imageDurationInput.addEventListener('input', () => {
629
  const value = parseFloat(imageDurationInput.value);
 
754
  // Limpiar mensajes y resetear el área de video
755
  hideMessage(generateVideoMessage);
756
  hideMessage(imageErrorMessage);
757
+ hideMessage(llmMessage);
758
  videoOutput.innerHTML = '<p>El video generado aparecerá aquí.</p>';
759
  resultVideo.classList.add('hidden');
760
  resultVideo.src = '';
761
  downloadVideoLink.classList.add('hidden');
762
 
763
+ // Recolectar valores de los inputs del usuario
764
+ const selectedLlmService = llmServiceSelect.value;
765
+ const genericApiKey = document.getElementById('genericApiKey').value.trim(); // Se recoge por si el backend lo requiere para local
766
+ const customLlmEndpoint = document.getElementById('customLlmEndpoint').value.trim(); // Se recoge por si el backend lo requiere para custom
767
  const selectedAspectRatio = document.querySelector('input[name="aspectRatio"]:checked').value;
768
  const imageDuration = parseFloat(imageDurationInput.value);
769
  const selectedVoiceService = voiceServiceSelect.value;
770
  const selectedLanguage = voiceLanguageSelect.value;
771
+ const shouldGenerateImages = generateImagesWithAICheckbox.checked;
772
+ const imageGenService = document.getElementById('imageGenerationService').value; // Se recoge por si el backend lo requiere
773
+ const customImageGenEndpoint = document.getElementById('customImageGenEndpoint').value.trim(); // Se recoge por si el backend lo requiere
774
+ const imageGenPrompt = imageGenerationPromptTextarea.value.trim();
775
  const script = scriptTextarea.value.trim();
776
+ const useLlmForScript = useLlmForScriptCheckbox.checked;
777
+
778
+ // Validaciones de entrada del usuario con modal personalizado
779
+ if (uploadedFiles.length === 0 && !shouldGenerateImages) {
780
+ showCustomModal('Faltan Imágenes', 'Por favor, sube al menos una foto O marca la opción "Generar imágenes con IA" y proporciona un prompt.');
781
  return;
782
  }
783
+ if (uploadedFiles.length > 0 && shouldGenerateImages) {
784
+ showCustomModal('Conflicto de Imágenes', 'Has subido fotos Y activado la generación de imágenes con IA. Por favor, elige solo una opción.');
785
+ return;
786
+ }
787
+ if (shouldGenerateImages && !imageGenPrompt) {
788
+ showCustomModal('Prompt de Imagen Requerido', 'Para generar imágenes con IA, debes proporcionar un prompt descriptivo.');
789
+ return;
790
+ }
791
+ if (!script && !useLlmForScript) {
792
+ showCustomModal('Falta el Guion', 'Por favor, escribe un guion para tu noticia O marca la opción "Usar LLM para generar/mejorar el guion".');
793
  return;
794
  }
 
795
  if (isNaN(imageDuration) || imageDuration <= 0) {
796
  showCustomModal('Duración de Imagen Inválida', 'Por favor, introduce una duración de imagen válida (un número positivo) para cada segmento de video.');
797
  return;
798
  }
799
+ // Aunque el backend Python solo usará Gemini, estas validaciones son para el UI
800
+ if (useLlmForScript && selectedLlmService === 'custom' && !customLlmEndpoint) {
801
+ showCustomModal('URL de LLM Personalizado Requerida', 'Por favor, introduce la URL del endpoint de tu LLM Personalizado.');
802
+ return;
803
+ }
804
+ if (shouldGenerateImages && imageGenService === 'custom_image_gen' && !customImageGenEndpoint) {
805
+ showCustomModal('URL de Servicio Personalizado Requerida', 'Para usar un servicio de generación de imágenes personalizado, debes proporcionar su URL de endpoint.');
806
+ return;
807
+ }
808
+
809
  // Mostrar spinner de carga y deshabilitar el botón
810
  loadingSpinner.style.display = 'block';
811
  loadingText.style.display = 'block';
812
  generateVideoBtn.disabled = true;
813
 
814
  try {
815
+ loadingText.textContent = 'Generando tu video con IA: texto, imágenes, audio y composición... esto puede tardar unos minutos.';
816
 
817
  // Crear objeto FormData para enviar datos y archivos al backend
818
  const formData = new FormData();
819
  formData.append('script', script);
820
+ formData.append('useLlmForScript', useLlmForScript);
821
+ formData.append('llmService', selectedLlmService);
822
+ formData.append('customLlmEndpoint', customLlmEndpoint);
823
  formData.append('aspectRatio', selectedAspectRatio);
824
  formData.append('imageDuration', imageDuration);
825
+ formData.append('shouldGenerateImages', shouldGenerateImages);
826
+ formData.append('imageGenerationPrompt', imageGenPrompt);
827
  formData.append('voiceService', selectedVoiceService);
828
  formData.append('voiceLanguage', selectedLanguage);
829
+ // Si el genericApiKey se proporciona en el frontend, se envía.
830
+ // En despliegues de HF, el backend debe obtenerlo de los secrets.
831
+ if (genericApiKey) {
832
+ formData.append('genericApiKey', genericApiKey);
833
+ }
834
+
835
  // Añadir archivos subidos al FormData
836
  uploadedFiles.forEach((file, index) => {
837
  formData.append(`photos`, file);
 
877
  thumbnailsContainer.innerHTML = 'Arrastra y suelta tus imágenes aquí o usa el botón de "Seleccionar imágenes".';
878
  thumbnailsContainer.classList.remove('has-files');
879
  photoCountDisplay.textContent = '0/10 fotos seleccionadas';
880
+ imageGenerationPromptTextarea.value = '';
881
+ generateImagesWithAICheckbox.checked = false;
882
+ imageGenerationOptionsDiv.classList.add('hidden');
883
+ llmServiceSelect.value = 'gemini';
884
+ customLlmEndpointGroup.classList.add('hidden');
885
+ customLlmEndpointInput.value = '';
886
+ useLlmForScriptCheckbox.checked = false;
887
 
888
  } catch (error) {
889
  console.error('Error al generar el vídeo:', error);