Spaces:
Running
Running
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.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|