gnosticdev commited on
Commit
66a8f1d
·
verified ·
1 Parent(s): 544bfec

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +124 -193
app.py CHANGED
@@ -15,6 +15,7 @@ import math
15
  import shutil
16
  import json
17
  from collections import Counter
 
18
 
19
  # Configuración de logging
20
  logging.basicConfig(
@@ -30,32 +31,28 @@ logger.info("="*80)
30
  logger.info("INICIO DE EJECUCIÓN - GENERADOR DE VIDEOS")
31
  logger.info("="*80)
32
 
33
- # Función para obtener voces disponibles dinámicamente
34
- async def get_available_voices():
35
- try:
36
- voices = await edge_tts.list_voices()
37
- # Filtrar solo voces en español
38
- es_voices = [v for v in voices if v['Locale'].startswith('es-')]
39
- return {f"{v['ShortName']} - {v['Locale']} ({v['Gender']})": v['ShortName'] for v in es_voices}
40
- except Exception as e:
41
- logger.error(f"Error al obtener voces disponibles: {str(e)}")
42
- return {}
43
-
44
- # Obtener las voces disponibles
45
- AVAILABLE_VOICES = asyncio.run(get_available_voices())
46
- if not AVAILABLE_VOICES:
47
- logger.warning("No se pudieron obtener voces dinámicamente. Usando voces predefinidas.")
48
- VOCES_PREDEFINIDAS = {
49
- "es-ES-JuanNeural": "Juan (España) - Masculino",
50
- "es-ES-ElviraNeural": "Elvira (España) - Femenino",
51
- "es-MX-JorgeNeural": "Jorge (México) - Masculino",
52
- "es-MX-DaliaNeural": "Dalia (México) - Femenino",
53
- }
54
- AVAILABLE_VOICES = {v: k for k, v in VOCES_PREDEFINIDAS.items()}
55
-
56
- # Establecer una voz por defecto
57
- DEFAULT_VOICE_ID = "es-ES-JuanNeural" if "es-ES-JuanNeural" in AVAILABLE_VOICES.values() else list(AVAILABLE_VOICES.values())[0]
58
- DEFAULT_VOICE_NAME = next((k for k, v in AVAILABLE_VOICES.items() if v == DEFAULT_VOICE_ID), list(AVAILABLE_VOICES.keys())[0])
59
  logger.info(f"Voz por defecto seleccionada: {DEFAULT_VOICE_NAME} ({DEFAULT_VOICE_ID})")
60
 
61
  # Clave API de Pexels
@@ -63,20 +60,27 @@ PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
63
  if not PEXELS_API_KEY:
64
  logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
65
 
66
- # Inicialización de modelos
67
  MODEL_NAME = "datificate/gpt2-small-spanish"
68
  logger.info(f"Inicializando modelo GPT-2: {MODEL_NAME}")
69
  tokenizer = None
70
  model = None
71
- try:
72
- tokenizer = GPT2Tokenizer.from_pretrained(MODEL_NAME)
73
- model = GPT2LMHeadModel.from_pretrained(MODEL_NAME).eval()
74
- if tokenizer.pad_token is None:
75
- tokenizer.pad_token = tokenizer.eos_token
76
- logger.info(f"Modelo GPT-2 cargado | Vocabulario: {len(tokenizer)} tokens")
77
- except Exception as e:
78
- logger.error(f"FALLA CRÍTICA al cargar GPT-2: {str(e)}", exc_info=True)
79
- tokenizer = model = None
 
 
 
 
 
 
 
80
 
81
  logger.info("Cargando modelo KeyBERT...")
82
  kw_model = None
@@ -94,35 +98,32 @@ def buscar_videos_pexels(query, api_key, per_page=5):
94
 
95
  logger.debug(f"Buscando en Pexels: '{query}' | Resultados: {per_page}")
96
  headers = {"Authorization": api_key}
97
- try:
98
- params = {
99
- "query": query,
100
- "per_page": per_page,
101
- "orientation": "landscape",
102
- "size": "medium"
103
- }
104
-
105
- response = requests.get(
106
- "https://api.pexels.com/videos/search",
107
- headers=headers,
108
- params=params,
109
- timeout=20
110
- )
111
- response.raise_for_status()
112
-
113
- data = response.json()
114
- videos = data.get('videos', [])
115
- logger.info(f"Pexels: {len(videos)} videos encontrados para '{query}'")
116
- return videos
117
-
118
- except requests.exceptions.RequestException as e:
119
- logger.error(f"Error de conexión Pexels para '{query}': {str(e)}")
120
- except json.JSONDecodeError:
121
- logger.error(f"Pexels: JSON inválido recibido | Status: {response.status_code} | Respuesta: {response.text[:200]}...")
122
- except Exception as e:
123
- logger.error(f"Error inesperado Pexels para '{query}': {str(e)}", exc_info=True)
124
-
125
- return []
126
 
127
  def generate_script(prompt, max_length=150):
128
  logger.info(f"Generando guión | Prompt: '{prompt[:50]}...' | Longitud máxima: {max_length}")
@@ -230,31 +231,36 @@ def download_video_file(url, temp_dir):
230
  file_name = f"video_dl_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.mp4"
231
  output_path = os.path.join(temp_dir, file_name)
232
 
233
- with requests.get(url, stream=True, timeout=60) as r:
234
- r.raise_for_status()
235
- with open(output_path, 'wb') as f:
236
- for chunk in r.iter_content(chunk_size=8192):
237
- f.write(chunk)
238
-
239
- if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
240
- logger.info(f"Video descargado exitosamente: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
241
- return output_path
242
- else:
243
- logger.warning(f"Descarga parece incompleta o vacía para {url[:80]}... Archivo: {output_path} Tamaño: {os.path.getsize(output_path) if os.path.exists(output_path) else 'N/A'} bytes")
244
- if os.path.exists(output_path):
245
- os.remove(output_path)
246
- return None
 
 
 
 
 
 
 
 
 
247
 
248
- except requests.exceptions.RequestException as e:
249
- logger.error(f"Error de descarga para {url[:80]}... : {str(e)}")
250
  except Exception as e:
251
- logger.error(f"Error inesperado descargando {url[:80]}... : {str(e)}", exc_info=True)
252
-
253
- return None
254
 
255
  def loop_audio_to_length(audio_clip, target_duration):
256
  logger.debug(f"Ajustando audio | Duración actual: {audio_clip.duration:.2f}s | Objetivo: {target_duration:.2f}s")
257
-
258
  if audio_clip is None or audio_clip.duration is None or audio_clip.duration <= 0:
259
  logger.warning("Input audio clip is invalid (None or zero duration), cannot loop.")
260
  try:
@@ -276,7 +282,6 @@ def loop_audio_to_length(audio_clip, target_duration):
276
 
277
  loops = math.ceil(target_duration / audio_clip.duration)
278
  logger.debug(f"Creando {loops} loops de audio")
279
-
280
  audio_segments = [audio_clip] * loops
281
  looped_audio = None
282
  final_looped_audio = None
@@ -292,9 +297,8 @@ def loop_audio_to_length(audio_clip, target_duration):
292
  raise ValueError("Invalid final subclipped audio.")
293
 
294
  return final_looped_audio
295
-
296
  except Exception as e:
297
- logger.error(f"Error concatenating/subclipping audio clips for looping: {strrance(e)}", exc_info=True)
298
  try:
299
  if audio_clip.duration is not None and audio_clip.duration > 0:
300
  logger.warning("Returning original audio clip (may be too short).")
@@ -303,7 +307,6 @@ def loop_audio_to_length(audio_clip, target_duration):
303
  pass
304
  logger.error("Fallback to original audio clip failed.")
305
  return AudioFileClip(filename="")
306
-
307
  finally:
308
  if looped_audio is not None and looped_audio is not final_looped_audio:
309
  try: looped_audio.close()
@@ -323,7 +326,6 @@ def extract_visual_keywords_from_script(script_text):
323
  logger.debug("Intentando extracción con KeyBERT...")
324
  keywords1 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(1, 1), stop_words='spanish', top_n=5)
325
  keywords2 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(2, 2), stop_words='spanish', top_n=3)
