gnosticdev commited on
Commit
544bfec
·
verified ·
1 Parent(s): d4bd22d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +256 -416
app.py CHANGED
@@ -9,7 +9,6 @@ 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
@@ -31,11 +30,38 @@ 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:
37
  logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
38
- # raise ValueError("API key de Pexels no configurada")
39
 
40
  # Inicialización de modelos
41
  MODEL_NAME = "datificate/gpt2-small-spanish"
@@ -61,48 +87,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 dinámicamente ---
65
- async def get_available_voices():
66
- logger.info("Obteniendo lista de voces disponibles de Edge TTS dinámicamente...")
67
- try:
68
- voices = await edge_tts.VoicesManager.create()
69
- # Retornar una lista de pares (Nombre a mostrar, ID de voz)
70
- voice_list_formatted = [(f"{v.Name} ({v.Locale})", v.Name) for v in voices.Voices]
71
- logger.info(f"Encontradas {len(voice_list_formatted)} voces de Edge TTS.")
72
- # Opcional: Ordenar por idioma/nombre
73
- voice_list_formatted.sort(key=lambda item: item[0])
74
- return voice_list_formatted
75
-
76
- except Exception as e:
77
- logger.error(f"Error obteniendo voces de Edge TTS dinámicamente: {str(e)}", exc_info=True)
78
- # Retornar una lista de voces por defecto si falla la API de Edge TTS
79
- logger.warning("No se pudieron obtener voces de Edge TTS dinámicamente. Usando lista de voces por defecto.")
80
- # Usar el formato (Nombre, ID)
81
- return [("Juan (España)", "es-ES-JuanNeural"), ("Elvira (España)", "es-ES-ElviraNeural"), ("Aria (United States)", "en-US-AriaNeural")]
82
-
83
- # Obtener las voces al inicio del script (esto PUEDE causar un fallo temprano si edge_tts.VoicesManager falla)
84
- logger.info("Inicializando lista de voces disponibles (llamada asíncrona)...")
85
- try:
86
- AVAILABLE_VOICES_TUPLES = asyncio.run(get_available_voices())
87
- # Extraer solo los IDs para validación rápida si es necesario
88
- AVAILABLE_VOICE_IDS = [v[1] for v in AVAILABLE_VOICES_TUPLES]
89
- except Exception as e:
90
- logger.critical(f"FALLO CRÍTICO al obtener voces de Edge TTS durante el inicio: {e}")
91
- # Si falla al obtenerlas dinámicamente, usar una lista de fallback estática para que la app pueda iniciar
92
- AVAILABLE_VOICES_TUPLES = [("Juan (España)", "es-ES-JuanNeural"), ("Elvira (España)", "es-ES-ElviraNeural"), ("Aria (United States)", "en-US-AriaNeural")]
93
- AVAILABLE_VOICE_IDS = [v[1] for v in AVAILABLE_VOICES_TUPLES]
94
- logger.warning("Usando lista estática de voces debido a fallo en la obtención dinámica.")
95
-
96
-
97
- # Establecer una voz por defecto inicial (usando el ID)
98
- DEFAULT_VOICE_ID = "es-ES-JuanNeural"
99
- # Asegurarse de que la voz por defecto existe en la lista obtenida (o el fallback)
100
- if DEFAULT_VOICE_ID not in AVAILABLE_VOICE_IDS:
101
- DEFAULT_VOICE_ID = AVAILABLE_VOICE_IDS[0] if AVAILABLE_VOICE_IDS else "en-US-AriaNeural" # Usar la primera voz si la de Juan no está
102
-
103
- logger.info(f"Voz por defecto seleccionada (ID): {DEFAULT_VOICE_ID}")
104
-
105
-
106
  def buscar_videos_pexels(query, api_key, per_page=5):
107
  if not api_key:
108
  logger.warning("No se puede buscar en Pexels: API Key no configurada.")
@@ -169,76 +153,59 @@ def generate_script(prompt, max_length=150):
169
  )
170
 
171
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
172
-
173
  cleaned_text = text.strip()
174
- # Limpieza mejorada de la frase de instrucción
175
  try:
176
- # Buscar el índice de inicio del prompt original dentro del texto generado
177
  prompt_in_output_idx = text.lower().find(prompt.lower())
178
  if prompt_in_output_idx != -1:
179
- # Tomar todo el texto DESPUÉS del prompt original
180
  cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
181
  logger.debug("Texto limpiado tomando parte después del prompt original.")
182
  else:
183
- # Fallback si el prompt original no está exacto en la salida: buscar la frase de instrucción base
184
- instruction_start_idx = text.find(instruction_phrase_start)
185
- if instruction_start_idx != -1:
186
- # Tomar texto después de la frase base (puede incluir el prompt)
187
- cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
188
- logger.debug("Texto limpiado tomando parte después de la frase de instrucción base.")
189
- else:
190
- # Si ni la frase de instrucción ni el prompt se encuentran, usar el texto original
191
- logger.warning("No se pudo identificar el inicio del guión generado. Usando texto generado completo.")
192
- cleaned_text = text.strip() # Limpieza básica
193
-
194
 
195
  except Exception as e:
196
- logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
197
- cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Limpieza básica como fallback
198
 
199
- # Asegurarse de que el texto resultante no sea solo la instrucción o vacío
200
- if not cleaned_text or len(cleaned_text) < 10: # Umbral de longitud mínima
201
- 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).")
202
- cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Fallback al texto original limpio
203
 
204
- # Limpieza final de caracteres especiales y espacios sobrantes
205
  cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
206
- cleaned_text = cleaned_text.lstrip(':').strip() # Quitar posibles ':' al inicio
207
- cleaned_text = cleaned_text.lstrip('.').strip() # Quitar posibles '.' al inicio
208
-
209
 
210
- # Intentar obtener al menos una oración completa si es posible para un inicio más limpio
211
  sentences = cleaned_text.split('.')
212
  if sentences and sentences[0].strip():
213
  final_text = sentences[0].strip() + '.'
214
- # Añadir la segunda oración si existe y es razonable
215
- 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
216
- final_text += " " + sentences[1].strip() + "."
217
- final_text = final_text.replace("..", ".") # Limpiar doble punto
218
-
219
  logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
220
  return final_text.strip()
221
 
222
  logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
223
- return cleaned_text.strip() # Si no se puede formar una oración, devolver el texto limpio tal cual
224
 
225
  except Exception as e:
226
- logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
227
  logger.warning("Usando prompt original como guion debido al error de generación.")
228
  return prompt.strip()
229
 
230
- # Función TTS ahora recibe el ID de voz a usar
231
- async def text_to_speech(text, output_path, voice_id): # <-- Recibe voice_id
232
- logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz ID: {voice_id} | Salida: {output_path}")
233
  if not text or not text.strip():
234
  logger.warning("Texto vacío para TTS")
235
  return False
236
- if not voice_id:
237
- logger.warning("Voice ID es None o vacío para TTS")
238
- return False
239
 
240
  try:
241
- communicate = edge_tts.Communicate(text, voice_id) # <-- Usar voice_id
242
  await communicate.save(output_path)
243
 
244
  if os.path.exists(output_path) and os.path.getsize(output_path) > 100:
@@ -249,7 +216,7 @@ async def text_to_speech(text, output_path, voice_id): # <-- Recibe voice_id
249
  return False
250
 
251
  except Exception as e:
252
- logger.error(f"Error en TTS con voz ID '{voice_id}': {str(e)}", exc_info=True)
253
  return False
254
 
255
  def download_video_file(url, temp_dir):
@@ -270,13 +237,13 @@ def download_video_file(url, temp_dir):
270
  f.write(chunk)
271
 
272
  if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
