Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -764,6 +764,7 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
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 |
return output_path
|
768 |
|
769 |
except ValueError as ve:
|
@@ -836,37 +837,43 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
836 |
logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
|
837 |
|
838 |
logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
|
839 |
-
|
840 |
def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
841 |
logger.info("="*80)
|
842 |
logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
|
|
|
843 |
input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
|
|
|
844 |
output_video = None
|
845 |
output_file = gr.update(value=None, visible=False)
|
846 |
status_msg = gr.update(value="⏳ Procesando...", interactive=False)
|
|
|
847 |
if not input_text or not input_text.strip():
|
848 |
logger.warning("Texto de entrada vacío.")
|
849 |
status_msg = gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
|
850 |
return output_video, output_file, status_msg
|
|
|
851 |
logger.info(f"Tipo de entrada: {prompt_type}")
|
852 |
logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
|
853 |
if musica_file:
|
854 |
logger.info(f"Archivo de música recibido: {musica_file}")
|
855 |
else:
|
856 |
logger.info("No se proporcionó archivo de música.")
|
|
|
857 |
try:
|
858 |
logger.info("Llamando a crear_video...")
|
859 |
video_path = crear_video(prompt_type, input_text, musica_file)
|
|
|
860 |
if video_path and os.path.exists(video_path):
|
861 |
logger.info(f"crear_video retornó path: {video_path}")
|
862 |
logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
|
863 |
output_video = video_path
|
864 |
output_file = gr.update(value=video_path, visible=True)
|
865 |
status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
|
866 |
-
print(f"\n\nLINK DE DESCARGA DIRECTO: file://{video_path}\n\n")
|
867 |
else:
|
868 |
logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
|
869 |
status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
|
|
|
870 |
except ValueError as ve:
|
871 |
logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
|
872 |
status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
|
@@ -877,403 +884,14 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
877 |
logger.info("Fin del handler run_app.")
|
878 |
return output_video, output_file, status_msg
|
879 |
|
880 |
-
def schedule_directory_deletion(directory_path, delay_hours=3):
|
881 |
-
import threading
|
882 |
-
import time
|
883 |
-
import shutil
|
884 |
-
def delete_directory():
|
885 |
-
time.sleep(delay_hours * 3600)
|
886 |
-
try:
|
887 |
-
if os.path.exists(directory_path):
|
888 |
-
shutil.rmtree(directory_path)
|
889 |
-
logger.info(f"Directorio temporal autoeliminado: {directory_path}")
|
890 |
-
except Exception as e:
|
891 |
-
logger.warning(f"No se pudo eliminar directorio {directory_path}: {str(e)}")
|
892 |
-
thread = threading.Thread(target=delete_directory)
|
893 |
-
thread.daemon = True
|
894 |
-
thread.start()
|
895 |
-
|
896 |
-
def crear_video(prompt_type, input_text, musica_file=None):
|
897 |
-
logger.info("="*80)
|
898 |
-
logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
|
899 |
-
logger.debug(f"Input: '{input_text[:100]}...'")
|
900 |
-
start_time = datetime.now()
|
901 |
-
temp_dir_intermediate = None
|
902 |
-
audio_tts_original = None
|
903 |
-
musica_audio_original = None
|
904 |
-
audio_tts = None
|
905 |
-
musica_audio = None
|
906 |
-
video_base = None
|
907 |
-
video_final = None
|
908 |
-
source_clips = []
|
909 |
-
clips_to_concatenate = []
|
910 |
-
try:
|
911 |
-
if prompt_type == "Generar Guion con IA":
|
912 |
-
guion = generate_script(input_text)
|
913 |
-
else:
|
914 |
-
guion = input_text.strip()
|
915 |
-
logger.info(f"Guion final ({len(guion)} chars): '{guion[:100]}...'")
|
916 |
-
if not guion.strip():
|
917 |
-
logger.error("El guion resultante está vacío o solo contiene espacios.")
|
918 |
-
raise ValueError("El guion está vacío.")
|
919 |
-
guion = guion.replace("na hora", "A la hora")
|
920 |
-
temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
|
921 |
-
logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
|
922 |
-
logger.info("Generando audio de voz...")
|
923 |
-
voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
|
924 |
-
tts_success = text_to_speech(guion, voz_path)
|
925 |
-
if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 1000:
|
926 |
-
logger.error(f"Fallo en la generación de voz. Archivo de audio no creado o es muy pequeño: {voz_path}")
|
927 |
-
raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
|
928 |
-
audio_tts_original = AudioFileClip(voz_path)
|
929 |
-
if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
|
930 |
-
logger.critical("Clip de audio TTS inicial es inválido (reader is None o duración <= 0) *después* de crear AudioFileClip.")
|
931 |
-
try: audio_tts_original.close()
|
932 |
-
except: pass
|
933 |
-
audio_tts_original = None
|
934 |
-
raise ValueError("Audio de voz generado es inválido después de procesamiento inicial.")
|
935 |
-
audio_tts = audio_tts_original
|
936 |
-
audio_duration = audio_tts_original.duration
|
937 |
-
logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
|
938 |
-
if audio_duration < 1.0:
|
939 |
-
logger.error(f"Duración audio voz ({audio_duration:.2f}s) es muy corta.")
|
940 |
-
raise ValueError("Generated voice audio is too short (min 1 second required).")
|
941 |
-
logger.info("Extrayendo palabras clave...")
|
942 |
-
try:
|
943 |
-
keywords = extract_visual_keywords_from_script(guion)
|
944 |
-
logger.info(f"Palabras clave identificadas: {keywords}")
|
945 |
-
except Exception as e:
|
946 |
-
logger.error(f"Error extrayendo keywords: {str(e)}", exc_info=True)
|
947 |
-
keywords = ["naturaleza", "paisaje"]
|
948 |
-
if not keywords:
|
949 |
-
keywords = ["video", "background"]
|
950 |
-
logger.info("Buscando videos en Pexels...")
|
951 |
-
videos_data = []
|
952 |
-
total_desired_videos = 10
|
953 |
-
per_page_per_keyword = max(1, total_desired_videos // len(keywords))
|
954 |
-
for keyword in keywords:
|
955 |
-
if len(videos_data) >= total_desired_videos: break
|
956 |
-
try:
|
957 |
-
videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=per_page_per_keyword)
|
958 |
-
if videos:
|
959 |
-
videos_data.extend(videos)
|
960 |
-
logger.info(f"Encontrados {len(videos)} videos para '{keyword}'. Total data: {len(videos_data)}")
|
961 |
-
except Exception as e:
|
962 |
-
logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
|
963 |
-
if len(videos_data) < total_desired_videos / 2:
|
964 |
-
logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
|
965 |
-
generic_keywords = ["nature", "city", "background", "abstract"]
|
966 |
-
for keyword in generic_keywords:
|
967 |
-
if len(videos_data) >= total_desired_videos: break
|
968 |
-
try:
|
969 |
-
videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=2)
|
970 |
-
if videos:
|
971 |
-
videos_data.extend(videos)
|
972 |
-
logger.info(f"Encontrados {len(videos)} videos para '{keyword}' (genérico). Total data: {len(videos_data)}")
|
973 |
-
except Exception as e:
|
974 |
-
logger.warning(f"Error buscando videos genéricos para '{keyword}': {str(e)}")
|
975 |
-
if not videos_data:
|
976 |
-
logger.error("No se encontraron videos en Pexels para ninguna palabra clave.")
|
977 |
-
raise ValueError("No se encontraron videos adecuados en Pexels.")
|
978 |
-
video_paths = []
|
979 |
-
logger.info(f"Intentando descargar {len(videos_data)} videos encontrados...")
|
980 |
-
for video in videos_data:
|
981 |
-
if 'video_files' not in video or not video['video_files']:
|
982 |
-
logger.debug(f"Saltando video sin archivos de video: {video.get('id')}")
|
983 |
-
continue
|
984 |
-
try:
|
985 |
-
best_quality = None
|
986 |
-
for vf in sorted(video['video_files'], key=lambda x: x.get('width', 0) * x.get('height', 0), reverse=True):
|
987 |
-
if 'link' in vf:
|
988 |
-
best_quality = vf
|
989 |
-
break
|
990 |
-
if best_quality and 'link' in best_quality:
|
991 |
-
path = download_video_file(best_quality['link'], temp_dir_intermediate)
|
992 |
-
if path:
|
993 |
-
video_paths.append(path)
|
994 |
-
logger.info(f"Video descargado OK desde {best_quality['link'][:50]}...")
|
995 |
-
else:
|
996 |
-
logger.warning(f"No se pudo descargar video desde {best_quality['link'][:50]}...")
|
997 |
-
else:
|
998 |
-
logger.warning(f"No se encontró enlace de descarga válido para video {video.get('id')}.")
|
999 |
-
except Exception as e:
|
1000 |
-
logger.warning(f"Error procesando/descargando video {video.get('id')}: {str(e)}")
|
1001 |
-
logger.info(f"Descargados {len(video_paths)} archivos de video utilizables.")
|
1002 |
-
if not video_paths:
|
1003 |
-
logger.error("No se pudo descargar ningún archivo de video utilizable.")
|
1004 |
-
raise ValueError("No se pudo descargar ningún video utilizable de Pexels.")
|
1005 |
-
logger.info("Procesando y concatenando videos descargados...")
|
1006 |
-
current_duration = 0
|
1007 |
-
min_clip_duration = 0.5
|
1008 |
-
max_clip_segment = 10.0
|
1009 |
-
for i, path in enumerate(video_paths):
|
1010 |
-
if current_duration >= audio_duration + max_clip_segment:
|
1011 |
-
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.")
|
1012 |
-
break
|
1013 |
-
clip = None
|
1014 |
-
try:
|
1015 |
-
logger.debug(f"[{i+1}/{len(video_paths)}] Abriendo clip: {path}")
|
1016 |
-
clip = VideoFileClip(path)
|
1017 |
-
source_clips.append(clip)
|
1018 |
-
if clip.reader is None or clip.duration is None or clip.duration <= 0:
|
1019 |
-
logger.warning(f"[{i+1}/{len(video_paths)}] Clip fuente {path} parece inválido (reader is None o duración <= 0). Saltando.")
|
1020 |
-
continue
|
1021 |
-
remaining_needed = audio_duration - current_duration
|
1022 |
-
potential_use_duration = min(clip.duration, max_clip_segment)
|
1023 |
-
if remaining_needed > 0:
|
1024 |
-
segment_duration = min(potential_use_duration, remaining_needed + min_clip_duration)
|
1025 |
-
segment_duration = max(min_clip_duration, segment_duration)
|
1026 |
-
segment_duration = min(segment_duration, clip.duration)
|
1027 |
-
if segment_duration >= min_clip_duration:
|
1028 |
-
try:
|
1029 |
-
sub = clip.subclip(0, segment_duration)
|
1030 |
-
if sub.reader is None or sub.duration is None or sub.duration <= 0:
|
1031 |
-
logger.warning(f"[{i+1}/{len(video_paths)}] Subclip generado de {path} es inválido. Saltando.")
|
1032 |
-
try: sub.close()
|
1033 |
-
except: pass
|
1034 |
-
continue
|
1035 |
-
clips_to_concatenate.append(sub)
|
1036 |
-
current_duration += sub.duration
|
1037 |
-
logger.debug(f"[{i+1}/{len(video_paths)}] Segmento añadido: {sub.duration:.1f}s (total video: {current_duration:.1f}/{audio_duration:.1f}s)")
|
1038 |
-
except Exception as sub_e:
|
1039 |
-
logger.warning(f"[{i+1}/{len(video_paths)}] Error creando subclip de {path} ({segment_duration:.1f}s): {str(sub_e)}")
|
1040 |
-
continue
|
1041 |
-
else:
|
1042 |
-
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.")
|
1043 |
-
else:
|
1044 |
-
logger.debug(f"[{i+1}/{len(video_paths)}] Duración de video base ya alcanzada. Saltando clip.")
|
1045 |
-
except Exception as e:
|
1046 |
-
logger.warning(f"[{i+1}/{len(video_paths)}] Error procesando video {path}: {str(e)}", exc_info=True)
|
1047 |
-
continue
|
1048 |
-
logger.info(f"Procesamiento de clips fuente finalizado. Se obtuvieron {len(clips_to_concatenate)} segmentos válidos.")
|
1049 |
-
if not clips_to_concatenate:
|
1050 |
-
logger.error("No hay segmentos de video válidos disponibles para crear la secuencia.")
|
1051 |
-
raise ValueError("No hay segmentos de video válidos disponibles para crear el video.")
|
1052 |
-
logger.info(f"Concatenando {len(clips_to_concatenate)} segmentos de video.")
|
1053 |
-
concatenated_base = None
|
1054 |
-
try:
|
1055 |
-
concatenated_base = concatenate_videoclips(clips_to_concatenate, method="chain")
|
1056 |
-
logger.info(f"Duración video base después de concatenación inicial: {concatenated_base.duration:.2f}s")
|
1057 |
-
if concatenated_base is None or concatenated_base.duration is None or concatenated_base.duration <= 0:
|
1058 |
-
logger.critical("Video base concatenado es inválido después de la primera concatenación (None o duración cero).")
|
1059 |
-
raise ValueError("Fallo al crear video base válido a partir de segmentos.")
|
1060 |
-
except Exception as e:
|
1061 |
-
logger.critical(f"Error durante la concatenación inicial: {str(e)}", exc_info=True)
|
1062 |
-
raise ValueError("Fallo durante la concatenación de video inicial.")
|
1063 |
-
finally:
|
1064 |
-
for clip_segment in clips_to_concatenate:
|
1065 |
-
try: clip_segment.close()
|
1066 |
-
except: pass
|
1067 |
-
clips_to_concatenate = []
|
1068 |
-
video_base = concatenated_base
|
1069 |
-
final_video_base = video_base
|
1070 |
-
if final_video_base.duration < audio_duration:
|
1071 |
-
logger.info(f"Video base ({final_video_base.duration:.2f}s) es más corto que el audio ({audio_duration:.2f}s). Repitiendo...")
|
1072 |
-
num_full_repeats = int(audio_duration // final_video_base.duration)
|
1073 |
-
remaining_duration = audio_duration % final_video_base.duration
|
1074 |
-
repeated_clips_list = [final_video_base] * num_full_repeats
|
1075 |
-
if remaining_duration > 0:
|
1076 |
-
try:
|
1077 |
-
remaining_clip = final_video_base.subclip(0, remaining_duration)
|
1078 |
-
if remaining_clip is None or remaining_clip.duration is None or remaining_clip.duration <= 0:
|
1079 |
-
logger.warning(f"Subclip generado para duración restante {remaining_duration:.2f}s es inválido. Saltando.")
|
1080 |
-
try: remaining_clip.close()
|
1081 |
-
except: pass
|
1082 |
-
else:
|
1083 |
-
repeated_clips_list.append(remaining_clip)
|
1084 |
-
logger.debug(f"Añadiendo segmento restante: {remaining_duration:.2f}s")
|
1085 |
-
except Exception as e:
|
1086 |
-
logger.warning(f"Error creando subclip para duración restante {remaining_duration:.2f}s: {str(e)}")
|
1087 |
-
if repeated_clips_list:
|
1088 |
-
logger.info(f"Concatenando {len(repeated_clips_list)} partes para repetición.")
|
1089 |
-
video_base_repeated = None
|
1090 |
-
try:
|
1091 |
-
video_base_repeated = concatenate_videoclips(repeated_clips_list, method="chain")
|
1092 |
-
logger.info(f"Duración del video base repetido: {video_base_repeated.duration:.2f}s")
|
1093 |
-
if video_base_repeated is None or video_base_repeated.duration is None or video_base_repeated.duration <= 0:
|
1094 |
-
logger.critical("Video base repetido concatenado es inválido.")
|
1095 |
-
raise ValueError("Fallo al crear video base repetido válido.")
|
1096 |
-
if final_video_base is not video_base_repeated:
|
1097 |
-
try: final_video_base.close()
|
1098 |
-
except: pass
|
1099 |
-
final_video_base = video_base_repeated
|
1100 |
-
except Exception as e:
|
1101 |
-
logger.critical(f"Error durante la concatenación de repetición: {str(e)}", exc_info=True)
|
1102 |
-
raise ValueError("Fallo durante la repetición de video.")
|
1103 |
-
finally:
|
1104 |
-
for clip in repeated_clips_list:
|
1105 |
-
if clip is not final_video_base:
|
1106 |
-
try: clip.close()
|
1107 |
-
except: pass
|
1108 |
-
if final_video_base.duration > audio_duration:
|
1109 |
-
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).")
|
1110 |
-
trimmed_video_base = None
|
1111 |
-
try:
|
1112 |
-
trimmed_video_base = final_video_base.subclip(0, audio_duration)
|
1113 |
-
if trimmed_video_base is None or trimmed_video_base.duration is None or trimmed_video_base.duration <= 0:
|
1114 |
-
logger.critical("Video base recortado es inválido.")
|
1115 |
-
raise ValueError("Fallo al crear video base recortado válido.")
|
1116 |
-
if final_video_base is not trimmed_video_base:
|
1117 |
-
try: final_video_base.close()
|
1118 |
-
except: pass
|
1119 |
-
final_video_base = trimmed_video_base
|
1120 |
-
except Exception as e:
|
1121 |
-
logger.critical(f"Error durante el recorte: {str(e)}", exc_info=True)
|
1122 |
-
raise ValueError("Fallo durante el recorte de video.")
|
1123 |
-
if final_video_base is None or final_video_base.duration is None or final_video_base.duration <= 0:
|
1124 |
-
logger.critical("Video base final es inválido antes de audio/escritura (None o duración cero).")
|
1125 |
-
raise ValueError("Video base final es inválido.")
|
1126 |
-
if final_video_base.size is None or final_video_base.size[0] <= 0 or final_video_base.size[1] <= 0:
|
1127 |
-
logger.critical(f"Video base final tiene tamaño inválido: {final_video_base.size}. No se puede escribir video.")
|
1128 |
-
raise ValueError("Video base final tiene tamaño inválido antes de escribir.")
|
1129 |
-
video_base = final_video_base
|
1130 |
-
logger.info("Procesando audio...")
|
1131 |
-
final_audio = audio_tts_original
|
1132 |
-
musica_audio_looped = None
|
1133 |
-
if musica_file:
|
1134 |
-
musica_audio_original = None
|
1135 |
-
try:
|
1136 |
-
music_path = os.path.join(temp_dir_intermediate, "musica_bg.mp3")
|
1137 |
-
shutil.copyfile(musica_file, music_path)
|
1138 |
-
logger.info(f"Música de fondo copiada a: {music_path}")
|
1139 |
-
musica_audio_original = AudioFileClip(music_path)
|
1140 |
-
if musica_audio_original.reader is None or musica_audio_original.duration is None or musica_audio_original.duration <= 0:
|
1141 |
-
logger.warning("Clip de música de fondo parece inválido o tiene duración cero. Saltando música.")
|
1142 |
-
try: musica_audio_original.close()
|
1143 |
-
except: pass
|
1144 |
-
musica_audio_original = None
|
1145 |
-
else:
|
1146 |
-
musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
|
1147 |
-
logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
|
1148 |
-
if musica_audio_looped is None or musica_audio_looped.duration is None or musica_audio_looped.duration <= 0:
|
1149 |
-
logger.warning("Clip de música de fondo loopeado es inválido. Saltando música.")
|
1150 |
-
try: musica_audio_looped.close()
|
1151 |
-
except: pass
|
1152 |
-
musica_audio_looped = None
|
1153 |
-
if musica_audio_looped:
|
1154 |
-
composite_audio = CompositeAudioClip([
|
1155 |
-
musica_audio_looped.volumex(0.2),
|
1156 |
-
audio_tts_original.volumex(1.0)
|
1157 |
-
])
|
1158 |
-
if composite_audio.duration is None or composite_audio.duration <= 0:
|
1159 |
-
logger.warning("Clip de audio compuesto es inválido (None o duración cero). Usando solo audio de voz.")
|
1160 |
-
try: composite_audio.close()
|
1161 |
-
except: pass
|
1162 |
-
final_audio = audio_tts_original
|
1163 |
-
else:
|
1164 |
-
logger.info("Mezcla de audio completada (voz + música).")
|
1165 |
-
final_audio = composite_audio
|
1166 |
-
musica_audio = musica_audio_looped
|
1167 |
-
except Exception as e:
|
1168 |
-
logger.warning(f"Error procesando música de fondo: {str(e)}", exc_info=True)
|
1169 |
-
final_audio = audio_tts_original
|
1170 |
-
musica_audio = None
|
1171 |
-
logger.warning("Usando solo audio de voz debido a un error con la música.")
|
1172 |
-
if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
|
1173 |
-
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.")
|
1174 |
-
try:
|
1175 |
-
if final_audio.duration > video_base.duration:
|
1176 |
-
trimmed_final_audio = final_audio.subclip(0, video_base.duration)
|
1177 |
-
if trimmed_final_audio is None or trimmed_final_audio.duration <= 0:
|
1178 |
-
logger.warning("Audio final recortado es inválido. Usando audio final original.")
|
1179 |
-
try: trimmed_final_audio.close()
|
1180 |
-
except: pass
|
1181 |
-
else:
|
1182 |
-
if final_audio is not trimmed_final_audio:
|
1183 |
-
try: final_audio.close()
|
1184 |
-
except: pass
|
1185 |
-
final_audio = trimmed_final_audio
|
1186 |
-
logger.warning("Audio final recortado para que coincida con la duración del video.")
|
1187 |
-
except Exception as e:
|
1188 |
-
logger.warning(f"Error ajustando duración del audio final: {str(e)}")
|
1189 |
-
logger.info("Renderizando video final...")
|
1190 |
-
video_final = video_base.set_audio(final_audio)
|
1191 |
-
if video_final is None or video_final.duration is None or video_final.duration <= 0:
|
1192 |
-
logger.critical("Clip de video final (con audio) es inválido antes de escribir (None o duración cero).")
|
1193 |
-
raise ValueError("Clip de video final es inválido antes de escribir.")
|
1194 |
-
output_filename = "final_video.mp4"
|
1195 |
-
output_path = os.path.join(temp_dir_intermediate, output_filename)
|
1196 |
-
logger.info(f"Escribiendo video final a: {output_path}")
|
1197 |
-
if not output_path or not isinstance(output_path, str):
|
1198 |
-
logger.critical(f"output_path no es válido: {output_path}")
|
1199 |
-
raise ValueError("El nombre del archivo de salida no es válido.")
|
1200 |
-
try:
|
1201 |
-
video_final.write_videofile(
|
1202 |
-
filename=output_path,
|
1203 |
-
fps=24,
|
1204 |
-
threads=4,
|
1205 |
-
codec="libx264",
|
1206 |
-
audio_codec="aac",
|
1207 |
-
preset="medium",
|
1208 |
-
logger='bar',
|
1209 |
-
bitrate="2000k",
|
1210 |
-
ffmpeg_params=['-vf', 'scale=1280:720']
|
1211 |
-
)
|
1212 |
-
except Exception as e:
|
1213 |
-
logger.critical(f"Error al escribir el video final: {str(e)}", exc_info=True)
|
1214 |
-
raise ValueError(f"Fallo al escribir el video final: {str(e)}")
|
1215 |
-
total_time = (datetime.now() - start_time).total_seconds()
|
1216 |
-
logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {output_path} | Tiempo total: {total_time:.2f}s")
|
1217 |
-
schedule_directory_deletion(temp_dir_intermediate)
|
1218 |
-
return output_path
|
1219 |
-
except ValueError as ve:
|
1220 |
-
logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
|
1221 |
-
raise ve
|
1222 |
-
except Exception as e:
|
1223 |
-
logger.critical(f"ERROR CRÍTICO NO CONTROLADO en crear_video: {str(e)}", exc_info=True)
|
1224 |
-
raise e
|
1225 |
-
finally:
|
1226 |
-
logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
|
1227 |
-
for clip in source_clips:
|
1228 |
-
try:
|
1229 |
-
clip.close()
|
1230 |
-
except Exception as e:
|
1231 |
-
logger.warning(f"Error cerrando clip de video fuente en finally: {str(e)}")
|
1232 |
-
for clip_segment in clips_to_concatenate:
|
1233 |
-
try:
|
1234 |
-
clip_segment.close()
|
1235 |
-
except Exception as e:
|
1236 |
-
logger.warning(f"Error cerrando segmento de video en finally: {str(e)}")
|
1237 |
-
if musica_audio is not None:
|
1238 |
-
try:
|
1239 |
-
musica_audio.close()
|
1240 |
-
except Exception as e:
|
1241 |
-
logger.warning(f"Error cerrando musica_audio (procesada) en finally: {str(e)}")
|
1242 |
-
if musica_audio_original is not None and musica_audio_original is not musica_audio:
|
1243 |
-
try:
|
1244 |
-
musica_audio_original.close()
|
1245 |
-
except Exception as e:
|
1246 |
-
logger.warning(f"Error cerrando musica_audio_original en finally: {str(e)}")
|
1247 |
-
if audio_tts is not None and audio_tts is not audio_tts_original:
|
1248 |
-
try:
|
1249 |
-
audio_tts.close()
|
1250 |
-
except Exception as e:
|
1251 |
-
logger.warning(f"Error cerrando audio_tts (procesada) en finally: {str(e)}")
|
1252 |
-
if audio_tts_original is not None:
|
1253 |
-
try:
|
1254 |
-
audio_tts_original.close()
|
1255 |
-
except Exception as e:
|
1256 |
-
logger.warning(f"Error cerrando audio_tts_original en finally: {str(e)}")
|
1257 |
-
if video_final is not None:
|
1258 |
-
try:
|
1259 |
-
video_final.close()
|
1260 |
-
except Exception as e:
|
1261 |
-
logger.warning(f"Error cerrando video_final en finally: {str(e)}")
|
1262 |
-
elif video_base is not None and video_base is not video_final:
|
1263 |
-
try:
|
1264 |
-
video_base.close()
|
1265 |
-
except Exception as e:
|
1266 |
-
logger.warning(f"Error cerrando video_base en finally: {str(e)}")
|
1267 |
-
if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
|
1268 |
-
schedule_directory_deletion(temp_dir_intermediate)
|
1269 |
-
logger.info(f"Directorio temporal {temp_dir_intermediate} programado para eliminación en 3 horas.")
|
1270 |
-
|
1271 |
with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
|
1272 |
.gradio-container {max-width: 800px; margin: auto;}
|
1273 |
h1 {text-align: center;}
|
1274 |
""") as app:
|
|
|
1275 |
gr.Markdown("# 🎬 Generador Automático de Videos con IA")
|
1276 |
gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
|
|
|
1277 |
with gr.Row():
|
1278 |
with gr.Column():
|
1279 |
prompt_type = gr.Radio(
|
@@ -1281,6 +899,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
1281 |
label="Método de Entrada",
|
1282 |
value="Generar Guion con IA"
|
1283 |
)
|
|
|
1284 |
with gr.Column(visible=True) as ia_guion_column:
|
1285 |
prompt_ia = gr.Textbox(
|
1286 |
label="Tema para IA",
|
@@ -1289,6 +908,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
1289 |
max_lines=4,
|
1290 |
value=""
|
1291 |
)
|
|
|
1292 |
with gr.Column(visible=False) as manual_guion_column:
|
1293 |
prompt_manual = gr.Textbox(
|
1294 |
label="Tu Guion Completo",
|
@@ -1297,13 +917,16 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
1297 |
max_lines=10,
|
1298 |
value=""
|
1299 |
)
|
|
|
1300 |
musica_input = gr.Audio(
|
1301 |
label="Música de fondo (opcional)",
|
1302 |
type="filepath",
|
1303 |
interactive=True,
|
1304 |
value=None
|
1305 |
)
|
|
|
1306 |
generate_btn = gr.Button("✨ Generar Video", variant="primary")
|
|
|
1307 |
with gr.Column():
|
1308 |
video_output = gr.Video(
|
1309 |
label="Previsualización del Video Generado",
|
@@ -1322,12 +945,14 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
1322 |
placeholder="Esperando acción...",
|
1323 |
value="Esperando entrada..."
|
1324 |
)
|
|
|
1325 |
prompt_type.change(
|
1326 |
lambda x: (gr.update(visible=x == "Generar Guion con IA"),
|
1327 |
gr.update(visible=x == "Usar Mi Guion")),
|
1328 |
inputs=prompt_type,
|
1329 |
outputs=[ia_guion_column, manual_guion_column]
|
1330 |
)
|
|
|
1331 |
generate_btn.click(
|
1332 |
lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
|
1333 |
outputs=[video_output, file_output, status_output],
|
@@ -1337,9 +962,11 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
1337 |
inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
|
1338 |
outputs=[video_output, file_output, status_output]
|
1339 |
)
|
|
|
1340 |
gr.Markdown("### Instrucciones:")
|
1341 |
gr.Markdown("""
|
1342 |
1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
|
|
|
1343 |
""")
|
1344 |
gr.Markdown("---")
|
1345 |
gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
|
@@ -1354,22 +981,13 @@ if __name__ == "__main__":
|
|
1354 |
logger.info("Clips base de MoviePy (como ColorClip) creados y cerrados exitosamente. FFmpeg parece accesible.")
|
1355 |
except Exception as e:
|
1356 |
logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
|
|
|
1357 |
except Exception as e:
|
1358 |
logger.critical(f"Fallo al importar MoviePy. Asegúrate de que está instalado. Error: {e}", exc_info=True)
|
|
|
1359 |
logger.info("Iniciando aplicación Gradio...")
|
1360 |
try:
|
1361 |
-
app.
|
1362 |
-
max_size=1,
|
1363 |
-
api_open=False
|
1364 |
-
).launch(
|
1365 |
-
server_name="0.0.0.0",
|
1366 |
-
server_port=7860,
|
1367 |
-
share=False,
|
1368 |
-
prevent_thread_lock=True,
|
1369 |
-
show_error=True,
|
1370 |
-
max_threads=1,
|
1371 |
-
timeout=10800 # 3 horas en segundos
|
1372 |
-
)
|
1373 |
except Exception as e:
|
1374 |
logger.critical(f"No se pudo iniciar la app: {str(e)}", exc_info=True)
|
1375 |
raise
|
|
|
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:
|
|
|
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):
|
842 |
logger.info("="*80)
|
843 |
logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
|
844 |
+
|
845 |
input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
|
846 |
+
|
847 |
output_video = None
|
848 |
output_file = gr.update(value=None, visible=False)
|
849 |
status_msg = gr.update(value="⏳ Procesando...", interactive=False)
|
850 |
+
|
851 |
if not input_text or not input_text.strip():
|
852 |
logger.warning("Texto de entrada vacío.")
|
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)
|
|
|
884 |
logger.info("Fin del handler run_app.")
|
885 |
return output_video, output_file, status_msg
|
886 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
887 |
with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
|
888 |
.gradio-container {max-width: 800px; margin: auto;}
|
889 |
h1 {text-align: center;}
|
890 |
""") as app:
|
891 |
+
|
892 |
gr.Markdown("# 🎬 Generador Automático de Videos con IA")
|
893 |
gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
|
894 |
+
|
895 |
with gr.Row():
|
896 |
with gr.Column():
|
897 |
prompt_type = gr.Radio(
|
|
|
899 |
label="Método de Entrada",
|
900 |
value="Generar Guion con IA"
|
901 |
)
|
902 |
+
|
903 |
with gr.Column(visible=True) as ia_guion_column:
|
904 |
prompt_ia = gr.Textbox(
|
905 |
label="Tema para IA",
|
|
|
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",
|
|
|
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")
|
929 |
+
|
930 |
with gr.Column():
|
931 |
video_output = gr.Video(
|
932 |
label="Previsualización del Video Generado",
|
|
|
945 |
placeholder="Esperando acción...",
|
946 |
value="Esperando entrada..."
|
947 |
)
|
948 |
+
|
949 |
prompt_type.change(
|
950 |
lambda x: (gr.update(visible=x == "Generar Guion con IA"),
|
951 |
gr.update(visible=x == "Usar Mi Guion")),
|
952 |
inputs=prompt_type,
|
953 |
outputs=[ia_guion_column, manual_guion_column]
|
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],
|
|
|
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 |
+
|
970 |
""")
|
971 |
gr.Markdown("---")
|
972 |
gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
|
|
|
981 |
logger.info("Clips base de MoviePy (como ColorClip) creados y cerrados exitosamente. FFmpeg parece accesible.")
|
982 |
except Exception as e:
|
983 |
logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
|
984 |
+
|
985 |
except Exception as e:
|
986 |
logger.critical(f"Fallo al importar MoviePy. Asegúrate de que está instalado. Error: {e}", exc_info=True)
|
987 |
+
|
988 |
logger.info("Iniciando aplicación Gradio...")
|
989 |
try:
|
990 |
+
app.launch(server_name="0.0.0.0", server_port=7860, share=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
991 |
except Exception as e:
|
992 |
logger.critical(f"No se pudo iniciar la app: {str(e)}", exc_info=True)
|
993 |
raise
|