Spaces:
Runtime error
Runtime error
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)
|