326
-
327
  all_keywords = keywords1 + keywords2
328
  all_keywords.sort(key=lambda item: item[1], reverse=True)
329
 
@@ -339,7 +341,6 @@ def extract_visual_keywords_from_script(script_text):
339
  if keywords_list:
340
  logger.debug(f"Palabras clave extraídas por KeyBERT: {keywords_list}")
341
  return keywords_list
342
-
343
  except Exception as e:
344
  logger.warning(f"KeyBERT falló: {str(e)}. Intentando método simple.")
345
 
@@ -348,14 +349,12 @@ def extract_visual_keywords_from_script(script_text):
348
  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", "nuestro", "vuestro", "este", "ese", "aquel", "esta", "esa", "aquella", "esto", "eso", "aquello", "mis", "tus", "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á"}
349
 
350
  valid_words = [word for word in words if len(word) > 3 and word not in stop_words]
351
-
352
  if not valid_words:
353
  logger.warning("No se encontraron palabras clave válidas con método simple. Usando palabras clave predeterminadas.")
354
  return ["naturaleza", "ciudad", "paisaje"]
355
 
356
  word_counts = Counter(valid_words)
357
  top_keywords = [word.replace(" ", "+") for word, _ in word_counts.most_common(5)]
358
-
359
  if not top_keywords:
360
  logger.warning("El método simple no produjo keywords. Usando palabras clave predeterminadas.")
361
  return ["naturaleza", "ciudad", "paisaje"]
@@ -369,13 +368,14 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
369
  logger.debug(f"Input: '{input_text[:100]}...'")
370
  logger.info(f"Voz seleccionada: {selected_voice}")
371
 
372
- if selected_voice not in AVAILABLE_VOICES.values():
 
 
373
  logger.warning(f"Voz seleccionada '{selected_voice}' no es válida. Usando voz por defecto: {DEFAULT_VOICE_ID}")
374
  selected_voice = DEFAULT_VOICE_ID
375
 
376
  start_time = datetime.now()
377
  temp_dir_intermediate = None
378
-
379
  audio_tts_original = None
380
  musica_audio_original = None
381
  audio_tts = None
@@ -384,6 +384,7 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
384
  video_final = None
385
  source_clips = []
386
  clips_to_concatenate = []
 
387
 
388
  try:
389
  if prompt_type == "Generar Guion con IA":
@@ -392,37 +393,23 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
392
  guion = input_text.strip()
393
 
394
  logger.info(f"Guion final ({len(guion)} chars): '{guion[:100]}...'")
395
-
396
  if not guion.strip():
397
  logger.error("El guion resultante está vacío o solo contiene espacios.")
398
  raise ValueError("El guion está vacío.")
399
 
400
  temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
401
  logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
402
- temp_intermediate_files = []
403
 
404
  logger.info("Generando audio de voz...")
405
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
406
-
407
- tts_success = False
408
- voices_to_try = [selected_voice, DEFAULT_VOICE_ID]
409
-
410
- for voice in voices_to_try:
411
- if not voice: continue
412
- logger.info(f"Intentando TTS con voz: {voice}...")
413
- tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=voice))
414
- if tts_success:
415
- break
416
- else:
417
- logger.warning(f"Fallo al generar TTS con voz '{voice}'.")
418
-
419
  if not tts_success:
420
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
421
 
422
  temp_intermediate_files.append(voz_path)
423
-
424
  audio_tts_original = AudioFileClip(voz_path)
425
-
426
  if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
427
  logger.critical("Clip de audio TTS inicial es inválido.")
428
  try: audio_tts_original.close()
@@ -432,7 +419,6 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
432
  audio_tts = audio_tts_original
433
  audio_duration = audio_tts_original.duration
434
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
435
-
436
  if audio_duration < 1.0:
437
  logger.error(f"Duración audio voz ({audio_duration:.2f}s) es muy corta.")
438
  raise ValueError("Generated voice audio is too short.")
@@ -452,7 +438,6 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
452
  videos_data = []
453
  total_desired_videos = 10
454
  per_page_per_keyword = max(1, total_desired_videos // len(keywords))
455
-
456
  for keyword in keywords:
457
  if len(videos_data) >= total_desired_videos: break
458
  try:
@@ -484,14 +469,12 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
484
  if 'video_files' not in video or not video['video_files']:
485
  logger.debug(f"Saltando video sin archivos de video: {video.get('id')}")
486
  continue
487
-
488
  try:
489
  best_quality = None
490
  for vf in sorted(video['video_files'], key=lambda x: x.get('width', 0) * x.get('height', 0), reverse=True):
491
  if 'link' in vf:
492
  best_quality = vf
493
  break
494
-
495
  if best_quality and 'link' in best_quality:
496
  path = download_video_file(best_quality['link'], temp_dir_intermediate)
497
  if path:
@@ -509,30 +492,24 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
509
  current_duration = 0
510
  min_clip_duration = 0.5
511
  max_clip_segment = 10.0
512
-
513
  for i, path in enumerate(video_paths):
514
  if current_duration >= audio_duration + max_clip_segment:
515
  logger.debug(f"Video base suficiente.")
516
  break
517
-
518
  clip = None
519
  try:
520
  logger.debug(f"[{i+1}/{len(video_paths)}] Abriendo clip: {path}")
521
  clip = VideoFileClip(path)
522
  source_clips.append(clip)
523
-
524
  if clip.reader is None or clip.duration is None or clip.duration <= 0:
525
  logger.warning(f"[{i+1}/{len(video_paths)}] Clip fuente {path} parece inválido.")
526
  continue
527
-
528
  remaining_needed = audio_duration - current_duration
529
  potential_use_duration = min(clip.duration, max_clip_segment)
530
-
531
  if remaining_needed > 0:
532
  segment_duration = min(potential_use_duration, remaining_needed + min_clip_duration)
533
  segment_duration = max(min_clip_duration, segment_duration)
534
  segment_duration = min(segment_duration, clip.duration)
535
-
536
  if segment_duration >= min_clip_duration:
537
  try:
538
  sub = clip.subclip(0, segment_duration)
@@ -541,19 +518,16 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
541
  try: sub.close()
542
  except: pass
543
  continue
544
-
545
  clips_to_concatenate.append(sub)
546
  current_duration += sub.duration
547
  except Exception as sub_e:
548
  logger.warning(f"[{i+1}/{len(video_paths)}] Error creando subclip: {str(sub_e)}")
549
  continue
550
-
551
  except Exception as e:
552
  logger.warning(f"[{i+1}/{len(video_paths)}] Error procesando video {path}: {str(e)}", exc_info=True)
553
  continue
554
 
555
  logger.info(f"Procesamiento de clips fuente finalizado. Se obtuvieron {len(clips_to_concatenate)} segmentos válidos.")
556
-
557
  if not clips_to_concatenate:
558
  logger.error("No hay segmentos de video válidos disponibles.")
559
  raise ValueError("No hay segmentos de video válidos.")
@@ -563,11 +537,9 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
563
  try:
564
  concatenated_base = concatenate_videoclips(clips_to_concatenate, method="chain")
565
  logger.info(f"Duración video base después de concatenación inicial: {concatenated_base.duration:.2f}s")
566
-
567
  if concatenated_base is None or concatenated_base.duration is None or concatenated_base.duration <= 0:
568
  logger.critical("Video base concatenado es inválido.")
569
  raise ValueError("Fallo al crear video base válido.")
570
-
571
  except Exception as e:
572
  logger.critical(f"Error durante la concatenación inicial: {str(e)}", exc_info=True)
573
  raise ValueError("Fallo durante la concatenación de video inicial.")
@@ -579,12 +551,10 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
579
 
580
  video_base = concatenated_base
581
  final_video_base = video_base
582
-
583
  if final_video_base.duration < audio_duration:
584
  logger.info(f"Video base ({final_video_base.duration:.2f}s) es más corto que el audio ({audio_duration:.2f}s). Repitiendo...")
585
  num_full_repeats = int(audio_duration // final_video_base.duration)
586
  remaining_duration = audio_duration % final_video_base.duration
587
-
588
  repeated_clips_list = [final_video_base] * num_full_repeats
589
  if remaining_duration > 0:
590
  try:
@@ -604,17 +574,13 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
604
  try:
605
  video_base_repeated = concatenate_videoclips(repeated_clips_list, method="chain")
606
  logger.info(f"Duración del video base repetido: {video_base_repeated.duration:.2f}s")
607
-
608
  if video_base_repeated is None or video_base_repeated.duration is None or video_base_repeated.duration <= 0:
609
  logger.critical("Video base repetido concatenado es inválido.")
610
  raise ValueError("Fallo al crear video base repetido válido.")
611
-
612
  if final_video_base is not video_base_repeated:
613
  try: final_video_base.close()
614
  except: pass
615
-
616
  final_video_base = video_base_repeated
617
-
618
  except Exception as e:
619
  logger.critical(f"Error durante la concatenación de repetición: {str(e)}", exc_info=True)
620
  raise ValueError("Fallo durante la repetición de video.")
@@ -632,13 +598,10 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
632
  if trimmed_video_base is None or trimmed_video_base.duration is None or trimmed_video_base.duration <= 0:
633
  logger.critical("Video base recortado es inválido.")
634
  raise ValueError("Fallo al crear video base recortado válido.")
635
-
636
  if final_video_base is not trimmed_video_base:
637
  try: final_video_base.close()
638
  except: pass
639
-
640
  final_video_base = trimmed_video_base
641
-
642
  except Exception as e:
643
  logger.critical(f"Error durante el recorte: {str(e)}", exc_info=True)
644
  raise ValueError("Fallo durante el recorte de video.")
@@ -652,11 +615,9 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
652
  raise ValueError("Video base final tiene tamaño inválido.")
653
 
654
  video_base = final_video_base
655
-
656
  logger.info("Procesando audio...")
657
  final_audio = audio_tts_original
658
  musica_audio_looped = None
659
-
660
  if musica_file:
661
  musica_audio_original = None
662
  try:
@@ -664,9 +625,7 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
664
  shutil.copyfile(musica_file, music_path)
665
  temp_intermediate_files.append(music_path)
666
  logger.info(f"Música de fondo copiada a: {music_path}")
667
-
668
  musica_audio_original = AudioFileClip(music_path)
669
-
670
  if musica_audio_original.reader is None or musica_audio_original.duration is None or musica_audio_original.duration <= 0:
671
  logger.warning("Clip de música de fondo parece inválido.")
672
  try: musica_audio_original.close()
@@ -675,19 +634,16 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
675
  else:
676
  musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
677
  logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
678
-
679
  if musica_audio_looped is None or musica_audio_looped.duration is None or musica_audio_looped.duration <= 0:
680
  logger.warning("Clip de música de fondo loopeado es inválido.")
681
  try: musica_audio_looped.close()
682
  except: pass
683
  musica_audio_looped = None
684
-
685
  if musica_audio_looped:
686
  composite_audio = CompositeAudioClip([
687
  musica_audio_looped.volumex(0.2),
688
  audio_tts_original.volumex(1.0)
689
  ])
690
-
691
  if composite_audio.duration is None or composite_audio.duration <= 0:
692
  logger.warning("Clip de audio compuesto es inválido.")
693
  try: composite_audio.close()
@@ -697,7 +653,6 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
697
  logger.info("Mezcla de audio completada (voz + música).")
698
  final_audio = composite_audio
699
  musica_audio = musica_audio_looped
700
-
701
  except Exception as e:
702
  logger.warning(f"Error procesando música de fondo: {str(e)}", exc_info=True)
703
  final_audio = audio_tts_original
@@ -723,7 +678,6 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
723
 
724
  logger.info("Renderizando video final...")
725
  video_final = video_base.set_audio(final_audio)
726
-
727
  if video_final is None or video_final.duration is None or video_final.duration <= 0:
728
  logger.critical("Clip de video final (con audio) es inválido.")
729
  raise ValueError("Clip de video final es inválido.")
@@ -731,9 +685,7 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
731
  output_filename = "final_video.mp4"
732
  output_path = os.path.join(temp_dir_intermediate, output_filename)
733
  logger.info(f"Escribiendo video final a: {output_path}")
734
-
735
  video_final.write_videofile(
736
- output_path,
737
  fps=24,
738
  threads=4,
739
  codec="libx264",
@@ -744,9 +696,7 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
744
 
745
  total_time = (datetime.now() - start_time).total_seconds()
746
  logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {output_path} | Tiempo total: {total_time:.2f}s")
747
-
748
  return output_path
749
-
750
  except ValueError as ve:
751
  logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
752
  raise ve
@@ -808,33 +758,24 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
808
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
809
  logger.info("="*80)
810
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
811
-
812
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
813
-
814
  output_video = None
815
  output_file = None
816
  status_msg = gr.update(value="⏳ Procesando...", interactive=False)
817
-
818
  if not input_text or not input_text.strip():
819
  logger.warning("Texto de entrada vacío.")
820
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
821
-
822
- if selected_voice not in AVAILABLE_VOICES.values():
 
 
823
  logger.warning(f"Voz seleccionada inválida: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE_ID}")
824
  selected_voice = DEFAULT_VOICE_ID
825
-
826
- logger.info(f"Tipo de entrada: {prompt_type}")
827
- logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
828
- if musica_file:
829
- logger.info(f"Archivo de música recibido: {musica_file}")
830
- else:
831
- logger.info("No se proporcionó archivo de música.")
832
  logger.info(f"Voz final a usar (ID): {selected_voice}")
833
 
834
  try:
835
  logger.info("Llamando a crear_video...")
836
  video_path = crear_video(prompt_type, input_text, selected_voice, musica_file)
837
-
838
  if video_path and os.path.exists(video_path):
839
  logger.info(f"crear_video retornó path: {video_path}")
840
  output_video = video_path
@@ -842,23 +783,20 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
842
  status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
843
  else:
844
  logger.error(f"crear_video no retornó un path válido: {video_path}")
845
- status_msg = gr.update(value="❌ Error: La generación del video falló.", interactive=False)
846
-
847
  except ValueError as ve:
848
- logger.warning(f"Error de validación: {str(ve)}")
849
  status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
850
  except Exception as e:
851
- logger.critical(f"Error crítico: {str(e)}", exc_info=True)
852
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
853
- finally:
854
- logger.info("Fin del handler run_app.")
855
- return output_video, output_file, status_msg
856
 
 
857
  with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
858
  .gradio-container {max-width: 800px; margin: auto;}
859
  h1 {text-align: center;}
860
  """) as app:
861
-
862
  gr.Markdown("# 🎬 Generador Automático de Videos con IA")
863
  gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
864
 
@@ -869,41 +807,36 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
869
  label="Método de Entrada",
870
  value="Generar Guion con IA"
871
  )
872
-
873
  with gr.Column(visible=True) as ia_guion_column:
874
  prompt_ia = gr.Textbox(
875
  label="Tema para IA",
876
  lines=2,
877
- placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer...",
878
  max_lines=4,
879
  value=""
880
  )
881
-
882
  with gr.Column(visible=False) as manual_guion_column:
883
  prompt_manual = gr.Textbox(
884
  label="Tu Guion Completo",
885
  lines=5,
886
- placeholder="Ej: En este video exploraremos los misterios del océano...",
887
  max_lines=10,
888
  value=""
889
  )
890
-
891
  musica_input = gr.Audio(
892
  label="Música de fondo (opcional)",
893
  type="filepath",
894
  interactive=True,
895
  value=None
896
  )
897
-
898
  voice_dropdown = gr.Dropdown(
899
  label="Seleccionar Voz para Guion",
900
- choices=list(AVAILABLE_VOICES.keys()),
901
- value=DEFAULT_VOICE_NAME,
902
  interactive=True
903
  )
904
-
905
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
906
-
907
  with gr.Column():
908
  video_output = gr.Video(
909
  label="Previsualización del Video Generado",
@@ -929,7 +862,6 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
929
  inputs=prompt_type,
930
  outputs=[ia_guion_column, manual_guion_column]
931
  )
932
-
933
  generate_btn.click(
934
  lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
935
  outputs=[video_output, file_output, status_output],
@@ -946,14 +878,14 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
946
 
947
  gr.Markdown("### Instrucciones:")
948
  gr.Markdown("""
949
- 1. **Clave API de Pexels:** Configura la variable de entorno `PEXELS_API_KEY`.
950
- 2. **Selecciona el tipo de entrada**: "Generar Guion con IA" o "Usar Mi Guion".
951
- 3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.).
952
- 4. **Selecciona la voz** deseada del desplegable.
953
- 5. **Haz clic en "✨ Generar Video"**.
954
- 6. Espera a que se procese el video. Verás el estado.
955
- 7. Descarga el video generado.
956
- 8. Revisa `video_generator_full.log` para detalles si hay errores.
957
  """)
958
  gr.Markdown("---")
959
  gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
@@ -965,12 +897,11 @@ if __name__ == "__main__":
965
  try:
966
  temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
967
  temp_clip.close()
968
- logger.info("Clips base de MoviePy creados y cerrados exitosamente.")
969
  except Exception as e:
970
- logger.critical(f"Fallo al crear clip base de MoviePy: {e}", exc_info=True)
971
  except Exception as e:
972
- logger.critical(f"Fallo al importar MoviePy: {e}", exc_info=True)
973
-
974
  logger.info("Iniciando aplicación Gradio...")
975
  try:
976
  app.launch(server_name="0.0.0.0", server_port=7860, share=False)
 
15
  import shutil
16
  import json
17
  from collections import Counter
18
+ import time
19
 
20
  # Configuración de logging
21
  logging.basicConfig(
 
31
  logger.info("INICIO DE EJECUCIÓN - GENERADOR DE VIDEOS")
32
  logger.info("="*80)
33
 
34
+ # CAMBIO: Lista estática de voces como en script 2
35
+ VOICES = {
36
+ "Español": [
37
+ "es-ES-JuanNeural", "es-ES-ElviraNeural", "es-MX-JorgeNeural", "es-MX-DaliaNeural",
38
+ "es-AR-TomasNeural", "es-AR-ElenaNeural", "es-CO-GonzaloNeural", "es-CO-SalomeNeural",
39
+ "es-CL-LorenzoNeural", "es-CL-CatalinaNeural", "es-PE-AlexNeural", "es-PE-CamilaNeural",
40
+ "es-VE-PaolaNeural", "es-VE-SebastianNeural", "es-US-AlonsoNeural", "es-US-PalomaNeural"
41
+ ]
42
+ }
43
+
44
+ # CAMBIO: Función para obtener las voces en formato de dropdown como en script 2
45
+ def get_voice_choices():
46
+ choices = []
47
+ for region, voice_list in VOICES.items():
48
+ for voice_id in voice_list:
49
+ choices.append((f"{voice_id} ({region})", voice_id))
50
+ return choices
51
+
52
+ # CAMBIO: Usamos lista estática en lugar de edge_tts.list_voices()
53
+ AVAILABLE_VOICES = get_voice_choices()
54
+ DEFAULT_VOICE_ID = "es-ES-JuanNeural"
55
+ DEFAULT_VOICE_NAME = next((name for name, vid in AVAILABLE_VOICES if vid == DEFAULT_VOICE_ID), DEFAULT_VOICE_ID)
 
 
 
 
56
  logger.info(f"Voz por defecto seleccionada: {DEFAULT_VOICE_NAME} ({DEFAULT_VOICE_ID})")
57
 
58
  # Clave API de Pexels
 
60
  if not PEXELS_API_KEY:
61
  logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
62
 
63
+ # Inicialización de modelos con reintentos (como en script 1b)
64
  MODEL_NAME = "datificate/gpt2-small-spanish"
65
  logger.info(f"Inicializando modelo GPT-2: {MODEL_NAME}")
66
  tokenizer = None
67
  model = None
68
+ MAX_RETRIES = 3
69
+ for attempt in range(MAX_RETRIES):
70
+ try:
71
+ tokenizer = GPT2Tokenizer.from_pretrained(MODEL_NAME, timeout=60)
72
+ model = GPT2LMHeadModel.from_pretrained(MODEL_NAME, timeout=60).eval()
73
+ if tokenizer.pad_token is None:
74
+ tokenizer.pad_token = tokenizer.eos_token
75
+ logger.info(f"Modelo GPT-2 cargado | Vocabulario: {len(tokenizer)} tokens")
76
+ break
77
+ except Exception as e:
78
+ logger.warning(f"Intento {attempt + 1} falló al cargar GPT-2: {str(e)}")
79
+ if attempt < MAX_RETRIES - 1:
80
+ time.sleep(5)
81
+ else:
82
+ logger.error(f"FALLA CRÍTICA al cargar GPT-2 después de {MAX_RETRIES} intentos: {str(e)}")
83
+ tokenizer = model = None
84
 
85
  logger.info("Cargando modelo KeyBERT...")
86
  kw_model = None
 
98
 
99
  logger.debug(f"Buscando en Pexels: '{query}' | Resultados: {per_page}")
100
  headers = {"Authorization": api_key}
101
+ for attempt in range(MAX_RETRIES):
102
+ try:
103
+ params = {
104
+ "query": query,
105
+ "per_page": per_page,
106
+ "orientation": "landscape",
107
+ "size": "medium"
108
+ }
109
+ response = requests.get(
110
+ "https://api.pexels.com/videos/search",
111
+ headers=headers,
112
+ params=params,
113
+ timeout=60
114
+ )
115
+ response.raise_for_status()
116
+ data = response.json()
117
+ videos = data.get('videos', [])
118
+ logger.info(f"Pexels: {len(videos)} videos encontrados para '{query}'")
119
+ return videos
120
+ except requests.exceptions.RequestException as e:
121
+ logger.warning(f"Intento {attempt + 1} falló en Pexels: {str(e)}")
122
+ if attempt < MAX_RETRIES - 1:
123
+ time.sleep(5)
124
+ else:
125
+ logger.error(f"Error de conexión Pexels para '{query}' después de {MAX_RETRIES} intentos: {str(e)}")
126
+ return []
 
 
 
127
 
128
  def generate_script(prompt, max_length=150):
129
  logger.info(f"Generando guión | Prompt: '{prompt[:50]}...' | Longitud máxima: {max_length}")
 
231
  file_name = f"video_dl_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.mp4"
232
  output_path = os.path.join(temp_dir, file_name)
233
 
234
+ for attempt in range(MAX_RETRIES):
235
+ try:
236
+ with requests.get(url, stream=True, timeout=60) as r:
237
+ r.raise_for_status()
238
+ with open(output_path, 'wb') as f:
239
+ for chunk in r.iter_content(chunk_size=8192):
240
+ f.write(chunk)
241
+
242
+ if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
243
+ logger.info(f"Video descargado exitosamente: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
244
+ return output_path
245
+ else:
246
+ logger.warning(f"Descarga parece incompleta o vacía para {url[:80]}...")
247
+ if os.path.exists(output_path):
248
+ os.remove(output_path)
249
+ return None
250
+ except requests.exceptions.RequestException as e:
251
+ logger.warning(f"Intento {attempt + 1} falló al descargar video: {str(e)}")
252
+ if attempt < MAX_RETRIES - 1:
253
+ time.sleep(5)
254
+ else:
255
+ logger.error(f"Error descargando video después de {MAX_RETRIES} intentos: {str(e)}")
256
+ return None
257
 
 
 
258
  except Exception as e:
259
+ logger.error(f"Error inesperado descargando {url[:80]}...: {str(e)}", exc_info=True)
260
+ return None
 
261
 
262
  def loop_audio_to_length(audio_clip, target_duration):
263
  logger.debug(f"Ajustando audio | Duración actual: {audio_clip.duration:.2f}s | Objetivo: {target_duration:.2f}s")
 
264
  if audio_clip is None or audio_clip.duration is None or audio_clip.duration <= 0:
265
  logger.warning("Input audio clip is invalid (None or zero duration), cannot loop.")
266
  try:
 
282
 
283
  loops = math.ceil(target_duration / audio_clip.duration)
284
  logger.debug(f"Creando {loops} loops de audio")
 
285
  audio_segments = [audio_clip] * loops
286
  looped_audio = None
287
  final_looped_audio = None
 
297
  raise ValueError("Invalid final subclipped audio.")
298
 
299
  return final_looped_audio
 
300
  except Exception as e:
301
+ logger.error(f"Error concatenating/subclipping audio clips for looping: {str(e)}", exc_info=True)
302
  try:
303
  if audio_clip.duration is not None and audio_clip.duration > 0:
304
  logger.warning("Returning original audio clip (may be too short).")
 
307
  pass
308
  logger.error("Fallback to original audio clip failed.")
309
  return AudioFileClip(filename="")
 
310
  finally:
311
  if looped_audio is not None and looped_audio is not final_looped_audio:
312
  try: looped_audio.close()
 
326
  logger.debug("Intentando extracción con KeyBERT...")
327
  keywords1 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(1, 1), stop_words='spanish', top_n=5)
328
  keywords2 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(2, 2), stop_words='spanish', top_n=3)
 
329
  all_keywords = keywords1 + keywords2
330
  all_keywords.sort(key=lambda item: item[1], reverse=True)
331
 
 
341
  if keywords_list:
342
  logger.debug(f"Palabras clave extraídas por KeyBERT: {keywords_list}")
343
  return keywords_list
 
344
  except Exception as e:
345
  logger.warning(f"KeyBERT falló: {str(e)}. Intentando método simple.")
346
 
 
349
  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", "nuestro", "vuestro", "este", "ese", "aquel", "esta", "esa", "aquella", "esto", "eso", "aquello", "mis", "tus", "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á"}
350
 
351
  valid_words = [word for word in words if len(word) > 3 and word not in stop_words]
 
352
  if not valid_words:
353
  logger.warning("No se encontraron palabras clave válidas con método simple. Usando palabras clave predeterminadas.")
354
  return ["naturaleza", "ciudad", "paisaje"]
355
 
356
  word_counts = Counter(valid_words)
357
  top_keywords = [word.replace(" ", "+") for word, _ in word_counts.most_common(5)]
 
358
  if not top_keywords:
359
  logger.warning("El método simple no produjo keywords. Usando palabras clave predeterminadas.")
360
  return ["naturaleza", "ciudad", "paisaje"]
 
368
  logger.debug(f"Input: '{input_text[:100]}...'")
369
  logger.info(f"Voz seleccionada: {selected_voice}")
370
 
371
+ # CAMBIO: Validación simple de la voz como en script 2
372
+ voice_ids = [vid for _, vid in AVAILABLE_VOICES]
373
+ if selected_voice not in voice_ids:
374
  logger.warning(f"Voz seleccionada '{selected_voice}' no es válida. Usando voz por defecto: {DEFAULT_VOICE_ID}")
375
  selected_voice = DEFAULT_VOICE_ID
376
 
377
  start_time = datetime.now()
378
  temp_dir_intermediate = None
 
379
  audio_tts_original = None
380
  musica_audio_original = None
381
  audio_tts = None
 
384
  video_final = None
385
  source_clips = []
386
  clips_to_concatenate = []
387
+ temp_intermediate_files = []
388
 
389
  try:
390
  if prompt_type == "Generar Guion con IA":
 
393
  guion = input_text.strip()
394
 
395
  logger.info(f"Guion final ({len(guion)} chars): '{guion[:100]}...'")
 
396
  if not guion.strip():
397
  logger.error("El guion resultante está vacío o solo contiene espacios.")
398
  raise ValueError("El guion está vacío.")
399
 
400
  temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
401
  logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
 
402
 
403
  logger.info("Generando audio de voz...")
404
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
405
+ # CAMBIO: Usamos la voz seleccionada directamente, sin fallbacks complejos
406
+ logger.info(f"Intentando TTS con voz: {selected_voice}...")
407
+ tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=selected_voice))
 
 
 
 
 
 
 
 
 
 
408
  if not tts_success:
409
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
410
 
411
  temp_intermediate_files.append(voz_path)
 
412
  audio_tts_original = AudioFileClip(voz_path)
 
413
  if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
414
  logger.critical("Clip de audio TTS inicial es inválido.")
415
  try: audio_tts_original.close()
 
419
  audio_tts = audio_tts_original
420
  audio_duration = audio_tts_original.duration
421
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
 
422
  if audio_duration < 1.0:
423
  logger.error(f"Duración audio voz ({audio_duration:.2f}s) es muy corta.")
424
  raise ValueError("Generated voice audio is too short.")
 
438
  videos_data = []
439
  total_desired_videos = 10
440
  per_page_per_keyword = max(1, total_desired_videos // len(keywords))
 
441
  for keyword in keywords:
442
  if len(videos_data) >= total_desired_videos: break
443
  try:
 
469
  if 'video_files' not in video or not video['video_files']:
470
  logger.debug(f"Saltando video sin archivos de video: {video.get('id')}")
471
  continue
 
472
  try:
473
  best_quality = None
474
  for vf in sorted(video['video_files'], key=lambda x: x.get('width', 0) * x.get('height', 0), reverse=True):
475
  if 'link' in vf:
476
  best_quality = vf
477
  break
 
478
  if best_quality and 'link' in best_quality:
479
  path = download_video_file(best_quality['link'], temp_dir_intermediate)
480
  if path:
 
492
  current_duration = 0
493
  min_clip_duration = 0.5
494
  max_clip_segment = 10.0
 
495
  for i, path in enumerate(video_paths):
496
  if current_duration >= audio_duration + max_clip_segment:
497
  logger.debug(f"Video base suficiente.")
498
  break
 
499
  clip = None
500
  try:
501
  logger.debug(f"[{i+1}/{len(video_paths)}] Abriendo clip: {path}")
502
  clip = VideoFileClip(path)
503
  source_clips.append(clip)
 
504
  if clip.reader is None or clip.duration is None or clip.duration <= 0:
505
  logger.warning(f"[{i+1}/{len(video_paths)}] Clip fuente {path} parece inválido.")
506
  continue
 
507
  remaining_needed = audio_duration - current_duration
508
  potential_use_duration = min(clip.duration, max_clip_segment)
 
509
  if remaining_needed > 0:
510
  segment_duration = min(potential_use_duration, remaining_needed + min_clip_duration)
511
  segment_duration = max(min_clip_duration, segment_duration)
512
  segment_duration = min(segment_duration, clip.duration)
 
513
  if segment_duration >= min_clip_duration:
514
  try:
515
  sub = clip.subclip(0, segment_duration)
 
518
  try: sub.close()
519
  except: pass
520
  continue
 
521
  clips_to_concatenate.append(sub)
522
  current_duration += sub.duration
523
  except Exception as sub_e:
524
  logger.warning(f"[{i+1}/{len(video_paths)}] Error creando subclip: {str(sub_e)}")
525
  continue
 
526
  except Exception as e:
527
  logger.warning(f"[{i+1}/{len(video_paths)}] Error procesando video {path}: {str(e)}", exc_info=True)
528
  continue
529
 
530
  logger.info(f"Procesamiento de clips fuente finalizado. Se obtuvieron {len(clips_to_concatenate)} segmentos válidos.")
 
531
  if not clips_to_concatenate:
532
  logger.error("No hay segmentos de video válidos disponibles.")
533
  raise ValueError("No hay segmentos de video válidos.")
 
537
  try:
538
  concatenated_base = concatenate_videoclips(clips_to_concatenate, method="chain")
539
  logger.info(f"Duración video base después de concatenación inicial: {concatenated_base.duration:.2f}s")
 
540
  if concatenated_base is None or concatenated_base.duration is None or concatenated_base.duration <= 0:
541
  logger.critical("Video base concatenado es inválido.")
542
  raise ValueError("Fallo al crear video base válido.")
 
543
  except Exception as e:
544
  logger.critical(f"Error durante la concatenación inicial: {str(e)}", exc_info=True)
545
  raise ValueError("Fallo durante la concatenación de video inicial.")
 
551
 
552
  video_base = concatenated_base
553
  final_video_base = video_base
 
554
  if final_video_base.duration < audio_duration:
555
  logger.info(f"Video base ({final_video_base.duration:.2f}s) es más corto que el audio ({audio_duration:.2f}s). Repitiendo...")
556
  num_full_repeats = int(audio_duration // final_video_base.duration)
557
  remaining_duration = audio_duration % final_video_base.duration
 
558
  repeated_clips_list = [final_video_base] * num_full_repeats
559
  if remaining_duration > 0:
560
  try:
 
574
  try:
575
  video_base_repeated = concatenate_videoclips(repeated_clips_list, method="chain")
576
  logger.info(f"Duración del video base repetido: {video_base_repeated.duration:.2f}s")
 
577
  if video_base_repeated is None or video_base_repeated.duration is None or video_base_repeated.duration <= 0:
578
  logger.critical("Video base repetido concatenado es inválido.")
579
  raise ValueError("Fallo al crear video base repetido válido.")
 
580
  if final_video_base is not video_base_repeated:
581
  try: final_video_base.close()
582
  except: pass
 
583
  final_video_base = video_base_repeated
 
584
  except Exception as e:
585
  logger.critical(f"Error durante la concatenación de repetición: {str(e)}", exc_info=True)
586
  raise ValueError("Fallo durante la repetición de video.")
 
598
  if trimmed_video_base is None or trimmed_video_base.duration is None or trimmed_video_base.duration <= 0:
599
  logger.critical("Video base recortado es inválido.")
600
  raise ValueError("Fallo al crear video base recortado válido.")
 
601
  if final_video_base is not trimmed_video_base:
602
  try: final_video_base.close()
603
  except: pass
 
604
  final_video_base = trimmed_video_base
 
605
  except Exception as e:
606
  logger.critical(f"Error durante el recorte: {str(e)}", exc_info=True)
607
  raise ValueError("Fallo durante el recorte de video.")
 
615
  raise ValueError("Video base final tiene tamaño inválido.")
616
 
617
  video_base = final_video_base
 
618
  logger.info("Procesando audio...")
619
  final_audio = audio_tts_original
620
  musica_audio_looped = None
 
621
  if musica_file:
622
  musica_audio_original = None
623
  try:
 
625
  shutil.copyfile(musica_file, music_path)
626
  temp_intermediate_files.append(music_path)
627
  logger.info(f"Música de fondo copiada a: {music_path}")
 
628
  musica_audio_original = AudioFileClip(music_path)
 
629
  if musica_audio_original.reader is None or musica_audio_original.duration is None or musica_audio_original.duration <= 0:
630
  logger.warning("Clip de música de fondo parece inválido.")
631
  try: musica_audio_original.close()
 
634
  else:
635
  musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
636
  logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
 
637
  if musica_audio_looped is None or musica_audio_looped.duration is None or musica_audio_looped.duration <= 0:
638
  logger.warning("Clip de música de fondo loopeado es inválido.")
639
  try: musica_audio_looped.close()
640
  except: pass
641
  musica_audio_looped = None
 
642
  if musica_audio_looped:
643
  composite_audio = CompositeAudioClip([
644
  musica_audio_looped.volumex(0.2),
645
  audio_tts_original.volumex(1.0)
646
  ])
 
647
  if composite_audio.duration is None or composite_audio.duration <= 0:
648
  logger.warning("Clip de audio compuesto es inválido.")
649
  try: composite_audio.close()
 
653
  logger.info("Mezcla de audio completada (voz + música).")
654
  final_audio = composite_audio
655
  musica_audio = musica_audio_looped
 
656
  except Exception as e:
657
  logger.warning(f"Error procesando música de fondo: {str(e)}", exc_info=True)
658
  final_audio = audio_tts_original
 
678
 
679
  logger.info("Renderizando video final...")
680
  video_final = video_base.set_audio(final_audio)
 
681
  if video_final is None or video_final.duration is None or video_final.duration <= 0:
682
  logger.critical("Clip de video final (con audio) es inválido.")
683
  raise ValueError("Clip de video final es inválido.")
 
685
  output_filename = "final_video.mp4"
686
  output_path = os.path.join(temp_dir_intermediate, output_filename)
687
  logger.info(f"Escribiendo video final a: {output_path}")
 
688
  video_final.write_videofile(
 
689
  fps=24,
690
  threads=4,
691
  codec="libx264",
 
696
 
697
  total_time = (datetime.now() - start_time).total_seconds()
698
  logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {output_path} | Tiempo total: {total_time:.2f}s")
 
699
  return output_path
 
700
  except ValueError as ve:
701
  logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
702
  raise ve
 
758
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
759
  logger.info("="*80)
760
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
 
761
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
 
762
  output_video = None
763
  output_file = None
764
  status_msg = gr.update(value="⏳ Procesando...", interactive=False)
 
765
  if not input_text or not input_text.strip():
766
  logger.warning("Texto de entrada vacío.")
767
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
768
+
769
+ # CAMBIO: Validación simple de la voz en run_app
770
+ voice_ids = [vid for _, vid in AVAILABLE_VOICES]
771
+ if selected_voice not in voice_ids:
772
  logger.warning(f"Voz seleccionada inválida: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE_ID}")
773
  selected_voice = DEFAULT_VOICE_ID
 
 
 
 
 
 
 
774
  logger.info(f"Voz final a usar (ID): {selected_voice}")
775
 
776
  try:
777
  logger.info("Llamando a crear_video...")
778
  video_path = crear_video(prompt_type, input_text, selected_voice, musica_file)
 
779
  if video_path and os.path.exists(video_path):
780
  logger.info(f"crear_video retornó path: {video_path}")
781
  output_video = video_path
 
783
  status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
784
  else:
785
  logger.error(f"crear_video no retornó un path válido: {video_path}")
786
+ status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
 
787
  except ValueError as ve:
788
+ logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
789
  status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
790
  except Exception as e:
791
+ logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
792
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
793
+ return output_video, output_file, status_msg
 
 
794
 
795
+ # Interfaz de Gradio
796
  with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
797
  .gradio-container {max-width: 800px; margin: auto;}
798
  h1 {text-align: center;}
799
  """) as app:
 
800
  gr.Markdown("# 🎬 Generador Automático de Videos con IA")
801
  gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
802
 
 
807
  label="Método de Entrada",
808
  value="Generar Guion con IA"
809
  )
 