273
- logger.info(f"Video descargado exitosamente: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
274
- return output_path
275
  else:
276
- logger.warning(f"Descarga parece incompleta o vacía para {url[:80]}... Archivo: {output_path} Tamaño: {os.path.getsize(output_path) if os.path.exists(output_path) else 'N/A'} bytes")
277
- if os.path.exists(output_path):
278
- os.remove(output_path)
279
- return None
280
 
281
  except requests.exceptions.RequestException as e:
282
  logger.error(f"Error de descarga para {url[:80]}... : {str(e)}")
@@ -294,17 +261,17 @@ def loop_audio_to_length(audio_clip, target_duration):
294
  sr = getattr(audio_clip, 'fps', 44100) if audio_clip else 44100
295
  return AudioClip(lambda t: 0, duration=target_duration, sr=sr)
296
  except Exception as e:
297
- logger.error(f"Could not create silence clip: {e}", exc_info=True)
298
- return AudioFileClip(filename="")
299
 
300
  if audio_clip.duration >= target_duration:
301
  logger.debug("Audio clip already longer or equal to target. Trimming.")
302
  trimmed_clip = audio_clip.subclip(0, target_duration)
303
  if trimmed_clip.duration is None or trimmed_clip.duration <= 0:
304
- logger.error("Trimmed audio clip is invalid.")
305
- try: trimmed_clip.close()
306
- except: pass
307
- return AudioFileClip(filename="")
308
  return trimmed_clip
309
 
310
  loops = math.ceil(target_duration / audio_clip.duration)
@@ -314,28 +281,26 @@ def loop_audio_to_length(audio_clip, target_duration):
314
  looped_audio = None
315
  final_looped_audio = None
316
  try:
317
- looped_audio = concatenate_audioclips(audio_segments)
318
-
319
- if looped_audio.duration is None or looped_audio.duration <= 0:
320
  logger.error("Concatenated audio clip is invalid (None or zero duration).")
321
  raise ValueError("Invalid concatenated audio.")
322
 
323
- final_looped_audio = looped_audio.subclip(0, target_duration)
324
-
325
- if final_looped_audio.duration is None or final_looped_audio.duration <= 0:
326
  logger.error("Final subclipped audio clip is invalid (None or zero duration).")
327
  raise ValueError("Invalid final subclipped audio.")
328
 
329
- return final_looped_audio
330
 
331
  except Exception as e:
332
- logger.error(f"Error concatenating/subclipping audio clips for looping: {str(e)}", exc_info=True)
333
  try:
334
- if audio_clip.duration is not None and audio_clip.duration > 0:
335
- logger.warning("Returning original audio clip (may be too short).")
336
- return audio_clip.subclip(0, min(audio_clip.duration, target_duration))
337
  except:
338
- pass
339
  logger.error("Fallback to original audio clip failed.")
340
  return AudioFileClip(filename="")
341
 
@@ -344,7 +309,6 @@ def loop_audio_to_length(audio_clip, target_duration):
344
  try: looped_audio.close()
345
  except: pass
346
 
347
-
348
  def extract_visual_keywords_from_script(script_text):
349
  logger.info("Extrayendo palabras clave del guion")
350
  if not script_text or not script_text.strip():
@@ -381,9 +345,7 @@ def extract_visual_keywords_from_script(script_text):
381
 
382
  logger.debug("Extrayendo palabras clave con método simple...")
383
  words = clean_text.lower().split()
384
- stop_words = {"el", "la", "los", "las", "de", "en", "y", "a", "que", "es", "un", "una", "con", "para", "del", "al", "por", "su", "sus", "se", "lo", "le", "me", "te", "nos", "os", "les", "mi", "tu",
385
- "nuestro", "vuestro", "este", "ese", "aquel", "esta", "esa", "aquella", "esto", "eso", "aquello", "mis", "tus",
386
- "nuestros", "vuestros", "estas", "esas", "aquellas", "si", "no", "más", "menos", "sin", "sobre", "bajo", "entre", "hasta", "desde", "durante", "mediante", "según", "versus", "via", "cada", "todo", "todos", "toda", "todas", "poco", "pocos", "poca", "pocas", "mucho", "muchos", "mucha", "muchas", "varios", "varias", "otro", "otros", "otra", "otras", "mismo", "misma", "mismos", "mismas", "tan", "tanto", "tanta", "tantos", "tantas", "tal", "tales", "cual", "cuales", "cuyo", "cuya", "cuyos", "cuyas", "quien", "quienes", "cuan", "cuanto", "cuanta", "cuantos", "cuantas", "como", "donde", "cuando", "porque", "aunque", "mientras", "siempre", "nunca", "jamás", "muy", "casi", "solo", "solamente", "incluso", "apenas", "quizás", "tal vez", "acaso", "claro", "cierto", "obvio", "evidentemente", "realmente", "simplemente", "generalmente", "especialmente", "principalmente", "posiblemente", "probablemente", "difícilmente", "fácilmente", "rápidamente", "lentamente", "bien", "mal", "mejor", "peor", "arriba", "abajo", "adelante", "atrás", "cerca", "lejos", "dentro", "fuera", "encima", "debajo", "frente", "detrás", "antes", "después", "luego", "pronto", "tarde", "todavía", "ya", "aun", "aún", "quizá"}
387
 
388
  valid_words = [word for word in words if len(word) > 3 and word not in stop_words]
389
 
@@ -395,18 +357,21 @@ def extract_visual_keywords_from_script(script_text):
395
  top_keywords = [word.replace(" ", "+") for word, _ in word_counts.most_common(5)]
396
 
397
  if not top_keywords:
398
- logger.warning("El método simple no produjo keywords. Usando palabras clave predeterminadas.")
399
- return ["naturaleza", "ciudad", "paisaje"]
400
 
401
  logger.info(f"Palabras clave finales: {top_keywords}")
402
  return top_keywords
403
 
404
- # crear_video ahora recibe la voz seleccionada (ID)
405
- def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): # <-- Recibe el ID
406
  logger.info("="*80)
407
  logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
408
  logger.debug(f"Input: '{input_text[:100]}...'")
409
- logger.info(f"Voz seleccionada (ID): {selected_voice_id}")
 
 
 
 
410
 
411
  start_time = datetime.now()
412
  temp_dir_intermediate = None
@@ -421,7 +386,6 @@ def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): #
421
  clips_to_concatenate = []
422
 
423
  try:
424
- # 1. Generar o usar guion
425
  if prompt_type == "Generar Guion con IA":
426
  guion = generate_script(input_text)
427
  else:
@@ -437,89 +401,42 @@ def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): #
437
  logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
438
  temp_intermediate_files = []
439
 
440
- # 2. Generar audio de voz usando la voz seleccionada, con reintentos si falla
441
  logger.info("Generando audio de voz...")
442
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
443
 
444
- # Lista de IDs de voces a intentar, empezando por el ID seleccionado por el usuario
445
- tts_voice_ids_to_try = [selected_voice_id]
446
- # Añadir IDs de voces de respaldo si no están ya en la lista y son diferentes al seleccionado
447
- # Usamos IDs de voz concretos como fallbacks adicionales
448
- fallback_juan_id = "es-ES-JuanNeural"
449
- fallback_elvira_id = "es-ES-ElviraNeural"
450
- fallback_aria_id = "en-US-AriaNeural"
451
-
452
- # Añadir fallbacks SÓLO si no son el selected_voice_id y no son None/vacío
453
- if fallback_juan_id and fallback_juan_id != selected_voice_id and fallback_juan_id not in tts_voice_ids_to_try:
454
- tts_voice_ids_to_try.append(fallback_juan_id)
455
- if fallback_elvira_id and fallback_elvira_id != selected_voice_id and fallback_elvira_id not in tts_voice_ids_to_try:
456
- tts_voice_ids_to_try.append(fallback_elvira_id)
457
- if fallback_aria_id and fallback_aria_id != selected_voice_id and fallback_aria_id not in tts_voice_ids_to_try:
458
- tts_voice_ids_to_try.append(fallback_aria_id)
459
-
460
- # Si la lista de AVAILABLE_VOICES obtenida al inicio del script es fiable y contiene más voces,
461
- # podrías añadir otros IDs de voces españolas de esa lista aquí como fallbacks adicionales.
462
- # Ejemplo:
463
- # spanish_available_ids = [v[1] for v in AVAILABLE_VOICES_TUPLES if v[1].startswith('es-')]
464
- # for voice_id in spanish_available_ids:
465
- # if voice_id and voice_id != selected_voice_id and voice_id not in tts_voice_ids_to_try:
466
- # tts_voice_ids_to_try.append(voice_id)
467
-
468
-
469
  tts_success = False
470
- tried_voice_ids = set() # Usar un set para rastrear IDs de voces intentadas
471
-
472
- for current_voice_id in tts_voice_ids_to_try:
473
- # Verificar si el ID de voz actual es válido (no None/vacío) Y no lo hemos intentado ya
474
- if not current_voice_id or current_voice_id in tried_voice_ids: continue
475
- tried_voice_ids.add(current_voice_id) # Marcar como intentado
476
 
477
- logger.info(f"Intentando TTS con voz ID: {current_voice_id}...")
478
- try:
479
- # LLAMADA REAL A LA FUNCIÓN TTS ASÍNCRONA - PASAR voice_id
480
- tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=current_voice_id)) # <-- Pasar el ID
481
- if tts_success:
482
- logger.info(f"TTS exitoso con voz ID '{current_voice_id}'.")
483
- break # *** SI TIENE ÉXITO AQUÍ, SALE DEL BUCLE Y USA ESTA VOZ ***
484
- except Exception as e:
485
- logger.warning(f"Fallo al generar TTS con voz ID '{current_voice_id}': {str(e)}", exc_info=True)
486
- pass # Continuar al siguiente intento si falla
487
 
488
- # Verificar si el archivo fue creado después de todos los intentos
489
- if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
490
- 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.")
491
- # Aquí podrías intentar crear un clip de silencio si falla *completamente* el TTS,
492
- # para evitar que todo el script falle. Pero por ahora, lanzamos el error.
493
- raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
494
 
495
- temp_intermediate_files.append(voz_path) # Añadir el path del archivo de audio si se creó exitosamente
496
 
497
  audio_tts_original = AudioFileClip(voz_path)
498
 
499
  if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
500
- logger.critical("Clip de audio TTS inicial es inválido (reader is None o duración <= 0) *después* de crear AudioFileClip.")
501
  try: audio_tts_original.close()
502
  except: pass
503
- audio_tts_original = None
504
- # Eliminar el archivo de audio defectuoso
505
- if os.path.exists(voz_path):
506
- try: os.remove(voz_path)
507
- except: pass
508
- # Si estaba en temp_intermediate_files, eliminarlo también de la lista
509
- if voz_path in temp_intermediate_files:
510
- temp_intermediate_files.remove(voz_path)
511
-
512
- raise ValueError("Audio de voz generado es inválido después de procesamiento inicial.")
513
 
514
  audio_tts = audio_tts_original
515
  audio_duration = audio_tts_original.duration
516
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
517
 
518
  if audio_duration < 1.0:
519
- logger.error(f"Duración audio voz ({audio_duration:.2f}s) es muy corta.")
520
- raise ValueError("Generated voice audio is too short (min 1 second required).")
521
 
522
- # 3. Extraer palabras clave
523
  logger.info("Extrayendo palabras clave...")
524
  try:
525
  keywords = extract_visual_keywords_from_script(guion)
@@ -529,9 +446,8 @@ def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): #
529
  keywords = ["naturaleza", "paisaje"]
530
 
531
  if not keywords:
532
- keywords = ["video", "background"]
533
 
534
- # 4. Buscar y descargar videos
535
  logger.info("Buscando videos en Pexels...")
536
  videos_data = []
537
  total_desired_videos = 10
@@ -543,7 +459,6 @@ def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): #
543
  videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=per_page_per_keyword)
544
  if videos:
545
  videos_data.extend(videos)
546
- logger.info(f"Encontrados {len(videos)} videos para '{keyword}'. Total data: {len(videos_data)}")
547
  except Exception as e:
548
  logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
549
 
@@ -551,17 +466,16 @@ def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): #
551
  logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
552
  generic_keywords = ["nature", "city", "background", "abstract"]
553
  for keyword in generic_keywords:
554
- if len(videos_data) >= total_desired_videos: break
555
- try:
556
  videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=2)
557
  if videos:
558
  videos_data.extend(videos)
559
- logger.info(f"Encontrados {len(videos)} videos para '{keyword}' (genérico). Total data: {len(videos_data)}")
560
- except Exception as e:
561
  logger.warning(f"Error buscando videos genéricos para '{keyword}': {str(e)}")
562
 
563
  if not videos_data:
564
- logger.error("No se encontraron videos en Pexels para ninguna palabra clave.")
565
  raise ValueError("No se encontraron videos adecuados en Pexels.")
566
 
567
  video_paths = []
@@ -583,12 +497,6 @@ def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): #
583
  if path:
584
  video_paths.append(path)
585
  temp_intermediate_files.append(path)
586
- logger.info(f"Video descargado OK desde {best_quality['link'][:50]}...")
587
- else:
588
- logger.warning(f"No se pudo descargar video desde {best_quality['link'][:50]}...")
589
- else:
590
- logger.warning(f"No se encontró enlace de descarga válido para video {video.get('id')}.")
591
-
592
  except Exception as e:
593
  logger.warning(f"Error procesando/descargando video {video.get('id')}: {str(e)}")
594
 
@@ -597,7 +505,6 @@ def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): #
597
  logger.error("No se pudo descargar ningún archivo de video utilizable.")
598
  raise ValueError("No se pudo descargar ningún video utilizable de Pexels.")
599
 
600
- # 5. Procesar y concatenar clips de video
601
  logger.info("Procesando y concatenando videos descargados...")
602
  current_duration = 0
603
  min_clip_duration = 0.5
@@ -605,7 +512,7 @@ def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): #
605
 
606
  for i, path in enumerate(video_paths):
607
  if current_duration >= audio_duration + max_clip_segment:
608
- logger.debug(f"Video base suficiente ({current_duration:.1f}s >= {audio_duration:.1f}s + {max_clip_segment:.1f}s buffer). Dejando de procesar clips fuente restantes.")
609
  break
610
 
611
  clip = None
@@ -615,37 +522,31 @@ def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): #
615
  source_clips.append(clip)
616
 
617
  if clip.reader is None or clip.duration is None or clip.duration <= 0:
618
- logger.warning(f"[{i+1}/{len(video_paths)}] Clip fuente {path} parece inválido (reader is None o duración <= 0). Saltando.")
619
  continue
620
 
621
  remaining_needed = audio_duration - current_duration
622
  potential_use_duration = min(clip.duration, max_clip_segment)
623
 
624
  if remaining_needed > 0:
625
- segment_duration = min(potential_use_duration, remaining_needed + min_clip_duration)
626
- segment_duration = max(min_clip_duration, segment_duration)
627
- segment_duration = min(segment_duration, clip.duration)
628
-
629
- if segment_duration >= min_clip_duration:
630
- try:
631
- sub = clip.subclip(0, segment_duration)
632
- if sub.reader is None or sub.duration is None or sub.duration <= 0:
633
- logger.warning(f"[{i+1}/{len(video_paths)}] Subclip generado de {path} es inválido. Saltando.")
634
- try: sub.close()
635
- except: pass
636
- continue
637
-
638
- clips_to_concatenate.append(sub)
639
- current_duration += sub.duration
640
- logger.debug(f"[{i+1}/{len(video_paths)}] Segmento añadido: {sub.duration:.1f}s (total video: {current_duration:.1f}/{audio_duration:.1f}s)")
641
-
642
- except Exception as sub_e:
643
- logger.warning(f"[{i+1}/{len(video_paths)}] Error creando subclip de {path} ({segment_duration:.1f}s): {str(sub_e)}")
644
- continue
645
- else:
646
- logger.debug(f"[{i+1}/{len(video_paths)}] Clip {path} ({clip.duration:.1f}s) no contribuye un segmento suficiente ({segment_duration:.1f}s necesario). Saltando.")
647
- else:
648
- logger.debug(f"[{i+1}/{len(video_paths)}] Duración de video base ya alcanzada. Saltando clip.")
649
 
650
  except Exception as e:
651
  logger.warning(f"[{i+1}/{len(video_paths)}] Error procesando video {path}: {str(e)}", exc_info=True)
@@ -654,8 +555,8 @@ def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): #
654
  logger.info(f"Procesamiento de clips fuente finalizado. Se obtuvieron {len(clips_to_concatenate)} segmentos válidos.")
655
 
656
  if not clips_to_concatenate:
657
- logger.error("No hay segmentos de video válidos disponibles para crear la secuencia.")
658
- raise ValueError("No hay segmentos de video válidos disponibles para crear el video.")
659
 
660
  logger.info(f"Concatenando {len(clips_to_concatenate)} segmentos de video.")
661
  concatenated_base = None
@@ -664,25 +565,23 @@ def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): #
664
  logger.info(f"Duración video base después de concatenación inicial: {concatenated_base.duration:.2f}s")
665
 
666
  if concatenated_base is None or concatenated_base.duration is None or concatenated_base.duration <= 0:
667
- logger.critical("Video base concatenado es inválido después de la primera concatenación (None o duración cero).")
668
- raise ValueError("Fallo al crear video base válido a partir de segmentos.")
669
 
670
  except Exception as e:
671
- logger.critical(f"Error durante la concatenación inicial: {str(e)}", exc_info=True)
672
- raise ValueError("Fallo durante la concatenación de video inicial.")
673
  finally:
674
- for clip_segment in clips_to_concatenate:
675
- try: clip_segment.close()
676
- except: pass
677
- clips_to_concatenate = []
678
 
679
  video_base = concatenated_base
680
-
681
  final_video_base = video_base
682
 
683
  if final_video_base.duration < audio_duration:
684
  logger.info(f"Video base ({final_video_base.duration:.2f}s) es más corto que el audio ({audio_duration:.2f}s). Repitiendo...")
685
-
686
  num_full_repeats = int(audio_duration // final_video_base.duration)
687
  remaining_duration = audio_duration % final_video_base.duration
688
 
@@ -691,26 +590,24 @@ def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): #
691
  try:
692
  remaining_clip = final_video_base.subclip(0, remaining_duration)
693
  if remaining_clip is None or remaining_clip.duration is None or remaining_clip.duration <= 0:
694
- logger.warning(f"Subclip generado para duración restante {remaining_duration:.2f}s es inválido. Saltando.")
695
  try: remaining_clip.close()
696
  except: pass
697
  else:
698
- repeated_clips_list.append(remaining_clip)
699
- logger.debug(f"Añadiendo segmento restante: {remaining_duration:.2f}s")
700
-
701
  except Exception as e:
702
- logger.warning(f"Error creando subclip para duración restante {remaining_duration:.2f}s: {str(e)}")
703
 
704
  if repeated_clips_list:
705
- logger.info(f"Concatenando {len(repeated_clips_list)} partes para repetición.")
706
- video_base_repeated = None
707
- try:
708
  video_base_repeated = concatenate_videoclips(repeated_clips_list, method="chain")
709
  logger.info(f"Duración del video base repetido: {video_base_repeated.duration:.2f}s")
710
 
711
  if video_base_repeated is None or video_base_repeated.duration is None or video_base_repeated.duration <= 0:
712
- logger.critical("Video base repetido concatenado es inválido.")
713
- raise ValueError("Fallo al crear video base repetido válido.")
714
 
715
  if final_video_base is not video_base_repeated:
716
  try: final_video_base.close()
@@ -718,52 +615,46 @@ def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): #
718
 
719
  final_video_base = video_base_repeated
720
 
721
- except Exception as e:
722
  logger.critical(f"Error durante la concatenación de repetición: {str(e)}", exc_info=True)
723
  raise ValueError("Fallo durante la repetición de video.")
724
- finally:
725
- if 'repeated_clips_list' in locals():
726
- for clip in repeated_clips_list:
727
- if clip is not final_video_base:
728
- try: clip.close()
729
- except: pass
730
-
731
 
732
  if final_video_base.duration > audio_duration:
733
- logger.info(f"Recortando video base ({final_video_base.duration:.2f}s) para que coincida con la duración del audio ({audio_duration:.2f}s).")
734
- trimmed_video_base = None
735
- try:
736
  trimmed_video_base = final_video_base.subclip(0, audio_duration)
737
  if trimmed_video_base is None or trimmed_video_base.duration is None or trimmed_video_base.duration <= 0:
738
- logger.critical("Video base recortado es inválido.")
739
- raise ValueError("Fallo al crear video base recortado válido.")
740
 
741
  if final_video_base is not trimmed_video_base:
742
- try: final_video_base.close()
743
- except: pass
744
 
745
  final_video_base = trimmed_video_base
746
 
747
- except Exception as e:
748
  logger.critical(f"Error durante el recorte: {str(e)}", exc_info=True)
749
  raise ValueError("Fallo durante el recorte de video.")
750
 
751
-
752
  if final_video_base is None or final_video_base.duration is None or final_video_base.duration <= 0:
753
- logger.critical("Video base final es inválido antes de audio/escritura (None o duración cero).")
754
- raise ValueError("Video base final es inválido.")
755
 
756
  if final_video_base.size is None or final_video_base.size[0] <= 0 or final_video_base.size[1] <= 0:
757
- logger.critical(f"Video base final tiene tamaño inválido: {final_video_base.size}. No se puede escribir video.")
758
- raise ValueError("Video base final tiene tamaño inválido antes de escribir.")
759
 
760
  video_base = final_video_base
761
 
762
- # 6. Manejar música de fondo
763
  logger.info("Procesando audio...")
764
-
765
  final_audio = audio_tts_original
766
-
767
  musica_audio_looped = None
768
 
769
  if musica_file:
@@ -777,36 +668,35 @@ def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): #
777
  musica_audio_original = AudioFileClip(music_path)
778
 
779
  if musica_audio_original.reader is None or musica_audio_original.duration is None or musica_audio_original.duration <= 0:
780
- logger.warning("Clip de música de fondo parece inválido o tiene duración cero. Saltando música.")
781
- try: musica_audio_original.close()
782
- except: pass
783
- musica_audio_original = None
784
  else:
785
- musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
786
- logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
787
-
788
- if musica_audio_looped is None or musica_audio_looped.duration is None or musica_audio_looped.duration <= 0:
789
- logger.warning("Clip de música de fondo loopeado es inválido. Saltando música.")
790
- try: musica_audio_looped.close()
791
- except: pass
792
- musica_audio_looped = None
793
 
 
 
 
 
 
794
 
795
  if musica_audio_looped:
796
  composite_audio = CompositeAudioClip([
797
- musica_audio_looped.volumex(0.2), # Volumen 20% para música
798
- audio_tts_original.volumex(1.0) # Volumen 100% para voz
799
  ])
800
 
801
  if composite_audio.duration is None or composite_audio.duration <= 0:
802
- logger.warning("Clip de audio compuesto es inválido (None o duración cero). Usando solo audio de voz.")
803
  try: composite_audio.close()
804
  except: pass
805
  final_audio = audio_tts_original
806
  else:
807
- logger.info("Mezcla de audio completada (voz + música).")
808
- final_audio = composite_audio
809
- musica_audio = musica_audio_looped # Asignar para limpieza
810
 
811
  except Exception as e:
812
  logger.warning(f"Error procesando música de fondo: {str(e)}", exc_info=True)
@@ -814,32 +704,29 @@ def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): #
814
  musica_audio = None
815
  logger.warning("Usando solo audio de voz debido a un error con la música.")
816
 
817
-
818
  if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
819
- logger.warning(f"Duración del audio final ({final_audio.duration:.2f}s) difiere significativamente del video base ({video_base.duration:.2f}s). Intentando recorte.")
820
  try:
821
- if final_audio.duration > video_base.duration:
822
- trimmed_final_audio = final_audio.subclip(0, video_base.duration)
823
- if trimmed_final_audio is None or trimmed_final_audio.duration <= 0:
824
- logger.warning("Audio final recortado es inválido. Usando audio final original.")
825
- try: trimmed_final_audio.close()
826
- except: pass
827
- else:
828
- if final_audio is not trimmed_final_audio:
829
- try: final_audio.close()
830
- except: pass
831
- final_audio = trimmed_final_audio
832
- logger.warning("Audio final recortado para que coincida con la duración del video.")
833
  except Exception as e:
834
- logger.warning(f"Error ajustando duración del audio final: {str(e)}")
835
 
836
- # 7. Crear video final
837
  logger.info("Renderizando video final...")
838
  video_final = video_base.set_audio(final_audio)
839
 
840
  if video_final is None or video_final.duration is None or video_final.duration <= 0:
841
- logger.critical("Clip de video final (con audio) es inválido antes de escribir (None o duración cero).")
842
- raise ValueError("Clip de video final es inválido antes de escribir.")
843
 
844
  output_filename = "final_video.mp4"
845
  output_path = os.path.join(temp_dir_intermediate, output_filename)
@@ -861,83 +748,67 @@ def crear_video(prompt_type, input_text, selected_voice_id, musica_file=None): #
861
  return output_path
862
 
863
  except ValueError as ve:
864
- logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
865
- raise ve
866
  except Exception as e:
867
  logger.critical(f"ERROR CRÍTICO NO CONTROLADO en crear_video: {str(e)}", exc_info=True)
868
  raise e
869
  finally:
870
  logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
871
-
872
  for clip in source_clips:
873
  try:
874
  clip.close()
875
  except Exception as e:
876
- logger.warning(f"Error cerrando clip de video fuente en finally: {str(e)}")
877
-
878
  for clip_segment in clips_to_concatenate:
879
- try:
880
- clip_segment.close()
881
- except Exception as e:
882
- logger.warning(f"Error cerrando segmento de video en finally: {str(e)}")
883
-
884
  if musica_audio is not None:
885
  try:
886
  musica_audio.close()
887
  except Exception as e:
888
- logger.warning(f"Error cerrando musica_audio (procesada) en finally: {str(e)}")
889
-
890
  if musica_audio_original is not None and musica_audio_original is not musica_audio:
891
- try:
892
- musica_audio_original.close()
893
- except Exception as e:
894
- logger.warning(f"Error cerrando musica_audio_original en finally: {str(e)}")
895
-
896
  if audio_tts is not None and audio_tts is not audio_tts_original:
897
- try:
898
- audio_tts.close()
899
- except Exception as e:
900
- logger.warning(f"Error cerrando audio_tts (procesada) en finally: {str(e)}")
901
-
902
  if audio_tts_original is not None:
903
- try:
904
- audio_tts_original.close()
905
- except Exception as e:
906
- logger.warning(f"Error cerrando audio_tts_original en finally: {str(e)}")
907
-
908
  if video_final is not None:
909
  try:
910
  video_final.close()
911
  except Exception as e:
912
- logger.warning(f"Error cerrando video_final en finally: {str(e)}")
913
  elif video_base is not None and video_base is not video_final:
914
- try:
915
- video_base.close()
916
- except Exception as e:
917
- logger.warning(f"Error cerrando video_base en finally: {str(e)}")
918
-
919
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
920
- final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4")
921
-
922
- for path in temp_intermediate_files:
923
- try:
924
- if os.path.isfile(path) and path != final_output_in_temp:
925
- logger.debug(f"Eliminando archivo temporal intermedio: {path}")
926
- os.remove(path)
927
- elif os.path.isfile(path) and path == final_output_in_temp:
928
- logger.debug(f"Saltando eliminación del archivo de video final: {path}")
929
- except Exception as e:
930
- logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
931
-
932
- logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
933
-
934
 
935
- # run_app ahora recibe todos los inputs, incluyendo la voz seleccionada (el ID)
936
- def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice_id): # <-- Recibe el ID
937
  logger.info("="*80)
938
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
939
 
940
- # Elegir el texto de entrada basado en el prompt_type
941
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
942
 
943
  output_video = None
@@ -946,17 +817,11 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice_i
946
 
947
  if not input_text or not input_text.strip():
948
  logger.warning("Texto de entrada vacío.")
949
- # Retornar None para video y archivo, actualizar estado con mensaje de error
950
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
951
 
952
- # Validar el ID de voz seleccionado. Si no es válido, usar el ID por defecto.
953
- # AVAILABLE_VOICE_IDS se obtiene al inicio.
954
- if selected_voice_id not in AVAILABLE_VOICE_IDS:
955
- logger.warning(f"Voz seleccionada inválida o no encontrada en la lista: '{selected_voice_id}'. Usando voz por defecto: {DEFAULT_VOICE_ID}.")
956
- selected_voice_id = DEFAULT_VOICE_ID # <-- Usar el ID de la voz por defecto
957
- else:
958
- logger.info(f"Voz seleccionada validada (ID): {selected_voice_id}")
959
-
960
 
961
  logger.info(f"Tipo de entrada: {prompt_type}")
962
  logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
@@ -964,35 +829,31 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice_i
964
  logger.info(f"Archivo de música recibido: {musica_file}")
965
  else:
966
  logger.info("No se proporcionó archivo de música.")
967
- logger.info(f"Voz final a usar (ID): {selected_voice_id}") # Loguear el ID de la voz final
968
 
969
  try:
970
  logger.info("Llamando a crear_video...")
971
- # Pasar el input_text elegido, la voz seleccionada (el ID) y el archivo de música a crear_video
972
- video_path = crear_video(prompt_type, input_text, selected_voice_id, musica_file) # <-- PASAR selected_voice_id a crear_video
973
 
974
  if video_path and os.path.exists(video_path):
975
  logger.info(f"crear_video retornó path: {video_path}")
976
- logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
977
- output_video = video_path # Establecer valor del componente de video
978
- output_file = video_path # Establecer valor del componente de archivo para descarga
979
  status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
980
  else:
981
- logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
982
- status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
983
 
984
  except ValueError as ve:
985
- logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
986
  status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
987
  except Exception as e:
988
- logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
989
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
990
  finally:
991
  logger.info("Fin del handler run_app.")
992
  return output_video, output_file, status_msg
993
 
994
-
995
- # Interfaz de Gradio
996
  with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
997
  .gradio-container {max-width: 800px; margin: auto;}
998
  h1 {text-align: center;}
@@ -1009,25 +870,22 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1009
  value="Generar Guion con IA"
1010
  )
1011
 
1012
- # Contenedores para los campos de texto para controlar la visibilidad
1013
  with gr.Column(visible=True) as ia_guion_column:
1014
  prompt_ia = gr.Textbox(
1015
  label="Tema para IA",
1016
  lines=2,
1017
- placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer, mostrando la belleza de la naturaleza...",
1018
  max_lines=4,
1019
  value=""
1020
- # visible=... <-- ¡NO DEBE ESTAR AQUÍ!
1021
  )
1022
 
1023
  with gr.Column(visible=False) as manual_guion_column:
1024
  prompt_manual = gr.Textbox(
1025
  label="Tu Guion Completo",
1026
  lines=5,
1027
- 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!",
1028
  max_lines=10,
1029
  value=""
1030
- # visible=... <-- ¡NO DEBE ESTAR AQUÍ!
1031
  )
1032
 
1033
  musica_input = gr.Audio(
@@ -1035,20 +893,14 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1035
  type="filepath",
1036
  interactive=True,
1037
  value=None
1038
- # visible=... <-- ¡NO DEBE ESTAR AQUÍ!
1039
  )
1040
 
1041
- # --- COMPONENTE: Selección de Voz ---
1042
- # Usamos AVAILABLE_VOICES_TUPLES para mostrar (Nombre, ID)
1043
  voice_dropdown = gr.Dropdown(
1044
  label="Seleccionar Voz para Guion",
1045
- choices=AVAILABLE_VOICES_TUPLES, # Usar la lista de tuplas (Nombre, ID)
1046
- value=DEFAULT_VOICE_ID, # Usar el ID de la voz por defecto
1047
  interactive=True
1048
- # visible=... <-- ¡NO DEBE ESTAR AQUÍ!
1049
  )
1050
- # --- FIN COMPONENTE ---
1051
-
1052
 
1053
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
1054
 
@@ -1057,13 +909,11 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1057
  label="Previsualización del Video Generado",
1058
  interactive=False,
1059
  height=400
1060
- # visible=... <-- ¡NO DEBE ESTAR AQUÍ!
1061
  )
1062
  file_output = gr.File(
1063
  label="Descargar Archivo de Video",
1064
  interactive=False,
1065
- visible=False # <-- ESTÁ BIEN AQUÍ
1066
- # visible=... <-- ¡NO DEBE ESTAR AQUÍ si ya está visible=False arriba!
1067
  )
1068
  status_output = gr.Textbox(
1069
  label="Estado",
@@ -1071,48 +921,39 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1071
  show_label=False,
1072
  placeholder="Esperando acción...",
1073
  value="Esperando entrada..."
1074
- # visible=... <-- ¡NO DEBE ESTAR AQUÍ!
1075
  )
1076
 
1077
- # Evento para mostrar/ocultar los campos de texto según el tipo de prompt
1078
  prompt_type.change(
1079
  lambda x: (gr.update(visible=x == "Generar Guion con IA"),
1080
  gr.update(visible=x == "Usar Mi Guion")),
1081
  inputs=prompt_type,
1082
- outputs=[ia_guion_column, manual_guion_column] # Apuntar a las Columnas contenedoras
1083
  )
1084
 
1085
- # Evento click del botón de generar video
1086
  generate_btn.click(
1087
- # Acción 1 (síncrona): Resetear salidas y establecer estado
1088
  lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
1089
  outputs=[video_output, file_output, status_output],
1090
- queue=True, # Usar la cola de Gradio
1091
  ).then(
1092
- # Acción 2 (asíncrona): Llamar a la función principal de procesamiento
1093
  run_app,
1094
- # PASAR TODOS LOS INPUTS DE LA INTERFAZ que run_app espera
1095
- inputs=[prompt_type, prompt_ia, prompt_manual, musica_input, voice_dropdown], # <-- Pasar los 5 inputs a run_app (el dropdown pasa el ID)
1096
- # run_app retornará los 3 outputs esperados
1097
  outputs=[video_output, file_output, status_output]
1098
  ).then(
1099
- # Acción 3 (síncrona): Hacer visible el enlace de descarga
1100
  lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
1101
  inputs=[video_output, file_output, status_output],
1102
  outputs=[file_output]
1103
  )
1104
 
1105
-
1106
  gr.Markdown("### Instrucciones:")
1107
  gr.Markdown("""
1108
- 1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
1109
- 2. **Selecciona el tipo de entrada**: "Generar Guion con IA" o "Usar Mi Guion".
1110
- 3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.).
1111
- 4. **Selecciona la voz** deseada del desplegable.
1112
- 5. **Haz clic en "✨ Generar Video"**.
1113
- 6. Espera a que se procese el video. Verás el estado.
1114
- 7. La previsualización aparecerá si es posible, y siempre un enlace **Descargar Archivo de Video** se mostrará si la generación fue exitosa.
1115
- 8. Revisa `video_generator_full.log` para detalles si hay errores.
1116
  """)
1117
  gr.Markdown("---")
1118
  gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
@@ -1124,12 +965,11 @@ if __name__ == "__main__":
1124
  try:
1125
  temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
1126
  temp_clip.close()
1127
- logger.info("Clips base de MoviePy creados y cerrados exitosamente. FFmpeg parece accesible.")
1128
  except Exception as e:
1129
- logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
1130
-
1131
  except Exception as e:
1132
- logger.critical(f"Fallo al importar MoviePy. Asegúrate de que está instalado. Error: {str(e)}", exc_info=True) # Loguear el error real
1133
 
1134
  logger.info("Iniciando aplicación Gradio...")
1135
  try:
 
9
  import torch
10
  from transformers import GPT2Tokenizer, GPT2LMHeadModel
11
  from keybert import KeyBERT
 
12
  from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
13
  import re
14
  import math
 
30
  logger.info("INICIO DE EJECUCIÓN - GENERADOR DE VIDEOS")
31
  logger.info("="*80)
32
 
33
+ # Función para obtener voces disponibles dinámicamente
34
+ async def get_available_voices():
35
+ try:
36
+ voices = await edge_tts.list_voices()
37
+ # Filtrar solo voces en español
38
+ es_voices = [v for v in voices if v['Locale'].startswith('es-')]
39
+ return {f"{v['ShortName']} - {v['Locale']} ({v['Gender']})": v['ShortName'] for v in es_voices}
40
+ except Exception as e:
41
+ logger.error(f"Error al obtener voces disponibles: {str(e)}")
42
+ return {}
43
+
44
+ # Obtener las voces disponibles
45
+ AVAILABLE_VOICES = asyncio.run(get_available_voices())
46
+ if not AVAILABLE_VOICES:
47
+ logger.warning("No se pudieron obtener voces dinámicamente. Usando voces predefinidas.")
48
+ VOCES_PREDEFINIDAS = {
49
+ "es-ES-JuanNeural": "Juan (España) - Masculino",
50
+ "es-ES-ElviraNeural": "Elvira (España) - Femenino",
51
+ "es-MX-JorgeNeural": "Jorge (México) - Masculino",
52
+ "es-MX-DaliaNeural": "Dalia (México) - Femenino",
53
+ }
54
+ AVAILABLE_VOICES = {v: k for k, v in VOCES_PREDEFINIDAS.items()}
55
+
56
+ # Establecer una voz por defecto
57
+ DEFAULT_VOICE_ID = "es-ES-JuanNeural" if "es-ES-JuanNeural" in AVAILABLE_VOICES.values() else list(AVAILABLE_VOICES.values())[0]
58
+ DEFAULT_VOICE_NAME = next((k for k, v in AVAILABLE_VOICES.items() if v == DEFAULT_VOICE_ID), list(AVAILABLE_VOICES.keys())[0])
59
+ logger.info(f"Voz por defecto seleccionada: {DEFAULT_VOICE_NAME} ({DEFAULT_VOICE_ID})")
60
+
61
  # Clave API de Pexels
62
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
63
  if not PEXELS_API_KEY:
64
  logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
 
65
 
66
  # Inicialización de modelos
67
  MODEL_NAME = "datificate/gpt2-small-spanish"
 
87
  logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
88
  kw_model = None
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  def buscar_videos_pexels(query, api_key, per_page=5):
91
  if not api_key:
92
  logger.warning("No se puede buscar en Pexels: API Key no configurada.")
 
153
  )
154
 
155
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
 
156
  cleaned_text = text.strip()
157
+
158
  try:
 
159
  prompt_in_output_idx = text.lower().find(prompt.lower())
160
  if prompt_in_output_idx != -1:
 
161
  cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
162
  logger.debug("Texto limpiado tomando parte después del prompt original.")
163
  else:
164
+ instruction_start_idx = text.find(instruction_phrase_start)
165
+ if instruction_start_idx != -1:
166
+ cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
167
+ logger.debug("Texto limpiado tomando parte después de la frase de instrucción base.")
168
+ else:
169
+ logger.warning("No se pudo identificar el inicio del guión generado. Usando texto generado completo.")
170
+ cleaned_text = text.strip()
 
 
 
 
171
 
172
  except Exception as e:
173
+ logger.warning(f"Error durante la limpieza heurística del guion de IA: {e}. Usando texto generado sin limpieza adicional.")
174
+ cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
175
 
176
+ if not cleaned_text or len(cleaned_text) < 10:
177
+ 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).")
178
+ cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
 
179
 
 
180
  cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
181
+ cleaned_text = cleaned_text.lstrip(':').strip()
182
+ cleaned_text = cleaned_text.lstrip('.').strip()
 
183
 
 
184
  sentences = cleaned_text.split('.')
185
  if sentences and sentences[0].strip():
186
  final_text = sentences[0].strip() + '.'
187
+ if len(sentences) > 1 and sentences[1].strip() and len(final_text.split()) < max_length * 0.7:
188
+ final_text += " " + sentences[1].strip() + "."
189
+ final_text = final_text.replace("..", ".")
 
 
190
  logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
191
  return final_text.strip()
192
 
193
  logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
194
+ return cleaned_text.strip()
195
 
196
  except Exception as e:
197
+ logger.error(f"Error generando guion con GPT-2: {str(e)}", exc_info=True)
198
  logger.warning("Usando prompt original como guion debido al error de generación.")
199
  return prompt.strip()
200
 
201
+ async def text_to_speech(text, output_path, voice):
202
+ logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
 
203
  if not text or not text.strip():
204
  logger.warning("Texto vacío para TTS")
205
  return False
 
 
 
206
 
207
  try:
208
+ communicate = edge_tts.Communicate(text, voice)
209
  await communicate.save(output_path)
210
 
211
  if os.path.exists(output_path) and os.path.getsize(output_path) > 100:
 
216
  return False
217
 
218
  except Exception as e:
219
+ logger.error(f"Error en TTS con voz '{voice}': {str(e)}", exc_info=True)
220
  return False
221
 
222
  def download_video_file(url, temp_dir):
 
237
  f.write(chunk)
238
 
239
  if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
240
+ logger.info(f"Video descargado exitosamente: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
241
+ return output_path
242
  else:
243
+ logger.warning(f"Descarga parece incompleta o vacía para {url[:80]}... Archivo: {output_path} Tamaño: {os.path.getsize(output_path) if os.path.exists(output_path) else 'N/A'} bytes")
244
+ if os.path.exists(output_path):
245
+ os.remove(output_path)
246
+ return None
247
 
248
  except requests.exceptions.RequestException as e:
249
  logger.error(f"Error de descarga para {url[:80]}... : {str(e)}")
 
261
  sr = getattr(audio_clip, 'fps', 44100) if audio_clip else 44100
262
  return AudioClip(lambda t: 0, duration=target_duration, sr=sr)
263
  except Exception as e:
264
+ logger.error(f"Could not create silence clip: {e}", exc_info=True)
265
+ return AudioFileClip(filename="")
266
 
267
  if audio_clip.duration >= target_duration:
268
  logger.debug("Audio clip already longer or equal to target. Trimming.")
269
  trimmed_clip = audio_clip.subclip(0, target_duration)
270
  if trimmed_clip.duration is None or trimmed_clip.duration <= 0:
271
+ logger.error("Trimmed audio clip is invalid.")
272
+ try: trimmed_clip.close()
273
+ except: pass
274
+ return AudioFileClip(filename="")
275
  return trimmed_clip
276
 
277
  loops = math.ceil(target_duration / audio_clip.duration)
 
281
  looped_audio = None
282
  final_looped_audio = None
283
  try:
284
+ looped_audio = concatenate_audioclips(audio_segments)
285
+ if looped_audio.duration is None or looped_audio.duration <= 0:
 
286
  logger.error("Concatenated audio clip is invalid (None or zero duration).")
287
  raise ValueError("Invalid concatenated audio.")
288
 
289
+ final_looped_audio = looped_audio.subclip(0, target_duration)
290
+ if final_looped_audio.duration is None or final_looped_audio.duration <= 0:
 
291
  logger.error("Final subclipped audio clip is invalid (None or zero duration).")
292
  raise ValueError("Invalid final subclipped audio.")
293
 
294
+ return final_looped_audio
295
 
296
  except Exception as e:
297
+ logger.error(f"Error concatenating/subclipping audio clips for looping: {strrance(e)}", exc_info=True)
298
  try:
299
+ if audio_clip.duration is not None and audio_clip.duration > 0:
300
+ logger.warning("Returning original audio clip (may be too short).")
301
+ return audio_clip.subclip(0, min(audio_clip.duration, target_duration))
302
  except:
303
+ pass
304
  logger.error("Fallback to original audio clip failed.")
305
  return AudioFileClip(filename="")
306
 
 
309
  try: looped_audio.close()
310
  except: pass
311
 
 
312
  def extract_visual_keywords_from_script(script_text):
313
  logger.info("Extrayendo palabras clave del guion")
314
  if not script_text or not script_text.strip():
 
345
 
346
  logger.debug("Extrayendo palabras clave con método simple...")
347
  words = clean_text.lower().split()
348
+ stop_words = {"el", "la", "los", "las", "de", "en", "y", "a", "que", "es", "un", "una", "con", "para", "del", "al", "por", "su", "sus", "se", "lo", "le", "me", "te", "nos", "os", "les", "mi", "tu", "nuestro", "vuestro", "este", "ese", "aquel", "esta", "esa", "aquella", "esto", "eso", "aquello", "mis", "tus", "nuestros", "vuestros", "estas", "esas", "aquellas", "si", "no", "más", "menos", "sin", "sobre", "bajo", "entre", "hasta", "desde", "durante", "mediante", "según", "versus", "via", "cada", "todo", "todos", "toda", "todas", "poco", "pocos", "poca", "pocas", "mucho", "muchos", "mucha", "muchas", "varios", "varias", "otro", "otros", "otra", "otras", "mismo", "misma", "mismos", "mismas", "tan", "tanto", "tanta", "tantos", "tantas", "tal", "tales", "cual", "cuales", "cuyo", "cuya", "cuyos", "cuyas", "quien", "quienes", "cuan", "cuanto", "cuanta", "cuantos", "cuantas", "como", "donde", "cuando", "porque", "aunque", "mientras", "siempre", "nunca", "jamás", "muy", "casi", "solo", "solamente", "incluso", "apenas", "quizás", "tal vez", "acaso", "claro", "cierto", "obvio", "evidentemente", "realmente", "simplemente", "generalmente", "especialmente", "principalmente", "posiblemente", "probablemente", "difícilmente", "fácilmente", "rápidamente", "lentamente", "bien", "mal", "mejor", "peor", "arriba", "abajo", "adelante", "atrás", "cerca", "lejos", "dentro", "fuera", "encima", "debajo", "frente", "detrás", "antes", "después", "luego", "pronto", "tarde", "todavía", "ya", "aun", "aún", "quizá"}
 
 
349
 
350
  valid_words = [word for word in words if len(word) > 3 and word not in stop_words]
351
 
 
357
  top_keywords = [word.replace(" ", "+") for word, _ in word_counts.most_common(5)]
358
 
359
  if not top_keywords:
360
+ logger.warning("El método simple no produjo keywords. Usando palabras clave predeterminadas.")
361
+ return ["naturaleza", "ciudad", "paisaje"]
362
 
363
  logger.info(f"Palabras clave finales: {top_keywords}")
364
  return top_keywords
365
 
366
+ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
 
367
  logger.info("="*80)
368
  logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
369
  logger.debug(f"Input: '{input_text[:100]}...'")
370
+ logger.info(f"Voz seleccionada: {selected_voice}")
371
+
372
+ if selected_voice not in AVAILABLE_VOICES.values():
373
+ logger.warning(f"Voz seleccionada '{selected_voice}' no es válida. Usando voz por defecto: {DEFAULT_VOICE_ID}")
374
+ selected_voice = DEFAULT_VOICE_ID
375
 
376
  start_time = datetime.now()
377
  temp_dir_intermediate = None
 
386
  clips_to_concatenate = []
387
 
388
  try:
 
389
  if prompt_type == "Generar Guion con IA":
390
  guion = generate_script(input_text)
391
  else:
 
401
  logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
402
  temp_intermediate_files = []
403
 
 
404
  logger.info("Generando audio de voz...")
405
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
406
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  tts_success = False
408
+ voices_to_try = [selected_voice, DEFAULT_VOICE_ID]
 
 
 
 
 
409
 
410
+ for voice in voices_to_try:
411
+ if not voice: continue
412
+ logger.info(f"Intentando TTS con voz: {voice}...")
413
+ tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=voice))
414
+ if tts_success:
415
+ break
416
+ else:
417
+ logger.warning(f"Fallo al generar TTS con voz '{voice}'.")
 
 
418
 
419
+ if not tts_success:
420
+ raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
 
 
 
 
421
 
422
+ temp_intermediate_files.append(voz_path)
423
 
424
  audio_tts_original = AudioFileClip(voz_path)
425
 
426
  if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
427
+ logger.critical("Clip de audio TTS inicial es inválido.")
428
  try: audio_tts_original.close()
429
  except: pass
430
+ raise ValueError("Audio de voz generado es inválido.")
 
 
 
 
 
 
 
 
 
431
 
432
  audio_tts = audio_tts_original
433
  audio_duration = audio_tts_original.duration
434
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
435
 
436
  if audio_duration < 1.0:
437
+ logger.error(f"Duración audio voz ({audio_duration:.2f}s) es muy corta.")
438
+ raise ValueError("Generated voice audio is too short.")
439
 
 
440
  logger.info("Extrayendo palabras clave...")
441
  try:
442
  keywords = extract_visual_keywords_from_script(guion)
 
446
  keywords = ["naturaleza", "paisaje"]
447
 
448
  if not keywords:
449
+ keywords = ["video", "background"]
450
 
 
451
  logger.info("Buscando videos en Pexels...")
452
  videos_data = []
453
  total_desired_videos = 10
 
459
  videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=per_page_per_keyword)
460
  if videos:
461
  videos_data.extend(videos)
 
462
  except Exception as e:
463
  logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
464
 
 
466
  logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
467
  generic_keywords = ["nature", "city", "background", "abstract"]
468
  for keyword in generic_keywords:
469
+ if len(videos_data) >= total_desired_videos: break
470
+ try:
471
  videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=2)
472
  if videos:
473
  videos_data.extend(videos)
474
+ except Exception as e:
 
475
  logger.warning(f"Error buscando videos genéricos para '{keyword}': {str(e)}")
476
 
477
  if not videos_data:
478
+ logger.error("No se encontraron videos en Pexels.")
479
  raise ValueError("No se encontraron videos adecuados en Pexels.")
480
 
481
  video_paths = []
 
497
  if path:
498
  video_paths.append(path)
499
  temp_intermediate_files.append(path)
 
 
 
 
 
 
500
  except Exception as e:
501
  logger.warning(f"Error procesando/descargando video {video.get('id')}: {str(e)}")
502
 
 
505
  logger.error("No se pudo descargar ningún archivo de video utilizable.")
506
  raise ValueError("No se pudo descargar ningún video utilizable de Pexels.")
507
 
 
508
  logger.info("Procesando y concatenando videos descargados...")
509
  current_duration = 0
510
  min_clip_duration = 0.5
 
512
 
513
  for i, path in enumerate(video_paths):
514
  if current_duration >= audio_duration + max_clip_segment:
515
+ logger.debug(f"Video base suficiente.")
516
  break
517
 
518
  clip = None
 
522
  source_clips.append(clip)
523
 
524
  if clip.reader is None or clip.duration is None or clip.duration <= 0:
525
+ logger.warning(f"[{i+1}/{len(video_paths)}] Clip fuente {path} parece inválido.")
526
  continue
527
 
528
  remaining_needed = audio_duration - current_duration
529
  potential_use_duration = min(clip.duration, max_clip_segment)
530
 
531
  if remaining_needed > 0:
532
+ segment_duration = min(potential_use_duration, remaining_needed + min_clip_duration)
533
+ segment_duration = max(min_clip_duration, segment_duration)
534
+ segment_duration = min(segment_duration, clip.duration)
535
+
536
+ if segment_duration >= min_clip_duration:
537
+ try:
538
+ sub = clip.subclip(0, segment_duration)
539
+ if sub.reader is None or sub.duration is None or sub.duration <= 0:
540
+ logger.warning(f"[{i+1}/{len(video_paths)}] Subclip inválido.")
541
+ try: sub.close()
542
+ except: pass
543
+ continue
544
+
545
+ clips_to_concatenate.append(sub)
546
+ current_duration += sub.duration
547
+ except Exception as sub_e:
548
+ logger.warning(f"[{i+1}/{len(video_paths)}] Error creando subclip: {str(sub_e)}")
549
+ continue
 
 
 
 
 
 
550
 
551
  except Exception as e:
552
  logger.warning(f"[{i+1}/{len(video_paths)}] Error procesando video {path}: {str(e)}", exc_info=True)
 
555
  logger.info(f"Procesamiento de clips fuente finalizado. Se obtuvieron {len(clips_to_concatenate)} segmentos válidos.")
556
 
557
  if not clips_to_concatenate:
558
+ logger.error("No hay segmentos de video válidos disponibles.")
559
+ raise ValueError("No hay segmentos de video válidos.")
560
 
561
  logger.info(f"Concatenando {len(clips_to_concatenate)} segmentos de video.")
562
  concatenated_base = None
 
565
  logger.info(f"Duración video base después de concatenación inicial: {concatenated_base.duration:.2f}s")
566
 
567
  if concatenated_base is None or concatenated_base.duration is None or concatenated_base.duration <= 0:
568
+ logger.critical("Video base concatenado es inválido.")
569
+ raise ValueError("Fallo al crear video base válido.")
570
 
571
  except Exception as e:
572
+ logger.critical(f"Error durante la concatenación inicial: {str(e)}", exc_info=True)
573
+ raise ValueError("Fallo durante la concatenación de video inicial.")
574
  finally:
575
+ for clip_segment in clips_to_concatenate:
576
+ try: clip_segment.close()
577
+ except: pass
578
+ clips_to_concatenate = []
579
 
580
  video_base = concatenated_base
 
581
  final_video_base = video_base
582
 
583
  if final_video_base.duration < audio_duration:
584
  logger.info(f"Video base ({final_video_base.duration:.2f}s) es más corto que el audio ({audio_duration:.2f}s). Repitiendo...")
 
585
  num_full_repeats = int(audio_duration // final_video_base.duration)
586
  remaining_duration = audio_duration % final_video_base.duration
587
 
 
590
  try:
591
  remaining_clip = final_video_base.subclip(0, remaining_duration)
592
  if remaining_clip is None or remaining_clip.duration is None or remaining_clip.duration <= 0:
593
+ logger.warning(f"Subclip generado para duración restante {remaining_duration:.2f}s es inválido.")
594
  try: remaining_clip.close()
595
  except: pass
596
  else:
597
+ repeated_clips_list.append(remaining_clip)
 
 
598
  except Exception as e:
599
+ logger.warning(f"Error creando subclip para duración restante {remaining_duration:.2f}s: {str(e)}")
600
 
601
  if repeated_clips_list:
602
+ logger.info(f"Concatenando {len(repeated_clips_list)} partes para repetición.")
603
+ video_base_repeated = None
604
+ try:
605
  video_base_repeated = concatenate_videoclips(repeated_clips_list, method="chain")
606
  logger.info(f"Duración del video base repetido: {video_base_repeated.duration:.2f}s")
607
 
608
  if video_base_repeated is None or video_base_repeated.duration is None or video_base_repeated.duration <= 0:
609
+ logger.critical("Video base repetido concatenado es inválido.")
610
+ raise ValueError("Fallo al crear video base repetido válido.")
611
 
612
  if final_video_base is not video_base_repeated:
613
  try: final_video_base.close()
 
615
 
616
  final_video_base = video_base_repeated
617
 
618
+ except Exception as e:
619
  logger.critical(f"Error durante la concatenación de repetición: {str(e)}", exc_info=True)
620
  raise ValueError("Fallo durante la repetición de video.")
621
+ finally:
622
+ for clip in repeated_clips_list:
623
+ if clip is not final_video_base:
624
+ try: clip.close()
625
+ except: pass
 
 
626
 
627
  if final_video_base.duration > audio_duration:
628
+ logger.info(f"Recortando video base ({final_video_base.duration:.2f}s) para coincidir con audio ({audio_duration:.2f}s).")
629
+ trimmed_video_base = None
630
+ try:
631
  trimmed_video_base = final_video_base.subclip(0, audio_duration)
632
  if trimmed_video_base is None or trimmed_video_base.duration is None or trimmed_video_base.duration <= 0:
633
+ logger.critical("Video base recortado es inválido.")
634
+ raise ValueError("Fallo al crear video base recortado válido.")
635
 
636
  if final_video_base is not trimmed_video_base:
637
+ try: final_video_base.close()
638
+ except: pass
639
 
640
  final_video_base = trimmed_video_base
641
 
642
+ except Exception as e:
643
  logger.critical(f"Error durante el recorte: {str(e)}", exc_info=True)
644
  raise ValueError("Fallo durante el recorte de video.")
645
 
 
646
  if final_video_base is None or final_video_base.duration is None or final_video_base.duration <= 0:
647
+ logger.critical("Video base final es inválido.")
648
+ raise ValueError("Video base final es inválido.")
649
 
650
  if final_video_base.size is None or final_video_base.size[0] <= 0 or final_video_base.size[1] <= 0:
651
+ logger.critical(f"Video base final tiene tamaño inválido: {final_video_base.size}.")
652
+ raise ValueError("Video base final tiene tamaño inválido.")
653
 
654
  video_base = final_video_base
655
 
 
656
  logger.info("Procesando audio...")
 
657
  final_audio = audio_tts_original
 
658
  musica_audio_looped = None
659
 
660
  if musica_file:
 
668
  musica_audio_original = AudioFileClip(music_path)
669
 
670
  if musica_audio_original.reader is None or musica_audio_original.duration is None or musica_audio_original.duration <= 0:
671
+ logger.warning("Clip de música de fondo parece inválido.")
672
+ try: musica_audio_original.close()
673
+ except: pass
674
+ musica_audio_original = None
675
  else:
676
+ musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
677
+ logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
 
 
 
 
 
 
678
 
679
+ if musica_audio_looped is None or musica_audio_looped.duration is None or musica_audio_looped.duration <= 0:
680
+ logger.warning("Clip de música de fondo loopeado es inválido.")
681
+ try: musica_audio_looped.close()
682
+ except: pass
683
+ musica_audio_looped = None
684
 
685
  if musica_audio_looped:
686
  composite_audio = CompositeAudioClip([
687
+ musica_audio_looped.volumex(0.2),
688
+ audio_tts_original.volumex(1.0)
689
  ])
690
 
691
  if composite_audio.duration is None or composite_audio.duration <= 0:
692
+ logger.warning("Clip de audio compuesto es inválido.")
693
  try: composite_audio.close()
694
  except: pass
695
  final_audio = audio_tts_original
696
  else:
697
+ logger.info("Mezcla de audio completada (voz + música).")
698
+ final_audio = composite_audio
699
+ musica_audio = musica_audio_looped
700
 
701
  except Exception as e:
702
  logger.warning(f"Error procesando música de fondo: {str(e)}", exc_info=True)
 
704
  musica_audio = None
705
  logger.warning("Usando solo audio de voz debido a un error con la música.")
706
 
 
707
  if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
708
+ logger.warning(f"Duración del audio final ({final_audio.duration:.2f}s) difiere del video base ({video_base.duration:.2f}s).")
709
  try:
710
+ if final_audio.duration > video_base.duration:
711
+ trimmed_final_audio = final_audio.subclip(0, video_base.duration)
712
+ if trimmed_final_audio is None or trimmed_final_audio.duration <= 0:
713
+ logger.warning("Audio final recortado es inválido.")
714
+ try: trimmed_final_audio.close()
715
+ except: pass
716
+ else:
717
+ if final_audio is not trimmed_final_audio:
718
+ try: final_audio.close()
719
+ except: pass
720
+ final_audio = trimmed_final_audio
 
721
  except Exception as e:
722
+ logger.warning(f"Error ajustando duración del audio final: {str(e)}")
723
 
 
724
  logger.info("Renderizando video final...")
725
  video_final = video_base.set_audio(final_audio)
726
 
727
  if video_final is None or video_final.duration is None or video_final.duration <= 0:
728
+ logger.critical("Clip de video final (con audio) es inválido.")
729
+ raise ValueError("Clip de video final es inválido.")
730
 
731
  output_filename = "final_video.mp4"
732
  output_path = os.path.join(temp_dir_intermediate, output_filename)
 
748
  return output_path
749
 
750
  except ValueError as ve:
751
+ logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
752
+ raise ve
753
  except Exception as e:
754
  logger.critical(f"ERROR CRÍTICO NO CONTROLADO en crear_video: {str(e)}", exc_info=True)
755
  raise e
756
  finally:
757
  logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
 
758
  for clip in source_clips:
759
  try:
760
  clip.close()
761
  except Exception as e:
762
+ logger.warning(f"Error cerrando clip de video fuente: {str(e)}")
 
763
  for clip_segment in clips_to_concatenate:
764
+ try:
765
+ clip_segment.close()
766
+ except Exception as e:
767
+ logger.warning(f"Error cerrando segmento de video: {str(e)}")
 
768
  if musica_audio is not None:
769
  try:
770
  musica_audio.close()
771
  except Exception as e:
772
+ logger.warning(f"Error cerrando musica_audio: {str(e)}")
 
773
  if musica_audio_original is not None and musica_audio_original is not musica_audio:
774
+ try:
775
+ musica_audio_original.close()
776
+ except Exception as e:
777
+ logger.warning(f"Error cerrando musica_audio_original: {str(e)}")
 
778
  if audio_tts is not None and audio_tts is not audio_tts_original:
779
+ try:
780
+ audio_tts.close()
781
+ except Exception as e:
782
+ logger.warning(f"Error cerrando audio_tts: {str(e)}")
 
783
  if audio_tts_original is not None:
784
+ try:
785
+ audio_tts_original.close()
786
+ except Exception as e:
787
+ logger.warning(f"Error cerrando audio_tts_original: {str(e)}")
 
788
  if video_final is not None:
789
  try:
790
  video_final.close()
791
  except Exception as e:
792
+ logger.warning(f"Error cerrando video_final: {str(e)}")
793
  elif video_base is not None and video_base is not video_final:
794
+ try:
795
+ video_base.close()
796
+ except Exception as e:
797
+ logger.warning(f"Error cerrando video_base: {str(e)}")
 
798
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
799
+ final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4")
800
+ for path in temp_intermediate_files:
801
+ try:
802
+ if os.path.isfile(path) and path != final_output_in_temp:
803
+ logger.debug(f"Eliminando archivo temporal intermedio: {path}")
804
+ os.remove(path)
805
+ except Exception as e:
806
+ logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
 
 
 
 
 
 
807
 
808
+ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
 
809
  logger.info("="*80)
810
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
811
 
 
812
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
813
 
814
  output_video = None
 
817
 
818
  if not input_text or not input_text.strip():
819
  logger.warning("Texto de entrada vacío.")
 
820
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
821
 
822
+ if selected_voice not in AVAILABLE_VOICES.values():
823
+ logger.warning(f"Voz seleccionada inválida: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE_ID}")
824
+ selected_voice = DEFAULT_VOICE_ID
 
 
 
 
 
825
 
826
  logger.info(f"Tipo de entrada: {prompt_type}")
827
  logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
 
829
  logger.info(f"Archivo de música recibido: {musica_file}")
830
  else:
831
  logger.info("No se proporcionó archivo de música.")
832
+ logger.info(f"Voz final a usar (ID): {selected_voice}")
833
 
834
  try:
835
  logger.info("Llamando a crear_video...")
836
+ video_path = crear_video(prompt_type, input_text, selected_voice, musica_file)
 
837
 
838
  if video_path and os.path.exists(video_path):
839
  logger.info(f"crear_video retornó path: {video_path}")
840
+ output_video = video_path
841
+ output_file = video_path
 
842
  status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
843
  else:
844
+ logger.error(f"crear_video no retornó un path válido: {video_path}")
845
+ status_msg = gr.update(value="❌ Error: La generación del video falló.", interactive=False)
846
 
847
  except ValueError as ve:
848
+ logger.warning(f"Error de validación: {str(ve)}")
849
  status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
850
  except Exception as e:
851
+ logger.critical(f"Error crítico: {str(e)}", exc_info=True)
852
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
853
  finally:
854
  logger.info("Fin del handler run_app.")
855
  return output_video, output_file, status_msg
856
 
 
 
857
  with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
858
  .gradio-container {max-width: 800px; margin: auto;}
859
  h1 {text-align: center;}
 
870
  value="Generar Guion con IA"
871
  )
872
 
 
873
  with gr.Column(visible=True) as ia_guion_column:
874
  prompt_ia = gr.Textbox(
875
  label="Tema para IA",
876
  lines=2,
877
+ placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer...",
878
  max_lines=4,
879
  value=""
 
880
  )
881
 
882
  with gr.Column(visible=False) as manual_guion_column:
883
  prompt_manual = gr.Textbox(
884
  label="Tu Guion Completo",
885
  lines=5,
886
+ placeholder="Ej: En este video exploraremos los misterios del océano...",
887
  max_lines=10,
888
  value=""
 
889
  )
890
 
891
  musica_input = gr.Audio(
 
893
  type="filepath",
894
  interactive=True,
895
  value=None
 
896
  )
897
 
 
 
898
  voice_dropdown = gr.Dropdown(
899
  label="Seleccionar Voz para Guion",
900
+ choices=list(AVAILABLE_VOICES.keys()),
901
+ value=DEFAULT_VOICE_NAME,
902
  interactive=True
 
903
  )
 
 
904
 
905
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
906
 
 
909
  label="Previsualización del Video Generado",
910
  interactive=False,
911
  height=400
 
912
  )
913
  file_output = gr.File(
914
  label="Descargar Archivo de Video",
915
  interactive=False,
916
+ visible=False
 
917
  )
918
  status_output = gr.Textbox(
919
  label="Estado",
 
921
  show_label=False,
922
  placeholder="Esperando acción...",
923
  value="Esperando entrada..."
 
924
  )
925
 
 
926
  prompt_type.change(
927
  lambda x: (gr.update(visible=x == "Generar Guion con IA"),
928
  gr.update(visible=x == "Usar Mi Guion")),
929
  inputs=prompt_type,
930
+ outputs=[ia_guion_column, manual_guion_column]
931
  )
932
 
 
933
  generate_btn.click(
 
934
  lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
935
  outputs=[video_output, file_output, status_output],
936
+ queue=True,
937
  ).then(
 
938
  run_app,
939
+ inputs=[prompt_type, prompt_ia, prompt_manual, musica_input, voice_dropdown],
 
 
940
  outputs=[video_output, file_output, status_output]
941
  ).then(
 
942
  lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
943
  inputs=[video_output, file_output, status_output],
944
  outputs=[file_output]
945
  )
946
 
 
947
  gr.Markdown("### Instrucciones:")
948
  gr.Markdown("""
949
+ 1. **Clave API de Pexels:** Configura la variable de entorno `PEXELS_API_KEY`.
950
+ 2. **Selecciona el tipo de entrada**: "Generar Guion con IA" o "Usar Mi Guion".
951
+ 3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.).
952
+ 4. **Selecciona la voz** deseada del desplegable.
953
+ 5. **Haz clic en "✨ Generar Video"**.
954
+ 6. Espera a que se procese el video. Verás el estado.
955
+ 7. Descarga el video generado.
956
+ 8. Revisa `video_generator_full.log` para detalles si hay errores.
957
  """)
958
  gr.Markdown("---")
959
  gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
 
965
  try:
966
  temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
967
  temp_clip.close()
968
+ logger.info("Clips base de MoviePy creados y cerrados exitosamente.")
969
  except Exception as e:
970
+ logger.critical(f"Fallo al crear clip base de MoviePy: {e}", exc_info=True)
 
971
  except Exception as e:
972
+ logger.critical(f"Fallo al importar MoviePy: {e}", exc_info=True)
973
 
974
  logger.info("Iniciando aplicación Gradio...")
975
  try: