Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -61,6 +61,36 @@ except Exception as e:
|
|
| 61 |
logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
|
| 62 |
kw_model = None
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
def buscar_videos_pexels(query, api_key, per_page=5):
|
| 65 |
if not api_key:
|
| 66 |
logger.warning("No se puede buscar en Pexels: API Key no configurada.")
|
|
@@ -129,53 +159,63 @@ def generate_script(prompt, max_length=150):
|
|
| 129 |
text = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 130 |
|
| 131 |
cleaned_text = text.strip()
|
|
|
|
| 132 |
try:
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
| 137 |
else:
|
|
|
|
| 138 |
instruction_start_idx = text.find(instruction_phrase_start)
|
| 139 |
if instruction_start_idx != -1:
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
| 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 |
-
|
| 153 |
-
|
| 154 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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
|
| 179 |
async def text_to_speech(text, output_path, voice):
|
| 180 |
logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
|
| 181 |
if not text or not text.strip():
|
|
@@ -346,10 +386,12 @@ def extract_visual_keywords_from_script(script_text):
|
|
| 346 |
logger.info(f"Palabras clave finales: {top_keywords}")
|
| 347 |
return top_keywords
|
| 348 |
|
| 349 |
-
|
|
|
|
| 350 |
logger.info("="*80)
|
| 351 |
logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
|
| 352 |
logger.debug(f"Input: '{input_text[:100]}...'")
|
|
|
|
| 353 |
|
| 354 |
start_time = datetime.now()
|
| 355 |
temp_dir_intermediate = None
|
|
@@ -380,35 +422,39 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 380 |
logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
|
| 381 |
temp_intermediate_files = []
|
| 382 |
|
| 383 |
-
# 2. Generar audio de voz con reintentos
|
| 384 |
logger.info("Generando audio de voz...")
|
| 385 |
voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
|
| 386 |
|
| 387 |
-
|
| 388 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
tts_success = False
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
|
| 392 |
-
|
| 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
|
| 400 |
-
break
|
| 401 |
except Exception as e:
|
| 402 |
-
|
| 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}")
|
| 406 |
-
elif not tts_success and attempt < retries - 1:
|
| 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(
|
| 412 |
raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
|
| 413 |
|
| 414 |
temp_intermediate_files.append(voz_path)
|
|
@@ -843,8 +889,8 @@ def crear_video(prompt_type, input_text, musica_file=None):
|
|
| 843 |
logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
|
| 844 |
|
| 845 |
|
| 846 |
-
#
|
| 847 |
-
def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
| 848 |
logger.info("="*80)
|
| 849 |
logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
|
| 850 |
|
|
@@ -860,17 +906,27 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 860 |
# Retornar None para video y archivo, actualizar estado con mensaje de error
|
| 861 |
return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
|
| 862 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 863 |
logger.info(f"Tipo de entrada: {prompt_type}")
|
| 864 |
logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
|
| 865 |
if musica_file:
|
| 866 |
logger.info(f"Archivo de música recibido: {musica_file}")
|
| 867 |
else:
|
| 868 |
logger.info("No se proporcionó archivo de música.")
|
|
|
|
| 869 |
|
| 870 |
try:
|
| 871 |
logger.info("Llamando a crear_video...")
|
| 872 |
-
# Pasar 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}")
|
|
@@ -890,12 +946,11 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 890 |
status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
|
| 891 |
finally:
|
| 892 |
logger.info("Fin del handler run_app.")
|
| 893 |
-
# Retornar las tres salidas esperadas por el evento click
|
| 894 |
return output_video, output_file, status_msg
|
| 895 |
|
| 896 |
|
| 897 |
# Interfaz de Gradio
|
| 898 |
-
|
| 899 |
.gradio-container {max-width: 800px; margin: auto;}
|
| 900 |
h1 {text-align: center;}
|
| 901 |
""") as app:
|
|
@@ -912,18 +967,16 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 912 |
)
|
| 913 |
|
| 914 |
# Contenedores para los campos de texto para controlar la visibilidad
|
| 915 |
-
# visible=True/False está aquí en la COLUMNA
|
| 916 |
with gr.Column(visible=True) as ia_guion_column:
|
| 917 |
prompt_ia = gr.Textbox(
|
| 918 |
label="Tema para IA",
|
| 919 |
lines=2,
|
| 920 |
-
placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer...",
|
| 921 |
max_lines=4,
|
| 922 |
value=""
|
| 923 |
-
# visible=...
|
| 924 |
)
|
| 925 |
|
| 926 |
-
# visible=True/False está aquí en la COLUMNA
|
| 927 |
with gr.Column(visible=False) as manual_guion_column:
|
| 928 |
prompt_manual = gr.Textbox(
|
| 929 |
label="Tu Guion Completo",
|
|
@@ -931,7 +984,7 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 931 |
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!",
|
| 932 |
max_lines=10,
|
| 933 |
value=""
|
| 934 |
-
|
| 935 |
)
|
| 936 |
|
| 937 |
musica_input = gr.Audio(
|
|
@@ -939,7 +992,7 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 939 |
type="filepath",
|
| 940 |
interactive=True,
|
| 941 |
value=None
|
| 942 |
-
|
| 943 |
)
|
| 944 |
|
| 945 |
# --- COMPONENTE: Selección de Voz ---
|
|
@@ -948,7 +1001,7 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 948 |
choices=AVAILABLE_VOICES,
|
| 949 |
value=DEFAULT_VOICE,
|
| 950 |
interactive=True
|
| 951 |
-
|
| 952 |
)
|
| 953 |
# --- FIN COMPONENTE ---
|
| 954 |
|
|
@@ -960,12 +1013,13 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 960 |
label="Previsualización del Video Generado",
|
| 961 |
interactive=False,
|
| 962 |
height=400
|
| 963 |
-
|
| 964 |
)
|
| 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",
|
|
@@ -973,7 +1027,7 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 973 |
show_label=False,
|
| 974 |
placeholder="Esperando acción...",
|
| 975 |
value="Esperando entrada..."
|
| 976 |
-
|
| 977 |
)
|
| 978 |
|
| 979 |
# Evento para mostrar/ocultar los campos de texto según el tipo de prompt
|
|
@@ -981,8 +1035,7 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 981 |
lambda x: (gr.update(visible=x == "Generar Guion con IA"),
|
| 982 |
gr.update(visible=x == "Usar Mi Guion")),
|
| 983 |
inputs=prompt_type,
|
| 984 |
-
|
| 985 |
-
outputs=[ia_guion_column, manual_guion_column]
|
| 986 |
)
|
| 987 |
|
| 988 |
# Evento click del botón de generar video
|
|
@@ -994,29 +1047,31 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
|
|
| 994 |
).then(
|
| 995 |
# Acción 2 (asíncrona): Llamar a la función principal
|
| 996 |
run_app,
|
| 997 |
-
# PASAR TODOS LOS INPUTS
|
| 998 |
-
|
| 999 |
-
inputs=[prompt_type, prompt_ia, prompt_manual, musica_input, voice_dropdown],
|
| 1000 |
# run_app retornará los 3 outputs esperados
|
| 1001 |
outputs=[video_output, file_output, status_output]
|
| 1002 |
).then(
|
| 1003 |
# Acción 3 (síncrona): Hacer visible el enlace de descarga
|
|
|
|
| 1004 |
lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
|
|
|
|
| 1005 |
inputs=[video_output, file_output, status_output],
|
|
|
|
| 1006 |
outputs=[file_output]
|
| 1007 |
)
|
| 1008 |
|
|
|
|
| 1009 |
gr.Markdown("### Instrucciones:")
|
| 1010 |
gr.Markdown("""
|
| 1011 |
1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
|
| 1012 |
-
2. **Selecciona el tipo de entrada**:
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
7. Si hay errores, revisa el log `video_generator_full.log` para más detalles.
|
| 1020 |
""")
|
| 1021 |
gr.Markdown("---")
|
| 1022 |
gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
|
|
@@ -1028,7 +1083,7 @@ if __name__ == "__main__":
|
|
| 1028 |
try:
|
| 1029 |
temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
|
| 1030 |
temp_clip.close()
|
| 1031 |
-
logger.info("Clips base de MoviePy
|
| 1032 |
except Exception as e:
|
| 1033 |
logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
|
| 1034 |
|
|
|
|
| 61 |
logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
|
| 62 |
kw_model = None
|
| 63 |
|
| 64 |
+
# --- Obtener voces de Edge TTS al inicio ---
|
| 65 |
+
async def get_available_voices():
|
| 66 |
+
logger.info("Obteniendo lista de voces disponibles de Edge TTS...")
|
| 67 |
+
try:
|
| 68 |
+
voices = await edge_tts.VoicesManager.create()
|
| 69 |
+
# Retornar solo voces en español si prefieres, o dejar todas
|
| 70 |
+
es_voices = [voice.Name for voice in voices.Voices if voice.Locale.startswith('es-')]
|
| 71 |
+
if es_voices:
|
| 72 |
+
logger.info(f"Encontradas {len(es_voices)} voces en español.")
|
| 73 |
+
return es_voices
|
| 74 |
+
else:
|
| 75 |
+
# Si no hay español, retornar todas las voces
|
| 76 |
+
all_voices = [voice.Name for voice in voices.Voices]
|
| 77 |
+
logger.warning(f"No se encontraron voces en español. Retornando {len(all_voices)} voces en todos los idiomas.")
|
| 78 |
+
return all_voices if all_voices else ["en-US-AriaNeural"] # Fallback si no hay ninguna
|
| 79 |
+
|
| 80 |
+
except Exception as e:
|
| 81 |
+
logger.error(f"Error obteniendo voces de Edge TTS: {str(e)}", exc_info=True)
|
| 82 |
+
# Retornar una lista de voces por defecto si falla la API de Edge TTS
|
| 83 |
+
logger.warning("No se pudieron obtener voces de Edge TTS. Usando lista de voces por defecto.")
|
| 84 |
+
return ["es-ES-JuanNeural", "es-ES-ElviraNeural", "en-US-AriaNeural"]
|
| 85 |
+
|
| 86 |
+
# Obtener las voces al inicio del script (esto puede tardar un poco)
|
| 87 |
+
logger.info("Inicializando lista de voces disponibles...")
|
| 88 |
+
AVAILABLE_VOICES = asyncio.run(get_available_voices())
|
| 89 |
+
# Establecer una voz por defecto inicial
|
| 90 |
+
DEFAULT_VOICE = "es-ES-JuanNeural" if "es-ES-JuanNeural" in AVAILABLE_VOICES else (AVAILABLE_VOICES[0] if AVAILABLE_VOICES else "en-US-AriaNeural")
|
| 91 |
+
logger.info(f"Voz por defecto seleccionada: {DEFAULT_VOICE}")
|
| 92 |
+
|
| 93 |
+
|
| 94 |
def buscar_videos_pexels(query, api_key, per_page=5):
|
| 95 |
if not api_key:
|
| 96 |
logger.warning("No se puede buscar en Pexels: API Key no configurada.")
|
|
|
|
| 159 |
text = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 160 |
|
| 161 |
cleaned_text = text.strip()
|
| 162 |
+
# Limpieza mejorada de la frase de instrucción
|
| 163 |
try:
|
| 164 |
+
# Buscar el índice de inicio del prompt original dentro del texto generado
|
| 165 |
+
prompt_in_output_idx = text.lower().find(prompt.lower())
|
| 166 |
+
if prompt_in_output_idx != -1:
|
| 167 |
+
# Tomar todo el texto DESPUÉS del prompt original
|
| 168 |
+
cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
|
| 169 |
+
logger.debug("Texto limpiado tomando parte después del prompt original.")
|
| 170 |
else:
|
| 171 |
+
# Fallback si el prompt original no está exacto en la salida: buscar la frase de instrucción base
|
| 172 |
instruction_start_idx = text.find(instruction_phrase_start)
|
| 173 |
if instruction_start_idx != -1:
|
| 174 |
+
# Tomar texto después de la frase base (puede incluir el prompt)
|
| 175 |
+
cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
|
| 176 |
+
logger.debug("Texto limpiado tomando parte después de la frase de instrucción base.")
|
| 177 |
+
else:
|
| 178 |
+
# Si ni la frase de instrucción ni el prompt se encuentran, usar el texto original
|
| 179 |
+
logger.warning("No se pudo identificar el inicio del guión generado. Usando texto generado completo.")
|
| 180 |
+
cleaned_text = text.strip() # Limpieza básica
|
| 181 |
+
|
| 182 |
|
| 183 |
except Exception as e:
|
| 184 |
logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
|
| 185 |
+
cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Limpieza básica como fallback
|
| 186 |
|
| 187 |
+
# Asegurarse de que el texto resultante no sea solo la instrucción o vacío
|
| 188 |
+
if not cleaned_text or len(cleaned_text) < 10: # Umbral de longitud mínima
|
| 189 |
+
logger.warning("El guión generado parece muy corto o vacío después de la limpieza heurística. Usando el texto generado original (sin limpieza adicional).")
|
| 190 |
+
cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Fallback al texto original limpio
|
| 191 |
|
| 192 |
+
# Limpieza final de caracteres especiales y espacios sobrantes
|
| 193 |
cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
|
| 194 |
+
cleaned_text = cleaned_text.lstrip(':').strip() # Quitar posibles ':' al inicio
|
| 195 |
+
cleaned_text = cleaned_text.lstrip('.').strip() # Quitar posibles '.' al inicio
|
| 196 |
+
|
| 197 |
|
| 198 |
+
# Intentar obtener al menos una oración completa si es posible para un inicio más limpio
|
| 199 |
sentences = cleaned_text.split('.')
|
| 200 |
if sentences and sentences[0].strip():
|
| 201 |
final_text = sentences[0].strip() + '.'
|
| 202 |
+
# Añadir la segunda oración si existe y es razonable
|
| 203 |
+
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
|
| 204 |
final_text += " " + sentences[1].strip() + "."
|
| 205 |
+
final_text = final_text.replace("..", ".") # Limpiar doble punto
|
| 206 |
|
| 207 |
logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
|
| 208 |
return final_text.strip()
|
| 209 |
|
| 210 |
logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
|
| 211 |
+
return cleaned_text.strip() # Si no se puede formar una oración, devolver el texto limpio tal cual
|
| 212 |
|
| 213 |
except Exception as e:
|
| 214 |
logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
|
| 215 |
logger.warning("Usando prompt original como guion debido al error de generación.")
|
| 216 |
return prompt.strip()
|
| 217 |
|
| 218 |
+
# Función TTS ahora recibe la voz a usar
|
| 219 |
async def text_to_speech(text, output_path, voice):
|
| 220 |
logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
|
| 221 |
if not text or not text.strip():
|
|
|
|
| 386 |
logger.info(f"Palabras clave finales: {top_keywords}")
|
| 387 |
return top_keywords
|
| 388 |
|
| 389 |
+
# crear_video ahora recibe la voz seleccionada
|
| 390 |
+
def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
|
| 391 |
logger.info("="*80)
|
| 392 |
logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
|
| 393 |
logger.debug(f"Input: '{input_text[:100]}...'")
|
| 394 |
+
logger.info(f"Voz seleccionada para TTS: {selected_voice}")
|
| 395 |
|
| 396 |
start_time = datetime.now()
|
| 397 |
temp_dir_intermediate = None
|
|
|
|
| 422 |
logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
|
| 423 |
temp_intermediate_files = []
|
| 424 |
|
| 425 |
+
# 2. Generar audio de voz usando la voz seleccionada, con reintentos si falla
|
| 426 |
logger.info("Generando audio de voz...")
|
| 427 |
voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
|
| 428 |
|
| 429 |
+
tts_voices_to_try = [selected_voice] # Intentar primero la voz seleccionada
|
| 430 |
+
# Añadir voces de respaldo si no están ya en la lista y son diferentes a la seleccionada
|
| 431 |
+
if "es-ES-JuanNeural" not in tts_voices_to_try: tts_voices_to_try.append("es-ES-JuanNeural")
|
| 432 |
+
if "es-ES-ElviraNeural" not in tts_voices_to_try: tts_voices_to_try.append("es-ES-ElviraNeural")
|
| 433 |
+
# Si la lista de voces disponibles es fiable, podrías usar un subconjunto ordenado para reintentos más amplios
|
| 434 |
+
# Ejemplo: for voice_id in [selected_voice] + sorted([v for v in AVAILABLE_VOICES if v.startswith('es-') and v != selected_voice]) + sorted([v for v in AVAILABLE_VOICES if not v.startswith('es-') and v != selected_voice]):
|
| 435 |
+
|
| 436 |
+
|
| 437 |
tts_success = False
|
| 438 |
+
tried_voices = set() # Usar un set para rastrear voces intentadas de forma eficiente
|
| 439 |
+
|
| 440 |
+
for current_voice in tts_voices_to_try:
|
| 441 |
+
if current_voice in tried_voices: continue # Evitar intentar la misma voz dos veces
|
| 442 |
+
tried_voices.add(current_voice)
|
| 443 |
|
| 444 |
+
logger.info(f"Intentando TTS con voz: {current_voice}...")
|
|
|
|
|
|
|
|
|
|
| 445 |
try:
|
| 446 |
tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=current_voice))
|
| 447 |
if tts_success:
|
| 448 |
+
logger.info(f"TTS exitoso con voz '{current_voice}'.")
|
| 449 |
+
break # Salir del bucle de reintentos si tiene éxito
|
| 450 |
except Exception as e:
|
| 451 |
+
logger.warning(f"Fallo al generar TTS con voz '{current_voice}': {str(e)}", exc_info=True)
|
| 452 |
+
pass # Continuar al siguiente intento
|
|
|
|
|
|
|
|
|
|
|
|
|
| 453 |
|
| 454 |
|
| 455 |
+
# Verificar si el archivo fue creado después de todos los intentos
|
| 456 |
if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
|
| 457 |
+
logger.error("Fallo en la generación de voz después de todos los intentos. Archivo de audio no creado o es muy pequeño.")
|
| 458 |
raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
|
| 459 |
|
| 460 |
temp_intermediate_files.append(voz_path)
|
|
|
|
| 889 |
logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
|
| 890 |
|
| 891 |
|
| 892 |
+
# CAMBIO CRÍTICO: run_app ahora recibe TODOS los inputs que Gradio le pasa desde el evento click
|
| 893 |
+
def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice): # <-- Recibe el valor del Dropdown
|
| 894 |
logger.info("="*80)
|
| 895 |
logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
|
| 896 |
|
|
|
|
| 906 |
# Retornar None para video y archivo, actualizar estado con mensaje de error
|
| 907 |
return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
|
| 908 |
|
| 909 |
+
# Validar la voz seleccionada. Si no es válida, usar la por defecto.
|
| 910 |
+
# AVAILABLE_VOICES se obtiene al inicio.
|
| 911 |
+
if selected_voice not in AVAILABLE_VOICES:
|
| 912 |
+
logger.warning(f"Voz seleccionada inválida o no encontrada en la lista: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE}.")
|
| 913 |
+
selected_voice = DEFAULT_VOICE
|
| 914 |
+
else:
|
| 915 |
+
logger.info(f"Voz seleccionada validada: {selected_voice}")
|
| 916 |
+
|
| 917 |
+
|
| 918 |
logger.info(f"Tipo de entrada: {prompt_type}")
|
| 919 |
logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
|
| 920 |
if musica_file:
|
| 921 |
logger.info(f"Archivo de música recibido: {musica_file}")
|
| 922 |
else:
|
| 923 |
logger.info("No se proporcionó archivo de música.")
|
| 924 |
+
logger.info(f"Voz final a usar: {selected_voice}") # Loguear la voz final que se usará
|
| 925 |
|
| 926 |
try:
|
| 927 |
logger.info("Llamando a crear_video...")
|
| 928 |
+
# Pasar el input_text elegido, la voz seleccionada y el archivo de música a crear_video
|
| 929 |
+
video_path = crear_video(prompt_type, input_text, selected_voice, musica_file) # <-- PASAR selected_voice a crear_video
|
| 930 |
|
| 931 |
if video_path and os.path.exists(video_path):
|
| 932 |
logger.info(f"crear_video retornó path: {video_path}")
|
|
|
|
| 946 |
status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
|
| 947 |
finally:
|
| 948 |
logger.info("Fin del handler run_app.")
|
|
|
|
| 949 |
return output_video, output_file, status_msg
|
| 950 |
|
| 951 |
|
| 952 |
# Interfaz de Gradio
|
| 953 |
+
with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
|
| 954 |
.gradio-container {max-width: 800px; margin: auto;}
|
| 955 |
h1 {text-align: center;}
|
| 956 |
""") as app:
|
|
|
|
| 967 |
)
|
| 968 |
|
| 969 |
# Contenedores para los campos de texto para controlar la visibilidad
|
|
|
|
| 970 |
with gr.Column(visible=True) as ia_guion_column:
|
| 971 |
prompt_ia = gr.Textbox(
|
| 972 |
label="Tema para IA",
|
| 973 |
lines=2,
|
| 974 |
+
placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer, mostrando la belleza de la naturaleza...",
|
| 975 |
max_lines=4,
|
| 976 |
value=""
|
| 977 |
+
# visible=... <-- ¡NO DEBE ESTAR AQUÍ!
|
| 978 |
)
|
| 979 |
|
|
|
|
| 980 |
with gr.Column(visible=False) as manual_guion_column:
|
| 981 |
prompt_manual = gr.Textbox(
|
| 982 |
label="Tu Guion Completo",
|
|
|
|
| 984 |
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!",
|
| 985 |
max_lines=10,
|
| 986 |
value=""
|
| 987 |
+
# visible=... <-- ¡NO DEBE ESTAR AQUÍ!
|
| 988 |
)
|
| 989 |
|
| 990 |
musica_input = gr.Audio(
|
|
|
|
| 992 |
type="filepath",
|
| 993 |
interactive=True,
|
| 994 |
value=None
|
| 995 |
+
# visible=... <-- ¡NO DEBE ESTAR AQUÍ!
|
| 996 |
)
|
| 997 |
|
| 998 |
# --- COMPONENTE: Selección de Voz ---
|
|
|
|
| 1001 |
choices=AVAILABLE_VOICES,
|
| 1002 |
value=DEFAULT_VOICE,
|
| 1003 |
interactive=True
|
| 1004 |
+
# visible=... <-- ¡NO DEBE ESTAR AQUÍ!
|
| 1005 |
)
|
| 1006 |
# --- FIN COMPONENTE ---
|
| 1007 |
|
|
|
|
| 1013 |
label="Previsualización del Video Generado",
|
| 1014 |
interactive=False,
|
| 1015 |
height=400
|
| 1016 |
+
# visible=... <-- ¡NO DEBE ESTAR AQUÍ!
|
| 1017 |
)
|
| 1018 |
file_output = gr.File(
|
| 1019 |
label="Descargar Archivo de Video",
|
| 1020 |
interactive=False,
|
| 1021 |
+
visible=False # <-- ESTÁ BIEN AQUÍ porque su visibilidad se controla por el último then()
|
| 1022 |
+
# visible=... <-- ¡NO DEBE ESTAR AQUÍ si ya está visible=False arriba!
|
| 1023 |
)
|
| 1024 |
status_output = gr.Textbox(
|
| 1025 |
label="Estado",
|
|
|
|
| 1027 |
show_label=False,
|
| 1028 |
placeholder="Esperando acción...",
|
| 1029 |
value="Esperando entrada..."
|
| 1030 |
+
# visible=... <-- ¡NO DEBE ESTAR AQUÍ!
|
| 1031 |
)
|
| 1032 |
|
| 1033 |
# Evento para mostrar/ocultar los campos de texto según el tipo de prompt
|
|
|
|
| 1035 |
lambda x: (gr.update(visible=x == "Generar Guion con IA"),
|
| 1036 |
gr.update(visible=x == "Usar Mi Guion")),
|
| 1037 |
inputs=prompt_type,
|
| 1038 |
+
outputs=[ia_guion_column, manual_guion_column] # Apuntar a las Columnas contenedoras
|
|
|
|
| 1039 |
)
|
| 1040 |
|
| 1041 |
# Evento click del botón de generar video
|
|
|
|
| 1047 |
).then(
|
| 1048 |
# Acción 2 (asíncrona): Llamar a la función principal
|
| 1049 |
run_app,
|
| 1050 |
+
# PASAR TODOS LOS INPUTS DE LA INTERFAZ que run_app espera
|
| 1051 |
+
inputs=[prompt_type, prompt_ia, prompt_manual, musica_input, voice_dropdown], # <-- Pasar los 5 inputs a run_app
|
|
|
|
| 1052 |
# run_app retornará los 3 outputs esperados
|
| 1053 |
outputs=[video_output, file_output, status_output]
|
| 1054 |
).then(
|
| 1055 |
# Acción 3 (síncrona): Hacer visible el enlace de descarga
|
| 1056 |
+
# Recibe las salidas de la Acción 2
|
| 1057 |
lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
|
| 1058 |
+
# Inputs para esta lambda son los outputs del .then() anterior
|
| 1059 |
inputs=[video_output, file_output, status_output],
|
| 1060 |
+
# Actualizamos la visibilidad del componente file_output
|
| 1061 |
outputs=[file_output]
|
| 1062 |
)
|
| 1063 |
|
| 1064 |
+
|
| 1065 |
gr.Markdown("### Instrucciones:")
|
| 1066 |
gr.Markdown("""
|
| 1067 |
1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
|
| 1068 |
+
2. **Selecciona el tipo de entrada**: "Generar Guion con IA" o "Usar Mi Guion".
|
| 1069 |
+
3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.).
|
| 1070 |
+
4. **Selecciona la voz** deseada del desplegable.
|
| 1071 |
+
5. **Haz clic en "✨ Generar Video"**.
|
| 1072 |
+
6. Espera a que se procese el video. Verás el estado.
|
| 1073 |
+
7. La previsualización aparecerá si es posible, y siempre un enlace **Descargar Archivo de Video** se mostrará si la generación fue exitosa.
|
| 1074 |
+
8. Revisa `video_generator_full.log` para detalles si hay errores.
|
|
|
|
| 1075 |
""")
|
| 1076 |
gr.Markdown("---")
|
| 1077 |
gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
|
|
|
|
| 1083 |
try:
|
| 1084 |
temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
|
| 1085 |
temp_clip.close()
|
| 1086 |
+
logger.info("Clips base de MoviePy creados y cerrados exitosamente. FFmpeg parece accesible.")
|
| 1087 |
except Exception as e:
|
| 1088 |
logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
|
| 1089 |
|