810
  with gr.Column(visible=True) as ia_guion_column:
811
  prompt_ia = gr.Textbox(
812
  label="Tema para IA",
813
  lines=2,
814
+ placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer, mostrando la belleza de la naturaleza...",
815
  max_lines=4,
816
  value=""
817
  )
 
818
  with gr.Column(visible=False) as manual_guion_column:
819
  prompt_manual = gr.Textbox(
820
  label="Tu Guion Completo",
821
  lines=5,
822
+ placeholder="Ej: En este video exploraremos los misterios del océano. Veremos la vida marina fascinante y los arrecifes de coral vibrantes. ¡Acompáñanos en esta aventura subacuática!",
823
  max_lines=10,
824
  value=""
825
  )
 
826
  musica_input = gr.Audio(
827
  label="Música de fondo (opcional)",
828
  type="filepath",
829
  interactive=True,
830
  value=None
831
  )
832
+ # CAMBIO: Dropdown de voces simplificado
833
  voice_dropdown = gr.Dropdown(
834
  label="Seleccionar Voz para Guion",
835
+ choices=AVAILABLE_VOICES,
836
+ value=DEFAULT_VOICE_ID,
837
  interactive=True
838
  )
 
839
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
 
840
  with gr.Column():
841
  video_output = gr.Video(
842
  label="Previsualización del Video Generado",
 
862
  inputs=prompt_type,
863
  outputs=[ia_guion_column, manual_guion_column]
864
  )
 
