Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -9,7 +9,6 @@ import torch
|
|
| 9 |
from transformers import GPT2Tokenizer, GPT2LMHeadModel
|
| 10 |
from keybert import KeyBERT
|
| 11 |
from TTS.api import TTS
|
| 12 |
-
# Importación correcta: Solo 'concatenate_videoclips'
|
| 13 |
from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
|
| 14 |
import re
|
| 15 |
import math
|
|
@@ -133,9 +132,9 @@ def generate_script(prompt, max_length=150):
|
|
| 133 |
|
| 134 |
cleaned_text = text.strip()
|
| 135 |
try:
|
| 136 |
-
instruction_end_idx = text.find(
|
| 137 |
if instruction_end_idx != -1:
|
| 138 |
-
cleaned_text = text[instruction_end_idx + len(
|
| 139 |
logger.debug("Instrucción inicial encontrada y eliminada del guión generado.")
|
| 140 |
else:
|
| 141 |
instruction_start_idx = text.find(instruction_phrase_start)
|
|
@@ -178,8 +177,6 @@ def generate_script(prompt, max_length=150):
|
|
| 178 |
logger.warning("Usando prompt original como guion debido al error de generación.")
|
| 179 |
return prompt.strip()
|
| 180 |
|
| 181 |
-
from TTS.api import TTS
|
| 182 |
-
|
| 183 |
def text_to_speech(text, output_path, voice=None):
|
| 184 |
logger.info(f"Convirtiendo texto a voz con Coqui TTS | Caracteres: {len(text)} | Salida: {output_path}")
|
| 185 |
if not text or not text.strip():
|
|
@@ -191,6 +188,7 @@ def text_to_speech(text, output_path, voice=None):
|
|
| 191 |
tts = TTS(model_name="tts_models/es/css10/vits", progress_bar=False, gpu=False)
|
| 192 |
|
| 193 |
# Limpiar y truncar texto
|
|
|
|
| 194 |
text = re.sub(r'[^\w\s.,!?áéíóúñÁÉÍÓÚÑ]', '', text)
|
| 195 |
if len(text) > 500:
|
| 196 |
logger.warning("Texto demasiado largo, truncando a 500 caracteres")
|
|
@@ -210,8 +208,6 @@ def text_to_speech(text, output_path, voice=None):
|
|
| 210 |
except Exception as e:
|
| 211 |
logger.error(f"Error TTS: {str(e)}", exc_info=True)
|
| 212 |
return False
|
| 213 |
-
|
| 214 |
-
#FIN DE ESTA MIERDA
|
| 215 |
|
| 216 |
def download_video_file(url, temp_dir):
|
| 217 |
if not url:
|
|
@@ -305,7 +301,6 @@ def loop_audio_to_length(audio_clip, target_duration):
|
|
| 305 |
try: looped_audio.close()
|
| 306 |
except: pass
|
| 307 |
|
| 308 |
-
|
| 309 |
def extract_visual_keywords_from_script(script_text):
|
| 310 |
logger.info("Extrayendo palabras clave del guion")
|
| 311 |
if not script_text or not script_text.strip():
|
|
@@ -392,40 +387,23 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 392 |
logger.error("El guion resultante está vacío o solo contiene espacios.")
|
| 393 |
raise ValueError("El guion está vacío.")
|
| 394 |
|
|
|
|
|
|
|
|
|
|
| 395 |
temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
|
| 396 |
logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
|
| 397 |
temp_intermediate_files = []
|
| 398 |
|
| 399 |
-
# 2. Generar audio de voz
|
| 400 |
logger.info("Generando audio de voz...")
|
| 401 |
voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
|
| 402 |
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
tts_success = False
|
| 406 |
-
retries = 3
|
| 407 |
-
|
| 408 |
-
for attempt in range(retries):
|
| 409 |
-
current_voice = primary_voice if attempt == 0 else fallback_voice
|
| 410 |
-
if attempt > 0: logger.warning(f"Reintentando TTS ({attempt+1}/{retries})...")
|
| 411 |
-
logger.info(f"Intentando TTS con voz: {current_voice}")
|
| 412 |
-
try:
|
| 413 |
-
tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=current_voice))
|
| 414 |
-
if tts_success:
|
| 415 |
-
logger.info(f"TTS exitoso en intento {attempt + 1} con voz {current_voice}.")
|
| 416 |
-
break
|
| 417 |
-
except Exception as e:
|
| 418 |
-
pass
|
| 419 |
-
|
| 420 |
-
if not tts_success and attempt == 0 and primary_voice != fallback_voice:
|
| 421 |
-
logger.warning(f"Fallo con voz {primary_voice}, intentando voz de respaldo: {fallback_voice}")
|
| 422 |
-
elif not tts_success and attempt < retries - 1:
|
| 423 |
-
logger.warning(f"Fallo con voz {current_voice}, reintentando...")
|
| 424 |
-
|
| 425 |
|
| 426 |
-
if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <=
|
| 427 |
-
|
| 428 |
-
|
| 429 |
|
| 430 |
temp_intermediate_files.append(voz_path)
|
| 431 |
|
|
@@ -655,7 +633,6 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 655 |
try: clip.close()
|
| 656 |
except: pass
|
| 657 |
|
| 658 |
-
|
| 659 |
if final_video_base.duration > audio_duration:
|
| 660 |
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).")
|
| 661 |
trimmed_video_base = None
|
|
@@ -675,7 +652,6 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 675 |
logger.critical(f"Error durante el recorte: {str(e)}", exc_info=True)
|
| 676 |
raise ValueError("Fallo durante el recorte de video.")
|
| 677 |
|
| 678 |
-
|
| 679 |
if final_video_base is None or final_video_base.duration is None or final_video_base.duration <= 0:
|
| 680 |
logger.critical("Video base final es inválido antes de audio/escritura (None o duración cero).")
|
| 681 |
raise ValueError("Video base final es inválido.")
|
|
@@ -718,7 +694,6 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 718 |
except: pass
|
| 719 |
musica_audio_looped = None
|
| 720 |
|
| 721 |
-
|
| 722 |
if musica_audio_looped:
|
| 723 |
composite_audio = CompositeAudioClip([
|
| 724 |
musica_audio_looped.volumex(0.2), # Volumen 20% para música
|
|
@@ -741,7 +716,6 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 741 |
musica_audio = None
|
| 742 |
logger.warning("Usando solo audio de voz debido a un error con la música.")
|
| 743 |
|
| 744 |
-
|
| 745 |
if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
|
| 746 |
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.")
|
| 747 |
try:
|
|
@@ -773,7 +747,6 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 773 |
logger.info(f"Escribiendo video final a: {output_path}")
|
| 774 |
|
| 775 |
video_final.write_videofile(
|
| 776 |
-
output_path,
|
| 777 |
fps=24,
|
| 778 |
threads=4,
|
| 779 |
codec="libx264",
|
|
@@ -858,8 +831,6 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 858 |
|
| 859 |
logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
|
| 860 |
|
| 861 |
-
|
| 862 |
-
# La función run_app ahora recibe todos los inputs de texto y el archivo de música
|
| 863 |
def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
| 864 |
logger.info("="*80)
|
| 865 |
logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
|
|
@@ -873,7 +844,6 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 873 |
|
| 874 |
if not input_text or not input_text.strip():
|
| 875 |
logger.warning("Texto de entrada vacío.")
|
| 876 |
-
# Retornar None para video y archivo, actualizar estado con mensaje de error
|
| 877 |
return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
|
| 878 |
|
| 879 |
logger.info(f"Tipo de entrada: {prompt_type}")
|
|
@@ -885,14 +855,13 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 885 |
|
| 886 |
try:
|
| 887 |
logger.info("Llamando a crear_video...")
|
| 888 |
-
# Pasar el input_text elegido y el archivo de música a crear_video
|
| 889 |
video_path = crear_video(prompt_type, input_text, musica_file)
|
| 890 |
|
| 891 |
if video_path and os.path.exists(video_path):
|
| 892 |
logger.info(f"crear_video retornó path: {video_path}")
|
| 893 |
logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
|
| 894 |
-
output_video = video_path
|
| 895 |
-
output_file = video_path
|
| 896 |
status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
|
| 897 |
else:
|
| 898 |
logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
|
|
@@ -906,10 +875,8 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 906 |
status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
|
| 907 |
finally:
|
| 908 |
logger.info("Fin del handler run_app.")
|
| 909 |
-
# Retornar las tres salidas esperadas por el evento click
|
| 910 |
return output_video, output_file, status_msg
|
| 911 |
|
| 912 |
-
|
| 913 |
# Interfaz de Gradio
|
| 914 |
with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
|
| 915 |
.gradio-container {max-width: 800px; margin: auto;}
|
|
@@ -927,8 +894,6 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 927 |
value="Generar Guion con IA"
|
| 928 |
)
|
| 929 |
|
| 930 |
-
# Contenedores para los campos de texto para controlar la visibilidad
|
| 931 |
-
# Nombrados para que coincidan con los outputs del evento change
|
| 932 |
with gr.Column(visible=True) as ia_guion_column:
|
| 933 |
prompt_ia = gr.Textbox(
|
| 934 |
label="Tema para IA",
|
|
@@ -965,7 +930,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 965 |
file_output = gr.File(
|
| 966 |
label="Descargar Archivo de Video",
|
| 967 |
interactive=False,
|
| 968 |
-
visible=False
|
| 969 |
)
|
| 970 |
status_output = gr.Textbox(
|
| 971 |
label="Estado",
|
|
@@ -975,42 +940,27 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 975 |
value="Esperando entrada..."
|
| 976 |
)
|
| 977 |
|
| 978 |
-
# Evento para mostrar/ocultar los campos de texto según el tipo de prompt
|
| 979 |
-
# Apuntar a los componentes Column padre para controlar la visibilidad
|
| 980 |
prompt_type.change(
|
| 981 |
lambda x: (gr.update(visible=x == "Generar Guion con IA"),
|
| 982 |
gr.update(visible=x == "Usar Mi Guion")),
|
| 983 |
inputs=prompt_type,
|
| 984 |
-
# Pasar los componentes Column
|
| 985 |
outputs=[ia_guion_column, manual_guion_column]
|
| 986 |
)
|
| 987 |
|
| 988 |
-
# Evento click del botón de generar video
|
| 989 |
generate_btn.click(
|
| 990 |
-
# Acción 1 (síncrona): Resetear salidas y establecer estado a procesando
|
| 991 |
-
# Retorna None para los 3 outputs iniciales
|
| 992 |
lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
|
| 993 |
outputs=[video_output, file_output, status_output],
|
| 994 |
-
queue=True,
|
| 995 |
).then(
|
| 996 |
-
# Acción 2 (asíncrona): Llamar a la función principal de procesamiento
|
| 997 |
run_app,
|
| 998 |
-
# PASAR TODOS LOS INPUTS DE LA INTERFAZ que run_app espera
|
| 999 |
inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
|
| 1000 |
-
# run_app retornará los 3 outputs esperados aquí
|
| 1001 |
outputs=[video_output, file_output, status_output]
|
| 1002 |
).then(
|
| 1003 |
-
# Acción 3 (síncrona): Hacer visible el enlace de descarga si se retornó un archivo
|
| 1004 |
-
# Esta función recibe las salidas de la Acción 2 (video_path, file_path, status_msg)
|
| 1005 |
-
# Solo necesitamos video_path o file_path para decidir si mostrar el enlace
|
| 1006 |
lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
|
| 1007 |
-
# Inputs son las salidas de la función .then() anterior
|
| 1008 |
inputs=[video_output, file_output, status_output],
|
| 1009 |
-
# Actualizamos la visibilidad del componente file_output
|
| 1010 |
outputs=[file_output]
|
| 1011 |
)
|
| 1012 |
|
| 1013 |
-
|
| 1014 |
gr.Markdown("### Instrucciones:")
|
| 1015 |
gr.Markdown("""
|
| 1016 |
1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
|
|
|
|
| 9 |
from transformers import GPT2Tokenizer, GPT2LMHeadModel
|
| 10 |
from keybert import KeyBERT
|
| 11 |
from TTS.api import TTS
|
|
|
|
| 12 |
from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
|
| 13 |
import re
|
| 14 |
import math
|
|
|
|
| 132 |
|
| 133 |
cleaned_text = text.strip()
|
| 134 |
try:
|
| 135 |
+
instruction_end_idx = text.find(instruction_phrase_start)
|
| 136 |
if instruction_end_idx != -1:
|
| 137 |
+
cleaned_text = text[instruction_end_idx + len(instruction_phrase_start):].strip()
|
| 138 |
logger.debug("Instrucción inicial encontrada y eliminada del guión generado.")
|
| 139 |
else:
|
| 140 |
instruction_start_idx = text.find(instruction_phrase_start)
|
|
|
|
| 177 |
logger.warning("Usando prompt original como guion debido al error de generación.")
|
| 178 |
return prompt.strip()
|
| 179 |
|
|
|
|
|
|
|
| 180 |
def text_to_speech(text, output_path, voice=None):
|
| 181 |
logger.info(f"Convirtiendo texto a voz con Coqui TTS | Caracteres: {len(text)} | Salida: {output_path}")
|
| 182 |
if not text or not text.strip():
|
|
|
|
| 188 |
tts = TTS(model_name="tts_models/es/css10/vits", progress_bar=False, gpu=False)
|
| 189 |
|
| 190 |
# Limpiar y truncar texto
|
| 191 |
+
text = text.replace("na hora", "A la hora")
|
| 192 |
text = re.sub(r'[^\w\s.,!?áéíóúñÁÉÍÓÚÑ]', '', text)
|
| 193 |
if len(text) > 500:
|
| 194 |
logger.warning("Texto demasiado largo, truncando a 500 caracteres")
|
|
|
|
| 208 |
except Exception as e:
|
| 209 |
logger.error(f"Error TTS: {str(e)}", exc_info=True)
|
| 210 |
return False
|
|
|
|
|
|
|
| 211 |
|
| 212 |
def download_video_file(url, temp_dir):
|
| 213 |
if not url:
|
|
|
|
| 301 |
try: looped_audio.close()
|
| 302 |
except: pass
|
| 303 |
|
|
|
|
| 304 |
def extract_visual_keywords_from_script(script_text):
|
| 305 |
logger.info("Extrayendo palabras clave del guion")
|
| 306 |
if not script_text or not script_text.strip():
|
|
|
|
| 387 |
logger.error("El guion resultante está vacío o solo contiene espacios.")
|
| 388 |
raise ValueError("El guion está vacío.")
|
| 389 |
|
| 390 |
+
# Corregir error tipográfico en el guion
|
| 391 |
+
guion = guion.replace("na hora", "A la hora")
|
| 392 |
+
|
| 393 |
temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
|
| 394 |
logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
|
| 395 |
temp_intermediate_files = []
|
| 396 |
|
| 397 |
+
# 2. Generar audio de voz
|
| 398 |
logger.info("Generando audio de voz...")
|
| 399 |
voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
|
| 400 |
|
| 401 |
+
# Llamar a text_to_speech directamente
|
| 402 |
+
tts_success = text_to_speech(guion, voz_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
|
| 404 |
+
if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 1000:
|
| 405 |
+
logger.error(f"Fallo en la generación de voz. Archivo de audio no creado o es muy pequeño: {voz_path}")
|
| 406 |
+
raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
|
| 407 |
|
| 408 |
temp_intermediate_files.append(voz_path)
|
| 409 |
|
|
|
|
| 633 |
try: clip.close()
|
| 634 |
except: pass
|
| 635 |
|
|
|
|
| 636 |
if final_video_base.duration > audio_duration:
|
| 637 |
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).")
|
| 638 |
trimmed_video_base = None
|
|
|
|
| 652 |
logger.critical(f"Error durante el recorte: {str(e)}", exc_info=True)
|
| 653 |
raise ValueError("Fallo durante el recorte de video.")
|
| 654 |
|
|
|
|
| 655 |
if final_video_base is None or final_video_base.duration is None or final_video_base.duration <= 0:
|
| 656 |
logger.critical("Video base final es inválido antes de audio/escritura (None o duración cero).")
|
| 657 |
raise ValueError("Video base final es inválido.")
|
|
|
|
| 694 |
except: pass
|
| 695 |
musica_audio_looped = None
|
| 696 |
|
|
|
|
| 697 |
if musica_audio_looped:
|
| 698 |
composite_audio = CompositeAudioClip([
|
| 699 |
musica_audio_looped.volumex(0.2), # Volumen 20% para música
|
|
|
|
| 716 |
musica_audio = None
|
| 717 |
logger.warning("Usando solo audio de voz debido a un error con la música.")
|
| 718 |
|
|
|
|
| 719 |
if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
|
| 720 |
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.")
|
| 721 |
try:
|
|
|
|
| 747 |
logger.info(f"Escribiendo video final a: {output_path}")
|
| 748 |
|
| 749 |
video_final.write_videofile(
|
|
|
|
| 750 |
fps=24,
|
| 751 |
threads=4,
|
| 752 |
codec="libx264",
|
|
|
|
| 831 |
|
| 832 |
logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
|
| 833 |
|
|
|
|
|
|
|
| 834 |
def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
| 835 |
logger.info("="*80)
|
| 836 |
logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
|
|
|
|
| 844 |
|
| 845 |
if not input_text or not input_text.strip():
|
| 846 |
logger.warning("Texto de entrada vacío.")
|
|
|
|
| 847 |
return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
|
| 848 |
|
| 849 |
logger.info(f"Tipo de entrada: {prompt_type}")
|
|
|
|
| 855 |
|
| 856 |
try:
|
| 857 |
logger.info("Llamando a crear_video...")
|
|
|
|
| 858 |
video_path = crear_video(prompt_type, input_text, musica_file)
|
| 859 |
|
| 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 = video_path
|
| 865 |
status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
|
| 866 |
else:
|
| 867 |
logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
|
|
|
|
| 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 |
# Interfaz de Gradio
|
| 881 |
with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
|
| 882 |
.gradio-container {max-width: 800px; margin: auto;}
|
|
|
|
| 894 |
value="Generar Guion con IA"
|
| 895 |
)
|
| 896 |
|
|
|
|
|
|
|
| 897 |
with gr.Column(visible=True) as ia_guion_column:
|
| 898 |
prompt_ia = gr.Textbox(
|
| 899 |
label="Tema para IA",
|
|
|
|
| 930 |
file_output = gr.File(
|
| 931 |
label="Descargar Archivo de Video",
|
| 932 |
interactive=False,
|
| 933 |
+
visible=False
|
| 934 |
)
|
| 935 |
status_output = gr.Textbox(
|
| 936 |
label="Estado",
|
|
|
|
| 940 |
value="Esperando entrada..."
|
| 941 |
)
|
| 942 |
|
|
|
|
|
|
|
| 943 |
prompt_type.change(
|
| 944 |
lambda x: (gr.update(visible=x == "Generar Guion con IA"),
|
| 945 |
gr.update(visible=x == "Usar Mi Guion")),
|
| 946 |
inputs=prompt_type,
|
|
|
|
| 947 |
outputs=[ia_guion_column, manual_guion_column]
|
| 948 |
)
|
| 949 |
|
|
|
|
| 950 |
generate_btn.click(
|
|
|
|
|
|
|
| 951 |
lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
|
| 952 |
outputs=[video_output, file_output, status_output],
|
| 953 |
+
queue=True,
|
| 954 |
).then(
|
|
|
|
| 955 |
run_app,
|
|
|
|
| 956 |
inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
|
|
|
|
| 957 |
outputs=[video_output, file_output, status_output]
|
| 958 |
).then(
|
|
|
|
|
|
|
|
|
|
| 959 |
lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
|
|
|
|
| 960 |
inputs=[video_output, file_output, status_output],
|
|
|
|
| 961 |
outputs=[file_output]
|
| 962 |
)
|
| 963 |
|
|
|
|
| 964 |
gr.Markdown("### Instrucciones:")
|
| 965 |
gr.Markdown("""
|
| 966 |
1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
|