gnosticdev commited on
Commit
06124ef
·
verified ·
1 Parent(s): 5426968

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +104 -362
app.py CHANGED
@@ -14,9 +14,11 @@ import math
14
  import shutil
15
  import json
16
  from collections import Counter
 
 
17
 
18
  # Variable global para TTS
19
- tts_model = None
20
 
21
  # Configuración de logging
22
  logging.basicConfig(
@@ -61,6 +63,19 @@ except Exception as e:
61
  logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
62
  kw_model = None
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  def buscar_videos_pexels(query, api_key, per_page=5):
65
  if not api_key:
66
  logger.warning("No se puede buscar en Pexels: API Key no configurada.")
@@ -182,20 +197,16 @@ def text_to_speech(text, output_path, voice=None):
182
  return False
183
 
184
  try:
185
- # Usar modelo específico para español, sin GPU
186
  tts = TTS(model_name="tts_models/es/css10/vits", progress_bar=False, gpu=False)
187
 
188
- # Limpiar y truncar texto
189
  text = text.replace("na hora", "A la hora")
190
  text = re.sub(r'[^\w\s.,!?áéíóúñÁÉÍÓÚÑ]', '', text)
191
  if len(text) > 500:
192
  logger.warning("Texto demasiado largo, truncando a 500 caracteres")
193
  text = text[:500]
194
 
195
- # Generar audio sin especificar idioma
196
  tts.tts_to_file(text=text, file_path=output_path)
197
 
198
- # Verificar archivo generado
199
  if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
200
  logger.info(f"Audio creado: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
201
  return True
@@ -362,6 +373,7 @@ def crear_video(prompt_type, input_text, musica_file=None):
362
 
363
  start_time = datetime.now()
364
  temp_dir_intermediate = None
 
365
 
366
  audio_tts_original = None
367
  musica_audio_original = None
@@ -384,8 +396,6 @@ def crear_video(prompt_type, input_text, musica_file=None):
384
  if not guion.strip():
385
  logger.error("El guion resultante está vacío o solo contiene espacios.")
386
  raise ValueError("El guion está vacío.")
387
-
388
- # Corregir error tipográfico en el guion
389
  guion = guion.replace("na hora", "A la hora")
390
 
391
  temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
@@ -395,25 +405,22 @@ def crear_video(prompt_type, input_text, musica_file=None):
395
  # 2. Generar audio de voz
396
  logger.info("Generando audio de voz...")
397
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
398
-
399
- # Llamar a text_to_speech directamente
400
  tts_success = text_to_speech(guion, voz_path)
401
 
402
  if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 1000:
403
  logger.error(f"Fallo en la generación de voz. Archivo de audio no creado o es muy pequeño: {voz_path}")
404
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
405
-
406
  temp_intermediate_files.append(voz_path)
407
 
408
  audio_tts_original = AudioFileClip(voz_path)
409
 
410
  if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
411
- logger.critical("Clip de audio TTS inicial es inválido (reader is None o duración <= 0) *después* de crear AudioFileClip.")
412
  try: audio_tts_original.close()
413
  except: pass
414
  audio_tts_original = None
415
  raise ValueError("Audio de voz generado es inválido después de procesamiento inicial.")
416
-
417
  audio_tts = audio_tts_original
418
  audio_duration = audio_tts_original.duration
419
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
@@ -430,7 +437,6 @@ def crear_video(prompt_type, input_text, musica_file=None):
430
  except Exception as e:
431
  logger.error(f"Error extrayendo keywords: {str(e)}", exc_info=True)
432
  keywords = ["naturaleza", "paisaje"]
433
-
434
  if not keywords:
435
  keywords = ["video", "background"]
436
 
@@ -446,20 +452,17 @@ def crear_video(prompt_type, input_text, musica_file=None):
446
  videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=per_page_per_keyword)
447
  if videos:
448
  videos_data.extend(videos)
449
- logger.info(f"Encontrados {len(videos)} videos para '{keyword}'. Total data: {len(videos_data)}")
450
  except Exception as e:
451
  logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
452
 
453
  if len(videos_data) < total_desired_videos / 2:
454
  logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
455
- generic_keywords = ["nature", "city", "background", "abstract"]
456
- for keyword in generic_keywords:
457
  if len(videos_data) >= total_desired_videos: break
458
  try:
459
  videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=2)
460
  if videos:
461
  videos_data.extend(videos)
462
- logger.info(f"Encontrados {len(videos)} videos para '{keyword}' (genérico). Total data: {len(videos_data)}")
463
  except Exception as e:
464
  logger.warning(f"Error buscando videos genéricos para '{keyword}': {str(e)}")
465
 
@@ -470,28 +473,14 @@ def crear_video(prompt_type, input_text, musica_file=None):
470
  video_paths = []
471
  logger.info(f"Intentando descargar {len(videos_data)} videos encontrados...")
