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 |
-
#
|
| 13 |
from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
|
| 14 |
import re
|
| 15 |
import math
|
|
@@ -35,7 +35,7 @@ logger.info("="*80)
|
|
| 35 |
PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
|
| 36 |
if not PEXELS_API_KEY:
|
| 37 |
logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
|
| 38 |
-
# raise ValueError("API key de Pexels no configurada")
|
| 39 |
|
| 40 |
# Inicialización de modelos
|
| 41 |
MODEL_NAME = "datificate/gpt2-small-spanish"
|
|
@@ -387,21 +387,19 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 387 |
primary_voice = "es-ES-JuanNeural"
|
| 388 |
fallback_voice = "es-ES-ElviraNeural" # Otra voz en español
|
| 389 |
tts_success = False
|
| 390 |
-
retries = 3
|
| 391 |
|
| 392 |
for attempt in range(retries):
|
| 393 |
current_voice = primary_voice if attempt == 0 else fallback_voice
|
| 394 |
if attempt > 0: logger.warning(f"Reintentando TTS ({attempt+1}/{retries})...")
|
| 395 |
logger.info(f"Intentando TTS con voz: {current_voice}")
|
| 396 |
try:
|
| 397 |
-
# Llamar a la función async text_to_speech
|
| 398 |
tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=current_voice))
|
| 399 |
if tts_success:
|
| 400 |
logger.info(f"TTS exitoso en intento {attempt + 1} con voz {current_voice}.")
|
| 401 |
-
break
|
| 402 |
except Exception as e:
|
| 403 |
-
|
| 404 |
-
pass # Continuar al siguiente intento
|
| 405 |
|
| 406 |
if not tts_success and attempt == 0 and primary_voice != fallback_voice:
|
| 407 |
logger.warning(f"Fallo con voz {primary_voice}, intentando voz de respaldo: {fallback_voice}")
|
|
@@ -409,14 +407,12 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 409 |
logger.warning(f"Fallo con voz {current_voice}, reintentando...")
|
| 410 |
|
| 411 |
|
| 412 |
-
# Verificar si el archivo fue creado después de todos los intentos
|
| 413 |
if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
|
| 414 |
logger.error(f"Fallo en la generación de voz después de {retries} intentos. Archivo de audio no creado o es muy pequeño.")
|
| 415 |
raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
|
| 416 |
|
| 417 |
-
temp_intermediate_files.append(voz_path)
|
| 418 |
|
| 419 |
-
# Continuar cargando el archivo de audio generado
|
| 420 |
audio_tts_original = AudioFileClip(voz_path)
|
| 421 |
|
| 422 |
if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
|
|
@@ -709,8 +705,8 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 709 |
|
| 710 |
if musica_audio_looped:
|
| 711 |
composite_audio = CompositeAudioClip([
|
| 712 |
-
musica_audio_looped.volumex(0.2),
|
| 713 |
-
audio_tts_original.volumex(1.0)
|
| 714 |
])
|
| 715 |
|
| 716 |
if composite_audio.duration is None or composite_audio.duration <= 0:
|
|
@@ -721,7 +717,7 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 721 |
else:
|
| 722 |
logger.info("Mezcla de audio completada (voz + música).")
|
| 723 |
final_audio = composite_audio
|
| 724 |
-
musica_audio = musica_audio_looped
|
| 725 |
|
| 726 |
except Exception as e:
|
| 727 |
logger.warning(f"Error procesando música de fondo: {str(e)}", exc_info=True)
|
|
@@ -847,12 +843,12 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 847 |
logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
|
| 848 |
|
| 849 |
|
| 850 |
-
#
|
| 851 |
def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
| 852 |
logger.info("="*80)
|
| 853 |
logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
|
| 854 |
|
| 855 |
-
#
|
| 856 |
input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
|
| 857 |
|
| 858 |
output_video = None
|
|
@@ -861,6 +857,7 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 861 |
|
| 862 |
if not input_text or not input_text.strip():
|
| 863 |
logger.warning("Texto de entrada vacío.")
|
|
|
|
| 864 |
return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
|
| 865 |
|
| 866 |
logger.info(f"Tipo de entrada: {prompt_type}")
|
|
@@ -872,14 +869,14 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 872 |
|
| 873 |
try:
|
| 874 |
logger.info("Llamando a crear_video...")
|
| 875 |
-
# Pasar el input_text elegido y el archivo de música
|
| 876 |
video_path = crear_video(prompt_type, input_text, musica_file)
|
| 877 |
|
| 878 |
if video_path and os.path.exists(video_path):
|
| 879 |
logger.info(f"crear_video retornó path: {video_path}")
|
| 880 |
logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
|
| 881 |
-
output_video = video_path
|
| 882 |
-
output_file = video_path
|
| 883 |
status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
|
| 884 |
else:
|
| 885 |
logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
|
|
@@ -893,6 +890,7 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 893 |
status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
|
| 894 |
finally:
|
| 895 |
logger.info("Fin del handler run_app.")
|
|
|
|
| 896 |
return output_video, output_file, status_msg
|
| 897 |
|
| 898 |
|
|
@@ -914,11 +912,12 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 914 |
)
|
| 915 |
|
| 916 |
# Contenedores para los campos de texto para controlar la visibilidad
|
|
|
|
| 917 |
with gr.Column(visible=True) as ia_guion_column:
|
| 918 |
prompt_ia = gr.Textbox(
|
| 919 |
label="Tema para IA",
|
| 920 |
lines=2,
|
| 921 |
-
placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer...",
|
| 922 |
max_lines=4,
|
| 923 |
value=""
|
| 924 |
)
|
|
@@ -927,7 +926,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 927 |
prompt_manual = gr.Textbox(
|
| 928 |
label="Tu Guion Completo",
|
| 929 |
lines=5,
|
| 930 |
-
placeholder="Ej: En este video exploraremos los misterios del océano
|
| 931 |
max_lines=10,
|
| 932 |
value=""
|
| 933 |
)
|
|
@@ -960,31 +959,38 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 960 |
value="Esperando entrada..."
|
| 961 |
)
|
| 962 |
|
| 963 |
-
#
|
|
|
|
| 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 |
-
#
|
| 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 a procesando
|
| 975 |
-
|
|
|
|
| 976 |
outputs=[video_output, file_output, status_output],
|
| 977 |
-
queue=True,
|
| 978 |
).then(
|
| 979 |
# Acción 2 (asíncrona): Llamar a la función principal de procesamiento
|
| 980 |
run_app,
|
| 981 |
-
#
|
| 982 |
inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
|
|
|
|
| 983 |
outputs=[video_output, file_output, status_output]
|
| 984 |
).then(
|
| 985 |
# Acción 3 (síncrona): Hacer visible el enlace de descarga si se retornó un archivo
|
| 986 |
-
|
| 987 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 988 |
outputs=[file_output]
|
| 989 |
)
|
| 990 |
|
|
|
|
| 9 |
import torch
|
| 10 |
from transformers import GPT2Tokenizer, GPT2LMHeadModel
|
| 11 |
from keybert import KeyBERT
|
| 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
|
|
|
|
| 35 |
PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
|
| 36 |
if not PEXELS_API_KEY:
|
| 37 |
logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
|
| 38 |
+
# raise ValueError("API key de Pexels no configurada")
|
| 39 |
|
| 40 |
# Inicialización de modelos
|
| 41 |
MODEL_NAME = "datificate/gpt2-small-spanish"
|
|
|
|
| 387 |
primary_voice = "es-ES-JuanNeural"
|
| 388 |
fallback_voice = "es-ES-ElviraNeural" # Otra voz en español
|
| 389 |
tts_success = False
|
| 390 |
+
retries = 3
|
| 391 |
|
| 392 |
for attempt in range(retries):
|
| 393 |
current_voice = primary_voice if attempt == 0 else fallback_voice
|
| 394 |
if attempt > 0: logger.warning(f"Reintentando TTS ({attempt+1}/{retries})...")
|
| 395 |
logger.info(f"Intentando TTS con voz: {current_voice}")
|
| 396 |
try:
|
|
|
|
| 397 |
tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=current_voice))
|
| 398 |
if tts_success:
|
| 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 |
if not tts_success and attempt == 0 and primary_voice != fallback_voice:
|
| 405 |
logger.warning(f"Fallo con voz {primary_voice}, intentando voz de respaldo: {fallback_voice}")
|
|
|
|
| 407 |
logger.warning(f"Fallo con voz {current_voice}, reintentando...")
|
| 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 después de {retries} intentos. Archivo de audio no creado o es muy pequeño.")
|
| 412 |
raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
|
| 413 |
|
| 414 |
+
temp_intermediate_files.append(voz_path)
|
| 415 |
|
|
|
|
| 416 |
audio_tts_original = AudioFileClip(voz_path)
|
| 417 |
|
| 418 |
if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
|
|
|
|
| 705 |
|
| 706 |
if musica_audio_looped:
|
| 707 |
composite_audio = CompositeAudioClip([
|
| 708 |
+
musica_audio_looped.volumex(0.2), # Volumen 20% para música
|
| 709 |
+
audio_tts_original.volumex(1.0) # Volumen 100% para voz
|
| 710 |
])
|
| 711 |
|
| 712 |
if composite_audio.duration is None or composite_audio.duration <= 0:
|
|
|
|
| 717 |
else:
|
| 718 |
logger.info("Mezcla de audio completada (voz + música).")
|
| 719 |
final_audio = composite_audio
|
| 720 |
+
musica_audio = musica_audio_looped # Asignar para limpieza
|
| 721 |
|
| 722 |
except Exception as e:
|
| 723 |
logger.warning(f"Error procesando música de fondo: {str(e)}", exc_info=True)
|
|
|
|
| 843 |
logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
|
| 844 |
|
| 845 |
|
| 846 |
+
# La función run_app ahora recibe todos los inputs de texto y el archivo de música
|
| 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 |
|
| 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}")
|
|
|
|
| 869 |
|
| 870 |
try:
|
| 871 |
logger.info("Llamando a crear_video...")
|
| 872 |
+
# Pasar el input_text elegido y el archivo de música a crear_video
|
| 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 # Establecer valor del componente de video
|
| 879 |
+
output_file = video_path # Establecer valor del componente de archivo para descarga
|
| 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 |
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 |
)
|
| 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, mostrando la belleza de la naturaleza...",
|
| 921 |
max_lines=4,
|
| 922 |
value=""
|
| 923 |
)
|
|
|
|
| 926 |
prompt_manual = gr.Textbox(
|
| 927 |
label="Tu Guion Completo",
|
| 928 |
lines=5,
|
| 929 |
+
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!",
|
| 930 |
max_lines=10,
|
| 931 |
value=""
|
| 932 |
)
|
|
|
|
| 959 |
value="Esperando entrada..."
|
| 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 a procesando
|
| 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, # Usar la cola de Gradio para tareas largas
|
| 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 que run_app espera
|
| 983 |
inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
|
| 984 |
+
# run_app retornará los 3 outputs esperados aquí
|
| 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 |
|