Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -31,6 +31,77 @@ logger.info("="*80)
|
|
| 31 |
logger.info("INICIO DE EJECUCI脫N - GENERADOR DE VIDEOS")
|
| 32 |
logger.info("="*80)
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
# Clave API de Pexels
|
| 35 |
PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
|
| 36 |
if not PEXELS_API_KEY:
|
|
@@ -61,36 +132,6 @@ except Exception as e:
|
|
| 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,63 +200,53 @@ def generate_script(prompt, max_length=150):
|
|
| 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 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 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 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 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()
|
| 186 |
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 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()
|
| 195 |
-
cleaned_text = cleaned_text.lstrip('.').strip()
|
| 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 |
-
|
| 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("..", ".")
|
| 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()
|
| 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
|
| 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,12 +417,11 @@ def extract_visual_keywords_from_script(script_text):
|
|
| 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
|
| 395 |
|
| 396 |
start_time = datetime.now()
|
| 397 |
temp_dir_intermediate = None
|
|
@@ -422,39 +452,35 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
|
|
| 422 |
logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
|
| 423 |
temp_intermediate_files = []
|
| 424 |
|
| 425 |
-
# 2. Generar audio de voz
|
| 426 |
logger.info("Generando audio de voz...")
|
| 427 |
voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
|
| 428 |
|
| 429 |
-
|
| 430 |
-
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 449 |
-
break
|
| 450 |
except Exception as e:
|
| 451 |
-
|
| 452 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 458 |
raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
|
| 459 |
|
| 460 |
temp_intermediate_files.append(voz_path)
|
|
@@ -504,6 +530,19 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
|
|
| 504 |
except Exception as e:
|
| 505 |
logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
|
| 506 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
if len(videos_data) < total_desired_videos / 2:
|
| 508 |
logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave gen茅ricas.")
|
| 509 |
generic_keywords = ["nature", "city", "background", "abstract"]
|
|
|
|
| 31 |
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",
|
| 38 |
+
"es-ES-ElviraNeural": "Elvira (Espa帽a) - Femenino",
|
| 39 |
+
"es-ES-AlvaroNeural": "脕lvaro (Espa帽a) - Masculino",
|
| 40 |
+
"es-ES-AbrilNeural": "Abril (Espa帽a) - Femenino",
|
| 41 |
+
"es-ES-ArnauNeural": "Arnau (Espa帽a) - Masculino",
|
| 42 |
+
"es-ES-DarioNeural": "Dar铆o (Espa帽a) - Masculino",
|
| 43 |
+
"es-ES-EliasNeural": "El铆as (Espa帽a) - Masculino",
|
| 44 |
+
"es-ES-EstrellaNeural": "Estrella (Espa帽a) - Femenino",
|
| 45 |
+
"es-ES-IreneNeural": "Irene (Espa帽a) - Femenino",
|
| 46 |
+
"es-ES-LaiaNeural": "Laia (Espa帽a) - Femenino",
|
| 47 |
+
"es-ES-LiaNeural": "L铆a (Espa帽a) - Femenino",
|
| 48 |
+
"es-ES-NilNeural": "Nil (Espa帽a) - Masculino",
|
| 49 |
+
"es-ES-SaulNeural": "Sa煤l (Espa帽a) - Masculino",
|
| 50 |
+
"es-ES-TeoNeural": "Teo (Espa帽a) - Masculino",
|
| 51 |
+
"es-ES-TrianaNeural": "Triana (Espa帽a) - Femenino",
|
| 52 |
+
"es-ES-VeraNeural": "Vera (Espa帽a) - Femenino"
|
| 53 |
+
},
|
| 54 |
+
"Espa帽ol (M茅xico)": {
|
| 55 |
+
"es-MX-JorgeNeural": "Jorge (M茅xico) - Masculino",
|
| 56 |
+
"es-MX-DaliaNeural": "Dalia (M茅xico) - Femenino",
|
| 57 |
+
"es-MX-BeatrizNeural": "Beatriz (M茅xico) - Femenino",
|
| 58 |
+
"es-MX-CandelaNeural": "Candela (M茅xico) - Femenino",
|
| 59 |
+
"es-MX-CarlotaNeural": "Carlota (M茅xico) - Femenino",
|
| 60 |
+
"es-MX-CecilioNeural": "Cecilio (M茅xico) - Masculino",
|
| 61 |
+
"es-MX-GerardoNeural": "Gerardo (M茅xico) - Masculino",
|
| 62 |
+
"es-MX-LarissaNeural": "Larissa (M茅xico) - Femenino",
|
| 63 |
+
"es-MX-LibertoNeural": "Liberto (M茅xico) - Masculino",
|
| 64 |
+
"es-MX-LucianoNeural": "Luciano (M茅xico) - Masculino",
|
| 65 |
+
"es-MX-MarinaNeural": "Marina (M茅xico) - Femenino",
|
| 66 |
+
"es-MX-NuriaNeural": "Nuria (M茅xico) - Femenino",
|
| 67 |
+
"es-MX-PelayoNeural": "Pelayo (M茅xico) - Masculino",
|
| 68 |
+
"es-MX-RenataNeural": "Renata (M茅xico) - Femenino",
|
| 69 |
+
"es-MX-YagoNeural": "Yago (M茅xico) - Masculino"
|
| 70 |
+
},
|
| 71 |
+
"Espa帽ol (Argentina)": {
|
| 72 |
+
"es-AR-TomasNeural": "Tom谩s (Argentina) - Masculino",
|
| 73 |
+
"es-AR-ElenaNeural": "Elena (Argentina) - Femenino"
|
| 74 |
+
},
|
| 75 |
+
"Espa帽ol (Colombia)": {
|
| 76 |
+
"es-CO-GonzaloNeural": "Gonzalo (Colombia) - Masculino",
|
| 77 |
+
"es-CO-SalomeNeural": "Salom茅 (Colombia) - Femenino"
|
| 78 |
+
},
|
| 79 |
+
"Espa帽ol (Chile)": {
|
| 80 |
+
"es-CL-LorenzoNeural": "Lorenzo (Chile) - Masculino",
|
| 81 |
+
"es-CL-CatalinaNeural": "Catalina (Chile) - Femenino"
|
| 82 |
+
},
|
| 83 |
+
"Espa帽ol (Per煤)": {
|
| 84 |
+
"es-PE-AlexNeural": "Alex (Per煤) - Masculino",
|
| 85 |
+
"es-PE-CamilaNeural": "Camila (Per煤) - Femenino"
|
| 86 |
+
},
|
| 87 |
+
"Espa帽ol (Venezuela)": {
|
| 88 |
+
"es-VE-PaolaNeural": "Paola (Venezuela) - Femenino",
|
| 89 |
+
"es-VE-SebastianNeural": "Sebasti谩n (Venezuela) - Masculino"
|
| 90 |
+
},
|
| 91 |
+
"Espa帽ol (Estados Unidos)": {
|
| 92 |
+
"es-US-AlonsoNeural": "Alonso (Estados Unidos) - Masculino",
|
| 93 |
+
"es-US-PalomaNeural": "Paloma (Estados Unidos) - Femenino"
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
# Funci贸n para obtener lista plana de voces para el dropdown
|
| 98 |
+
def get_voice_choices():
|
| 99 |
+
choices = []
|
| 100 |
+
for region, voices in VOCES_DISPONIBLES.items():
|
| 101 |
+
for voice_id, voice_name in voices.items():
|
| 102 |
+
choices.append((voice_name, voice_id))
|
| 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:
|
|
|
|
| 132 |
logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
|
| 133 |
kw_model = None
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
def buscar_videos_pexels(query, api_key, per_page=5):
|
| 136 |
if not api_key:
|
| 137 |
logger.warning("No se puede buscar en Pexels: API Key no configurada.")
|
|
|
|
| 200 |
text = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 201 |
|
| 202 |
cleaned_text = text.strip()
|
|
|
|
| 203 |
try:
|
| 204 |
+
instruction_end_idx = text.find(instruction_phrase)
|
| 205 |
+
if instruction_end_idx != -1:
|
| 206 |
+
cleaned_text = text[instruction_end_idx + len(instruction_phrase):].strip()
|
| 207 |
+
logger.debug("Instrucci贸n inicial encontrada y eliminada del gui贸n generado.")
|
|
|
|
|
|
|
| 208 |
else:
|
|
|
|
| 209 |
instruction_start_idx = text.find(instruction_phrase_start)
|
| 210 |
if instruction_start_idx != -1:
|
| 211 |
+
prompt_in_output_idx = text.find(prompt, instruction_start_idx)
|
| 212 |
+
if prompt_in_output_idx != -1:
|
| 213 |
+
cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
|
| 214 |
+
logger.debug("Instrucci贸n base y prompt encontrados y eliminados del gui贸n generado.")
|
| 215 |
+
else:
|
| 216 |
+
cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
|
| 217 |
+
logger.debug("Instrucci贸n base encontrada, eliminada del gui贸n generado (sin prompt detectado).")
|
|
|
|
| 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 |
+
if not cleaned_text or len(cleaned_text) < 10:
|
| 224 |
+
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).")
|
| 225 |
+
cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
|
|
|
|
| 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 |
+
if len(sentences) > 1 and sentences[1].strip() and len(final_text.split()) < max_length * 0.7:
|
|
|
|
| 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 con voz especificada
|
| 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 |
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}")
|
| 423 |
logger.debug(f"Input: '{input_text[:100]}...'")
|
| 424 |
+
logger.info(f"Voz seleccionada: {selected_voice}")
|
| 425 |
|
| 426 |
start_time = datetime.now()
|
| 427 |
temp_dir_intermediate = 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 y voz de respaldo
|
| 456 |
logger.info("Generando audio de voz...")
|
| 457 |
voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
|
| 458 |
|
| 459 |
+
primary_voice = selected_voice
|
| 460 |
+
fallback_voice = "es-ES-ElviraNeural" if selected_voice != "es-ES-ElviraNeural" else "es-ES-JuanNeural"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
tts_success = False
|
| 462 |
+
retries = 3
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
|
| 464 |
+
for attempt in range(retries):
|
| 465 |
+
current_voice = primary_voice if attempt == 0 else fallback_voice
|
| 466 |
+
if attempt > 0: logger.warning(f"Reintentando TTS ({attempt+1}/{retries})...")
|
| 467 |
+
logger.info(f"Intentando TTS con voz: {current_voice}")
|
| 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 en intento {attempt + 1} con voz {current_voice}.")
|
| 472 |
+
break
|
| 473 |
except Exception as e:
|
| 474 |
+
pass
|
| 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(f"Fallo en la generaci贸n de voz despu茅s de {retries} intentos. Archivo de audio no creado o es muy peque帽o.")
|
| 484 |
raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
|
| 485 |
|
| 486 |
temp_intermediate_files.append(voz_path)
|
|
|
|
| 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: break
|
| 538 |
+
try:
|
| 539 |
+
videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=2)
|
| 540 |
+
if videos:
|
| 541 |
+
videos_data.extend(videos)
|
| 542 |
+
logger.info(f"Encontrados {len(videos)} videos para '{keyword}' (gen茅rico). Total data: {len {len(videos)} videos para '{keyword}'. Total data: {len(videos_data)}")
|
| 543 |
+
except Exception as e:
|
| 544 |
+
logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
|
| 545 |
+
|
| 546 |
if len(videos_data) < total_desired_videos / 2:
|
| 547 |
logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave gen茅ricas.")
|
| 548 |
generic_keywords = ["nature", "city", "background", "abstract"]
|