Spaces:
Sleeping
Sleeping
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.
|