gnosticdev commited on
Commit
ebe9863
·
verified ·
1 Parent(s): 74c0237

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +142 -0
app.py CHANGED
@@ -1,4 +1,27 @@
 
1
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import asyncio
3
  import logging
4
  import tempfile
@@ -106,6 +129,7 @@ for text, voice_id in AVAILABLE_VOICES:
106
  if voice_id == DEFAULT_VOICE_ID:
107
  DEFAULT_VOICE_NAME = text
108
  break
 
109
  if DEFAULT_VOICE_ID not in [v[1] for v in AVAILABLE_VOICES]:
110
  DEFAULT_VOICE_ID = AVAILABLE_VOICES[0][1] if AVAILABLE_VOICES else "en-US-AriaNeural"
111
  DEFAULT_VOICE_NAME = AVAILABLE_VOICES[0][0] if AVAILABLE_VOICES else "Aria (United States) - Female"
@@ -118,6 +142,7 @@ if not PEXELS_API_KEY:
118
 
119
  MODEL_NAME = "datificate/gpt2-small-spanish"
120
  logger.info(f"Inicializando modelo GPT-2: {MODEL_NAME}")
 
121
  tokenizer = None
122
  model = None
123
  try:
@@ -176,13 +201,16 @@ def generate_script(prompt, max_length=150):
176
  if not tokenizer or not model:
177
  logger.warning("Modelos GPT-2 no disponibles - Usando prompt original como guion.")
178
  return prompt.strip()
 
179
  instruction_phrase_start = "Escribe un guion corto, interesante y coherente sobre:"
180
  ai_prompt = f"{instruction_phrase_start} {prompt}"
 
181
  try:
182
  inputs = tokenizer(ai_prompt, return_tensors="pt", truncation=True, max_length=512)
183
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
184
  model.to(device)
185
  inputs = {k: v.to(device) for k, v in inputs.items()}
 
186
  outputs = model.generate(
187
  **inputs,
188
  max_length=max_length + inputs[list(inputs.keys())[0]].size(1),
@@ -195,8 +223,10 @@ def generate_script(prompt, max_length=150):
195
  eos_token_id=tokenizer.eos_token_id,
196
  no_repeat_ngram_size=3
197
  )
 
198
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
199
  cleaned_text = text.strip()
 
200
  try:
201
  prompt_in_output_idx = text.lower().find(prompt.lower())
202
  if prompt_in_output_idx != -1:
@@ -213,12 +243,15 @@ def generate_script(prompt, max_length=150):
213
  except Exception as e:
214
  logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
215
  cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
 
216
  if not cleaned_text or len(cleaned_text) < 10:
217
  logger.warning("El guión generado parece muy corto o vacío después de la limpieza heurística. Usando el texto generado original (sin limpieza adicional).")
218
  cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
 
219
  cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
220
  cleaned_text = cleaned_text.lstrip(':').strip()
221
  cleaned_text = cleaned_text.lstrip('.').strip()
 
222
  sentences = cleaned_text.split('.')
223
  if sentences and sentences[0].strip():
224
  final_text = sentences[0].strip() + '.'
@@ -227,6 +260,7 @@ def generate_script(prompt, max_length=150):
227
  final_text = final_text.replace("..", ".")
228
  logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
229
  return final_text.strip()
 
230
  logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
231
  return cleaned_text.strip()
232
  except Exception as e:
@@ -261,11 +295,13 @@ def download_video_file(url, temp_dir):
261
  os.makedirs(temp_dir, exist_ok=True)
262
  file_name = f"video_dl_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.mp4"
263
  output_path = os.path.join(temp_dir, file_name)
 
264
  with requests.get(url, stream=True, timeout=60) as r:
265
  r.raise_for_status()
266
  with open(output_path, 'wb') as f:
267
  for chunk in r.iter_content(chunk_size=8192):
268
  f.write(chunk)
 
269
  if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
270
  logger.info(f"Video descargado exitosamente: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
271
  return output_path
@@ -290,6 +326,7 @@ def loop_audio_to_length(audio_clip, target_duration):
290
  except Exception as e:
291
  logger.error(f"Could not create silence clip: {e}", exc_info=True)
292
  return AudioFileClip(filename="")
 
293
  if audio_clip.duration >= target_duration:
294
  logger.debug("Audio clip already longer or equal to target. Trimming.")
295
  trimmed_clip = audio_clip.subclip(0, target_duration)
@@ -301,20 +338,24 @@ def loop_audio_to_length(audio_clip, target_duration):
301
  pass
302
  return AudioFileClip(filename="")
303
  return trimmed_clip
 
304
  loops = math.ceil(target_duration / audio_clip.duration)
305
  logger.debug(f"Creando {loops} loops de audio")
306
  audio_segments = [audio_clip] * loops
307
  looped_audio = None
308
  final_looped_audio = None
 
309
  try:
310
  looped_audio = concatenate_audioclips(audio_segments)
311
  if looped_audio.duration is None or looped_audio.duration <= 0:
312
  logger.error("Concatenated audio clip is invalid (None or zero duration).")
313
  raise ValueError("Invalid concatenated audio.")
 
314
  final_looped_audio = looped_audio.subclip(0, target_duration)
315
  if final_looped_audio.duration is None or final_looped_audio.duration <= 0:
316
  logger.error("Final subclipped audio clip is invalid (None or zero duration).")
317
  raise ValueError("Invalid final subclipped audio.")
 
318
  return final_looped_audio
319
  except Exception as e:
320
  logger.error(f"Error concatenating/subclipping audio clips for looping: {str(e)}", exc_info=True)
@@ -338,8 +379,10 @@ def extract_visual_keywords_from_script(script_text):
338
  if not script_text or not script_text.strip():
339
  logger.warning("Guion vacío, no se pueden extraer palabras clave.")
340
  return ["naturaleza", "ciudad", "paisaje"]
 
341
  clean_text = re.sub(r'[^\w\sáéíóúñÁÉÍÓÚÑ]', '', script_text)
342
  keywords_list = []
 
343
  if kw_model:
344
  try:
345
  logger.debug("Intentando extracción con KeyBERT...")
@@ -347,6 +390,7 @@ def extract_visual_keywords_from_script(script_text):
347
  keywords2 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(2, 2), stop_words='spanish', top_n=3)
348
  all_keywords = keywords1 + keywords2
349
  all_keywords.sort(key=lambda item: item[1], reverse=True)
 
350
  seen_keywords = set()
351
  for keyword, score in all_keywords:
352
  formatted_keyword = keyword.lower().replace(" ", "+")
@@ -355,25 +399,30 @@ def extract_visual_keywords_from_script(script_text):
355
  seen_keywords.add(formatted_keyword)
356
  if len(keywords_list) >= 5:
357
  break
 
358
  if keywords_list:
359
  logger.debug(f"Palabras clave extraídas por KeyBERT: {keywords_list}")
360
  return keywords_list
361
  except Exception as e:
362
  logger.warning(f"KeyBERT falló: {str(e)}. Intentando método simple.")
 
363
  logger.debug("Extrayendo palabras clave con método simple...")
364
  words = clean_text.lower().split()
365
  stop_words = {"el", "la", "los", "las", "de", "en", "y", "a", "que", "es", "un", "una", "con", "para", "del", "al", "por", "su", "sus", "se", "lo", "le", "me", "te", "nos", "os", "les", "mi", "tu",
366
  "nuestro", "vuestro", "este", "ese", "aquel", "esta", "esa", "aquella", "esto", "eso", "aquello", "mis", "tus",
367
  "nuestros", "vuestros", "estas", "esas", "aquellas", "si", "no", "más", "menos", "sin", "sobre", "bajo", "entre", "hasta", "desde", "durante", "mediante", "según", "versus", "via", "cada", "todo", "todos", "toda", "todas", "poco", "pocos", "poca", "pocas", "mucho", "muchos", "mucha", "muchas", "varios", "varias", "otro", "otros", "otra", "otras", "mismo", "misma", "mismos", "mismas", "tan", "tanto", "tanta", "tantos", "tantas", "tal", "tales", "cual", "cuales", "cuyo", "cuya", "cuyos", "cuyas", "quien", "quienes", "cuan", "cuanto", "cuanta", "cuantos", "cuantas", "como", "donde", "cuando", "porque", "aunque", "mientras", "siempre", "nunca", "jamás", "muy", "casi", "solo", "solamente", "incluso", "apenas", "quizás", "tal vez", "acaso", "claro", "cierto", "obvio", "evidentemente", "realmente", "simplemente", "generalmente", "especialmente", "principalmente", "posiblemente", "probablemente", "difícilmente", "fácilmente", "rápidamente", "lentamente", "bien", "mal", "mejor", "peor", "arriba", "abajo", "adelante", "atrás", "cerca", "lejos", "dentro", "fuera", "encima", "debajo", "frente", "detrás", "antes", "después", "luego", "pronto", "tarde", "todavía", "ya", "aun", "aún", "quizá"}
 