472
  for video in videos_data:
473
- if 'video_files' not in video or not video['video_files']:
474
- logger.debug(f"Saltando video sin archivos de video: {video.get('id')}")
475
- continue
476
-
477
  try:
478
- best_quality = None
479
- for vf in sorted(video['video_files'], key=lambda x: x.get('width', 0) * x.get('height', 0), reverse=True):
480
- if 'link' in vf:
481
- best_quality = vf
482
- break
483
-
484
- if best_quality and 'link' in best_quality:
485
  path = download_video_file(best_quality['link'], temp_dir_intermediate)
486
  if path:
487
  video_paths.append(path)
488
  temp_intermediate_files.append(path)
489
- logger.info(f"Video descargado OK desde {best_quality['link'][:50]}...")
490
- else:
491
- logger.warning(f"No se pudo descargar video desde {best_quality['link'][:50]}...")
492
- else:
493
- logger.warning(f"No se encontró enlace de descarga válido para video {video.get('id')}.")
494
-
495
  except Exception as e:
496
  logger.warning(f"Error procesando/descargando video {video.get('id')}: {str(e)}")
497
 
@@ -507,335 +496,115 @@ def crear_video(prompt_type, input_text, musica_file=None):
507
  max_clip_segment = 10.0
508
 
509
  for i, path in enumerate(video_paths):
510
- if current_duration >= audio_duration + max_clip_segment:
511
- 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.")
512
- break
513
-
514
  clip = None
515
  try:
516
- logger.debug(f"[{i+1}/{len(video_paths)}] Abriendo clip: {path}")
517
  clip = VideoFileClip(path)
518
  source_clips.append(clip)
519
-
520
- if clip.reader is None or clip.duration is None or clip.duration <= 0:
521
- logger.warning(f"[{i+1}/{len(video_paths)}] Clip fuente {path} parece inválido (reader is None o duración <= 0). Saltando.")
522
- continue
523
-
524
  remaining_needed = audio_duration - current_duration
525
- potential_use_duration = min(clip.duration, max_clip_segment)
526
-
527
  if remaining_needed > 0:
528
- segment_duration = min(potential_use_duration, remaining_needed + min_clip_duration)
529
  segment_duration = max(min_clip_duration, segment_duration)
530
- segment_duration = min(segment_duration, clip.duration)
531
-
532
  if segment_duration >= min_clip_duration:
533
- try:
534
- sub = clip.subclip(0, segment_duration)
535
- if sub.reader is None or sub.duration is None or sub.duration <= 0:
536
- logger.warning(f"[{i+1}/{len(video_paths)}] Subclip generado de {path} es inválido. Saltando.")
537
- try: sub.close()
538
- except: pass
539
- continue
540
-
541
- clips_to_concatenate.append(sub)
542
- current_duration += sub.duration
543
- logger.debug(f"[{i+1}/{len(video_paths)}] Segmento añadido: {sub.duration:.1f}s (total video: {current_duration:.1f}/{audio_duration:.1f}s)")
544
-
545
- except Exception as sub_e:
546
- logger.warning(f"[{i+1}/{len(video_paths)}] Error creando subclip de {path} ({segment_duration:.1f}s): {str(sub_e)}")
547
- continue
548
- else:
549
- logger.debug(f"[{i+1}/{len(video_paths)}] Clip {path} ({clip.duration:.1f}s) no contribuye un segmento suficiente ({segment_duration:.1f}s necesario). Saltando.")
550
- else:
551
- logger.debug(f"[{i+1}/{len(video_paths)}] Duración de video base ya alcanzada. Saltando clip.")
552
-
553
  except Exception as e:
554
- logger.warning(f"[{i+1}/{len(video_paths)}] Error procesando video {path}: {str(e)}", exc_info=True)
555
- continue
556
-
557
- logger.info(f"Procesamiento de clips fuente finalizado. Se obtuvieron {len(clips_to_concatenate)} segmentos válidos.")
558
 
559
  if not clips_to_concatenate:
560
  logger.error("No hay segmentos de video válidos disponibles para crear la secuencia.")
561
  raise ValueError("No hay segmentos de video válidos disponibles para crear el video.")
562
 
563
  logger.info(f"Concatenando {len(clips_to_concatenate)} segmentos de video.")
