gnosticdev commited on
Commit
90d3598
·
verified ·
1 Parent(s): 0104a90

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +491 -14
app.py CHANGED
@@ -842,42 +842,31 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
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) # Inicialmente oculto
846
  status_msg = gr.update(value="⏳ Procesando...", interactive=False)
847
-
848
  if not input_text or not input_text.strip():
849
  logger.warning("Texto de entrada vacío.")
850
  status_msg = gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
851
  return output_video, output_file, status_msg
852
-
853
  logger.info(f"Tipo de entrada: {prompt_type}")
854
  logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
855
-
856
  if musica_file:
857
  logger.info(f"Archivo de música recibido: {musica_file}")
858
  else:
859
  logger.info("No se proporcionó archivo de música.")
860
-
861
  try:
862
  logger.info("Llamando a crear_video...")
863
  video_path = crear_video(prompt_type, input_text, musica_file)
864
-
865
  if video_path and os.path.exists(video_path):
866
  logger.info(f"crear_video retornó path: {video_path}")
867
  logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
868
-
869
- # ASIGNACIÓN CORRECTA - HACER VISIBLE EL BOTÓN DE DESCARGA
870
  output_video = video_path
871
- output_file = gr.update(value=video_path, visible=True) # CAMBIAR A TRUE
872
  status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
873
-
874
- # IMPRIMIR LINK DIRECTO EN CONSOLA
875
  print(f"\n\nLINK DE DESCARGA DIRECTO: file://{video_path}\n\n")
876
-
877
  else:
878
  logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
879
  status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
880
-
881
  except ValueError as ve:
882
  logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
883
  status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
@@ -886,4 +875,492 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
886
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
887
  finally:
888
  logger.info("Fin del handler run_app.")
889
- return output_video, output_file, status_msg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)
 
875
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
876
  finally:
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
+ )
1210
+ except Exception as e:
1211
+ logger.critical(f"Error al escribir el video final: {str(e)}", exc_info=True)
1212
+ raise ValueError(f"Fallo al escribir el video final: {str(e)}")
1213
+ total_time = (datetime.now() - start_time).total_seconds()
1214
+ logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {output_path} | Tiempo total: {total_time:.2f}s")
1215
+ schedule_directory_deletion(temp_dir_intermediate)
1216
+ return output_path
1217
+ except ValueError as ve:
1218
+ logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
1219
+ raise ve
1220
+ except Exception as e:
1221
+ logger.critical(f"ERROR CRÍTICO NO CONTROLADO en crear_video: {str(e)}", exc_info=True)
1222
+ raise e
1223
+ finally:
1224
+ logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
1225
+ for clip in source_clips:
1226
+ try:
1227
+ clip.close()
1228
+ except Exception as e:
1229
+ logger.warning(f"Error cerrando clip de video fuente en finally: {str(e)}")
1230
+ for clip_segment in clips_to_concatenate:
1231
+ try:
1232
+ clip_segment.close()
1233
+ except Exception as e:
1234
+ logger.warning(f"Error cerrando segmento de video en finally: {str(e)}")
1235
+ if musica_audio is not None:
1236
+ try:
1237
+ musica_audio.close()
1238
+ except Exception as e:
1239
+ logger.warning(f"Error cerrando musica_audio (procesada) en finally: {str(e)}")
1240
+ if musica_audio_original is not None and musica_audio_original is not musica_audio:
1241
+ try:
1242
+ musica_audio_original.close()
1243
+ except Exception as e:
1244
+ logger.warning(f"Error cerrando musica_audio_original en finally: {str(e)}")
1245
+ if audio_tts is not None and audio_tts is not audio_tts_original:
1246
+ try:
1247
+ audio_tts.close()
1248
+ except Exception as e:
1249
+ logger.warning(f"Error cerrando audio_tts (procesada) en finally: {str(e)}")
1250
+ if audio_tts_original is not None:
1251
+ try:
1252
+ audio_tts_original.close()
1253
+ except Exception as e:
1254
+ logger.warning(f"Error cerrando audio_tts_original en finally: {str(e)}")
1255
+ if video_final is not None:
1256
+ try:
1257
+ video_final.close()
1258
+ except Exception as e:
1259
+ logger.warning(f"Error cerrando video_final en finally: {str(e)}")
1260
+ elif video_base is not None and video_base is not video_final:
1261
+ try:
1262
+ video_base.close()
1263
+ except Exception as e:
1264
+ logger.warning(f"Error cerrando video_base en finally: {str(e)}")
1265
+ if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
1266
+ schedule_directory_deletion(temp_dir_intermediate)
1267
+ logger.info(f"Directorio temporal {temp_dir_intermediate} programado para eliminación en 3 horas.")
1268
+
1269
+ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
1270
+ .gradio-container {max-width: 800px; margin: auto;}
1271
+ h1 {text-align: center;}
1272
+ """) as app:
1273
+ gr.Markdown("# 🎬 Generador Automático de Videos con IA")
1274
+ gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
1275
+ with gr.Row():
1276
+ with gr.Column():
1277
+ prompt_type = gr.Radio(
1278
+ ["Generar Guion con IA", "Usar Mi Guion"],
1279
+ label="Método de Entrada",
1280
+ value="Generar Guion con IA"
1281
+ )
1282
+ with gr.Column(visible=True) as ia_guion_column:
1283
+ prompt_ia = gr.Textbox(
1284
+ label="Tema para IA",
1285
+ lines=2,
1286
+ placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer, mostrando la belleza de la naturaleza...",
1287
+ max_lines=4,
1288
+ value=""
1289
+ )
1290
+ with gr.Column(visible=False) as manual_guion_column:
1291
+ prompt_manual = gr.Textbox(
1292
+ label="Tu Guion Completo",
1293
+ lines=5,
1294
+ 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!",
1295
+ max_lines=10,
1296
+ value=""
1297
+ )
1298
+ musica_input = gr.Audio(
1299
+ label="Música de fondo (opcional)",
1300
+ type="filepath",
1301
+ interactive=True,
1302
+ value=None
1303
+ )
1304
+ generate_btn = gr.Button("✨ Generar Video", variant="primary")
1305
+ with gr.Column():
1306
+ video_output = gr.Video(
1307
+ label="Previsualización del Video Generado",
1308
+ interactive=False,
1309
+ height=400
1310
+ )
1311
+ file_output = gr.File(
1312
+ label="Descargar Archivo de Video",
1313
+ interactive=False,
1314
+ visible=False
1315
+ )
1316
+ status_output = gr.Textbox(
1317
+ label="Estado",
1318
+ interactive=False,
1319
+ show_label=False,
1320
+ placeholder="Esperando acción...",
1321
+ value="Esperando entrada..."
1322
+ )
1323
+ prompt_type.change(
1324
+ lambda x: (gr.update(visible=x == "Generar Guion con IA"),
1325
+ gr.update(visible=x == "Usar Mi Guion")),
1326
+ inputs=prompt_type,
1327
+ outputs=[ia_guion_column, manual_guion_column]
1328
+ )
1329
+ generate_btn.click(
1330
+ lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
1331
+ outputs=[video_output, file_output, status_output],
1332
+ queue=True,
1333
+ ).then(
1334
+ run_app,
1335
+ inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
1336
+ outputs=[video_output, file_output, status_output]
1337
+ )
1338
+ gr.Markdown("### Instrucciones:")
1339
+ gr.Markdown("""
1340
+ 1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
1341
+ """)
1342
+ gr.Markdown("---")
1343
+ gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
1344
+
1345
+ if __name__ == "__main__":
1346
+ logger.info("Verificando dependencias críticas...")
1347
+ try:
1348
+ from moviepy.editor import ColorClip
1349
+ try:
1350
+ temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
1351
+ temp_clip.close()
1352
+ logger.info("Clips base de MoviePy (como ColorClip) creados y cerrados exitosamente. FFmpeg parece accesible.")
1353
+ except Exception as e:
1354
+ logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
1355
+ except Exception as e:
1356
+ logger.critical(f"Fallo al importar MoviePy. Asegúrate de que está instalado. Error: {e}", exc_info=True)
1357
+ logger.info("Iniciando aplicación Gradio...")
1358
+ try:
1359
+ app.queue(concurrency_count=1, max_size=1, api_open=False).launch(
1360
+ server_name="0.0.0.0",
1361
+ server_port=7860,
1362
+ share=False
1363
+ )
1364
+ except Exception as e:
1365
+ logger.critical(f"No se pudo iniciar la app: {str(e)}", exc_info=True)
1366
+ raise