Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
@@ -859,11 +859,11 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
|
|
859 |
except Exception as e:
|
860 |
logger.warning(f"Error ajustando duración del audio final: {str(e)}")
|
861 |
|
862 |
-
|
863 |
output_filename = f"video_{int(time.time())}.mp4" # Nombre único con timestamp
|
864 |
output_path = os.path.join(temp_dir_intermediate, output_filename)
|
865 |
|
866 |
-
# Escribir el video
|
867 |
video_final.write_videofile(
|
868 |
output_path,
|
869 |
fps=24,
|
@@ -878,28 +878,27 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
|
|
878 |
logger='bar'
|
879 |
)
|
880 |
|
881 |
-
#
|
|
|
882 |
try:
|
883 |
-
|
884 |
-
permanent_path = f"/tmp/{output_filename}"
|
885 |
-
shutil.copy(output_path, permanent_path) # Usamos copy() en lugar de move()
|
886 |
-
|
887 |
-
# Cierra los clips para liberar memoria
|
888 |
-
video_final.close()
|
889 |
-
if 'video_base' in locals():
|
890 |
-
video_base.close()
|
891 |
-
|
892 |
logger.info(f"Video guardado permanentemente en: {permanent_path}")
|
893 |
-
return permanent_path
|
894 |
-
|
895 |
except Exception as move_error:
|
896 |
logger.error(f"Error moviendo archivo: {str(move_error)}. Usando path original.")
|
897 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
898 |
|
899 |
total_time = (datetime.now() - start_time).total_seconds()
|
900 |
-
logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {
|
901 |
|
902 |
-
return
|
903 |
|
904 |
except ValueError as ve:
|
905 |
logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
|
@@ -970,214 +969,4 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
|
|
970 |
except Exception as e:
|
971 |
logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
|
972 |
|
973 |
-
logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
|
974 |
-
|
975 |
-
|
976 |
-
# run_app ahora recibe todos los inputs, incluyendo la voz seleccionada
|
977 |
-
def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice): # <-- Recibe el valor del Dropdown
|
978 |
-
logger.info("="*80)
|
979 |
-
logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
|
980 |
-
|
981 |
-
# Elegir el texto de entrada basado en el prompt_type
|
982 |
-
input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
|
983 |
-
|
984 |
-
output_video = None
|
985 |
-
output_file = None
|
986 |
-
status_msg = gr.update(value="⏳ Procesando...", interactive=False)
|
987 |
-
|
988 |
-
if not input_text or not input_text.strip():
|
989 |
-
logger.warning("Texto de entrada vacío.")
|
990 |
-
# Retornar None para video y archivo, actualizar estado con mensaje de error
|
991 |
-
return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
|
992 |
-
|
993 |
-
# Validar la voz seleccionada. Si no es válida, usar la por defecto.
|
994 |
-
# AVAILABLE_VOICES se obtiene al inicio. Hay que buscar si el voice_id existe en la lista de pares (nombre, id)
|
995 |
-
voice_ids_disponibles = [v[1] for v in AVAILABLE_VOICES]
|
996 |
-
if selected_voice not in voice_ids_disponibles:
|
997 |
-
logger.warning(f"Voz seleccionada inválida o no encontrada en la lista: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE_ID}.")
|
998 |
-
selected_voice = DEFAULT_VOICE_ID # <-- Usar el ID de la voz por defecto
|
999 |
-
else:
|
1000 |
-
logger.info(f"Voz seleccionada validada: {selected_voice}")
|
1001 |
-
|
1002 |
-
|
1003 |
-
logger.info(f"Tipo de entrada: {prompt_type}")
|
1004 |
-
logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
|
1005 |
-
if musica_file:
|
1006 |
-
logger.info(f"Archivo de música recibido: {musica_file}")
|
1007 |
-
else:
|
1008 |
-
logger.info("No se proporcionó archivo de música.")
|
1009 |
-
logger.info(f"Voz final a usar (ID): {selected_voice}") # Loguear el ID de la voz final
|
1010 |
-
|
1011 |
-
try:
|
1012 |
-
logger.info("Llamando a crear_video...")
|
1013 |
-
# Pasar el input_text elegido, la voz seleccionada (el ID) y el archivo de música a crear_video
|
1014 |
-
video_path = crear_video(prompt_type, input_text, selected_voice, musica_file) # <-- PASAR selected_voice (ID) a crear_video
|
1015 |
-
|
1016 |
-
if video_path and os.path.exists(video_path):
|
1017 |
-
logger.info(f"crear_video retornó path: {video_path}")
|
1018 |
-
logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
|
1019 |
-
output_video = video_path # Establecer valor del componente de video
|
1020 |
-
output_file = video_path # Establecer valor del componente de archivo para descarga
|
1021 |
-
status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
|
1022 |
-
else:
|
1023 |
-
logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
|
1024 |
-
status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
|
1025 |
-
|
1026 |
-
except ValueError as ve:
|
1027 |
-
logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
|
1028 |
-
status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
|
1029 |
-
except Exception as e:
|
1030 |
-
logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
|
1031 |
-
status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
|
1032 |
-
finally:
|
1033 |
-
logger.info("Fin del handler run_app.")
|
1034 |
-
return output_video, output_file, status_msg
|
1035 |
-
|
1036 |
-
|
1037 |
-
# Interfaz de Gradio
|
1038 |
-
with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
|
1039 |
-
.gradio-container {max-width: 800px; margin: auto;}
|
1040 |
-
h1 {text-align: center;}
|
1041 |
-
""") as app:
|
1042 |
-
|
1043 |
-
gr.Markdown("# 🎬 Generador Automático de Videos con IA")
|
1044 |
-
gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
|
1045 |
-
|
1046 |
-
with gr.Row():
|
1047 |
-
with gr.Column():
|
1048 |
-
prompt_type = gr.Radio(
|
1049 |
-
["Generar Guion con IA", "Usar Mi Guion"],
|
1050 |
-
label="Método de Entrada",
|
1051 |
-
value="Generar Guion con IA"
|
1052 |
-
)
|
1053 |
-
|
1054 |
-
# Contenedores para los campos de texto para controlar la visibilidad
|
1055 |
-
with gr.Column(visible=True) as ia_guion_column:
|
1056 |
-
prompt_ia = gr.Textbox(
|
1057 |
-
label="Tema para IA",
|
1058 |
-
lines=2,
|
1059 |
-
placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer, mostrando la belleza de la naturaleza...",
|
1060 |
-
max_lines=4,
|
1061 |
-
value=""
|
1062 |
-
# visible=... <-- ¡NO DEBE ESTAR AQUÍ!
|
1063 |
-
)
|
1064 |
-
|
1065 |
-
with gr.Column(visible=False) as manual_guion_column:
|
1066 |
-
prompt_manual = gr.Textbox(
|
1067 |
-
label="Tu Guion Completo",
|
1068 |
-
lines=5,
|
1069 |
-
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!",
|
1070 |
-
max_lines=10,
|
1071 |
-
value=""
|
1072 |
-
# visible=... <-- ¡NO DEBE ESTAR AQUÍ!
|
1073 |
-
)
|
1074 |
-
|
1075 |
-
musica_input = gr.Audio(
|
1076 |
-
label="Música de fondo (opcional)",
|
1077 |
-
type="filepath",
|
1078 |
-
interactive=True,
|
1079 |
-
value=None
|
1080 |
-
# visible=... <-- ¡NO DEBE ESTAR AQUÍ!
|
1081 |
-
)
|
1082 |
-
|
1083 |
-
# --- COMPONENTE: Selección de Voz ---
|
1084 |
-
voice_dropdown = gr.Dropdown(
|
1085 |
-
label="Seleccionar Voz para Guion",
|
1086 |
-
choices=AVAILABLE_VOICES, # Usar la lista obtenida al inicio
|
1087 |
-
value=DEFAULT_VOICE_ID, # Usar el ID de la voz por defecto calculada
|
1088 |
-
interactive=True
|
1089 |
-
# visible=... <-- ¡NO DEBE ESTAR AQUÍ!
|
1090 |
-
)
|
1091 |
-
# --- FIN COMPONENTE ---
|
1092 |
-
|
1093 |
-
|
1094 |
-
generate_btn = gr.Button("✨ Generar Video", variant="primary")
|
1095 |
-
|
1096 |
-
with gr.Column():
|
1097 |
-
video_output = gr.Video(
|
1098 |
-
label="Previsualización del Video Generado",
|
1099 |
-
interactive=False,
|
1100 |
-
height=400
|
1101 |
-
# visible=... <-- ¡NO DEBE ESTAR AQUÍ!
|
1102 |
-
)
|
1103 |
-
file_output = gr.File(
|
1104 |
-
label="Descargar Archivo de Video",
|
1105 |
-
interactive=False,
|
1106 |
-
visible=False # <-- ESTÁ BIEN AQUÍ
|
1107 |
-
# visible=... <-- ¡NO DEBE ESTAR AQUÍ si ya está visible=False arriba!
|
1108 |
-
)
|
1109 |
-
status_output = gr.Textbox(
|
1110 |
-
label="Estado",
|
1111 |
-
interactive=False,
|
1112 |
-
show_label=False,
|
1113 |
-
placeholder="Esperando acción...",
|
1114 |
-
value="Esperando entrada..."
|
1115 |
-
# visible=... <-- ¡NO DEBE ESTAR AQUÍ!
|
1116 |
-
)
|
1117 |
-
|
1118 |
-
# Evento para mostrar/ocultar los campos de texto según el tipo de prompt
|
1119 |
-
prompt_type.change(
|
1120 |
-
lambda x: (gr.update(visible=x == "Generar Guion con IA"),
|
1121 |
-
gr.update(visible=x == "Usar Mi Guion")),
|
1122 |
-
inputs=prompt_type,
|
1123 |
-
outputs=[ia_guion_column, manual_guion_column] # Apuntar a las Columnas contenedoras
|
1124 |
-
)
|
1125 |
-
|
1126 |
-
# Evento click del botón de generar video
|
1127 |
-
generate_btn.click(
|
1128 |
-
# Acción 1 (síncrona): Resetear salidas y establecer estado
|
1129 |
-
lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
|
1130 |
-
outputs=[video_output, file_output, status_output],
|
1131 |
-
queue=True, # Usar la cola de Gradio
|
1132 |
-
).then(
|
1133 |
-
# Acción 2 (asíncrona): Llamar a la función principal
|
1134 |
-
run_app,
|
1135 |
-
# PASAR TODOS LOS INPUTS DE LA INTERFAZ que run_app espera
|
1136 |
-
inputs=[prompt_type, prompt_ia, prompt_manual, musica_input, voice_dropdown], # <-- Pasar los 5 inputs a run_app
|
1137 |
-
# run_app retornará los 3 outputs esperados
|
1138 |
-
outputs=[video_output, file_output, status_output]
|
1139 |
-
).then(
|
1140 |
-
# Acción 3 (síncrona): Hacer visible el enlace de descarga
|
1141 |
-
lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
|
1142 |
-
inputs=[video_output, file_output, status_output],
|
1143 |
-
outputs=[file_output]
|
1144 |
-
)
|
1145 |
-
|
1146 |
-
|
1147 |
-
gr.Markdown("### Instrucciones:")
|
1148 |
-
gr.Markdown("""
|
1149 |
-
1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
|
1150 |
-
2. **Selecciona el tipo de entrada**: "Generar Guion con IA" o "Usar Mi Guion".
|
1151 |
-
3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.).
|
1152 |
-
4. **Selecciona la voz** deseada del desplegable.
|
1153 |
-
5. **Haz clic en "✨ Generar Video"**.
|
1154 |
-
6. Espera a que se procese el video. Verás el estado.
|
1155 |
-
7. La previsualización aparecerá si es posible, y siempre un enlace **Descargar Archivo de Video** se mostrará si la generación fue exitosa.
|
1156 |
-
8. Revisa `video_generator_full.log` para detalles si hay errores.
|
1157 |
-
""")
|
1158 |
-
gr.Markdown("---")
|
1159 |
-
gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
|
1160 |
-
|
1161 |
-
if __name__ == "__main__":
|
1162 |
-
logger.info("Verificando dependencias críticas...")
|
1163 |
-
try:
|
1164 |
-
from moviepy.editor import ColorClip
|
1165 |
-
try:
|
1166 |
-
temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
|
1167 |
-
temp_clip.close()
|
1168 |
-
logger.info("Clips base de MoviePy creados y cerrados exitosamente. FFmpeg parece accesible.")
|
1169 |
-
except Exception as e:
|
1170 |
-
logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
|
1171 |
-
|
1172 |
-
except Exception as e:
|
1173 |
-
logger.critical(f"Fallo al importar MoviePy. Asegúrate de que está instalado. Error: {e}", exc_info=True)
|
1174 |
-
|
1175 |
-
# Solución para el timeout de Gradio - Añadir esta línea
|
1176 |
-
os.environ['GRADIO_SERVER_TIMEOUT'] = '6000' # 600 segundos = 10 minutos
|
1177 |
-
|
1178 |
-
logger.info("Iniciando aplicación Gradio...")
|
1179 |
-
try:
|
1180 |
-
app.launch(server_name="0.0.0.0", server_port=7860, share=False)
|
1181 |
-
except Exception as e:
|
1182 |
-
logger.critical(f"No se pudo iniciar la app: {str(e)}", exc_info=True)
|
1183 |
-
raise
|
|
|
859 |
except Exception as e:
|
860 |
logger.warning(f"Error ajustando duración del audio final: {str(e)}")
|
861 |
|
862 |
+
# 7. Crear video final (INDENTACIÓN ORIGINAL)
|
863 |
output_filename = f"video_{int(time.time())}.mp4" # Nombre único con timestamp
|
864 |
output_path = os.path.join(temp_dir_intermediate, output_filename)
|
865 |
|
866 |
+
# Escribir el video
|
867 |
video_final.write_videofile(
|
868 |
output_path,
|
869 |
fps=24,
|
|
|
878 |
logger='bar'
|
879 |
)
|
880 |
|
881 |
+
# Mover a ubicación permanente en /tmp
|
882 |
+
permanent_path = f"/tmp/{output_filename}"
|
883 |
try:
|
884 |
+
shutil.copy(output_path, permanent_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
885 |
logger.info(f"Video guardado permanentemente en: {permanent_path}")
|
|
|
|
|
886 |
except Exception as move_error:
|
887 |
logger.error(f"Error moviendo archivo: {str(move_error)}. Usando path original.")
|
888 |
+
permanent_path = output_path
|
889 |
+
|
890 |
+
# Cierra los clips para liberar memoria
|
891 |
+
try:
|
892 |
+
video_final.close()
|
893 |
+
if 'video_base' in locals() and video_base is not None and video_base is not video_final:
|
894 |
+
video_base.close()
|
895 |
+
except Exception as close_error:
|
896 |
+
logger.error(f"Error cerrando clips: {str(close_error)}")
|
897 |
|
898 |
total_time = (datetime.now() - start_time).total_seconds()
|
899 |
+
logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {permanent_path} | Tiempo total: {total_time:.2f}s")
|
900 |
|
901 |
+
return permanent_path
|
902 |
|
903 |
except ValueError as ve:
|
904 |
logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
|
|
|
969 |
except Exception as e:
|
970 |
logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
|
971 |
|
972 |
+
logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|