gnosticdev commited on
Commit
1fa048a
verified
1 Parent(s): ee6df46

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +123 -84
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
- # 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,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 para TTS: {selected_voice}")
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 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)
@@ -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"]