564
- concatenated_base = None
565
- try:
566
- concatenated_base = concatenate_videoclips(clips_to_concatenate, method="chain")
567
- logger.info(f"Duración video base después de concatenación inicial: {concatenated_base.duration:.2f}s")
568
-
569
- if concatenated_base is None or concatenated_base.duration is None or concatenated_base.duration <= 0:
570
- logger.critical("Video base concatenado es inválido después de la primera concatenación (None o duración cero).")
571
- raise ValueError("Fallo al crear video base válido a partir de segmentos.")
572
-
573
- except Exception as e:
574
- logger.critical(f"Error durante la concatenación inicial: {str(e)}", exc_info=True)
575
- raise ValueError("Fallo durante la concatenación de video inicial.")
576
- finally:
577
- for clip_segment in clips_to_concatenate:
578
- try: clip_segment.close()
579
- except: pass
580
- clips_to_concatenate = []
581
-
582
- video_base = concatenated_base
583
-
584
- final_video_base = video_base
585
-
586
- if final_video_base.duration < audio_duration:
587
- logger.info(f"Video base ({final_video_base.duration:.2f}s) es más corto que el audio ({audio_duration:.2f}s). Repitiendo...")
588
-
589
- num_full_repeats = int(audio_duration // final_video_base.duration)
590
- remaining_duration = audio_duration % final_video_base.duration
591
-
592
- repeated_clips_list = [final_video_base] * num_full_repeats
593
- if remaining_duration > 0:
594
- try:
595
- remaining_clip = final_video_base.subclip(0, remaining_duration)
596
- if remaining_clip is None or remaining_clip.duration is None or remaining_clip.duration <= 0:
597
- logger.warning(f"Subclip generado para duración restante {remaining_duration:.2f}s es inválido. Saltando.")
598
- try: remaining_clip.close()
599
- except: pass
600
- else:
601
- repeated_clips_list.append(remaining_clip)
602
- logger.debug(f"Añadiendo segmento restante: {remaining_duration:.2f}s")
603
-
604
- except Exception as e:
605
- logger.warning(f"Error creando subclip para duración restante {remaining_duration:.2f}s: {str(e)}")
606
-
607
- if repeated_clips_list:
608
- logger.info(f"Concatenando {len(repeated_clips_list)} partes para repetición.")
609
- video_base_repeated = None
610
- try:
611
- video_base_repeated = concatenate_videoclips(repeated_clips_list, method="chain")
612
- logger.info(f"Duración del video base repetido: {video_base_repeated.duration:.2f}s")
613
-
614
- if video_base_repeated is None or video_base_repeated.duration is None or video_base_repeated.duration <= 0:
615
- logger.critical("Video base repetido concatenado es inválido.")
616
- raise ValueError("Fallo al crear video base repetido válido.")
617
 
618
- if final_video_base is not video_base_repeated:
619
- try: final_video_base.close()
620
- except: pass
621
 
622
- final_video_base = video_base_repeated
 
 
623
 
624
- except Exception as e:
625
- logger.critical(f"Error durante la concatenación de repetición: {str(e)}", exc_info=True)
626
- raise ValueError("Fallo durante la repetición de video.")
627
- finally:
628
- if 'repeated_clips_list' in locals():
629
- for clip in repeated_clips_list:
630
- if clip is not final_video_base:
631
- try: clip.close()
632
- except: pass
633
-
634
- if final_video_base.duration > audio_duration:
635
- 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).")
636
- trimmed_video_base = None
637
- try:
638
- trimmed_video_base = final_video_base.subclip(0, audio_duration)
639
- if trimmed_video_base is None or trimmed_video_base.duration is None or trimmed_video_base.duration <= 0:
640
- logger.critical("Video base recortado es inválido.")
641
- raise ValueError("Fallo al crear video base recortado válido.")
642
-
643
- if final_video_base is not trimmed_video_base:
644
- try: final_video_base.close()
645
- except: pass
646
-
647
- final_video_base = trimmed_video_base
648
-
649
- except Exception as e:
650
- logger.critical(f"Error durante el recorte: {str(e)}", exc_info=True)
651
- raise ValueError("Fallo durante el recorte de video.")
652
-
653
- if final_video_base is None or final_video_base.duration is None or final_video_base.duration <= 0:
654
- logger.critical("Video base final es inválido antes de audio/escritura (None o duración cero).")
655
  raise ValueError("Video base final es inválido.")
656
-
657
- if final_video_base.size is None or final_video_base.size[0] <= 0 or final_video_base.size[1] <= 0:
658
- logger.critical(f"Video base final tiene tamaño inválido: {final_video_base.size}. No se puede escribir video.")
659
- raise ValueError("Video base final tiene tamaño inválido antes de escribir.")
660
-
661
- video_base = final_video_base
662
-
663
  # 6. Manejar música de fondo
664
  logger.info("Procesando audio...")
665
-
666
  final_audio = audio_tts_original
667
-
668
- musica_audio_looped = None
669
-
670
  if musica_file:
671
- musica_audio_original = None
672
  try:
673
  music_path = os.path.join(temp_dir_intermediate, "musica_bg.mp3")
674
  shutil.copyfile(musica_file, music_path)
675
  temp_intermediate_files.append(music_path)
676
- logger.info(f"Música de fondo copiada a: {music_path}")
677
-
678
  musica_audio_original = AudioFileClip(music_path)
679
-
680
- if musica_audio_original.reader is None or musica_audio_original.duration is None or musica_audio_original.duration <= 0:
681
- logger.warning("Clip de música de fondo parece inválido o tiene duración cero. Saltando música.")
682
- try: musica_audio_original.close()
683
- except: pass
684
- musica_audio_original = None
685
- else:
686
- musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
687
- logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
688
-
689
- if musica_audio_looped is None or musica_audio_looped.duration is None or musica_audio_looped.duration <= 0:
690
- logger.warning("Clip de música de fondo loopeado es inválido. Saltando música.")
691
- try: musica_audio_looped.close()
692
- except: pass
693
- musica_audio_looped = None
694
-
695
- if musica_audio_looped:
696
- composite_audio = CompositeAudioClip([
697
- musica_audio_looped.volumex(0.2), # Volumen 20% para música
698
- audio_tts_original.volumex(1.0) # Volumen 100% para voz
699
- ])
700
-
701
- if composite_audio.duration is None or composite_audio.duration <= 0:
702
- logger.warning("Clip de audio compuesto es inválido (None o duración cero). Usando solo audio de voz.")
703
- try: composite_audio.close()
704
- except: pass
705
- final_audio = audio_tts_original
706
- else:
707
- logger.info("Mezcla de audio completada (voz + música).")
708
- final_audio = composite_audio
709
- musica_audio = musica_audio_looped # Asignar para limpieza
710
-
711
  except Exception as e:
712
- logger.warning(f"Error procesando música de fondo: {str(e)}", exc_info=True)
713
- final_audio = audio_tts_original
714
- musica_audio = None
715
- logger.warning("Usando solo audio de voz debido a un error con la música.")
716
 
717
  if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
718
- 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.")
719
- try:
720
- if final_audio.duration > video_base.duration:
721
- trimmed_final_audio = final_audio.subclip(0, video_base.duration)
722
- if trimmed_final_audio is None or trimmed_final_audio.duration <= 0:
723
- logger.warning("Audio final recortado es inválido. Usando audio final original.")
724
- try: trimmed_final_audio.close()
725
- except: pass
726
- else:
727
- if final_audio is not trimmed_final_audio:
728
- try: final_audio.close()
729
- except: pass
730
- final_audio = trimmed_final_audio
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
 
735
- # 7. Crear video final
736
  logger.info("Renderizando video final...")
737
  video_final = video_base.set_audio(final_audio)
738
 
739
- if video_final is None or video_final.duration is None or video_final.duration <= 0:
740
- logger.critical("Clip de video final (con audio) es inválido antes de escribir (None o duración cero).")
741
- raise ValueError("Clip de video final es inválido antes de escribir.")
742
-
743
  output_filename = "final_video.mp4"
744
  output_path = os.path.join(temp_dir_intermediate, output_filename)
745
  logger.info(f"Escribiendo video final a: {output_path}")
746
 
747
- if not output_path or not isinstance(output_path, str):
748
- logger.critical(f"output_path no es válido: {output_path}")
749
- raise ValueError("El nombre del archivo de salida no es válido.")
750
-
751
- try:
752
- video_final.write_videofile(
753
- filename=output_path,
754
- fps=24,
755
- threads=4,
756
- codec="libx264",
757
- audio_codec="aac",
758
- preset="medium",
759
- logger='bar'
760
- )
761
- except Exception as e:
762
- logger.critical(f"Error al escribir el video final: {str(e)}", exc_info=True)
763
- raise ValueError(f"Fallo al escribir el video final: {str(e)}")
764
 
765
  total_time = (datetime.now() - start_time).total_seconds()
766
  logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {output_path} | Tiempo total: {total_time:.2f}s")
767
 
768
  return output_path
769
 
770
- except ValueError as ve:
771
- logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
772
- raise ve
773
  except Exception as e:
774
- logger.critical(f"ERROR CRÍTICO NO CONTROLADO en crear_video: {str(e)}", exc_info=True)
775
- raise e
776
  finally:
777
  logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