368
  valid_words = [word for word in words if len(word) > 3 and word not in stop_words]
369
  if not valid_words:
370
  logger.warning("No se encontraron palabras clave válidas con método simple. Usando palabras clave predeterminadas.")
371
  return ["espiritual", "terror", "matrix", "arcontes", "galaxia", "creepy", "magia", "gangstalking","conspiracy",]
 
372
  word_counts = Counter(valid_words)
373
  top_keywords = [word.replace(" ", "+") for word, _ in word_counts.most_common(5)]
374
  if not top_keywords:
375
  logger.warning("El método simple no produjo keywords. Usando palabras clave predeterminadas.")
376
  return ["espiritual", "terror", "matrix", "arcontes", "galaxia", "creepy", "magia", "gangstalking","conspiracy",]
 
377
  logger.info(f"Palabras clave finales: {top_keywords}")
378
  return top_keywords
379
 
@@ -383,6 +432,7 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
383
  logger.debug(f"Input: '{input_text[:100]}...'")
384
  logger.info(f"Voz seleccionada: {selected_voice}")
385
  start_time = datetime.now()
 
386
  temp_dir_intermediate = None
387
  output_filename = None
388
  permanent_path = None
@@ -395,19 +445,24 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
395
  video_final = None
396
  source_clips = []
397
  clips_to_concatenate = []
 
398
  try:
399
  if prompt_type == "Generar Guion con IA":
400
  guion = generate_script(input_text)
401
  else:
402
  guion = input_text.strip()
 
403
  logger.info(f"Guion final ({len(guion)} chars): '{guion[:100]}...'")
404
  if not guion.strip():
405
  logger.error("El guion resultante está vacío o solo contiene espacios.")
406
  raise ValueError("El guion está vacío.")
 
407
  temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
408
  logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
 
409
  logger.info("Generando audio de voz...")
410
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
 
411
  tts_voices_to_try = [selected_voice]
412
  fallback_juan = "es-ES-JuanNeural"
413
  fallback_elvira = "es-ES-ElviraNeural"
@@ -415,6 +470,7 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
415
  tts_voices_to_try.append(fallback_juan)
416
  if fallback_elvira and fallback_elvira != selected_voice and fallback_elvira not in tts_voices_to_try:
417
  tts_voices_to_try.append(fallback_elvira)
 
418
  tts_success = False
419
  tried_voices = set()
420
  for current_voice in tts_voices_to_try:
@@ -430,9 +486,11 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
430
  except Exception as e:
431
  logger.warning(f"Fallo al generar TTS con voz '{current_voice}': {str(e)}", exc_info=True)
432
  pass
 
433
  if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
434
  logger.error("Fallo en la generación de voz después de todos los intentos. Archivo de audio no creado o es muy pequeño.")
435
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
 
436
  temp_intermediate_files.append(voz_path)
437
  audio_tts_original = AudioFileClip(voz_path)
438
  if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
@@ -450,12 +508,14 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
450
  if voz_path in temp_intermediate_files:
451
  temp_intermediate_files.remove(voz_path)
452
  raise ValueError("Audio de voz generado es inválido después de procesamiento inicial.")
 
453
  audio_tts = audio_tts_original
454
  audio_duration = audio_tts_original.duration
455
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
456
  if audio_duration < 1.0:
457
  logger.error(f"Duración audio voz ({audio_duration:.2f}s) es muy corta.")
458
  raise ValueError("Generated voice audio is too short (min 1 second required).")
 
459
  logger.info("Extrayendo palabras clave...")
460
  try:
461
  keywords = extract_visual_keywords_from_script(guion)
@@ -463,12 +523,15 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
463
  except Exception as e:
464
  logger.error(f"Error extrayendo keywords: {str(e)}", exc_info=True)
465
  keywords = ["naturaleza", "paisaje"]
 
466
  if not keywords:
467
  keywords = ["video", "background"]
 
468
  logger.info("Buscando videos en Pexels...")
469
  videos_data = []
470
  total_desired_videos = 10
471
  per_page_per_keyword = max(1, total_desired_videos // len(keywords))
 
472
  for keyword in keywords:
473
  if len(videos_data) >= total_desired_videos:
474
  break
@@ -479,9 +542,11 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
479
  logger.info(f"Encontrados {len(videos)} videos para '{keyword}'. Total data: {len(videos_data)}")
480
  except Exception as e:
481
  logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
 
482
  if len(videos_data) < total_desired_videos / 2:
483
  logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
484
  generic_keywords = ["mystery", "alien", "ufo", "conspiracy", "paranormal", "supernatural", "horror", "fear", "suspense", "secret", "government", "cover_up", "simulation", "matrix", "apocalypse", "dystopian", "shadow", "occult", "unexplained", "creepy", "extraterrestrial", "abduction", "experiment", "secret_society", "illuminati", "new_world_order", "ancient_aliens", "ufo_sighting", "cryptid", "bigfoot", "loch_ness", "ghost", "haunting", "spirit", "demon", "possession", "exorcism", "witchcraft", "ritual", "cursed", "urban_legend", "myth", "legend", "folklore", "scary", "terror", "panic", "anxiety", "dread", "nightmare", "dark", "gloomy", "fog", "haunted", "cemetery", "asylum", "abandoned", "ruins", "underground", "tunnel", "bunker", "lab", "experiment", "government_secret", "mind_control", "brainwash", "propaganda", "surveillance", "spy", "whistleblower", "leak", "anonymous", "hack", "cyber", "virtual_reality", "ai", "artificial_intelligence", "robot", "cyborg", "apocalyptic", "post_apocalyptic", "zombie", "outbreak", "pandemic", "contagion", "biohazard", "radiation", "nuclear", "doomsday", "armageddon", "revelation", "prophecy", "symbolism", "hidden_meaning", "enigma", "puzzle", "code", "cipher", "mysterious", "unidentified", "anomaly", "glitch", "time_travel", "parallel_universe", "dimension", "portal"]
 
485
  for keyword in generic_keywords:
486
  if len(videos_data) >= total_desired_videos:
487
  break
@@ -492,21 +557,26 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
492
  logger.info(f"Encontrados {len(videos)} videos para '{keyword}' (genérico). Total data: {len(videos_data)}")
493
  except Exception as e:
494
  logger.warning(f"Error buscando videos genéricos para '{keyword}': {str(e)}")
 
495
  if not videos_data:
496
  logger.error("No se encontraron videos en Pexels para ninguna palabra clave.")
497
  raise ValueError("No se encontraron videos adecuados en Pexels.")
 
498
  video_paths = []
499
  logger.info(f"Intentando descargar {len(videos_data)} videos encontrados...")
 
500
  for video in videos_data:
501
  if 'video_files' not in video or not video['video_files']:
502
  logger.debug(f"Saltando video sin archivos de video: {video.get('id')}")
503
  continue
 
504
  try:
505
  best_quality = None
506
  for vf in sorted(video['video_files'], key=lambda x: x.get('width', 0) * x.get('height', 0), reverse=True):
507
  if 'link' in vf:
508
  best_quality = vf
509
  break
 
510
  if best_quality and 'link' in best_quality:
511
  path = download_video_file(best_quality['link'], temp_dir_intermediate)
512
  if path:
@@ -519,32 +589,40 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
519
  logger.warning(f"No se encontró enlace de descarga válido para video {video.get('id')}.")
520
  except Exception as e:
521
  logger.warning(f"Error procesando/descargando video {video.get('id')}: {str(e)}")
 
522
  logger.info(f"Descargados {len(video_paths)} archivos de video utilizables.")
523
  if not video_paths:
524
  logger.error("No se pudo descargar ningún archivo de video utilizable.")
525
  raise ValueError("No se pudo descargar ningún video utilizable de Pexels.")
 
526
  logger.info("Procesando y concatenando videos descargados...")
527
  current_duration = 0
528
  min_clip_duration = 0.5
529
  max_clip_segment = 10.0
 
530
  for i, path in enumerate(video_paths):
531
  if current_duration >= audio_duration + max_clip_segment:
532
  logger.debug(f"Video base suficiente ({current_duration:.1f}s >= {audio_duration:.1f}s + {max_clip_segment:.1f}s buffer). Dejando de procesar clips fuente restantes.")
533
  break
 
534
  clip = None
535
  try:
536
  logger.debug(f"[{i+1}/{len(video_paths)}] Abriendo clip: {path}")
537
  clip = VideoFileClip(path)
538
  source_clips.append(clip)
 
539
  if clip.reader is None or clip.duration is None or clip.duration <= 0:
540
  logger.warning(f"[{i+1}/{len(video_paths)}] Clip fuente {path} parece inválido (reader is None o duración <= 0). Saltando.")
541
  continue
 
542
  remaining_needed = audio_duration - current_duration
543
  potential_use_duration = min(clip.duration, max_clip_segment)
 
544
  if remaining_needed > 0:
545
  segment_duration = min(potential_use_duration, remaining_needed + min_clip_duration)
546
  segment_duration = max(min_clip_duration, segment_duration)
547
  segment_duration = min(segment_duration, clip.duration)
 
548
  if segment_duration >= min_clip_duration:
549
  try:
550
  sub = clip.subclip(0, segment_duration)
@@ -555,6 +633,7 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
555
  except:
556
  pass
557
  continue
 
558
  clips_to_concatenate.append(sub)
559
  current_duration += sub.duration
560
  logger.debug(f"[{i+1}/{len(video_paths)}] Segmento añadido: {sub.duration:.1f}s (total video: {current_duration:.1f}/{audio_duration:.1f}s)")
@@ -568,12 +647,15 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
568
  except Exception as e:
569
  logger.warning(f"[{i+1}/{len(video_paths)}] Error procesando video {path}: {str(e)}", exc_info=True)
570
  continue
 
571
  logger.info(f"Procesamiento de clips fuente finalizado. Se obtuvieron {len(clips_to_concatenate)} segmentos válidos.")
572
  if not clips_to_concatenate:
573
  logger.error("No hay segmentos de video válidos disponibles para crear la secuencia.")
574
  raise ValueError("No hay segmentos de video válidos disponibles para crear el video.")
 
575
  logger.info(f"Concatenando {len(clips_to_concatenate)} segmentos de video.")
576
  concatenated_base = None
 
577
  try:
578
  concatenated_base = concatenate_videoclips(clips_to_concatenate, method="chain")
579
  logger.info(f"Duración video base después de concatenación inicial: {concatenated_base.duration:.2f}s")
@@ -590,12 +672,15 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
590
  except:
591
  pass
592
  clips_to_concatenate = []
 
593
  video_base = concatenated_base
594
  final_video_base = video_base
 
595
  if final_video_base.duration < audio_duration:
596
  logger.info(f"Video base ({final_video_base.duration:.2f}s) es más corto que el audio ({audio_duration:.2f}s). Repitiendo...")
597
  num_full_repeats = int(audio_duration // final_video_base.duration)
598
  remaining_duration = audio_duration % final_video_base.duration
 
599
  repeated_clips_list = [final_video_base] * num_full_repeats
600
  if remaining_duration > 0:
601
  try:
@@ -611,6 +696,7 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
611
  logger.debug(f"Añadiendo segmento restante: {remaining_duration:.2f}s")
612
  except Exception as e:
613
  logger.warning(f"Error creando subclip para duración restante {remaining_duration:.2f}s: {str(e)}")
 
614
  if repeated_clips_list:
615
  logger.info(f"Concatenando {len(repeated_clips_list)} partes para repetición.")
616
  video_base_repeated = None
@@ -620,6 +706,7 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
620
  if video_base_repeated is None or video_base_repeated.duration is None or video_base_repeated.duration <= 0:
621
  logger.critical("Video base repetido concatenado es inválido.")
622
  raise ValueError("Fallo al crear video base repetido válido.")
 
623
  if final_video_base is not video_base_repeated:
624
  try:
625
  final_video_base.close()
@@ -637,6 +724,7 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
637
  clip.close()
638
  except:
639
  pass
 
640
  if final_video_base.duration > audio_duration:
641
  logger.info(f"Recortando video base ({final_video_base.duration:.2f}s) para que coincida con la duración del audio ({audio_duration:.2f}s).")
642
  trimmed_video_base = None
@@ -645,6 +733,7 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
645
  if trimmed_video_base is None or trimmed_video_base.duration is None or trimmed_video_base.duration <= 0:
646
  logger.critical("Video base recortado es inválido.")
647
  raise ValueError("Fallo al crear video base recortado válido.")
 
648
  if final_video_base is not trimmed_video_base:
649
  try:
650
  final_video_base.close()
@@ -654,16 +743,21 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
654
  except Exception as e:
655
  logger.critical(f"Error durante el recorte: {str(e)}", exc_info=True)
656
  raise ValueError("Fallo durante el recorte de video.")
 
657
  if final_video_base is None or final_video_base.duration is None or final_video_base.duration <= 0:
658
  logger.critical("Video base final es inválido antes de audio/escritura (None o duración cero).")
659
  raise ValueError("Video base final es inválido.")
 
660
  if final_video_base.size is None or final_video_base.size[0] <= 0 or final_video_base.size[1] <= 0:
661
  logger.critical(f"Video base final tiene tamaño inválido: {final_video_base.size}. No se puede escribir video.")
662
  raise ValueError("Video base final tiene tamaño inválido antes de escribir.")
 
663
  video_base = final_video_base
 
664
  logger.info("Procesando audio...")
665
  final_audio = audio_tts_original
666
  musica_audio_looped = None
 
667
  if musica_file:
668
  musica_audio_original = None
669
  try:
@@ -671,6 +765,7 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
671
  shutil.copyfile(musica_file, music_path)
672
  temp_intermediate_files.append(music_path)
673
  logger.info(f"Música de fondo copiada a: {music_path}")
 
674
  musica_audio_original = AudioFileClip(music_path)
675
  if musica_audio_original.reader is None or musica_audio_original.duration is None or musica_audio_original.duration <= 0:
676
  logger.warning("Clip de música de fondo parece inválido o tiene duración cero. Saltando música.")
@@ -682,6 +777,7 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
682
  else:
683
  musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
684
  logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
 
685
  if musica_audio_looped is None or musica_audio_looped.duration is None or musica_audio_looped.duration <= 0:
686
  logger.warning("Clip de música de fondo loopeado es inválido. Saltando música.")
687
  try:
@@ -689,11 +785,13 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
689
  except:
690
  pass
691
  musica_audio_looped = None
 
692
  if musica_audio_looped:
693
  composite_audio = CompositeAudioClip([
694
  musica_audio_looped.volumex(0.2),
695
  audio_tts_original.volumex(1.0)
696
  ])
 
697
  if composite_audio.duration is None or composite_audio.duration <= 0:
698
  logger.warning("Clip de audio compuesto es inválido (None o duración cero). Usando solo audio de voz.")
699
  try:
@@ -710,6 +808,7 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
710
  final_audio = audio_tts_original
711
  musica_audio = None
712
  logger.warning("Usando solo audio de voz debido a un error con la música.")
 
713
  if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
714
  logger.warning(f"Duración del audio final ({final_audio.duration:.2f}s) difiere significativamente del video base ({video_base.duration:.2f}s). Intentando recorte.")
715
  try:
@@ -731,10 +830,13 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
731
  logger.warning("Audio final recortado para que coincida con la duración del video.")
732
  except Exception as e:
733
  logger.warning(f"Error ajustando duración del audio final: {str(e)}")
 
734
  video_final = video_base.set_audio(final_audio)
 
735
  output_filename = f"video_{int(time.time())}.mp4"
736
  output_path = os.path.join(temp_dir_intermediate, output_filename)
737
  permanent_path = f"/tmp/{output_filename}"
 
738
  video_final.write_videofile(
739
  output_path,
740
  fps=24,
@@ -748,21 +850,25 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
748
  ],
749
  logger='bar'
750
  )
 
751
  try:
752
  shutil.copy(output_path, permanent_path)
753
  logger.info(f"Video guardado permanentemente en: {permanent_path}")
754
  except Exception as move_error:
755
  logger.error(f"Error moviendo archivo: {str(move_error)}. Usando path original.")
756
  permanent_path = output_path
 
757
  try:
758
  video_final.close()
759
  if 'video_base' in locals() and video_base is not None and video_base is not video_final:
760
  video_base.close()
761
  except Exception as close_error:
762
  logger.error(f"Error cerrando clips: {str(close_error)}")
 
763
  total_time = (datetime.now() - start_time).total_seconds()
764
  logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {permanent_path} | Tiempo total: {total_time:.2f}s")
765
  return permanent_path
 
766
  except ValueError as ve:
767
  logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
768
  raise ve
@@ -771,36 +877,43 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
771
  raise e
772
  finally:
773
  logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
 
774
  for clip in source_clips:
775
  try:
776
  clip.close()
777
  except Exception as e:
778
  logger.warning(f"Error cerrando clip de video fuente en finally: {str(e)}")
 
779
  for clip_segment in clips_to_concatenate:
780
  try:
781
  clip_segment.close()
782
  except Exception as e:
783
  logger.warning(f"Error cerrando segmento de video en finally: {str(e)}")
 
784
  if musica_audio is not None:
785
  try:
786
  musica_audio.close()
787
  except Exception as e:
788
  logger.warning(f"Error cerrando musica_audio (procesada) en finally: {str(e)}")
 
789
  if musica_audio_original is not None and musica_audio_original is not musica_audio:
790
  try:
791
  musica_audio_original.close()
792
  except Exception as e:
793
  logger.warning(f"Error cerrando musica_audio_original en finally: {str(e)}")
 
794
  if audio_tts is not None and audio_tts is not audio_tts_original:
795
  try:
796
  audio_tts.close()
797
  except Exception as e:
798
  logger.warning(f"Error cerrando audio_tts (procesada) en finally: {str(e)}")
 
799
  if audio_tts_original is not None:
800
  try:
801
  audio_tts_original.close()
802
  except Exception as e:
803
  logger.warning(f"Error cerrando audio_tts_original en finalmente: {str(e)}")
 
804
  if video_final is not None:
805
  try:
806
  video_final.close()
@@ -811,10 +924,12 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
811
  video_base.close()
812
  except Exception as e:
813
  logger.warning(f"Error cerrando video_base en finally: {str(e)}")
 
814
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
815
  final_output_in_temp = None
816
  if output_filename:
817
  final_output_in_temp = os.path.join(temp_dir_intermediate, output_filename)
 
818
  for path in temp_intermediate_files:
819
  try:
820
  if os.path.isfile(path) and path != final_output_in_temp and path != permanent_path:
@@ -824,34 +939,43 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
824
  logger.debug(f"Saltando eliminación del archivo de video final: {path}")
825
  except Exception as e:
826
  logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
 
827
  logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
828
 
829
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
830
  logger.info("="*80)
831
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
 
832
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
833
  output_video = None
834
  output_file = None
835
  status_msg = gr.update(value="⏳ Procesando...", interactive=False)
 
836
  if not input_text or not input_text.strip():
837
  logger.warning("Texto de entrada vacío.")
838
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
 
839
  voice_ids_disponibles = [v[1] for v in AVAILABLE_VOICES]
840
  if selected_voice not in voice_ids_disponibles:
841
  logger.warning(f"Voz seleccionada inválida o no encontrada en la lista: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE_ID}.")
842
  selected_voice = DEFAULT_VOICE_ID
843
  else:
844
  logger.info(f"Voz seleccionada validada: {selected_voice}")
 
845
  logger.info(f"Tipo de entrada: {prompt_type}")
846
  logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
 
847
  if musica_file:
848
  logger.info(f"Archivo de música recibido: {musica_file}")
849
  else:
850
  logger.info("No se proporcionó archivo de música.")
 
851
  logger.info(f"Voz final a usar (ID): {selected_voice}")
 
852
  try:
853
  logger.info("Llamando a crear_video...")
854
  video_path = crear_video(prompt_type, input_text, selected_voice, musica_file)
 
855
  if video_path and os.path.exists(video_path):
856
  logger.info(f"crear_video retornó path: {video_path}")
857
  logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
@@ -861,12 +985,15 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
861
  else:
862
  logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
863
  status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
 
864
  except ValueError as ve:
865
  logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
866
  status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
 
867
  except Exception as e:
868
  logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
869
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
 
870
  finally:
871
  logger.info("Fin del handler run_app.")
872
  return output_video, output_file, status_msg
@@ -875,8 +1002,10 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
875
  .gradio-container {max-width: 800px; margin: auto;}
876
  h1 {text-align: center;}
877
  """) as app:
 
878
  gr.Markdown("# 🎬 Generador Automático de Videos con IA")
879
  gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
 
880
  with gr.Row():
881
  with gr.Column():
882
  prompt_type = gr.Radio(
@@ -884,6 +1013,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
884
  label="Método de Entrada",
885
  value="Generar Guion con IA"
886
  )
 
887
  with gr.Column(visible=True) as ia_guion_column:
888
  prompt_ia = gr.Textbox(
889
  label="Tema para IA",
@@ -892,6 +1022,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
892
  max_lines=4,
893
  value=""
894
  )
 
895
  with gr.Column(visible=False) as manual_guion_column:
896
  prompt_manual = gr.Textbox(
897
  label="Tu Guion Completo",
@@ -900,30 +1031,36 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
900
  max_lines=10,
901
  value=""
902
  )
 
903
  musica_input = gr.Audio(
904
  label="Música de fondo (opcional)",
905
  type="filepath",
906
  interactive=True,
907
  value=None
908
  )
 
909
  voice_dropdown = gr.Dropdown(
910
  label="Seleccionar Voz para Guion",
911
  choices=AVAILABLE_VOICES,
912
  value=DEFAULT_VOICE_ID,
913
  interactive=True
914
  )
 
915
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
 
916
  with gr.Column():
917
  video_output = gr.Video(
918
  label="Previsualización del Video Generado",
919
  interactive=False,
920
  height=400
921
  )
 
922
  file_output = gr.File(
923
  label="Descargar Archivo de Video",
924
  interactive=False,
925
  visible=False
926
  )
 
927
  status_output = gr.Textbox(
928
  label="Estado",
929
  interactive=False,
@@ -931,12 +1068,14 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
931
  placeholder="Esperando acción...",
932
  value="Esperando entrada..."
933
  )
 
934
  prompt_type.change(
935
  lambda x: (gr.update(visible=x == "Generar Guion con IA"),
936
  gr.update(visible=x == "Usar Mi Guion")),
937
  inputs=prompt_type,
938
  outputs=[ia_guion_column, manual_guion_column]
939
  )
 
940
  generate_btn.click(
941
  lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
942
  outputs=[video_output, file_output, status_output],
@@ -950,6 +1089,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
950
  inputs=[video_output, file_output, status_output],
951
  outputs=[file_output]
952
  )
 
953
  gr.Markdown("### Instrucciones:")
954
  gr.Markdown("""
955
  1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
@@ -961,6 +1101,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
961
  7. La previsualización aparecerá si es posible, y siempre un enlace **Descargar Archivo de Video** se mostrará si la generación fue exitosa.
962
  8. Revisa `video_generator_full.log` para detalles si hay errores.
963
  """)
 
964
  gr.Markdown("---")
965
  gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
966
 
@@ -976,6 +1117,7 @@ if __name__ == "__main__":
976
  logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
977
  except Exception as e:
978
  logger.critical(f"Fallo al importar MoviePy. Asegúrate de que está instalado. Error: {e}", exc_info=True)
 
979
  os.environ['GRADIO_SERVER_TIMEOUT'] = '1200'
980
  logger.info("Iniciando aplicación Gradio...")
981
  try:
 
1
+ python
2
  import os
3
+ import subprocess
4
+ import sys
5
+ import importlib
6
+
7
+ # Instalar dependencias faltantes
8
+ required_packages = [
9
+ 'moviepy',
10
+ 'edge-tts',
11
+ 'gradio',
12
+ 'torch',
13
+ 'transformers',
14
+ 'keybert',
15
+ 'requests'
16
+ ]
17
+
18
+ for package in required_packages:
19
+ try:
20
+ importlib.import_module(package.replace('-', '_'))
21
+ except ImportError:
22
+ print(f"Instalando {package}...")
23
+ subprocess.check_call([sys.executable, "-m", "pip", "install", package])
24
+
25
  import asyncio
26
  import logging
27
  import tempfile
 
129
  if voice_id == DEFAULT_VOICE_ID:
130
  DEFAULT_VOICE_NAME = text
131
  break
132
+
133
  if DEFAULT_VOICE_ID not in [v[1] for v in AVAILABLE_VOICES]:
134
  DEFAULT_VOICE_ID = AVAILABLE_VOICES[0][1] if AVAILABLE_VOICES else "en-US-AriaNeural"
135
  DEFAULT_VOICE_NAME = AVAILABLE_VOICES[0][0] if AVAILABLE_VOICES else "Aria (United States) - Female"
 
142
 
143
  MODEL_NAME = "datificate/gpt2-small-spanish"
144
  logger.info(f"Inicializando modelo GPT-2: {MODEL_NAME}")
145
+
146
  tokenizer = None
147
  model = None
148
  try:
 
201
  if not tokenizer or not model:
202
  logger.warning("Modelos GPT-2 no disponibles - Usando prompt original como guion.")
203
  return prompt.strip()
204
+
205
  instruction_phrase_start = "Escribe un guion corto, interesante y coherente sobre:"
206
  ai_prompt = f"{instruction_phrase_start} {prompt}"
207
+
208
  try:
209
  inputs = tokenizer(ai_prompt, return_tensors="pt", truncation=True, max_length=512)
210
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
211
  model.to(device)
212
  inputs = {k: v.to(device) for k, v in inputs.items()}
213
+
214
  outputs = model.generate(
215
  **inputs,
216
  max_length=max_length + inputs[list(inputs.keys())[0]].size(1),
 
223
  eos_token_id=tokenizer.eos_token_id,
224
  no_repeat_ngram_size=3
225
  )
226
+
227
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
228
  cleaned_text = text.strip()
229
+
230
  try:
231
  prompt_in_output_idx = text.lower().find(prompt.lower())
232
  if prompt_in_output_idx != -1:
 
243
  except Exception as e:
244
  logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
245
  cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
246
+
247
  if not cleaned_text or len(cleaned_text) < 10:
248
  logger.warning("El guión generado parece muy corto o vacío después de la limpieza heurística. Usando el texto generado original (sin limpieza adicional).")
249
  cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
250
+
251
  cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
252
  cleaned_text = cleaned_text.lstrip(':').strip()
253
  cleaned_text = cleaned_text.lstrip('.').strip()
254
+
255
  sentences = cleaned_text.split('.')
256
  if sentences and sentences[0].strip():
257
  final_text = sentences[0].strip() + '.'
 
260
  final_text = final_text.replace("..", ".")
261
  logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
262
  return final_text.strip()
263
+
264
  logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
265
  return cleaned_text.strip()
266
  except Exception as e:
 
295
  os.makedirs(temp_dir, exist_ok=True)
296
  file_name = f"video_dl_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.mp4"
297
  output_path = os.path.join(temp_dir, file_name)
298
+
299
  with requests.get(url, stream=True, timeout=60) as r:
300
  r.raise_for_status()
301
  with open(output_path, 'wb') as f:
302
  for chunk in r.iter_content(chunk_size=8192):
303
  f.write(chunk)
304
+
305
  if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
306
  logger.info(f"Video descargado exitosamente: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
307
  return output_path
 
326
  except Exception as e:
327
  logger.error(f"Could not create silence clip: {e}", exc_info=True)
328
  return AudioFileClip(filename="")
329
+
330
  if audio_clip.duration >= target_duration:
331
  logger.debug("Audio clip already longer or equal to target. Trimming.")
332
  trimmed_clip = audio_clip.subclip(0, target_duration)
 
338
  pass
339
  return AudioFileClip(filename="")
340
  return trimmed_clip
341
+
342
  loops = math.ceil(target_duration / audio_clip.duration)
343
  logger.debug(f"Creando {loops} loops de audio")
344
  audio_segments = [audio_clip] * loops
345
  looped_audio = None
346
  final_looped_audio = None
347
+
348
  try:
349
  looped_audio = concatenate_audioclips(audio_segments)
350
  if looped_audio.duration is None or looped_audio.duration <= 0:
351
  logger.error("Concatenated audio clip is invalid (None or zero duration).")
352
  raise ValueError("Invalid concatenated audio.")
353
+
354
  final_looped_audio = looped_audio.subclip(0, target_duration)
355
  if final_looped_audio.duration is None or final_looped_audio.duration <= 0:
356
  logger.error("Final subclipped audio clip is invalid (None or zero duration).")
357
  raise ValueError("Invalid final subclipped audio.")
358
+
359
  return final_looped_audio
360
  except Exception as e:
361
  logger.error(f"Error concatenating/subclipping audio clips for looping: {str(e)}", exc_info=True)
 
379
  if not script_text or not script_text.strip():
380
  logger.warning("Guion vacío, no se pueden extraer palabras clave.")
381
  return ["naturaleza", "ciudad", "paisaje"]
382
+
383
  clean_text = re.sub(r'[^\w\sáéíóúñÁÉÍÓÚÑ]', '', script_text)
384
  keywords_list = []
385
+
386
  if kw_model:
387
  try:
388
  logger.debug("Intentando extracción con KeyBERT...")
 
390
  keywords2 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(2, 2), stop_words='spanish', top_n=3)
391
  all_keywords = keywords1 + keywords2
392
  all_keywords.sort(key=lambda item: item[1], reverse=True)
393
+
394
  seen_keywords = set()
395
  for keyword, score in all_keywords:
396
  formatted_keyword = keyword.lower().replace(" ", "+")
 
399
  seen_keywords.add(formatted_keyword)
400
  if len(keywords_list) >= 5:
401
  break
402
+
403
  if keywords_list:
404
  logger.debug(f"Palabras clave extraídas por KeyBERT: {keywords_list}")
405
  return keywords_list
406
  except Exception as e:
407
  logger.warning(f"KeyBERT falló: {str(e)}. Intentando método simple.")
408
+
409
  logger.debug("Extrayendo palabras clave con método simple...")
410
  words = clean_text.lower().split()
411
  stop_words = {"el", "la", "los", "las", "de", "en", "y", "a", "que", "es", "un", "una", "con", "para", "del", "al", "por", "su", "sus", "se", "lo", "le", "me", "te", "nos", "os", "les", "mi", "tu",
412
  "nuestro", "vuestro", "este", "ese", "aquel", "esta", "esa", "aquella", "esto", "eso", "aquello", "mis", "tus",
413
  "nuestros", "vuestros", "estas", "esas", "aquellas", "si", "no", "más", "menos", "sin", "sobre", "bajo", "entre", "hasta", "desde", "durante", "mediante", "según", "versus", "via", "cada", "todo", "todos", "toda", "todas", "poco", "pocos", "poca", "pocas", "mucho", "muchos", "mucha", "muchas", "varios", "varias", "otro", "otros", "otra", "otras", "mismo", "misma", "mismos", "mismas", "tan", "tanto", "tanta", "tantos", "tantas", "tal", "tales", "cual", "cuales", "cuyo", "cuya", "cuyos", "cuyas", "quien", "quienes", "cuan", "cuanto", "cuanta", "cuantos", "cuantas", "como", "donde", "cuando", "porque", "aunque", "mientras", "siempre", "nunca", "jamás", "muy", "casi", "solo", "solamente", "incluso", "apenas", "quizás", "tal vez", "acaso", "claro", "cierto", "obvio", "evidentemente", "realmente", "simplemente", "generalmente", "especialmente", "principalmente", "posiblemente", "probablemente", "difícilmente", "fácilmente", "rápidamente", "lentamente", "bien", "mal", "mejor", "peor", "arriba", "abajo", "adelante", "atrás", "cerca", "lejos", "dentro", "fuera", "encima", "debajo", "frente", "detrás", "antes", "después", "luego", "pronto", "tarde", "todavía", "ya", "aun", "aún", "quizá"}
414
+
415
  valid_words = [word for word in words if len(word) > 3 and word not in stop_words]
416
  if not valid_words:
417
  logger.warning("No se encontraron palabras clave válidas con método simple. Usando palabras clave predeterminadas.")
418
  return ["espiritual", "terror", "matrix", "arcontes", "galaxia", "creepy", "magia", "gangstalking","conspiracy",]
419
+
420
  word_counts = Counter(valid_words)
421
  top_keywords = [word.replace(" ", "+") for word, _ in word_counts.most_common(5)]
422
  if not top_keywords:
423
  logger.warning("El método simple no produjo keywords. Usando palabras clave predeterminadas.")
424
  return ["espiritual", "terror", "matrix", "arcontes", "galaxia", "creepy", "magia", "gangstalking","conspiracy",]
425
+
426
  logger.info(f"Palabras clave finales: {top_keywords}")
427
  return top_keywords
428
 
 
432
  logger.debug(f"Input: '{input_text[:100]}...'")
433
  logger.info(f"Voz seleccionada: {selected_voice}")
434
  start_time = datetime.now()
435
+
436
  temp_dir_intermediate = None
437
  output_filename = None
438
  permanent_path = None
 
445
  video_final = None
446
  source_clips = []
447
  clips_to_concatenate = []
448
+
449
  try:
450
  if prompt_type == "Generar Guion con IA":
451
  guion = generate_script(input_text)
452
  else:
453
  guion = input_text.strip()
454
+
455
  logger.info(f"Guion final ({len(guion)} chars): '{guion[:100]}...'")
456
  if not guion.strip():
457
  logger.error("El guion resultante está vacío o solo contiene espacios.")
458
  raise ValueError("El guion está vacío.")
459
+
460
  temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
461
  logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
462
+
463
  logger.info("Generando audio de voz...")
464
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
465
+
466
  tts_voices_to_try = [selected_voice]
467
  fallback_juan = "es-ES-JuanNeural"
468
  fallback_elvira = "es-ES-ElviraNeural"
 
470
  tts_voices_to_try.append(fallback_juan)
471
  if fallback_elvira and fallback_elvira != selected_voice and fallback_elvira not in tts_voices_to_try:
472
  tts_voices_to_try.append(fallback_elvira)
473
+
474
  tts_success = False
475
  tried_voices = set()
476
  for current_voice in tts_voices_to_try:
 
486
  except Exception as e:
487
  logger.warning(f"Fallo al generar TTS con voz '{current_voice}': {str(e)}", exc_info=True)
488
  pass
489
+
490
  if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
491
  logger.error("Fallo en la generación de voz después de todos los intentos. Archivo de audio no creado o es muy pequeño.")
492
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
493
+
494
  temp_intermediate_files.append(voz_path)
495
  audio_tts_original = AudioFileClip(voz_path)
496
  if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
 
508
  if voz_path in temp_intermediate_files:
509
  temp_intermediate_files.remove(voz_path)
510
  raise ValueError("Audio de voz generado es inválido después de procesamiento inicial.")
511
+
512
  audio_tts = audio_tts_original
513
  audio_duration = audio_tts_original.duration
514
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
515
  if audio_duration < 1.0:
516
  logger.error(f"Duración audio voz ({audio_duration:.2f}s) es muy corta.")
517
  raise ValueError("Generated voice audio is too short (min 1 second required).")
518
+
519
  logger.info("Extrayendo palabras clave...")
520
  try:
521
  keywords = extract_visual_keywords_from_script(guion)
 
523
  except Exception as e:
524
  logger.error(f"Error extrayendo keywords: {str(e)}", exc_info=True)
525
  keywords = ["naturaleza", "paisaje"]
526
+
527
  if not keywords:
528
  keywords = ["video", "background"]
529
+
530
  logger.info("Buscando videos en Pexels...")
531
  videos_data = []
532
  total_desired_videos = 10
533
  per_page_per_keyword = max(1, total_desired_videos // len(keywords))
534
+
535
  for keyword in keywords:
536
  if len(videos_data) >= total_desired_videos:
537
  break
 
542
  logger.info(f"Encontrados {len(videos)} videos para '{keyword}'. Total data: {len(videos_data)}")
543
  except Exception as e:
544
  logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
545
+
546
  if len(videos_data) < total_desired_videos / 2:
547
  logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
548
  generic_keywords = ["mystery", "alien", "ufo", "conspiracy", "paranormal", "supernatural", "horror", "fear", "suspense", "secret", "government", "cover_up", "simulation", "matrix", "apocalypse", "dystopian", "shadow", "occult", "unexplained", "creepy", "extraterrestrial", "abduction", "experiment", "secret_society", "illuminati", "new_world_order", "ancient_aliens", "ufo_sighting", "cryptid", "bigfoot", "loch_ness", "ghost", "haunting", "spirit", "demon", "possession", "exorcism", "witchcraft", "ritual", "cursed", "urban_legend", "myth", "legend", "folklore", "scary", "terror", "panic", "anxiety", "dread", "nightmare", "dark", "gloomy", "fog", "haunted", "cemetery", "asylum", "abandoned", "ruins", "underground", "tunnel", "bunker", "lab", "experiment", "government_secret", "mind_control", "brainwash", "propaganda", "surveillance", "spy", "whistleblower", "leak", "anonymous", "hack", "cyber", "virtual_reality", "ai", "artificial_intelligence", "robot", "cyborg", "apocalyptic", "post_apocalyptic", "zombie", "outbreak", "pandemic", "contagion", "biohazard", "radiation", "nuclear", "doomsday", "armageddon", "revelation", "prophecy", "symbolism", "hidden_meaning", "enigma", "puzzle", "code", "cipher", "mysterious", "unidentified", "anomaly", "glitch", "time_travel", "parallel_universe", "dimension", "portal"]
549
+
550
  for keyword in generic_keywords:
551
  if len(videos_data) >= total_desired_videos:
552
  break
 
557
  logger.info(f"Encontrados {len(videos)} videos para '{keyword}' (genérico). Total data: {len(videos_data)}")
558
  except Exception as e:
559
  logger.warning(f"Error buscando videos genéricos para '{keyword}': {str(e)}")
560
+
561
  if not videos_data:
562
  logger.error("No se encontraron videos en Pexels para ninguna palabra clave.")
563
  raise ValueError("No se encontraron videos adecuados en Pexels.")
564
+
565
  video_paths = []
566
  logger.info(f"Intentando descargar {len(videos_data)} videos encontrados...")
567
+
568
  for video in videos_data:
569
  if 'video_files' not in video or not video['video_files']:
570
  logger.debug(f"Saltando video sin archivos de video: {video.get('id')}")
571
  continue
572
+
573
  try:
574
  best_quality = None
575
  for vf in sorted(video['video_files'], key=lambda x: x.get('width', 0) * x.get('height', 0), reverse=True):
576
  if 'link' in vf:
577
  best_quality = vf
578
  break
579
+
580
  if best_quality and 'link' in best_quality:
581
  path = download_video_file(best_quality['link'], temp_dir_intermediate)
582
  if path:
 
589
  logger.warning(f"No se encontró enlace de descarga válido para video {video.get('id')}.")
590
  except Exception as e:
591
  logger.warning(f"Error procesando/descargando video {video.get('id')}: {str(e)}")
592
+
593
  logger.info(f"Descargados {len(video_paths)} archivos de video utilizables.")
594
  if not video_paths:
595
  logger.error("No se pudo descargar ningún archivo de video utilizable.")
596
  raise ValueError("No se pudo descargar ningún video utilizable de Pexels.")
597
+
598
  logger.info("Procesando y concatenando videos descargados...")
599
  current_duration = 0
600
  min_clip_duration = 0.5
601
  max_clip_segment = 10.0
602
+
603
  for i, path in enumerate(video_paths):
604
  if current_duration >= audio_duration + max_clip_segment:
605
  logger.debug(f"Video base suficiente ({current_duration:.1f}s >= {audio_duration:.1f}s + {max_clip_segment:.1f}s buffer). Dejando de procesar clips fuente restantes.")
606
  break
607
+
608
  clip = None
609
  try:
610
  logger.debug(f"[{i+1}/{len(video_paths)}] Abriendo clip: {path}")
611
  clip = VideoFileClip(path)
612
  source_clips.append(clip)
613
+
614
  if clip.reader is None or clip.duration is None or clip.duration <= 0:
615
  logger.warning(f"[{i+1}/{len(video_paths)}] Clip fuente {path} parece inválido (reader is None o duración <= 0). Saltando.")
616
  continue
617
+
618
  remaining_needed = audio_duration - current_duration
619
  potential_use_duration = min(clip.duration, max_clip_segment)
620
+
621
  if remaining_needed > 0:
622
  segment_duration = min(potential_use_duration, remaining_needed + min_clip_duration)
623
  segment_duration = max(min_clip_duration, segment_duration)
624
  segment_duration = min(segment_duration, clip.duration)
625
+
626
  if segment_duration >= min_clip_duration:
627
  try:
628
  sub = clip.subclip(0, segment_duration)
 
633
  except:
634
  pass
635
  continue
636
+
637
  clips_to_concatenate.append(sub)
638
  current_duration += sub.duration
639
  logger.debug(f"[{i+1}/{len(video_paths)}] Segmento añadido: {sub.duration:.1f}s (total video: {current_duration:.1f}/{audio_duration:.1f}s)")
 
647
  except Exception as e:
648
  logger.warning(f"[{i+1}/{len(video_paths)}] Error procesando video {path}: {str(e)}", exc_info=True)
649
  continue
650
+
651
  logger.info(f"Procesamiento de clips fuente finalizado. Se obtuvieron {len(clips_to_concatenate)} segmentos válidos.")
652
  if not clips_to_concatenate:
653
  logger.error("No hay segmentos de video válidos disponibles para crear la secuencia.")
654
  raise ValueError("No hay segmentos de video válidos disponibles para crear el video.")
655
+
656
  logger.info(f"Concatenando {len(clips_to_concatenate)} segmentos de video.")
657
  concatenated_base = None
658
+
659
  try:
660
  concatenated_base = concatenate_videoclips(clips_to_concatenate, method="chain")
661
  logger.info(f"Duración video base después de concatenación inicial: {concatenated_base.duration:.2f}s")
 
672
  except:
673
  pass
674
  clips_to_concatenate = []
675
+
676
  video_base = concatenated_base
677
  final_video_base = video_base
678
+
679
  if final_video_base.duration < audio_duration:
680
  logger.info(f"Video base ({final_video_base.duration:.2f}s) es más corto que el audio ({audio_duration:.2f}s). Repitiendo...")
681
  num_full_repeats = int(audio_duration // final_video_base.duration)
682
  remaining_duration = audio_duration % final_video_base.duration
683
+
684
  repeated_clips_list = [final_video_base] * num_full_repeats
685
  if remaining_duration > 0:
686
  try:
 
696
  logger.debug(f"Añadiendo segmento restante: {remaining_duration:.2f}s")
697
  except Exception as e:
698
  logger.warning(f"Error creando subclip para duración restante {remaining_duration:.2f}s: {str(e)}")
699
+
700
  if repeated_clips_list:
701
  logger.info(f"Concatenando {len(repeated_clips_list)} partes para repetición.")
702
  video_base_repeated = None
 
706
  if video_base_repeated is None or video_base_repeated.duration is None or video_base_repeated.duration <= 0:
707
  logger.critical("Video base repetido concatenado es inválido.")
708
  raise ValueError("Fallo al crear video base repetido válido.")
709
+
710
  if final_video_base is not video_base_repeated:
711
  try:
712
  final_video_base.close()
 
724
  clip.close()
725
  except:
726
  pass
727
+
728
  if final_video_base.duration > audio_duration:
729
  logger.info(f"Recortando video base ({final_video_base.duration:.2f}s) para que coincida con la duración del audio ({audio_duration:.2f}s).")
730
  trimmed_video_base = None
 
733
  if trimmed_video_base is None or trimmed_video_base.duration is None or trimmed_video_base.duration <= 0:
734
  logger.critical("Video base recortado es inválido.")
735
  raise ValueError("Fallo al crear video base recortado válido.")
736
+
737
  if final_video_base is not trimmed_video_base:
738
  try:
739
  final_video_base.close()
 
743
  except Exception as e:
744
  logger.critical(f"Error durante el recorte: {str(e)}", exc_info=True)
745
  raise ValueError("Fallo durante el recorte de video.")
746
+
747
  if final_video_base is None or final_video_base.duration is None or final_video_base.duration <= 0:
748
  logger.critical("Video base final es inválido antes de audio/escritura (None o duración cero).")
749
  raise ValueError("Video base final es inválido.")
750
+
751
  if final_video_base.size is None or final_video_base.size[0] <= 0 or final_video_base.size[1] <= 0:
752
  logger.critical(f"Video base final tiene tamaño inválido: {final_video_base.size}. No se puede escribir video.")
753
  raise ValueError("Video base final tiene tamaño inválido antes de escribir.")
754
+
755
  video_base = final_video_base
756
+
757
  logger.info("Procesando audio...")
758
  final_audio = audio_tts_original
759
  musica_audio_looped = None
760
+
761
  if musica_file:
762
  musica_audio_original = None
763
  try:
 
765
  shutil.copyfile(musica_file, music_path)
766
  temp_intermediate_files.append(music_path)
767
  logger.info(f"Música de fondo copiada a: {music_path}")
768
+
769
  musica_audio_original = AudioFileClip(music_path)
770
  if musica_audio_original.reader is None or musica_audio_original.duration is None or musica_audio_original.duration <= 0:
771
  logger.warning("Clip de música de fondo parece inválido o tiene duración cero. Saltando música.")
 
777
  else:
778
  musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
779
  logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
780
+
781
  if musica_audio_looped is None or musica_audio_looped.duration is None or musica_audio_looped.duration <= 0:
782
  logger.warning("Clip de música de fondo loopeado es inválido. Saltando música.")
783
  try:
 
785
  except:
786
  pass
787
  musica_audio_looped = None
788
+
789
  if musica_audio_looped:
790
  composite_audio = CompositeAudioClip([
791
  musica_audio_looped.volumex(0.2),
792
  audio_tts_original.volumex(1.0)
793
  ])
794
+
795
  if composite_audio.duration is None or composite_audio.duration <= 0:
796
  logger.warning("Clip de audio compuesto es inválido (None o duración cero). Usando solo audio de voz.")
797
  try:
 
808
  final_audio = audio_tts_original
809
  musica_audio = None
810
  logger.warning("Usando solo audio de voz debido a un error con la música.")
811
+
812
  if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
813
  logger.warning(f"Duración del audio final ({final_audio.duration:.2f}s) difiere significativamente del video base ({video_base.duration:.2f}s). Intentando recorte.")
814
  try:
 
830
  logger.warning("Audio final recortado para que coincida con la duración del video.")
831
  except Exception as e:
832
  logger.warning(f"Error ajustando duración del audio final: {str(e)}")
833
+
834
  video_final = video_base.set_audio(final_audio)
835
+
836
  output_filename = f"video_{int(time.time())}.mp4"
837
  output_path = os.path.join(temp_dir_intermediate, output_filename)
838
  permanent_path = f"/tmp/{output_filename}"
839
+
840
  video_final.write_videofile(
841
  output_path,
842
  fps=24,
 
850
  ],
851
  logger='bar'
852
  )
853
+
854
  try:
855
  shutil.copy(output_path, permanent_path)
856
  logger.info(f"Video guardado permanentemente en: {permanent_path}")
857
  except Exception as move_error:
858
  logger.error(f"Error moviendo archivo: {str(move_error)}. Usando path original.")
859
  permanent_path = output_path
860
+
861
  try:
862
  video_final.close()
863
  if 'video_base' in locals() and video_base is not None and video_base is not video_final:
864
  video_base.close()
865
  except Exception as close_error:
866
  logger.error(f"Error cerrando clips: {str(close_error)}")
867
+
868
  total_time = (datetime.now() - start_time).total_seconds()
869
  logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {permanent_path} | Tiempo total: {total_time:.2f}s")
870
  return permanent_path
871
+
872
  except ValueError as ve:
873
  logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
874
  raise ve
 
877
  raise e
878
  finally:
879
  logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
880
+
881
  for clip in source_clips:
882
  try:
883
  clip.close()
884
  except Exception as e:
885
  logger.warning(f"Error cerrando clip de video fuente en finally: {str(e)}")
886
+
887
  for clip_segment in clips_to_concatenate:
888
  try:
889
  clip_segment.close()
890
  except Exception as e:
891
  logger.warning(f"Error cerrando segmento de video en finally: {str(e)}")
892
+
893
  if musica_audio is not None:
894
  try:
895
  musica_audio.close()
896
  except Exception as e:
897
  logger.warning(f"Error cerrando musica_audio (procesada) en finally: {str(e)}")
898
+
899
  if musica_audio_original is not None and musica_audio_original is not musica_audio:
900
  try:
901
  musica_audio_original.close()
902
  except Exception as e:
903
  logger.warning(f"Error cerrando musica_audio_original en finally: {str(e)}")
904
+
905
  if audio_tts is not None and audio_tts is not audio_tts_original:
906
  try:
907
  audio_tts.close()
908
  except Exception as e:
909
  logger.warning(f"Error cerrando audio_tts (procesada) en finally: {str(e)}")
910
+
911
  if audio_tts_original is not None:
912
  try:
913
  audio_tts_original.close()
914
  except Exception as e:
915
  logger.warning(f"Error cerrando audio_tts_original en finalmente: {str(e)}")
916
+
917
  if video_final is not None:
918
  try:
919
  video_final.close()
 
924
  video_base.close()
925
  except Exception as e:
926
  logger.warning(f"Error cerrando video_base en finally: {str(e)}")
927
+
928
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
929
  final_output_in_temp = None
930
  if output_filename:
931
  final_output_in_temp = os.path.join(temp_dir_intermediate, output_filename)
932
+
933
  for path in temp_intermediate_files:
934
  try:
935
  if os.path.isfile(path) and path != final_output_in_temp and path != permanent_path:
 
939
  logger.debug(f"Saltando eliminación del archivo de video final: {path}")
940
  except Exception as e:
941
  logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
942
+
943
  logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
944
 
945
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
946
  logger.info("="*80)
947
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
948
+
949
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
950
  output_video = None
951
  output_file = None
952
  status_msg = gr.update(value="⏳ Procesando...", interactive=False)
953
+
954
  if not input_text or not input_text.strip():
955
  logger.warning("Texto de entrada vacío.")
956
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
957
+
958
  voice_ids_disponibles = [v[1] for v in AVAILABLE_VOICES]
959
  if selected_voice not in voice_ids_disponibles:
960
  logger.warning(f"Voz seleccionada inválida o no encontrada en la lista: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE_ID}.")
961
  selected_voice = DEFAULT_VOICE_ID
962
  else:
963
  logger.info(f"Voz seleccionada validada: {selected_voice}")
964
+
965
  logger.info(f"Tipo de entrada: {prompt_type}")
966
  logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
967
+
968
  if musica_file:
969
  logger.info(f"Archivo de música recibido: {musica_file}")
970
  else:
971
  logger.info("No se proporcionó archivo de música.")
972
+
973
  logger.info(f"Voz final a usar (ID): {selected_voice}")
974
+
975
  try:
976
  logger.info("Llamando a crear_video...")
977
  video_path = crear_video(prompt_type, input_text, selected_voice, musica_file)
978
+
979
  if video_path and os.path.exists(video_path):
980
  logger.info(f"crear_video retornó path: {video_path}")
981
  logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
 
985
  else:
986
  logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
987
  status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
988
+
989
  except ValueError as ve:
990
  logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
991
  status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
992
+
993
  except Exception as e:
994
  logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
995
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
996
+
997
  finally:
998
  logger.info("Fin del handler run_app.")
999
  return output_video, output_file, status_msg
 
1002
  .gradio-container {max-width: 800px; margin: auto;}
1003
  h1 {text-align: center;}
1004
  """) as app:
1005
+
1006
  gr.Markdown("# 🎬 Generador Automático de Videos con IA")
1007
  gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
1008
+
1009
  with gr.Row():
1010
  with gr.Column():
1011
  prompt_type = gr.Radio(
 
1013
  label="Método de Entrada",
1014
  value="Generar Guion con IA"
1015
  )
1016
+
1017
  with gr.Column(visible=True) as ia_guion_column:
1018
  prompt_ia = gr.Textbox(
1019
  label="Tema para IA",
 
1022
  max_lines=4,
1023
  value=""
1024
  )
1025
+
1026
  with gr.Column(visible=False) as manual_guion_column:
1027
  prompt_manual = gr.Textbox(
1028
  label="Tu Guion Completo",
 
1031
  max_lines=10,
1032
  value=""
1033
  )
1034
+
1035
  musica_input = gr.Audio(
1036
  label="Música de fondo (opcional)",
1037
  type="filepath",
1038
  interactive=True,
1039
  value=None
1040
  )
1041
+
1042
  voice_dropdown = gr.Dropdown(
1043
  label="Seleccionar Voz para Guion",
1044
  choices=AVAILABLE_VOICES,
1045
  value=DEFAULT_VOICE_ID,
1046
  interactive=True
1047
  )
1048
+
1049
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
1050
+
1051
  with gr.Column():