865
  generate_btn.click(
866
  lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
867
  outputs=[video_output, file_output, status_output],
 
878
 
879
  gr.Markdown("### Instrucciones:")
880
  gr.Markdown("""
881
+ 1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
882
+ 2. **Selecciona el tipo de entrada**: "Generar Guion con IA" o "Usar Mi Guion".
883
+ 3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.).
884
+ 4. **Selecciona la voz** deseada del desplegable.
885
+ 5. **Haz clic en "✨ Generar Video"**.
886
+ 6. Espera a que se procese el video. Verás el estado.
887
+ 7. La previsualización aparecerá si es posible, y siempre un enlace **Descargar Archivo de Video** se mostrará si la generación fue exitosa.
888
+ 8. Revisa `video_generator_full.log` para detalles si hay errores.
889
  """)
890
  gr.Markdown("---")
891
  gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
 
897
  try:
898
  temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
899
  temp_clip.close()
900
+ logger.info("Clips base de MoviePy creados y cerrados exitosamente. FFmpeg parece accesible.")
901
  except Exception as e:
902
+ logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
903
  except Exception as e:
904
+ logger.critical(f"Fallo al importar MoviePy. Asegúrate de que está instalado. Error: {e}", exc_info=True)
 
905
  logger.info("Iniciando aplicación Gradio...")
906
  try:
907
  app.launch(server_name="0.0.0.0", server_port=7860, share=False)