778
-
779
- for clip in source_clips:
780
- try:
781
- clip.close()
782
- except Exception as e:
783
- logger.warning(f"Error cerrando clip de video fuente en finally: {str(e)}")
784
-
785
- for clip_segment in clips_to_concatenate:
786
- try:
787
- clip_segment.close()
788
- except Exception as e:
789
- logger.warning(f"Error cerrando segmento de video en finally: {str(e)}")
790
-
791
- if musica_audio is not None:
792
- try:
793
- musica_audio.close()
794
- except Exception as e:
795
- logger.warning(f"Error cerrando musica_audio (procesada) en finally: {str(e)}")
796
-
797
- if musica_audio_original is not None and musica_audio_original is not musica_audio:
798
- try:
799
- musica_audio_original.close()
800
- except Exception as e:
801
- logger.warning(f"Error cerrando musica_audio_original en finally: {str(e)}")
802
-
803
- if audio_tts is not None and audio_tts is not audio_tts_original:
804
- try:
805
- audio_tts.close()
806
- except Exception as e:
807
- logger.warning(f"Error cerrando audio_tts (procesada) en finally: {str(e)}")
808
-
809
- if audio_tts_original is not None:
810
- try:
811
- audio_tts_original.close()
812
- except Exception as e:
813
- logger.warning(f"Error cerrando audio_tts_original en finally: {str(e)}")
814
-
815
- if video_final is not None:
816
- try:
817
- video_final.close()
818
- except Exception as e:
819
- logger.warning(f"Error cerrando video_final en finally: {str(e)}")
820
- elif video_base is not None and video_base is not video_final:
821
- try:
822
- video_base.close()
823
- except Exception as e:
824
- logger.warning(f"Error cerrando video_base en finally: {str(e)}")
825
-
826
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
827
  final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4")
828
-
829
  for path in temp_intermediate_files:
830
- try:
831
- if os.path.isfile(path) and path != final_output_in_temp:
832
- logger.debug(f"Eliminando archivo temporal intermedio: {path}")
833
  os.remove(path)
834
- elif os.path.isfile(path) and path == final_output_in_temp:
835
- logger.debug(f"Saltando eliminación del archivo de video final: {path}")
836
- except Exception as e:
837
- logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
838
-
839
  logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
840
 
841
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
@@ -853,30 +622,29 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
853
  status_msg = gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
854
  return output_video, output_file, status_msg
855
 
856
- logger.info(f"Tipo de entrada: {prompt_type}")
857
- logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
858
- if musica_file:
859
- logger.info(f"Archivo de música recibido: {musica_file}")
860
- else:
861
- logger.info("No se proporcionó archivo de música.")
862
-
863
  try:
864
  logger.info("Llamando a crear_video...")
865
  video_path = crear_video(prompt_type, input_text, musica_file)
866
 
867
  if video_path and os.path.exists(video_path):
868
  logger.info(f"crear_video retornó path: {video_path}")
869
- logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
870
  output_video = video_path
871
  output_file = gr.update(value=video_path, visible=True)
872
  status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
 
 
 
 
 
 
 
 
 
 
873
  else:
874
  logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
875
- status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
876
 
877
- except ValueError as ve:
878
- logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
879
- status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
880
  except Exception as e:
881
  logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
882
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
@@ -904,25 +672,21 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
904
  prompt_ia = gr.Textbox(
905
  label="Tema para IA",
906
  lines=2,
907
- placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer, mostrando la belleza de la naturaleza...",
908
- max_lines=4,
909
- value=""
910
  )
911
 
912
  with gr.Column(visible=False) as manual_guion_column:
913
  prompt_manual = gr.Textbox(
914
  label="Tu Guion Completo",
915
  lines=5,
916
- 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!",
917
- max_lines=10,
918
- value=""
919
  )
920
 
921
  musica_input = gr.Audio(
922
  label="Música de fondo (opcional)",
923
- type="filepath",
924
- interactive=True,
925
- value=None
926
  )
927
 
928
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
@@ -942,8 +706,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
942
  label="Estado",
943
  interactive=False,
944
  show_label=False,
945
- placeholder="Esperando acción...",
946
- value="Esperando entrada..."
947
  )
948
 
949
  prompt_type.change(
@@ -954,7 +717,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
954
  )
955
 
956
  generate_btn.click(
957
- lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
958
  outputs=[video_output, file_output, status_output],
959
  queue=True,
960
  ).then(
@@ -962,36 +725,15 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
962
  inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
963
  outputs=[video_output, file_output, status_output]
964
  )
965
-
966
  gr.Markdown("### Instrucciones:")
967
- gr.Markdown("""
968
- 1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
969
- 2. **Selecciona el tipo de entrada**:
970
- - "Generar Guion con IA": Describe brevemente un tema (ej. "La belleza de las montañas"). La IA generará un guion corto.
971
- - "Usar Mi Guion": Escribe el guion completo que quieres para el video.
972
- 3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.) para usar como música de fondo.
973
- 4. **Haz clic en "✨ Generar Video"**.
974
- 5. Espera a que se procese el video. El tiempo de espera puede variar. Verás el estado en el cuadro de texto.
975
- 6. La previsualización del video aparecerá arriba (puede fallar para archivos grandes), y un enlace **Descargar Archivo de Video** se mostrará si la generación fue exitosa.
976
- 7. Si hay errores, revisa el log `video_generator_full.log` para más detalles.
977
- """)
978
- gr.Markdown("---")
979
- gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
980
 
981
- if __name__ == "__main__":
982
- logger.info("Verificando dependencias críticas...")
983
- try:
984
- from moviepy.editor import ColorClip
985
- try:
986
- temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
987
- temp_clip.close()
988
- logger.info("Clips base de MoviePy (como ColorClip) creados y cerrados exitosamente. FFmpeg parece accesible.")
989
- except Exception as e:
990
- logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
991
-
992
- except Exception as e:
993
- logger.critical(f"Fallo al importar MoviePy. Asegúrate de que está instalado. Error: {e}", exc_info=True)
994
 
 
995
  logger.info("Iniciando aplicación Gradio...")
996
  try:
997
  app.launch(server_name="0.0.0.0", server_port=7860, share=False)
 
14
  import shutil
15
  import json
16
  from collections import Counter
17
+ import threading
18
+ import time
19
 
20
  # Variable global para TTS
21
+ tts_model = None
22
 
23
  # Configuración de logging
24
  logging.basicConfig(
 
63
  logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
64
  kw_model = None
65
 
66
+ def schedule_deletion(directory_path, delay_seconds):
67
+ """Programa la eliminación de un directorio después de un cierto tiempo."""
68
+ logger.info(f"PROGRAMADA eliminación del directorio '{directory_path}' en {delay_seconds / 3600:.1f} horas.")
69
+ time.sleep(delay_seconds)
70
+ try:
71
+ if os.path.isdir(directory_path):
72
+ shutil.rmtree(directory_path)
73
+ logger.info(f"Directorio temporal '{directory_path}' eliminado exitosamente.")
74
+ else:
75
+ logger.warning(f"No se pudo eliminar: '{directory_path}' no es un directorio válido o ya fue eliminado.")
76
+ except Exception as e:
77
+ logger.error(f"Error durante la eliminación programada de '{directory_path}': {str(e)}")
78
+
79
  def buscar_videos_pexels(query, api_key, per_page=5):
80
  if not api_key:
81
  logger.warning("No se puede buscar en Pexels: API Key no configurada.")
 
197
  return False
198
 
199
  try:
 
200
  tts = TTS(model_name="tts_models/es/css10/vits", progress_bar=False, gpu=False)
201
 
 
202
  text = text.replace("na hora", "A la hora")
203
  text = re.sub(r'[^\w\s.,!?áéíóúñÁÉÍÓÚÑ]', '', text)
204
  if len(text) > 500:
205
  logger.warning("Texto demasiado largo, truncando a 500 caracteres")
206
  text = text[:500]
207
 
 
208
  tts.tts_to_file(text=text, file_path=output_path)
209
 
 
210
  if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
211
  logger.info(f"Audio creado: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
212
  return True
 
373
 
374
  start_time = datetime.now()
375
  temp_dir_intermediate = None
376
+ TARGET_RESOLUTION = (1280, 720) # *** MODIFICACIÓN: Resolución 720p ***
377
 
378
  audio_tts_original = None
379
  musica_audio_original = None
 
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
  guion = guion.replace("na hora", "A la hora")
400
 
401
  temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
 
405
  # 2. Generar audio de voz
406
  logger.info("Generando audio de voz...")
407
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
 
 
408
  tts_success = text_to_speech(guion, voz_path)
409
 
410
  if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 1000:
411
  logger.error(f"Fallo en la generación de voz. Archivo de audio no creado o es muy pequeño: {voz_path}")
412
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
 
413
  temp_intermediate_files.append(voz_path)
414
 
415
  audio_tts_original = AudioFileClip(voz_path)
416
 
417
  if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
418
+ logger.critical("Clip de audio TTS inicial es inválido (reader is None o duración <= 0).")
419
  try: audio_tts_original.close()
420
  except: pass
421
  audio_tts_original = None
422
  raise ValueError("Audio de voz generado es inválido después de procesamiento inicial.")
423
+
424
  audio_tts = audio_tts_original
425
  audio_duration = audio_tts_original.duration
426
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
 
437
  except Exception as e:
438
  logger.error(f"Error extrayendo keywords: {str(e)}", exc_info=True)
439
  keywords = ["naturaleza", "paisaje"]
 
440
  if not keywords:
441
  keywords = ["video", "background"]
442
 
 
452
  videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=per_page_per_keyword)
453
  if videos:
454
  videos_data.extend(videos)
 
455
  except Exception as e:
456
  logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
457
 
458
  if len(videos_data) < total_desired_videos / 2:
459
  logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
460
+ for keyword in ["nature", "city", "background", "abstract"]:
 
461
  if len(videos_data) >= total_desired_videos: break
462
  try:
463
  videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=2)
464
  if videos:
465
  videos_data.extend(videos)
 
466
  except Exception as e:
467
  logger.warning(f"Error buscando videos genéricos para '{keyword}': {str(e)}")
468
 
 
473
  video_paths = []
474
  logger.info(f"Intentando descargar {len(videos_data)} videos encontrados...")
475
  for video in videos_data:
476
+ if 'video_files' not in video or not video['video_files']: continue
 
 
 
477
  try:
478
+ best_quality = next((vf for vf in sorted(video['video_files'], key=lambda x: x.get('width', 0), reverse=True) if 'link' in vf), None)
479
+ if best_quality:
 
 
 
 
 
480
  path = download_video_file(best_quality['link'], temp_dir_intermediate)
481
  if path:
482
  video_paths.append(path)
483
  temp_intermediate_files.append(path)
 
 
 
 
 
 
484
  except Exception as e:
485
  logger.warning(f"Error procesando/descargando video {video.get('id')}: {str(e)}")
486
 
 
496
  max_clip_segment = 10.0
497
 
498
  for i, path in enumerate(video_paths):
499
+ if current_duration >= audio_duration + max_clip_segment: break
 
 
 
500
  clip = None
501
  try:
 
502
  clip = VideoFileClip(path)
503
  source_clips.append(clip)
504
+ if clip.reader is None or clip.duration is None or clip.duration <= 0: continue
505
+
 
 
 
506
  remaining_needed = audio_duration - current_duration
 
 
507
  if remaining_needed > 0:
508
+ segment_duration = min(min(clip.duration, max_clip_segment), remaining_needed + min_clip_duration)
509
  segment_duration = max(min_clip_duration, segment_duration)
 
 
510
  if segment_duration >= min_clip_duration:
511
+ try:
512
+ sub_raw = clip.subclip(0, segment_duration)
513
+ # *** MODIFICACIÓN: Redimensionar y recortar cada subclip a 720p ***
514
+ sub = sub_raw.resize(height=TARGET_RESOLUTION[1]).crop(x_center='center', width=TARGET_RESOLUTION[0], height=TARGET_RESOLUTION[1])
515
+ sub_raw.close() # Liberar memoria del clip intermedio
516
+
517
+ if sub.reader is None or sub.duration is None or sub.duration <= 0:
518
+ try: sub.close()
519
+ except: pass
520
+ continue
521
+
522
+ clips_to_concatenate.append(sub)
523
+ current_duration += sub.duration
524
+ logger.debug(f"Segmento añadido: {sub.duration:.1f}s (total video: {current_duration:.1f}/{audio_duration:.1f}s)")
525
+ except Exception as sub_e:
526
+ logger.warning(f"Error creando subclip de {path}: {str(sub_e)}")
 
 
 
 
527
  except Exception as e:
528
+ logger.warning(f"Error procesando video {path}: {str(e)}", exc_info=True)
 
 
 
529
 
530
  if not clips_to_concatenate:
531
  logger.error("No hay segmentos de video válidos disponibles para crear la secuencia.")
532
  raise ValueError("No hay segmentos de video válidos disponibles para crear el video.")
533
 
534
  logger.info(f"Concatenando {len(clips_to_concatenate)} segmentos de video.")
535
+ video_base = concatenate_videoclips(clips_to_concatenate, method="chain")
536
+ for seg in clips_to_concatenate:
537
+ try: seg.close()
538
+ except: pass
539
+ clips_to_concatenate = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
 
541
+ if video_base.duration < audio_duration:
542
+ logger.info(f"Video base ({video_base.duration:.2f}s) más corto que audio ({audio_duration:.2f}s). Repitiendo...")
543
+ video_base = video_base.loop(duration=audio_duration)
544
 
545
+ if video_base.duration > audio_duration:
546
+ logger.info(f"Recortando video base ({video_base.duration:.2f}s) para coincidir con audio ({audio_duration:.2f}s).")
547
+ video_base = video_base.subclip(0, audio_duration)
548
 
549
+ if video_base is None or video_base.duration is None or video_base.duration <= 0:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  raise ValueError("Video base final es inválido.")
551
+
 
 
 
 
 
 
552
  # 6. Manejar música de fondo
553
  logger.info("Procesando audio...")
 
554
  final_audio = audio_tts_original
 
 
 
555
  if musica_file:
 
556
  try:
557
  music_path = os.path.join(temp_dir_intermediate, "musica_bg.mp3")
558
  shutil.copyfile(musica_file, music_path)
559
  temp_intermediate_files.append(music_path)
 
 
560
  musica_audio_original = AudioFileClip(music_path)
561
+ if musica_audio_original.duration > 0:
562
+ musica_audio = loop_audio_to_length(musica_audio_original, video_base.duration)
563
+ if musica_audio and musica_audio.duration > 0:
564
+ final_audio = CompositeAudioClip([musica_audio.volumex(0.2), audio_tts_original.volumex(1.0)])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
  except Exception as e:
566
+ logger.warning(f"Error procesando música: {str(e)}", exc_info=True)
 
 
 
567
 
568
  if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
569
+ logger.warning(f"Ajustando duración de audio final ({final_audio.duration:.2f}s) a la del video ({video_base.duration:.2f}s).")
570
+ final_audio = final_audio.subclip(0, video_base.duration)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
 
572
+ # 7. Crear video final
573
  logger.info("Renderizando video final...")
574
  video_final = video_base.set_audio(final_audio)
575
 
 
 
 
 
576
  output_filename = "final_video.mp4"
577
  output_path = os.path.join(temp_dir_intermediate, output_filename)
578
  logger.info(f"Escribiendo video final a: {output_path}")
579
 
580
+ video_final.write_videofile(
581
+ filename=output_path, fps=24, threads=4, codec="libx264",
582
+ audio_codec="aac", preset="medium", logger='bar'
583
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
584
 
585
  total_time = (datetime.now() - start_time).total_seconds()
586
  logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {output_path} | Tiempo total: {total_time:.2f}s")
587
 
588
  return output_path
589
 
 
 
 
590
  except Exception as e:
591
+ logger.critical(f"ERROR CRÍTICO en crear_video: {str(e)}", exc_info=True)
592
+ raise
593
  finally:
594
  logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
595
+ clips_to_close = [video_base, video_final, audio_tts_original, musica_audio_original, musica_audio] + source_clips + clips_to_concatenate
596
+ for clip in clips_to_close:
597
+ if clip:
598
+ try: clip.close()
599
+ except Exception: pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
600
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
601
  final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4")
 
602
  for path in temp_intermediate_files:
603
+ if os.path.isfile(path) and path != final_output_in_temp:
604
+ try:
 
605
  os.remove(path)
606
+ logger.debug(f"Eliminando archivo temporal intermedio: {path}")
607
+ except Exception: pass
 
 
 
608
  logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
609
 
610
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
 
622
  status_msg = gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
623
  return output_video, output_file, status_msg
624
 
 
 
 
 
 
 
 
625
  try:
626
  logger.info("Llamando a crear_video...")
627
  video_path = crear_video(prompt_type, input_text, musica_file)
628
 
629
  if video_path and os.path.exists(video_path):
630
  logger.info(f"crear_video retornó path: {video_path}")
 
631
  output_video = video_path
632
  output_file = gr.update(value=video_path, visible=True)
633
  status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
634
+
635
+ # *** MODIFICACIÓN: Programar la eliminación automática del directorio del video ***
636
+ temp_dir_to_delete = os.path.dirname(video_path)
637
+ delay_hours = 3
638
+ deletion_thread = threading.Thread(
639
+ target=schedule_deletion,
640
+ args=(temp_dir_to_delete, delay_hours * 3600)
641
+ )
642
+ deletion_thread.daemon = True
643
+ deletion_thread.start()
644
  else:
645
  logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
646
+ status_msg = gr.update(value="❌ Error: La generación del video falló.", interactive=False)
647
 
 
 
 
648
  except Exception as e:
649
  logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
650
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
 
672
  prompt_ia = gr.Textbox(
673
  label="Tema para IA",
674
  lines=2,
675
+ placeholder="Ej: Un paisaje natural con montañas y ríos...",
676
+ max_lines=4
 
677
  )
678
 
679
  with gr.Column(visible=False) as manual_guion_column:
680
  prompt_manual = gr.Textbox(
681
  label="Tu Guion Completo",
682
  lines=5,
683
+ placeholder="Ej: En este video exploraremos los misterios del océano...",
684
+ max_lines=10
 
685
  )
686
 
687
  musica_input = gr.Audio(
688
  label="Música de fondo (opcional)",
689
+ type="filepath"
 
 
690
  )
691
 
692
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
 
706
  label="Estado",
707
  interactive=False,
708
  show_label=False,
709
+ placeholder="Esperando acción..."
 
710
  )
711
 
712
  prompt_type.change(
 
717
  )
718
 
719
  generate_btn.click(
720
+ lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.")),
721
  outputs=[video_output, file_output, status_output],
722
  queue=True,
723
  ).then(
 
725
  inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
726
  outputs=[video_output, file_output, status_output]
727
  )
 
728
  gr.Markdown("### Instrucciones:")
729
+ gr.Markdown("1. **Clave API de Pexels:** Asegúrate de tener la variable de entorno `PEXELS_API_KEY`.\n"
730
+ "2. **Selecciona el método** y escribe tu tema o guion.\n"
731
+ "3. **Sube música** (opcional).\n"
732
+ "4. Haz clic en **Generar Video** y espera.\n"
733
+ "5. El video generado se eliminará automáticamente del servidor después de 3 horas.")
 
 
 
 
 
 
 
 
734
 
 
 
 
 
 
 
 
 
 
 
 
 
 
735
 
736
+ if __name__ == "__main__":
737
  logger.info("Iniciando aplicación Gradio...")
738
  try:
739
  app.launch(server_name="0.0.0.0", server_port=7860, share=False)