1052
  video_output = gr.Video(
1053
  label="Previsualización del Video Generado",
1054
  interactive=False,
1055
  height=400
1056
  )
1057
+
1058
  file_output = gr.File(
1059
  label="Descargar Archivo de Video",
1060
  interactive=False,
1061
  visible=False
1062
  )
1063
+
1064
  status_output = gr.Textbox(
1065
  label="Estado",
1066
  interactive=False,
 
1068
  placeholder="Esperando acción...",
1069
  value="Esperando entrada..."
1070
  )
1071
+
1072
  prompt_type.change(
1073
  lambda x: (gr.update(visible=x == "Generar Guion con IA"),
1074
  gr.update(visible=x == "Usar Mi Guion")),
1075
  inputs=prompt_type,
1076
  outputs=[ia_guion_column, manual_guion_column]
1077
  )
1078
+
1079
  generate_btn.click(
1080
  lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
1081
  outputs=[video_output, file_output, status_output],
 
1089
  inputs=[video_output, file_output, status_output],
1090
  outputs=[file_output]
1091
  )
1092
+
1093
  gr.Markdown("### Instrucciones:")
1094
  gr.Markdown("""
1095
  1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
 
1101
  7. La previsualización aparecerá si es posible, y siempre un enlace **Descargar Archivo de Video** se mostrará si la generación fue exitosa.
1102
  8. Revisa `video_generator_full.log` para detalles si hay errores.
1103
  """)
1104
+
1105
  gr.Markdown("---")
1106
  gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
1107
 
 
1117
  logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
1118
  except Exception as e:
1119
  logger.critical(f"Fallo al importar MoviePy. Asegúrate de que está instalado. Error: {e}", exc_info=True)
1120
+
1121
  os.environ['GRADIO_SERVER_TIMEOUT'] = '1200'
1122
  logger.info("Iniciando aplicación Gradio...")
1123
  try: