Spaces:
Running
Running
Update app.py
Browse files
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)
|
| 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)
|
| 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
|