Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -9,7 +9,7 @@ import gradio as gr
|
|
| 9 |
import torch
|
| 10 |
from transformers import GPT2Tokenizer, GPT2LMHeadModel
|
| 11 |
from keybert import KeyBERT
|
| 12 |
-
# Importación correcta
|
| 13 |
from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
|
| 14 |
import re
|
| 15 |
import math
|
|
@@ -61,6 +61,34 @@ except Exception as e:
|
|
| 61 |
logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
|
| 62 |
kw_model = None
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
def buscar_videos_pexels(query, api_key, per_page=5):
|
| 65 |
if not api_key:
|
| 66 |
logger.warning("No se puede buscar en Pexels: API Key no configurada.")
|
|
@@ -175,7 +203,7 @@ def generate_script(prompt, max_length=150):
|
|
| 175 |
logger.warning("Usando prompt original como guion debido al error de generación.")
|
| 176 |
return prompt.strip()
|
| 177 |
|
| 178 |
-
# Función TTS
|
| 179 |
async def text_to_speech(text, output_path, voice):
|
| 180 |
logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
|
| 181 |
if not text or not text.strip():
|
|
@@ -346,10 +374,11 @@ def extract_visual_keywords_from_script(script_text):
|
|
| 346 |
logger.info(f"Palabras clave finales: {top_keywords}")
|
| 347 |
return top_keywords
|
| 348 |
|
| 349 |
-
def crear_video(prompt_type, input_text, musica_file=None):
|
| 350 |
logger.info("="*80)
|
| 351 |
logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
|
| 352 |
logger.debug(f"Input: '{input_text[:100]}...'")
|
|
|
|
| 353 |
|
| 354 |
start_time = datetime.now()
|
| 355 |
temp_dir_intermediate = None
|
|
@@ -380,35 +409,31 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 380 |
logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
|
| 381 |
temp_intermediate_files = []
|
| 382 |
|
| 383 |
-
# 2. Generar audio de voz
|
| 384 |
logger.info("Generando audio de voz...")
|
| 385 |
voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
|
| 386 |
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
tts_success = False
|
| 390 |
-
retries = 3
|
| 391 |
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
logger.info(f"TTS exitoso en intento {attempt + 1} con voz {current_voice}.")
|
| 400 |
-
break
|
| 401 |
-
except Exception as e:
|
| 402 |
-
pass
|
| 403 |
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
|
|
|
| 408 |
|
| 409 |
|
|
|
|
| 410 |
if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
|
| 411 |
-
logger.error(f"Fallo en la generación de voz
|
| 412 |
raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
|
| 413 |
|
| 414 |
temp_intermediate_files.append(voz_path)
|
|
@@ -843,12 +868,11 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 843 |
logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
|
| 844 |
|
| 845 |
|
| 846 |
-
#
|
| 847 |
-
def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
| 848 |
logger.info("="*80)
|
| 849 |
logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
|
| 850 |
|
| 851 |
-
# Elegir el texto de entrada basado en el prompt_type
|
| 852 |
input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
|
| 853 |
|
| 854 |
output_video = None
|
|
@@ -857,26 +881,30 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 857 |
|
| 858 |
if not input_text or not input_text.strip():
|
| 859 |
logger.warning("Texto de entrada vacío.")
|
| 860 |
-
# Retornar None para video y archivo, actualizar estado con mensaje de error
|
| 861 |
return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
|
| 862 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 863 |
logger.info(f"Tipo de entrada: {prompt_type}")
|
| 864 |
logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
|
| 865 |
if musica_file:
|
| 866 |
logger.info(f"Archivo de música recibido: {musica_file}")
|
| 867 |
else:
|
| 868 |
logger.info("No se proporcionó archivo de música.")
|
|
|
|
| 869 |
|
| 870 |
try:
|
| 871 |
logger.info("Llamando a crear_video...")
|
| 872 |
-
# Pasar
|
| 873 |
-
video_path = crear_video(prompt_type, input_text, musica_file)
|
| 874 |
|
| 875 |
if video_path and os.path.exists(video_path):
|
| 876 |
logger.info(f"crear_video retornó path: {video_path}")
|
| 877 |
logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
|
| 878 |
-
output_video = video_path
|
| 879 |
-
output_file = video_path
|
| 880 |
status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
|
| 881 |
else:
|
| 882 |
logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
|
|
@@ -890,7 +918,6 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 890 |
status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
|
| 891 |
finally:
|
| 892 |
logger.info("Fin del handler run_app.")
|
| 893 |
-
# Retornar las tres salidas esperadas por el evento click
|
| 894 |
return output_video, output_file, status_msg
|
| 895 |
|
| 896 |
|
|
@@ -912,12 +939,11 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 912 |
)
|
| 913 |
|
| 914 |
# Contenedores para los campos de texto para controlar la visibilidad
|
| 915 |
-
# Nombrados para que coincidan con los outputs del evento change
|
| 916 |
with gr.Column(visible=True) as ia_guion_column:
|
| 917 |
prompt_ia = gr.Textbox(
|
| 918 |
label="Tema para IA",
|
| 919 |
lines=2,
|
| 920 |
-
placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer
|
| 921 |
max_lines=4,
|
| 922 |
value=""
|
| 923 |
)
|
|
@@ -937,6 +963,16 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 937 |
interactive=True,
|
| 938 |
value=None
|
| 939 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 940 |
|
| 941 |
generate_btn = gr.Button("✨ Generar Video", variant="primary")
|
| 942 |
|
|
@@ -949,7 +985,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 949 |
file_output = gr.File(
|
| 950 |
label="Descargar Archivo de Video",
|
| 951 |
interactive=False,
|
| 952 |
-
visible=False
|
| 953 |
)
|
| 954 |
status_output = gr.Textbox(
|
| 955 |
label="Estado",
|
|
@@ -960,37 +996,30 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 960 |
)
|
| 961 |
|
| 962 |
# Evento para mostrar/ocultar los campos de texto según el tipo de prompt
|
| 963 |
-
# Apuntar a los componentes Column padre para controlar la visibilidad
|
| 964 |
prompt_type.change(
|
| 965 |
lambda x: (gr.update(visible=x == "Generar Guion con IA"),
|
| 966 |
gr.update(visible=x == "Usar Mi Guion")),
|
| 967 |
inputs=prompt_type,
|
| 968 |
-
# Pasar los componentes Column
|
| 969 |
outputs=[ia_guion_column, manual_guion_column]
|
| 970 |
)
|
| 971 |
|
| 972 |
# Evento click del botón de generar video
|
| 973 |
generate_btn.click(
|
| 974 |
-
# Acción 1 (síncrona): Resetear salidas y establecer estado
|
| 975 |
-
# Retorna None para los 3 outputs iniciales
|
| 976 |
lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
|
| 977 |
outputs=[video_output, file_output, status_output],
|
| 978 |
-
queue=True,
|
| 979 |
).then(
|
| 980 |
# Acción 2 (asíncrona): Llamar a la función principal de procesamiento
|
| 981 |
run_app,
|
| 982 |
-
# PASAR TODOS LOS INPUTS DE LA INTERFAZ
|
| 983 |
-
inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
|
| 984 |
-
# run_app retornará los 3 outputs esperados
|
| 985 |
outputs=[video_output, file_output, status_output]
|
| 986 |
).then(
|
| 987 |
# Acción 3 (síncrona): Hacer visible el enlace de descarga si se retornó un archivo
|
| 988 |
-
# Esta función recibe las salidas de la Acción 2 (video_path, file_path, status_msg)
|
| 989 |
-
# Solo necesitamos video_path o file_path para decidir si mostrar el enlace
|
| 990 |
lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
|
| 991 |
-
# Inputs son las salidas de la función .then() anterior
|
| 992 |
inputs=[video_output, file_output, status_output],
|
| 993 |
-
# Actualizamos la visibilidad del componente file_output
|
| 994 |
outputs=[file_output]
|
| 995 |
)
|
| 996 |
|
|
@@ -1002,10 +1031,11 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 1002 |
- "Generar Guion con IA": Describe brevemente un tema (ej. "La belleza de las montañas"). La IA generará un guion corto.
|
| 1003 |
- "Usar Mi Guion": Escribe el guion completo que quieres para el video.
|
| 1004 |
3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.) para usar como música de fondo.
|
| 1005 |
-
4. **
|
| 1006 |
-
5.
|
| 1007 |
-
6.
|
| 1008 |
-
7.
|
|
|
|
| 1009 |
""")
|
| 1010 |
gr.Markdown("---")
|
| 1011 |
gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
|
|
|
|
| 9 |
import torch
|
| 10 |
from transformers import GPT2Tokenizer, GPT2LMHeadModel
|
| 11 |
from keybert import KeyBERT
|
| 12 |
+
# Importación correcta
|
| 13 |
from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
|
| 14 |
import re
|
| 15 |
import math
|
|
|
|
| 61 |
logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
|
| 62 |
kw_model = None
|
| 63 |
|
| 64 |
+
# --- NUEVA FUNCIÓN: Obtener voces de Edge TTS ---
|
| 65 |
+
async def get_available_voices():
|
| 66 |
+
logger.info("Obteniendo lista de voces disponibles de Edge TTS...")
|
| 67 |
+
try:
|
| 68 |
+
voices = await edge_tts.VoicesManager.create()
|
| 69 |
+
# Filtrar solo voces en español si prefieres, o dejar todas
|
| 70 |
+
# es_voices = [voice.Name for voice in voices.Voices if voice.Locale.startswith('es-')]
|
| 71 |
+
# return es_voices if es_voices else [voice.Name for voice in voices.Voices]
|
| 72 |
+
|
| 73 |
+
# O simplemente retornar todas las voces
|
| 74 |
+
all_voices = [voice.Name for voice in voices.Voices]
|
| 75 |
+
logger.info(f"Encontradas {len(all_voices)} voces de Edge TTS.")
|
| 76 |
+
return all_voices
|
| 77 |
+
|
| 78 |
+
except Exception as e:
|
| 79 |
+
logger.error(f"Error obteniendo voces de Edge TTS: {str(e)}", exc_info=True)
|
| 80 |
+
# Retornar una lista de voces por defecto si falla la API de Edge TTS
|
| 81 |
+
logger.warning("No se pudieron obtener voces de Edge TTS. Usando lista de voces por defecto.")
|
| 82 |
+
return ["es-ES-JuanNeural", "es-ES-ElviraNeural", "en-US-AriaNeural"]
|
| 83 |
+
|
| 84 |
+
# Obtener las voces al inicio del script (esto puede tardar un poco)
|
| 85 |
+
logger.info("Inicializando lista de voces disponibles...")
|
| 86 |
+
AVAILABLE_VOICES = asyncio.run(get_available_voices())
|
| 87 |
+
# Establecer una voz por defecto inicial
|
| 88 |
+
DEFAULT_VOICE = "es-ES-JuanNeural" if "es-ES-JuanNeural" in AVAILABLE_VOICES else (AVAILABLE_VOICES[0] if AVAILABLE_VOICES else "en-US-AriaNeural")
|
| 89 |
+
logger.info(f"Voz por defecto seleccionada: {DEFAULT_VOICE}")
|
| 90 |
+
|
| 91 |
+
|
| 92 |
def buscar_videos_pexels(query, api_key, per_page=5):
|
| 93 |
if not api_key:
|
| 94 |
logger.warning("No se puede buscar en Pexels: API Key no configurada.")
|
|
|
|
| 203 |
logger.warning("Usando prompt original como guion debido al error de generación.")
|
| 204 |
return prompt.strip()
|
| 205 |
|
| 206 |
+
# Función TTS ahora recibe la voz a usar
|
| 207 |
async def text_to_speech(text, output_path, voice):
|
| 208 |
logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
|
| 209 |
if not text or not text.strip():
|
|
|
|
| 374 |
logger.info(f"Palabras clave finales: {top_keywords}")
|
| 375 |
return top_keywords
|
| 376 |
|
| 377 |
+
def crear_video(prompt_type, input_text, selected_voice, musica_file=None): # <-- AHORA RECIBE selected_voice
|
| 378 |
logger.info("="*80)
|
| 379 |
logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
|
| 380 |
logger.debug(f"Input: '{input_text[:100]}...'")
|
| 381 |
+
logger.info(f"Voz seleccionada para TTS: {selected_voice}") # <-- LOGUEAR la voz seleccionada
|
| 382 |
|
| 383 |
start_time = datetime.now()
|
| 384 |
temp_dir_intermediate = None
|
|
|
|
| 409 |
logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
|
| 410 |
temp_intermediate_files = []
|
| 411 |
|
| 412 |
+
# 2. Generar audio de voz usando la voz seleccionada
|
| 413 |
logger.info("Generando audio de voz...")
|
| 414 |
voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
|
| 415 |
|
| 416 |
+
# Ya no necesitamos reintentos/fallback aquí, la voz viene seleccionada
|
| 417 |
+
tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=selected_voice))
|
|
|
|
|
|
|
| 418 |
|
| 419 |
+
# Si falla la generación con la voz seleccionada, intentar con una voz de respaldo
|
| 420 |
+
if not tts_success:
|
| 421 |
+
logger.warning(f"La generación de TTS falló con la voz seleccionada '{selected_voice}'. Intentando con voz de respaldo 'es-ES-ElviraNeural'.")
|
| 422 |
+
fallback_voice = "es-ES-ElviraNeural"
|
| 423 |
+
if selected_voice == fallback_voice: # Evitar reintentar con la misma voz fallida
|
| 424 |
+
fallback_voice = "en-US-AriaNeural" # O alguna otra conocida
|
| 425 |
+
logger.warning(f"La voz de respaldo era la misma que falló. Intentando otra voz de respaldo: {fallback_voice}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
|
| 427 |
+
tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=fallback_voice))
|
| 428 |
+
if tts_success:
|
| 429 |
+
logger.info(f"TTS exitoso con voz de respaldo: {fallback_voice}.")
|
| 430 |
+
else:
|
| 431 |
+
logger.error(f"La generación de TTS falló también con la voz de respaldo.")
|
| 432 |
|
| 433 |
|
| 434 |
+
# Verificar si el archivo fue creado después de los intentos
|
| 435 |
if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
|
| 436 |
+
logger.error(f"Fallo en la generación de voz. Archivo de audio no creado o es muy pequeño.")
|
| 437 |
raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
|
| 438 |
|
| 439 |
temp_intermediate_files.append(voz_path)
|
|
|
|
| 868 |
logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
|
| 869 |
|
| 870 |
|
| 871 |
+
# run_app ahora recibe todos los inputs, incluyendo la voz seleccionada
|
| 872 |
+
def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice): # <-- AHORA RECIBE selected_voice
|
| 873 |
logger.info("="*80)
|
| 874 |
logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
|
| 875 |
|
|
|
|
| 876 |
input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
|
| 877 |
|
| 878 |
output_video = None
|
|
|
|
| 881 |
|
| 882 |
if not input_text or not input_text.strip():
|
| 883 |
logger.warning("Texto de entrada vacío.")
|
|
|
|
| 884 |
return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
|
| 885 |
|
| 886 |
+
if not selected_voice or selected_voice not in AVAILABLE_VOICES:
|
| 887 |
+
logger.warning(f"Voz seleccionada inválida o vacía: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE}.")
|
| 888 |
+
selected_voice = DEFAULT_VOICE # Usar voz por defecto si la seleccionada es inválida
|
| 889 |
+
|
| 890 |
logger.info(f"Tipo de entrada: {prompt_type}")
|
| 891 |
logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
|
| 892 |
if musica_file:
|
| 893 |
logger.info(f"Archivo de música recibido: {musica_file}")
|
| 894 |
else:
|
| 895 |
logger.info("No se proporcionó archivo de música.")
|
| 896 |
+
logger.info(f"Voz seleccionada (validada): {selected_voice}") # Loguear la voz validada
|
| 897 |
|
| 898 |
try:
|
| 899 |
logger.info("Llamando a crear_video...")
|
| 900 |
+
# Pasar la voz seleccionada a crear_video
|
| 901 |
+
video_path = crear_video(prompt_type, input_text, selected_voice, musica_file) # <-- PASAR selected_voice
|
| 902 |
|
| 903 |
if video_path and os.path.exists(video_path):
|
| 904 |
logger.info(f"crear_video retornó path: {video_path}")
|
| 905 |
logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
|
| 906 |
+
output_video = video_path
|
| 907 |
+
output_file = video_path
|
| 908 |
status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
|
| 909 |
else:
|
| 910 |
logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
|
|
|
|
| 918 |
status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
|
| 919 |
finally:
|
| 920 |
logger.info("Fin del handler run_app.")
|
|
|
|
| 921 |
return output_video, output_file, status_msg
|
| 922 |
|
| 923 |
|
|
|
|
| 939 |
)
|
| 940 |
|
| 941 |
# Contenedores para los campos de texto para controlar la visibilidad
|
|
|
|
| 942 |
with gr.Column(visible=True) as ia_guion_column:
|
| 943 |
prompt_ia = gr.Textbox(
|
| 944 |
label="Tema para IA",
|
| 945 |
lines=2,
|
| 946 |
+
placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer...",
|
| 947 |
max_lines=4,
|
| 948 |
value=""
|
| 949 |
)
|
|
|
|
| 963 |
interactive=True,
|
| 964 |
value=None
|
| 965 |
)
|
| 966 |
+
|
| 967 |
+
# --- NUEVO COMPONENTE: Selección de Voz ---
|
| 968 |
+
voice_dropdown = gr.Dropdown(
|
| 969 |
+
label="Seleccionar Voz para Guion",
|
| 970 |
+
choices=AVAILABLE_VOICES, # Usar la lista obtenida al inicio
|
| 971 |
+
value=DEFAULT_VOICE, # Usar la voz por defecto calculada
|
| 972 |
+
interactive=True
|
| 973 |
+
)
|
| 974 |
+
# --- FIN NUEVO COMPONENTE ---
|
| 975 |
+
|
| 976 |
|
| 977 |
generate_btn = gr.Button("✨ Generar Video", variant="primary")
|
| 978 |
|
|
|
|
| 985 |
file_output = gr.File(
|
| 986 |
label="Descargar Archivo de Video",
|
| 987 |
interactive=False,
|
| 988 |
+
visible=False
|
| 989 |
)
|
| 990 |
status_output = gr.Textbox(
|
| 991 |
label="Estado",
|
|
|
|
| 996 |
)
|
| 997 |
|
| 998 |
# Evento para mostrar/ocultar los campos de texto según el tipo de prompt
|
|
|
|
| 999 |
prompt_type.change(
|
| 1000 |
lambda x: (gr.update(visible=x == "Generar Guion con IA"),
|
| 1001 |
gr.update(visible=x == "Usar Mi Guion")),
|
| 1002 |
inputs=prompt_type,
|
|
|
|
| 1003 |
outputs=[ia_guion_column, manual_guion_column]
|
| 1004 |
)
|
| 1005 |
|
| 1006 |
# Evento click del botón de generar video
|
| 1007 |
generate_btn.click(
|
| 1008 |
+
# Acción 1 (síncrona): Resetear salidas y establecer estado
|
|
|
|
| 1009 |
lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
|
| 1010 |
outputs=[video_output, file_output, status_output],
|
| 1011 |
+
queue=True,
|
| 1012 |
).then(
|
| 1013 |
# Acción 2 (asíncrona): Llamar a la función principal de procesamiento
|
| 1014 |
run_app,
|
| 1015 |
+
# PASAR TODOS LOS INPUTS DE LA INTERFAZ a run_app
|
| 1016 |
+
inputs=[prompt_type, prompt_ia, prompt_manual, musica_input, voice_dropdown], # <-- AHORA PASAMOS voice_dropdown
|
| 1017 |
+
# run_app retornará los 3 outputs esperados
|
| 1018 |
outputs=[video_output, file_output, status_output]
|
| 1019 |
).then(
|
| 1020 |
# Acción 3 (síncrona): Hacer visible el enlace de descarga si se retornó un archivo
|
|
|
|
|
|
|
| 1021 |
lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
|
|
|
|
| 1022 |
inputs=[video_output, file_output, status_output],
|
|
|
|
| 1023 |
outputs=[file_output]
|
| 1024 |
)
|
| 1025 |
|
|
|
|
| 1031 |
- "Generar Guion con IA": Describe brevemente un tema (ej. "La belleza de las montañas"). La IA generará un guion corto.
|
| 1032 |
- "Usar Mi Guion": Escribe el guion completo que quieres para el video.
|
| 1033 |
3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.) para usar como música de fondo.
|
| 1034 |
+
4. **Selecciona la voz** para el guion.
|
| 1035 |
+
5. **Haz clic en "✨ Generar Video"**.
|
| 1036 |
+
6. Espera a que se procese el video. El tiempo de espera puede variar. Verás el estado en el cuadro de texto.
|
| 1037 |
+
7. La previsualización del video aparecerá arriba (puede fallar para archivos grandes), y un enlace **Descargar Archivo de Video** se mostrará si la generación fue exitosa.
|
| 1038 |
+
8. Si hay errores, revisa el log `video_generator_full.log` para más detalles.
|
| 1039 |
""")
|
| 1040 |
gr.Markdown("---")
|
| 1041 |
gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
|