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 |
-
# Importación correcta
|
| 13 |
from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
|
| 14 |
import re
|
| 15 |
import math
|
|
@@ -32,6 +32,7 @@ logger.info("INICIO DE EJECUCIÓN - GENERADOR DE VIDEOS")
|
|
| 32 |
logger.info("="*80)
|
| 33 |
|
| 34 |
# Diccionario de voces TTS disponibles organizadas por idioma
|
|
|
|
| 35 |
VOCES_DISPONIBLES = {
|
| 36 |
"Español (España)": {
|
| 37 |
"es-ES-JuanNeural": "Juan (España) - Masculino",
|
|
@@ -99,9 +100,32 @@ def get_voice_choices():
|
|
| 99 |
choices = []
|
| 100 |
for region, voices in VOCES_DISPONIBLES.items():
|
| 101 |
for voice_id, voice_name in voices.items():
|
| 102 |
-
|
|
|
|
| 103 |
return choices
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
# Clave API de Pexels
|
| 106 |
PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
|
| 107 |
if not PEXELS_API_KEY:
|
|
@@ -200,53 +224,63 @@ def generate_script(prompt, max_length=150):
|
|
| 200 |
text = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 201 |
|
| 202 |
cleaned_text = text.strip()
|
|
|
|
| 203 |
try:
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
|
|
|
|
|
|
| 208 |
else:
|
|
|
|
| 209 |
instruction_start_idx = text.find(instruction_phrase_start)
|
| 210 |
if instruction_start_idx != -1:
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
|
|
|
| 218 |
|
| 219 |
except Exception as e:
|
| 220 |
logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
|
| 221 |
-
cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
|
| 222 |
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
|
|
|
| 226 |
|
|
|
|
| 227 |
cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
|
| 228 |
-
cleaned_text = cleaned_text.lstrip(':').strip()
|
| 229 |
-
cleaned_text = cleaned_text.lstrip('.').strip()
|
|
|
|
| 230 |
|
|
|
|
| 231 |
sentences = cleaned_text.split('.')
|
| 232 |
if sentences and sentences[0].strip():
|
| 233 |
final_text = sentences[0].strip() + '.'
|
| 234 |
-
|
|
|
|
| 235 |
final_text += " " + sentences[1].strip() + "."
|
| 236 |
-
final_text = final_text.replace("..", ".")
|
| 237 |
|
| 238 |
logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
|
| 239 |
return final_text.strip()
|
| 240 |
|
| 241 |
logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
|
| 242 |
-
return cleaned_text.strip()
|
| 243 |
|
| 244 |
except Exception as e:
|
| 245 |
logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
|
| 246 |
logger.warning("Usando prompt original como guion debido al error de generación.")
|
| 247 |
return prompt.strip()
|
| 248 |
|
| 249 |
-
# Función TTS
|
| 250 |
async def text_to_speech(text, output_path, voice):
|
| 251 |
logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
|
| 252 |
if not text or not text.strip():
|
|
@@ -417,6 +451,7 @@ def extract_visual_keywords_from_script(script_text):
|
|
| 417 |
logger.info(f"Palabras clave finales: {top_keywords}")
|
| 418 |
return top_keywords
|
| 419 |
|
|
|
|
| 420 |
def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
|
| 421 |
logger.info("="*80)
|
| 422 |
logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
|
|
@@ -452,35 +487,40 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
|
|
| 452 |
logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
|
| 453 |
temp_intermediate_files = []
|
| 454 |
|
| 455 |
-
# 2. Generar audio de voz con reintentos
|
| 456 |
logger.info("Generando audio de voz...")
|
| 457 |
voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
|
| 458 |
|
| 459 |
-
|
| 460 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
tts_success = False
|
| 462 |
-
|
| 463 |
|
| 464 |
-
for
|
| 465 |
-
current_voice
|
| 466 |
-
|
| 467 |
-
|
|
|
|
| 468 |
try:
|
| 469 |
tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=current_voice))
|
| 470 |
if tts_success:
|
| 471 |
-
logger.info(f"TTS exitoso
|
| 472 |
-
break
|
| 473 |
except Exception as e:
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
if not tts_success and attempt == 0 and primary_voice != fallback_voice:
|
| 477 |
-
logger.warning(f"Fallo con voz {primary_voice}, intentando voz de respaldo: {fallback_voice}")
|
| 478 |
-
elif not tts_success and attempt < retries - 1:
|
| 479 |
-
logger.warning(f"Fallo con voz {current_voice}, reintentando...")
|
| 480 |
-
|
| 481 |
|
|
|
|
| 482 |
if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
|
| 483 |
-
logger.error(
|
| 484 |
raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
|
| 485 |
|
| 486 |
temp_intermediate_files.append(voz_path)
|
|
@@ -530,20 +570,6 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
|
|
| 530 |
except Exception as e:
|
| 531 |
logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
|
| 532 |
|
| 533 |
-
if len(videos_data) < total_desired_videos / 2:
|
| 534 |
-
logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
|
| 535 |
-
generic_keywords = ["nature", "city", "background", "abstract"]
|
| 536 |
-
for keyword in generic_keywords:
|
| 537 |
-
if len(videos_data) >= total_desired_videos:
|
| 538 |
-
break
|
| 539 |
-
try:
|
| 540 |
-
videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=2)
|
| 541 |
-
if videos:
|
| 542 |
-
videos_data.extend(videos)
|
| 543 |
-
logger.info(f"Encontrados {len(videos)} videos para '{keyword}' (genérico). Total data: {len(videos_data)}")
|
| 544 |
-
except Exception as e:
|
| 545 |
-
logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
|
| 546 |
-
|
| 547 |
if len(videos_data) < total_desired_videos / 2:
|
| 548 |
logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
|
| 549 |
generic_keywords = ["nature", "city", "background", "abstract"]
|
|
@@ -929,7 +955,7 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
|
|
| 929 |
logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
|
| 930 |
|
| 931 |
|
| 932 |
-
#
|
| 933 |
def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice): # <-- Recibe el valor del Dropdown
|
| 934 |
logger.info("="*80)
|
| 935 |
logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
|
|
@@ -947,10 +973,11 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
|
|
| 947 |
return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
|
| 948 |
|
| 949 |
# Validar la voz seleccionada. Si no es válida, usar la por defecto.
|
| 950 |
-
# AVAILABLE_VOICES se obtiene al inicio.
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
selected_voice
|
|
|
|
| 954 |
else:
|
| 955 |
logger.info(f"Voz seleccionada validada: {selected_voice}")
|
| 956 |
|
|
@@ -961,12 +988,12 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
|
|
| 961 |
logger.info(f"Archivo de música recibido: {musica_file}")
|
| 962 |
else:
|
| 963 |
logger.info("No se proporcionó archivo de música.")
|
| 964 |
-
logger.info(f"Voz final a usar: {selected_voice}") # Loguear la voz final
|
| 965 |
|
| 966 |
try:
|
| 967 |
logger.info("Llamando a crear_video...")
|
| 968 |
-
# Pasar el input_text elegido, la voz seleccionada y el archivo de música a crear_video
|
| 969 |
-
video_path = crear_video(prompt_type, input_text, selected_voice, musica_file) # <-- PASAR selected_voice a crear_video
|
| 970 |
|
| 971 |
if video_path and os.path.exists(video_path):
|
| 972 |
logger.info(f"crear_video retornó path: {video_path}")
|
|
@@ -1038,8 +1065,8 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 1038 |
# --- COMPONENTE: Selección de Voz ---
|
| 1039 |
voice_dropdown = gr.Dropdown(
|
| 1040 |
label="Seleccionar Voz para Guion",
|
| 1041 |
-
choices=AVAILABLE_VOICES,
|
| 1042 |
-
value=
|
| 1043 |
interactive=True
|
| 1044 |
# visible=... <-- ¡NO DEBE ESTAR AQUÍ!
|
| 1045 |
)
|
|
@@ -1058,7 +1085,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 1058 |
file_output = gr.File(
|
| 1059 |
label="Descargar Archivo de Video",
|
| 1060 |
interactive=False,
|
| 1061 |
-
visible=False # <-- ESTÁ BIEN AQUÍ
|
| 1062 |
# visible=... <-- ¡NO DEBE ESTAR AQUÍ si ya está visible=False arriba!
|
| 1063 |
)
|
| 1064 |
status_output = gr.Textbox(
|
|
@@ -1093,11 +1120,8 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
| 1093 |
outputs=[video_output, file_output, status_output]
|
| 1094 |
).then(
|
| 1095 |
# Acción 3 (síncrona): Hacer visible el enlace de descarga
|
| 1096 |
-
# Recibe las salidas de la Acción 2
|
| 1097 |
lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
|
| 1098 |
-
# Inputs para esta lambda son los outputs del .then() anterior
|
| 1099 |
inputs=[video_output, file_output, status_output],
|
| 1100 |
-
# Actualizamos la visibilidad del componente file_output
|
| 1101 |
outputs=[file_output]
|
| 1102 |
)
|
| 1103 |
|
|
|
|
| 9 |
import torch
|
| 10 |
from transformers import GPT2Tokenizer, GPT2LMHeadModel
|
| 11 |
from keybert import KeyBERT
|
| 12 |
+
# Importación correcta
|
| 13 |
from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
|
| 14 |
import re
|
| 15 |
import math
|
|
|
|
| 32 |
logger.info("="*80)
|
| 33 |
|
| 34 |
# Diccionario de voces TTS disponibles organizadas por idioma
|
| 35 |
+
# Puedes expandir esta lista si conoces otros IDs de voz de Edge TTS
|
| 36 |
VOCES_DISPONIBLES = {
|
| 37 |
"Español (España)": {
|
| 38 |
"es-ES-JuanNeural": "Juan (España) - Masculino",
|
|
|
|
| 100 |
choices = []
|
| 101 |
for region, voices in VOCES_DISPONIBLES.items():
|
| 102 |
for voice_id, voice_name in voices.items():
|
| 103 |
+
# Formato: (Texto a mostrar en el dropdown, Valor que se pasa)
|
| 104 |
+
choices.append((f"{voice_name} ({region})", voice_id))
|
| 105 |
return choices
|
| 106 |
|
| 107 |
+
# Obtener las voces al inicio del script
|
| 108 |
+
# Usamos la lista predefinida por ahora para evitar el error de inicio con la API
|
| 109 |
+
# Si deseas obtenerlas dinámicamente, descomenta la siguiente línea y comenta la que usa get_voice_choices()
|
| 110 |
+
# AVAILABLE_VOICES = asyncio.run(get_available_voices())
|
| 111 |
+
AVAILABLE_VOICES = get_voice_choices() # <-- Usamos la lista predefinida y aplanada
|
| 112 |
+
# Establecer una voz por defecto inicial
|
| 113 |
+
DEFAULT_VOICE_ID = "es-ES-JuanNeural" # ID de Juan
|
| 114 |
+
|
| 115 |
+
# Buscar el nombre amigable para la voz por defecto si existe
|
| 116 |
+
DEFAULT_VOICE_NAME = DEFAULT_VOICE_ID
|
| 117 |
+
for text, voice_id in AVAILABLE_VOICES:
|
| 118 |
+
if voice_id == DEFAULT_VOICE_ID:
|
| 119 |
+
DEFAULT_VOICE_NAME = text
|
| 120 |
+
break
|
| 121 |
+
# Si Juan no está en la lista (ej. lista de fallback), usar la primera voz disponible
|
| 122 |
+
if DEFAULT_VOICE_ID not in [v[1] for v in AVAILABLE_VOICES]:
|
| 123 |
+
DEFAULT_VOICE_ID = AVAILABLE_VOICES[0][1] if AVAILABLE_VOICES else "en-US-AriaNeural"
|
| 124 |
+
DEFAULT_VOICE_NAME = AVAILABLE_VOICES[0][0] if AVAILABLE_VOICES else "Aria (United States) - Female" # Fallback name
|
| 125 |
+
|
| 126 |
+
logger.info(f"Voz por defecto seleccionada (ID): {DEFAULT_VOICE_ID}")
|
| 127 |
+
|
| 128 |
+
|
| 129 |
# Clave API de Pexels
|
| 130 |
PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
|
| 131 |
if not PEXELS_API_KEY:
|
|
|
|
| 224 |
text = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 225 |
|
| 226 |
cleaned_text = text.strip()
|
| 227 |
+
# Limpieza mejorada de la frase de instrucción
|
| 228 |
try:
|
| 229 |
+
# Buscar el índice de inicio del prompt original dentro del texto generado
|
| 230 |
+
prompt_in_output_idx = text.lower().find(prompt.lower())
|
| 231 |
+
if prompt_in_output_idx != -1:
|
| 232 |
+
# Tomar todo el texto DESPUÉS del prompt original
|
| 233 |
+
cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
|
| 234 |
+
logger.debug("Texto limpiado tomando parte después del prompt original.")
|
| 235 |
else:
|
| 236 |
+
# Fallback si el prompt original no está exacto en la salida: buscar la frase de instrucción base
|
| 237 |
instruction_start_idx = text.find(instruction_phrase_start)
|
| 238 |
if instruction_start_idx != -1:
|
| 239 |
+
# Tomar texto después de la frase base (puede incluir el prompt)
|
| 240 |
+
cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
|
| 241 |
+
logger.debug("Texto limpiado tomando parte después de la frase de instrucción base.")
|
| 242 |
+
else:
|
| 243 |
+
# Si ni la frase de instrucción ni el prompt se encuentran, usar el texto original
|
| 244 |
+
logger.warning("No se pudo identificar el inicio del guión generado. Usando texto generado completo.")
|
| 245 |
+
cleaned_text = text.strip() # Limpieza básica
|
| 246 |
+
|
| 247 |
|
| 248 |
except Exception as e:
|
| 249 |
logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
|
| 250 |
+
cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Limpieza básica como fallback
|
| 251 |
|
| 252 |
+
# Asegurarse de que el texto resultante no sea solo la instrucción o vacío
|
| 253 |
+
if not cleaned_text or len(cleaned_text) < 10: # Umbral de longitud mínima
|
| 254 |
+
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).")
|
| 255 |
+
cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Fallback al texto original limpio
|
| 256 |
|
| 257 |
+
# Limpieza final de caracteres especiales y espacios sobrantes
|
| 258 |
cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
|
| 259 |
+
cleaned_text = cleaned_text.lstrip(':').strip() # Quitar posibles ':' al inicio
|
| 260 |
+
cleaned_text = cleaned_text.lstrip('.').strip() # Quitar posibles '.' al inicio
|
| 261 |
+
|
| 262 |
|
| 263 |
+
# Intentar obtener al menos una oración completa si es posible para un inicio más limpio
|
| 264 |
sentences = cleaned_text.split('.')
|
| 265 |
if sentences and sentences[0].strip():
|
| 266 |
final_text = sentences[0].strip() + '.'
|
| 267 |
+
# Añadir la segunda oración si existe y es razonable
|
| 268 |
+
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
|
| 269 |
final_text += " " + sentences[1].strip() + "."
|
| 270 |
+
final_text = final_text.replace("..", ".") # Limpiar doble punto
|
| 271 |
|
| 272 |
logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
|
| 273 |
return final_text.strip()
|
| 274 |
|
| 275 |
logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
|
| 276 |
+
return cleaned_text.strip() # Si no se puede formar una oración, devolver el texto limpio tal cual
|
| 277 |
|
| 278 |
except Exception as e:
|
| 279 |
logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
|
| 280 |
logger.warning("Usando prompt original como guion debido al error de generación.")
|
| 281 |
return prompt.strip()
|
| 282 |
|
| 283 |
+
# Función TTS ahora recibe la voz a usar
|
| 284 |
async def text_to_speech(text, output_path, voice):
|
| 285 |
logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
|
| 286 |
if not text or not text.strip():
|
|
|
|
| 451 |
logger.info(f"Palabras clave finales: {top_keywords}")
|
| 452 |
return top_keywords
|
| 453 |
|
| 454 |
+
# crear_video ahora recibe la voz seleccionada
|
| 455 |
def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
|
| 456 |
logger.info("="*80)
|
| 457 |
logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
|
|
|
|
| 487 |
logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
|
| 488 |
temp_intermediate_files = []
|
| 489 |
|
| 490 |
+
# 2. Generar audio de voz usando la voz seleccionada, con reintentos si falla
|
| 491 |
logger.info("Generando audio de voz...")
|
| 492 |
voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
|
| 493 |
|
| 494 |
+
tts_voices_to_try = [selected_voice] # Intentar primero la voz seleccionada
|
| 495 |
+
# Añadir voces de respaldo si no están ya en la lista y son diferentes a la seleccionada
|
| 496 |
+
# Nos aseguramos de no añadir None o IDs vacíos a la lista de reintento
|
| 497 |
+
if "es-ES-JuanNeural" not in tts_voices_to_try and "es-ES-JuanNeural" is not None: tts_voices_to_try.append("es-ES-JuanNeural")
|
| 498 |
+
if "es-ES-ElviraNeural" not in tts_voices_to_try and "es-ES-ElviraNeural" is not None: tts_voices_to_try.append("es-ES-ElviraNeural")
|
| 499 |
+
# Si la lista de voces disponibles es fiable, podrías usar un subconjunto ordenado para reintentos más amplios
|
| 500 |
+
# Opcional: si AVAILABLE_VOICES es fiable, podrías usar un subconjunto ordenado para reintentos
|
| 501 |
+
# Ejemplo: for voice_id in [selected_voice] + sorted([v[1] for v in AVAILABLE_VOICES if v[1].startswith('es-') and v[1] != selected_voice]) + sorted([v[1] for v in AVAILABLE_VOICES if not v[1].startswith('es-') and v[1] != selected_voice]):
|
| 502 |
+
|
| 503 |
+
|
| 504 |
tts_success = False
|
| 505 |
+
tried_voices = set() # Usar un set para rastrear voces intentadas de forma eficiente
|
| 506 |
|
| 507 |
+
for current_voice in tts_voices_to_try:
|
| 508 |
+
if not current_voice or current_voice in tried_voices: continue # Evitar intentar IDs None/vacíos o duplicados
|
| 509 |
+
tried_voices.add(current_voice)
|
| 510 |
+
|
| 511 |
+
logger.info(f"Intentando TTS con voz: {current_voice}...")
|
| 512 |
try:
|
| 513 |
tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=current_voice))
|
| 514 |
if tts_success:
|
| 515 |
+
logger.info(f"TTS exitoso con voz '{current_voice}'.")
|
| 516 |
+
break # Salir del bucle de reintento si tiene éxito
|
| 517 |
except Exception as e:
|
| 518 |
+
logger.warning(f"Fallo al generar TTS con voz '{current_voice}': {str(e)}", exc_info=True)
|
| 519 |
+
pass # Continuar al siguiente intento
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
|
| 521 |
+
# Verificar si el archivo fue creado después de todos los intentos
|
| 522 |
if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
|
| 523 |
+
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.")
|
| 524 |
raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
|
| 525 |
|
| 526 |
temp_intermediate_files.append(voz_path)
|
|
|
|
| 570 |
except Exception as e:
|
| 571 |
logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
|
| 572 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
if len(videos_data) < total_desired_videos / 2:
|
| 574 |
logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
|
| 575 |
generic_keywords = ["nature", "city", "background", "abstract"]
|
|
|
|
| 955 |
logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
|
| 956 |
|
| 957 |
|
| 958 |
+
# run_app ahora recibe todos los inputs, incluyendo la voz seleccionada
|
| 959 |
def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice): # <-- Recibe el valor del Dropdown
|
| 960 |
logger.info("="*80)
|
| 961 |
logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
|
|
|
|
| 973 |
return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
|
| 974 |
|
| 975 |
# Validar la voz seleccionada. Si no es válida, usar la por defecto.
|
| 976 |
+
# AVAILABLE_VOICES se obtiene al inicio. Hay que buscar si el voice_id existe en la lista de pares (nombre, id)
|
| 977 |
+
voice_ids_disponibles = [v[1] for v in AVAILABLE_VOICES]
|
| 978 |
+
if selected_voice not in voice_ids_disponibles:
|
| 979 |
+
logger.warning(f"Voz seleccionada inválida o no encontrada en la lista: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE_ID}.")
|
| 980 |
+
selected_voice = DEFAULT_VOICE_ID # <-- Usar el ID de la voz por defecto
|
| 981 |
else:
|
| 982 |
logger.info(f"Voz seleccionada validada: {selected_voice}")
|
| 983 |
|
|
|
|
| 988 |
logger.info(f"Archivo de música recibido: {musica_file}")
|
| 989 |
else:
|
| 990 |
logger.info("No se proporcionó archivo de música.")
|
| 991 |
+
logger.info(f"Voz final a usar (ID): {selected_voice}") # Loguear el ID de la voz final
|
| 992 |
|
| 993 |
try:
|
| 994 |
logger.info("Llamando a crear_video...")
|
| 995 |
+
# Pasar el input_text elegido, la voz seleccionada (el ID) y el archivo de música a crear_video
|
| 996 |
+
video_path = crear_video(prompt_type, input_text, selected_voice, musica_file) # <-- PASAR selected_voice (ID) a crear_video
|
| 997 |
|
| 998 |
if video_path and os.path.exists(video_path):
|
| 999 |
logger.info(f"crear_video retornó path: {video_path}")
|
|
|
|
| 1065 |
# --- COMPONENTE: Selección de Voz ---
|
| 1066 |
voice_dropdown = gr.Dropdown(
|
| 1067 |
label="Seleccionar Voz para Guion",
|
| 1068 |
+
choices=AVAILABLE_VOICES, # Usar la lista obtenida al inicio
|
| 1069 |
+
value=DEFAULT_VOICE_ID, # Usar el ID de la voz por defecto calculada
|
| 1070 |
interactive=True
|
| 1071 |
# visible=... <-- ¡NO DEBE ESTAR AQUÍ!
|
| 1072 |
)
|
|
|
|
| 1085 |
file_output = gr.File(
|
| 1086 |
label="Descargar Archivo de Video",
|
| 1087 |
interactive=False,
|
| 1088 |
+
visible=False # <-- ESTÁ BIEN AQUÍ
|
| 1089 |
# visible=... <-- ¡NO DEBE ESTAR AQUÍ si ya está visible=False arriba!
|
| 1090 |
)
|
| 1091 |
status_output = gr.Textbox(
|
|
|
|
| 1120 |
outputs=[video_output, file_output, status_output]
|
| 1121 |
).then(
|
| 1122 |
# Acción 3 (síncrona): Hacer visible el enlace de descarga
|
|
|
|
| 1123 |
lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
|
|
|
|
| 1124 |
inputs=[video_output, file_output, status_output],
|
|
|
|
| 1125 |
outputs=[file_output]
|
| 1126 |
)
|
| 1127 |
|