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 |
-
# CORRECCIÓN CRÍTICA DEFINITIVA
|
| 13 |
from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
|
| 14 |
import re
|
| 15 |
import math
|
|
@@ -102,24 +102,20 @@ def generate_script(prompt, max_length=150):
|
|
| 102 |
logger.info(f"Generando guión | Prompt: '{prompt[:50]}...' | Longitud máxima: {max_length}")
|
| 103 |
if not tokenizer or not model:
|
| 104 |
logger.warning("Modelos GPT-2 no disponibles - Usando prompt original como guion.")
|
| 105 |
-
return prompt.strip()
|
| 106 |
|
| 107 |
-
# Frase de instrucción que se le da a la IA
|
| 108 |
instruction_phrase_start = "Escribe un guion corto, interesante y coherente sobre:"
|
| 109 |
-
# Construir el prompt exacto que se le pasará a la IA
|
| 110 |
ai_prompt = f"{instruction_phrase_start} {prompt}"
|
| 111 |
|
| 112 |
try:
|
| 113 |
-
# Generar texto usando el prompt completo
|
| 114 |
inputs = tokenizer(ai_prompt, return_tensors="pt", truncation=True, max_length=512)
|
| 115 |
-
|
| 116 |
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 117 |
model.to(device)
|
| 118 |
inputs = {k: v.to(device) for k, v in inputs.items()}
|
| 119 |
|
| 120 |
outputs = model.generate(
|
| 121 |
**inputs,
|
| 122 |
-
max_length=max_length + inputs[list(inputs.keys())[0]].size(1),
|
| 123 |
do_sample=True,
|
| 124 |
top_p=0.9,
|
| 125 |
top_k=40,
|
|
@@ -132,68 +128,55 @@ def generate_script(prompt, max_length=150):
|
|
| 132 |
|
| 133 |
text = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 134 |
|
| 135 |
-
# --- Limpiar la frase de instrucción inicial de la salida de la IA ---
|
| 136 |
cleaned_text = text.strip()
|
| 137 |
-
# Intentar encontrar el inicio de la respuesta real después de la instrucción
|
| 138 |
-
# A veces la IA repite el prompt o la instrucción
|
| 139 |
try:
|
| 140 |
-
# Buscar el final de la frase de instrucción literal en la salida
|
| 141 |
instruction_end_idx = text.find(instruction_phrase)
|
| 142 |
if instruction_end_idx != -1:
|
| 143 |
-
# Tomar el texto que viene *después* de la instrucción exacta
|
| 144 |
cleaned_text = text[instruction_end_idx + len(instruction_phrase):].strip()
|
| 145 |
logger.debug("Instrucción inicial encontrada y eliminada del guión generado.")
|
| 146 |
else:
|
| 147 |
-
# Si no se encuentra la frase exacta, buscar solo el inicio de la instrucción base
|
| 148 |
instruction_start_idx = text.find(instruction_phrase_start)
|
| 149 |
if instruction_start_idx != -1:
|
| 150 |
-
# Tomar texto después de la frase base + prompt (heurística)
|
| 151 |
prompt_in_output_idx = text.find(prompt, instruction_start_idx)
|
| 152 |
if prompt_in_output_idx != -1:
|
| 153 |
cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
|
| 154 |
logger.debug("Instrucción base y prompt encontrados y eliminados del guión generado.")
|
| 155 |
else:
|
| 156 |
-
# Fallback: si la instrucción base está pero no el prompt después, tomar después de la instrucción base
|
| 157 |
cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
|
| 158 |
logger.debug("Instrucción base encontrada, eliminada del guión generado (sin prompt detectado).")
|
| 159 |
|
| 160 |
except Exception as e:
|
| 161 |
logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
|
| 162 |
-
cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
|
| 163 |
|
| 164 |
-
|
| 165 |
-
if not cleaned_text or len(cleaned_text) < 10: # Umbral de longitud mínima
|
| 166 |
logger.warning("El guión generado parece muy corto o vacío después de la limpieza. Usando el texto generado original (sin limpieza heurística).")
|
| 167 |
-
cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
|
| 168 |
|
| 169 |
-
# Limpieza final de caracteres especiales y espacios
|
| 170 |
cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
|
| 171 |
-
cleaned_text = cleaned_text.lstrip(':').strip()
|
| 172 |
-
cleaned_text = cleaned_text.lstrip('.').strip()
|
| 173 |
|
| 174 |
-
|
| 175 |
-
# Intentar obtener al menos una oración completa si es posible
|
| 176 |
sentences = cleaned_text.split('.')
|
| 177 |
if sentences and sentences[0].strip():
|
| 178 |
final_text = sentences[0].strip() + '.'
|
| 179 |
-
|
| 180 |
-
if len(sentences) > 1 and sentences[1].strip() and len(final_text.split()) < max_length * 0.7: # Usar un 70% de max_length como umbral
|
| 181 |
final_text += " " + sentences[1].strip() + "."
|
| 182 |
-
final_text = final_text.replace("..", ".")
|
| 183 |
|
| 184 |
logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
|
| 185 |
return final_text.strip()
|
| 186 |
|
| 187 |
logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
|
| 188 |
-
return cleaned_text.strip()
|
| 189 |
|
| 190 |
except Exception as e:
|
| 191 |
logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
|
| 192 |
logger.warning("Usando prompt original como guion debido al error de generación.")
|
| 193 |
return prompt.strip()
|
| 194 |
|
| 195 |
-
#
|
| 196 |
-
async def text_to_speech(text, output_path, voice
|
| 197 |
logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
|
| 198 |
if not text or not text.strip():
|
| 199 |
logger.warning("Texto vacío para TTS")
|
|
@@ -211,7 +194,7 @@ async def text_to_speech(text, output_path, voice="es-ES-JuanNeural"):
|
|
| 211 |
return False
|
| 212 |
|
| 213 |
except Exception as e:
|
| 214 |
-
logger.error(f"Error en TTS: {str(e)}", exc_info=True)
|
| 215 |
return False
|
| 216 |
|
| 217 |
def download_video_file(url, temp_dir):
|
|
@@ -397,26 +380,54 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 397 |
logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
|
| 398 |
temp_intermediate_files = []
|
| 399 |
|
| 400 |
-
# 2. Generar audio de voz
|
| 401 |
logger.info("Generando audio de voz...")
|
| 402 |
voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
|
| 403 |
-
# Usar voz de Juan
|
| 404 |
-
if not asyncio.run(text_to_speech(guion, voz_path, voice="es-ES-JuanNeural")):
|
| 405 |
-
logger.error("Fallo en generación de voz")
|
| 406 |
-
raise ValueError("Error generando voz a partir del guion.")
|
| 407 |
-
temp_intermediate_files.append(voz_path)
|
| 408 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
audio_tts_original = AudioFileClip(voz_path)
|
| 410 |
|
| 411 |
if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
|
| 412 |
-
logger.critical("Clip de audio TTS inicial es inválido (reader is None o duración <= 0).")
|
| 413 |
try: audio_tts_original.close()
|
| 414 |
except: pass
|
| 415 |
audio_tts_original = None
|
| 416 |
-
raise ValueError("Audio de voz generado es inválido.")
|
| 417 |
|
| 418 |
-
audio_tts = audio_tts_original
|
| 419 |
-
audio_duration = audio_tts_original.duration
|
| 420 |
logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
|
| 421 |
|
| 422 |
if audio_duration < 1.0:
|
|
@@ -590,7 +601,7 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 590 |
num_full_repeats = int(audio_duration // final_video_base.duration)
|
| 591 |
remaining_duration = audio_duration % final_video_base.duration
|
| 592 |
|
| 593 |
-
repeated_clips_list = [final_video_base] * num_full_repeats
|
| 594 |
if remaining_duration > 0:
|
| 595 |
try:
|
| 596 |
remaining_clip = final_video_base.subclip(0, remaining_duration)
|
|
@@ -628,7 +639,7 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 628 |
finally:
|
| 629 |
if 'repeated_clips_list' in locals():
|
| 630 |
for clip in repeated_clips_list:
|
| 631 |
-
if clip is not final_video_base:
|
| 632 |
try: clip.close()
|
| 633 |
except: pass
|
| 634 |
|
|
@@ -686,7 +697,6 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 686 |
except: pass
|
| 687 |
musica_audio_original = None
|
| 688 |
else:
|
| 689 |
-
# Usar la duración correcta del video base para loopear la música
|
| 690 |
musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
|
| 691 |
logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
|
| 692 |
|
|
@@ -775,21 +785,18 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 775 |
finally:
|
| 776 |
logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
|
| 777 |
|
| 778 |
-
# Cerrar todos los clips de video fuente iniciales abiertos
|
| 779 |
for clip in source_clips:
|
| 780 |
try:
|
| 781 |
clip.close()
|
| 782 |
except Exception as e:
|
| 783 |
logger.warning(f"Error cerrando clip de video fuente en finally: {str(e)}")
|
| 784 |
|
| 785 |
-
# Cerrar cualquier segmento de video que quede en la lista (debería estar vacía si tuvo éxito)
|
| 786 |
for clip_segment in clips_to_concatenate:
|
| 787 |
try:
|
| 788 |
clip_segment.close()
|
| 789 |
except Exception as e:
|
| 790 |
logger.warning(f"Error cerrando segmento de video en finally: {str(e)}")
|
| 791 |
|
| 792 |
-
# Cerrar clips de audio en orden: música loopeada, música original (si es diferente), TTS original
|
| 793 |
if musica_audio is not None: # musica_audio holds the potentially looped clip
|
| 794 |
try:
|
| 795 |
musica_audio.close()
|
|
@@ -802,7 +809,6 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 802 |
except Exception as e:
|
| 803 |
logger.warning(f"Error cerrando musica_audio_original en finally: {str(e)}")
|
| 804 |
|
| 805 |
-
# audio_tts actualmente solo contiene audio_tts_original, pero se mantiene la estructura
|
| 806 |
if audio_tts is not None and audio_tts is not audio_tts_original:
|
| 807 |
try:
|
| 808 |
audio_tts.close()
|
|
@@ -815,26 +821,22 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 815 |
except Exception as e:
|
| 816 |
logger.warning(f"Error cerrando audio_tts_original en finally: {str(e)}")
|
| 817 |
|
| 818 |
-
|
| 819 |
-
# Cerrar clips de video en orden: video_final (debería cerrar sus componentes), luego video_base (si es diferente de video_final)
|
| 820 |
if video_final is not None:
|
| 821 |
try:
|
| 822 |
video_final.close()
|
| 823 |
except Exception as e:
|
| 824 |
logger.warning(f"Error cerrando video_final en finally: {str(e)}")
|
| 825 |
-
elif video_base is not None and video_base is not video_final:
|
| 826 |
try:
|
| 827 |
video_base.close()
|
| 828 |
except Exception as e:
|
| 829 |
logger.warning(f"Error cerrando video_base en finally: {str(e)}")
|
| 830 |
|
| 831 |
-
# Limpiar archivos intermedios, pero NO el archivo de video final
|
| 832 |
if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
|
| 833 |
final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4")
|
| 834 |
|
| 835 |
for path in temp_intermediate_files:
|
| 836 |
try:
|
| 837 |
-
# Verificar explícitamente que la ruta no sea la ruta de salida del video final antes de eliminar
|
| 838 |
if os.path.isfile(path) and path != final_output_in_temp:
|
| 839 |
logger.debug(f"Eliminando archivo temporal intermedio: {path}")
|
| 840 |
os.remove(path)
|
|
@@ -843,18 +845,19 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 843 |
except Exception as e:
|
| 844 |
logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
|
| 845 |
|
| 846 |
-
# El directorio temporal *persistirá* porque contiene el archivo final
|
| 847 |
logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
|
| 848 |
|
| 849 |
|
|
|
|
| 850 |
def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
| 851 |
logger.info("="*80)
|
| 852 |
logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
|
| 853 |
|
|
|
|
| 854 |
input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
|
| 855 |
|
| 856 |
output_video = None
|
| 857 |
-
output_file = None
|
| 858 |
status_msg = gr.update(value="⏳ Procesando...", interactive=False)
|
| 859 |
|
| 860 |
if not input_text or not input_text.strip():
|
|
@@ -870,30 +873,27 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 870 |
|
| 871 |
try:
|
| 872 |
logger.info("Llamando 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
|
| 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}")
|
| 883 |
-
# Dejar las salidas de video y archivo como None
|
| 884 |
status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
|
| 885 |
|
| 886 |
except ValueError as ve:
|
| 887 |
logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
|
| 888 |
-
# Dejar las salidas de video y archivo como None
|
| 889 |
status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
|
| 890 |
except Exception as e:
|
| 891 |
logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
|
| 892 |
-
# Dejar las salidas de video y archivo como None
|
| 893 |
status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
|
| 894 |
finally:
|
| 895 |
logger.info("Fin del handler run_app.")
|
| 896 |
-
# Retornar las tres salidas
|
| 897 |
return output_video, output_file, status_msg
|
| 898 |
|
| 899 |
|
|
@@ -914,23 +914,26 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 914 |
value="Generar Guion con IA"
|
| 915 |
)
|
| 916 |
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
|
|
|
|
|
|
|
|
|
| 934 |
|
| 935 |
musica_input = gr.Audio(
|
| 936 |
label="Música de fondo (opcional)",
|
|
@@ -943,13 +946,12 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 943 |
|
| 944 |
with gr.Column():
|
| 945 |
video_output = gr.Video(
|
| 946 |
-
label="Previsualización del Video Generado",
|
| 947 |
interactive=False,
|
| 948 |
height=400
|
| 949 |
)
|
| 950 |
-
# Añadir el componente File para la descarga
|
| 951 |
file_output = gr.File(
|
| 952 |
-
label="Descargar Archivo de Video",
|
| 953 |
interactive=False,
|
| 954 |
visible=False # Ocultar inicialmente
|
| 955 |
)
|
|
@@ -961,30 +963,33 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 961 |
value="Esperando entrada..."
|
| 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 |
-
outputs=[
|
| 969 |
)
|
| 970 |
|
| 971 |
-
#
|
| 972 |
generate_btn.click(
|
| 973 |
-
# Acción 1: Resetear salidas y establecer estado a procesando
|
| 974 |
-
lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar 2-5 minutos o más para videos largos.", interactive=False)),
|
| 975 |
outputs=[video_output, file_output, status_output],
|
| 976 |
-
queue=True, #
|
| 977 |
).then(
|
| 978 |
-
# Acción 2: Llamar a la función principal de procesamiento
|
| 979 |
run_app,
|
|
|
|
| 980 |
inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
|
| 981 |
-
outputs=[video_output, file_output, status_output] # Coincidir las 3 salidas
|
| 982 |
).then(
|
| 983 |
-
# Acción 3: Hacer visible el enlace de descarga si se retornó un archivo
|
| 984 |
-
# Verificar si file_output tiene un valor
|
| 985 |
-
lambda video_path, file_path: gr.update(visible=file_path is not None),
|
| 986 |
-
inputs=[video_output, file_output], #
|
| 987 |
-
outputs=[file_output] # Actualizar visibilidad
|
| 988 |
)
|
| 989 |
|
| 990 |
|
|
@@ -1019,6 +1024,8 @@ if __name__ == "__main__":
|
|
| 1019 |
|
| 1020 |
logger.info("Iniciando aplicación Gradio...")
|
| 1021 |
try:
|
|
|
|
|
|
|
| 1022 |
app.launch(server_name="0.0.0.0", server_port=7860, share=False)
|
| 1023 |
except Exception as e:
|
| 1024 |
logger.critical(f"No se pudo iniciar la app: {str(e)}", exc_info=True)
|
|
|
|
| 9 |
import torch
|
| 10 |
from transformers import GPT2Tokenizer, GPT2LMHeadModel
|
| 11 |
from keybert import KeyBERT
|
| 12 |
+
# CORRECCIÓN CRÍTICA DEFINITIVA DEL TYPO DE IMPORTACIÓN
|
| 13 |
from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
|
| 14 |
import re
|
| 15 |
import math
|
|
|
|
| 102 |
logger.info(f"Generando guión | Prompt: '{prompt[:50]}...' | Longitud máxima: {max_length}")
|
| 103 |
if not tokenizer or not model:
|
| 104 |
logger.warning("Modelos GPT-2 no disponibles - Usando prompt original como guion.")
|
| 105 |
+
return prompt.strip()
|
| 106 |
|
|
|
|
| 107 |
instruction_phrase_start = "Escribe un guion corto, interesante y coherente sobre:"
|
|
|
|
| 108 |
ai_prompt = f"{instruction_phrase_start} {prompt}"
|
| 109 |
|
| 110 |
try:
|
|
|
|
| 111 |
inputs = tokenizer(ai_prompt, return_tensors="pt", truncation=True, max_length=512)
|
|
|
|
| 112 |
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 113 |
model.to(device)
|
| 114 |
inputs = {k: v.to(device) for k, v in inputs.items()}
|
| 115 |
|
| 116 |
outputs = model.generate(
|
| 117 |
**inputs,
|
| 118 |
+
max_length=max_length + inputs[list(inputs.keys())[0]].size(1),
|
| 119 |
do_sample=True,
|
| 120 |
top_p=0.9,
|
| 121 |
top_k=40,
|
|
|
|
| 128 |
|
| 129 |
text = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 130 |
|
|
|
|
| 131 |
cleaned_text = text.strip()
|
|
|
|
|
|
|
| 132 |
try:
|
|
|
|
| 133 |
instruction_end_idx = text.find(instruction_phrase)
|
| 134 |
if instruction_end_idx != -1:
|
|
|
|
| 135 |
cleaned_text = text[instruction_end_idx + len(instruction_phrase):].strip()
|
| 136 |
logger.debug("Instrucción inicial encontrada y eliminada del guión generado.")
|
| 137 |
else:
|
|
|
|
| 138 |
instruction_start_idx = text.find(instruction_phrase_start)
|
| 139 |
if instruction_start_idx != -1:
|
|
|
|
| 140 |
prompt_in_output_idx = text.find(prompt, instruction_start_idx)
|
| 141 |
if prompt_in_output_idx != -1:
|
| 142 |
cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
|
| 143 |
logger.debug("Instrucción base y prompt encontrados y eliminados del guión generado.")
|
| 144 |
else:
|
|
|
|
| 145 |
cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
|
| 146 |
logger.debug("Instrucción base encontrada, eliminada del guión generado (sin prompt detectado).")
|
| 147 |
|
| 148 |
except Exception as e:
|
| 149 |
logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
|
| 150 |
+
cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
|
| 151 |
|
| 152 |
+
if not cleaned_text or len(cleaned_text) < 10:
|
|
|
|
| 153 |
logger.warning("El guión generado parece muy corto o vacío después de la limpieza. Usando el texto generado original (sin limpieza heurística).")
|
| 154 |
+
cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
|
| 155 |
|
|
|
|
| 156 |
cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
|
| 157 |
+
cleaned_text = cleaned_text.lstrip(':').strip()
|
| 158 |
+
cleaned_text = cleaned_text.lstrip('.').strip()
|
| 159 |
|
|
|
|
|
|
|
| 160 |
sentences = cleaned_text.split('.')
|
| 161 |
if sentences and sentences[0].strip():
|
| 162 |
final_text = sentences[0].strip() + '.'
|
| 163 |
+
if len(sentences) > 1 and sentences[1].strip() and len(final_text.split()) < max_length * 0.7:
|
|
|
|
| 164 |
final_text += " " + sentences[1].strip() + "."
|
| 165 |
+
final_text = final_text.replace("..", ".")
|
| 166 |
|
| 167 |
logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
|
| 168 |
return final_text.strip()
|
| 169 |
|
| 170 |
logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
|
| 171 |
+
return cleaned_text.strip()
|
| 172 |
|
| 173 |
except Exception as e:
|
| 174 |
logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
|
| 175 |
logger.warning("Usando prompt original como guion debido al error de generación.")
|
| 176 |
return prompt.strip()
|
| 177 |
|
| 178 |
+
# Función TTS con voz especificada
|
| 179 |
+
async def text_to_speech(text, output_path, voice): # voice is now a parameter
|
| 180 |
logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
|
| 181 |
if not text or not text.strip():
|
| 182 |
logger.warning("Texto vacío para TTS")
|
|
|
|
| 194 |
return False
|
| 195 |
|
| 196 |
except Exception as e:
|
| 197 |
+
logger.error(f"Error en TTS con voz '{voice}': {str(e)}", exc_info=True)
|
| 198 |
return False
|
| 199 |
|
| 200 |
def download_video_file(url, temp_dir):
|
|
|
|
| 380 |
logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
|
| 381 |
temp_intermediate_files = []
|
| 382 |
|
| 383 |
+
# 2. Generar audio de voz con reintentos y voz de respaldo
|
| 384 |
logger.info("Generando audio de voz...")
|
| 385 |
voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
|
| 387 |
+
primary_voice = "es-ES-JuanNeural"
|
| 388 |
+
fallback_voice = "es-ES-ElviraNeural" # Otra voz en español
|
| 389 |
+
tts_success = False
|
| 390 |
+
retries = 3 # Número de intentos
|
| 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 # Salir del bucle de reintentos si tiene éxito
|
| 402 |
+
except Exception as e:
|
| 403 |
+
# La excepción ya se registra dentro de text_to_speech
|
| 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}")
|
| 408 |
+
elif not tts_success and attempt < retries - 1:
|
| 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) # Añadir a la lista de limpieza si se creó
|
| 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:
|
| 423 |
+
logger.critical("Clip de audio TTS inicial es inválido (reader is None o duración <= 0) *después* de crear AudioFileClip.")
|
| 424 |
try: audio_tts_original.close()
|
| 425 |
except: pass
|
| 426 |
audio_tts_original = None
|
| 427 |
+
raise ValueError("Audio de voz generado es inválido después de procesamiento inicial.")
|
| 428 |
|
| 429 |
+
audio_tts = audio_tts_original
|
| 430 |
+
audio_duration = audio_tts_original.duration
|
| 431 |
logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
|
| 432 |
|
| 433 |
if audio_duration < 1.0:
|
|
|
|
| 601 |
num_full_repeats = int(audio_duration // final_video_base.duration)
|
| 602 |
remaining_duration = audio_duration % final_video_base.duration
|
| 603 |
|
| 604 |
+
repeated_clips_list = [final_video_base] * num_full_repeats
|
| 605 |
if remaining_duration > 0:
|
| 606 |
try:
|
| 607 |
remaining_clip = final_video_base.subclip(0, remaining_duration)
|
|
|
|
| 639 |
finally:
|
| 640 |
if 'repeated_clips_list' in locals():
|
| 641 |
for clip in repeated_clips_list:
|
| 642 |
+
if clip is not final_video_base:
|
| 643 |
try: clip.close()
|
| 644 |
except: pass
|
| 645 |
|
|
|
|
| 697 |
except: pass
|
| 698 |
musica_audio_original = None
|
| 699 |
else:
|
|
|
|
| 700 |
musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
|
| 701 |
logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
|
| 702 |
|
|
|
|
| 785 |
finally:
|
| 786 |
logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
|
| 787 |
|
|
|
|
| 788 |
for clip in source_clips:
|
| 789 |
try:
|
| 790 |
clip.close()
|
| 791 |
except Exception as e:
|
| 792 |
logger.warning(f"Error cerrando clip de video fuente en finally: {str(e)}")
|
| 793 |
|
|
|
|
| 794 |
for clip_segment in clips_to_concatenate:
|
| 795 |
try:
|
| 796 |
clip_segment.close()
|
| 797 |
except Exception as e:
|
| 798 |
logger.warning(f"Error cerrando segmento de video en finally: {str(e)}")
|
| 799 |
|
|
|
|
| 800 |
if musica_audio is not None: # musica_audio holds the potentially looped clip
|
| 801 |
try:
|
| 802 |
musica_audio.close()
|
|
|
|
| 809 |
except Exception as e:
|
| 810 |
logger.warning(f"Error cerrando musica_audio_original en finally: {str(e)}")
|
| 811 |
|
|
|
|
| 812 |
if audio_tts is not None and audio_tts is not audio_tts_original:
|
| 813 |
try:
|
| 814 |
audio_tts.close()
|
|
|
|
| 821 |
except Exception as e:
|
| 822 |
logger.warning(f"Error cerrando audio_tts_original en finally: {str(e)}")
|
| 823 |
|
|
|
|
|
|
|
| 824 |
if video_final is not None:
|
| 825 |
try:
|
| 826 |
video_final.close()
|
| 827 |
except Exception as e:
|
| 828 |
logger.warning(f"Error cerrando video_final en finally: {str(e)}")
|
| 829 |
+
elif video_base is not None and video_base is not video_final:
|
| 830 |
try:
|
| 831 |
video_base.close()
|
| 832 |
except Exception as e:
|
| 833 |
logger.warning(f"Error cerrando video_base en finally: {str(e)}")
|
| 834 |
|
|
|
|
| 835 |
if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
|
| 836 |
final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4")
|
| 837 |
|
| 838 |
for path in temp_intermediate_files:
|
| 839 |
try:
|
|
|
|
| 840 |
if os.path.isfile(path) and path != final_output_in_temp:
|
| 841 |
logger.debug(f"Eliminando archivo temporal intermedio: {path}")
|
| 842 |
os.remove(path)
|
|
|
|
| 845 |
except Exception as e:
|
| 846 |
logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
|
| 847 |
|
|
|
|
| 848 |
logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
|
| 849 |
|
| 850 |
|
| 851 |
+
# CAMBIO CRÍTICO: run_app ahora toma 4 argumentos
|
| 852 |
def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
| 853 |
logger.info("="*80)
|
| 854 |
logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
|
| 855 |
|
| 856 |
+
# La lógica para elegir el texto de entrada YA ESTÁ AQUÍ
|
| 857 |
input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
|
| 858 |
|
| 859 |
output_video = None
|
| 860 |
+
output_file = None
|
| 861 |
status_msg = gr.update(value="⏳ Procesando...", interactive=False)
|
| 862 |
|
| 863 |
if not input_text or not input_text.strip():
|
|
|
|
| 873 |
|
| 874 |
try:
|
| 875 |
logger.info("Llamando a crear_video...")
|
| 876 |
+
# Pasar el input_text elegido y el archivo de música
|
| 877 |
video_path = crear_video(prompt_type, input_text, musica_file)
|
| 878 |
|
| 879 |
if video_path and os.path.exists(video_path):
|
| 880 |
logger.info(f"crear_video retornó path: {video_path}")
|
| 881 |
logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
|
| 882 |
+
output_video = video_path
|
| 883 |
+
output_file = video_path
|
| 884 |
status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
|
| 885 |
else:
|
| 886 |
logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
|
|
|
|
| 887 |
status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
|
| 888 |
|
| 889 |
except ValueError as ve:
|
| 890 |
logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
|
|
|
|
| 891 |
status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
|
| 892 |
except Exception as e:
|
| 893 |
logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
|
|
|
|
| 894 |
status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
|
| 895 |
finally:
|
| 896 |
logger.info("Fin del handler run_app.")
|
|
|
|
| 897 |
return output_video, output_file, status_msg
|
| 898 |
|
| 899 |
|
|
|
|
| 914 |
value="Generar Guion con IA"
|
| 915 |
)
|
| 916 |
|
| 917 |
+
# Estos inputs siempre se pasan a run_app, independientemente de cuál se use
|
| 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 |
+
# Hacer visible/oculto por el evento change
|
| 925 |
+
visible=True
|
| 926 |
+
)
|
| 927 |
+
|
| 928 |
+
prompt_manual = gr.Textbox(
|
| 929 |
+
label="Tu Guion Completo",
|
| 930 |
+
lines=5,
|
| 931 |
+
placeholder="Ej: En este video exploraremos los misterios del océano...",
|
| 932 |
+
max_lines=10,
|
| 933 |
+
value="",
|
| 934 |
+
# Hacer visible/oculto por el evento change
|
| 935 |
+
visible=False # Oculto por defecto
|
| 936 |
+
)
|
| 937 |
|
| 938 |
musica_input = gr.Audio(
|
| 939 |
label="Música de fondo (opcional)",
|
|
|
|
| 946 |
|
| 947 |
with gr.Column():
|
| 948 |
video_output = gr.Video(
|
| 949 |
+
label="Previsualización del Video Generado",
|
| 950 |
interactive=False,
|
| 951 |
height=400
|
| 952 |
)
|
|
|
|
| 953 |
file_output = gr.File(
|
| 954 |
+
label="Descargar Archivo de Video",
|
| 955 |
interactive=False,
|
| 956 |
visible=False # Ocultar inicialmente
|
| 957 |
)
|
|
|
|
| 963 |
value="Esperando entrada..."
|
| 964 |
)
|
| 965 |
|
| 966 |
+
# Evento para mostrar/ocultar los campos de texto según el tipo de prompt
|
| 967 |
+
# Ahora usamos las Columnas para controlar la visibilidad
|
| 968 |
prompt_type.change(
|
| 969 |
lambda x: (gr.update(visible=x == "Generar Guion con IA"),
|
| 970 |
gr.update(visible=x == "Usar Mi Guion")),
|
| 971 |
inputs=prompt_type,
|
| 972 |
+
outputs=[prompt_ia.parent, prompt_manual.parent] # Apuntar a las Columnas padre
|
| 973 |
)
|
| 974 |
|
| 975 |
+
# Evento click del botón de generar video
|
| 976 |
generate_btn.click(
|
| 977 |
+
# Acción 1 (síncrona): Resetear salidas y establecer estado a procesando
|
| 978 |
+
lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar 2-5 minutos o más para videos largos.", interactive=False)),
|
| 979 |
outputs=[video_output, file_output, status_output],
|
| 980 |
+
queue=True, # Usar la cola de Gradio para tareas largas
|
| 981 |
).then(
|
| 982 |
+
# Acción 2 (asíncrona): Llamar a la función principal de procesamiento
|
| 983 |
run_app,
|
| 984 |
+
# CAMBIO CRÍTICO: Pasar los 4 argumentos definidos por run_app
|
| 985 |
inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
|
| 986 |
+
outputs=[video_output, file_output, status_output] # Coincidir las 3 salidas de run_app
|
| 987 |
).then(
|
| 988 |
+
# Acción 3 (síncrona): Hacer visible el enlace de descarga si se retornó un archivo
|
| 989 |
+
# Verificar si file_output tiene un valor (el path)
|
| 990 |
+
lambda video_path, file_path: gr.update(visible=file_path is not None),
|
| 991 |
+
inputs=[video_output, file_output], # Usar ambas salidas como entrada para esta función
|
| 992 |
+
outputs=[file_output] # Actualizar visibilidad del componente file_output
|
| 993 |
)
|
| 994 |
|
| 995 |
|
|
|
|
| 1024 |
|
| 1025 |
logger.info("Iniciando aplicación Gradio...")
|
| 1026 |
try:
|
| 1027 |
+
# Gradio Queue maneja tareas largas, no es necesario un ajuste global de timeout aquí.
|
| 1028 |
+
# El timeout se gestiona por solicitud o por el límite del worker de la cola.
|
| 1029 |
app.launch(server_name="0.0.0.0", server_port=7860, share=False)
|
| 1030 |
except Exception as e:
|
| 1031 |
logger.critical(f"No se pudo iniciar la app: {str(e)}", exc_info=True)
|