gnosticdev commited on
Commit
a840c4f
·
verified ·
1 Parent(s): 2035bc4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +208 -682
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
@@ -32,7 +31,6 @@ logger.info("INICIO DE EJECUCIÓN - GENERADOR DE VIDEOS")
32
  logger.info("="*80)
33
 
34
  # Diccionario de voces TTS disponibles organizadas por idioma
35
- # Puedes expandir esta lista si conoces otros IDs de voz de Edge TTS
36
  VOCES_DISPONIBLES = {
37
  "Español (España)": {
38
  "es-ES-JuanNeural": "Juan (España) - Masculino",
@@ -100,37 +98,26 @@ def get_voice_choices():
100
  choices = []
101
  for region, voices in VOCES_DISPONIBLES.items():
102
  for voice_id, voice_name in voices.items():
103
- # Formato: (Texto a mostrar en el dropdown, Valor que se pasa)
104
  choices.append((f"{voice_name} ({region})", voice_id))
105
  return choices
106
 
107
  # Obtener las voces al inicio del script
108
- # Usamos la lista predefinida por ahora para evitar el error de inicio con la API
109
- # Si deseas obtenerlas dinámicamente, descomenta la siguiente línea y comenta la que usa get_voice_choices()
110
- # AVAILABLE_VOICES = asyncio.run(get_available_voices())
111
- AVAILABLE_VOICES = get_voice_choices() # <-- Usamos la lista predefinida y aplanada
112
- # Establecer una voz por defecto inicial
113
- DEFAULT_VOICE_ID = "es-ES-JuanNeural" # ID de Juan
114
-
115
- # Buscar el nombre amigable para la voz por defecto si existe
116
  DEFAULT_VOICE_NAME = DEFAULT_VOICE_ID
117
  for text, voice_id in AVAILABLE_VOICES:
118
  if voice_id == DEFAULT_VOICE_ID:
119
  DEFAULT_VOICE_NAME = text
120
  break
121
- # Si Juan no está en la lista (ej. lista de fallback), usar la primera voz disponible
122
  if DEFAULT_VOICE_ID not in [v[1] for v in AVAILABLE_VOICES]:
123
  DEFAULT_VOICE_ID = AVAILABLE_VOICES[0][1] if AVAILABLE_VOICES else "en-US-AriaNeural"
124
- DEFAULT_VOICE_NAME = AVAILABLE_VOICES[0][0] if AVAILABLE_VOICES else "Aria (United States) - Female" # Fallback name
125
-
126
  logger.info(f"Voz por defecto seleccionada (ID): {DEFAULT_VOICE_ID}")
127
 
128
-
129
  # Clave API de Pexels
130
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
131
  if not PEXELS_API_KEY:
132
  logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
133
- # raise ValueError("API key de Pexels no configurada")
134
 
135
  # Inicialización de modelos
136
  MODEL_NAME = "datificate/gpt2-small-spanish"
@@ -170,7 +157,6 @@ def buscar_videos_pexels(query, api_key, per_page=5):
170
  "orientation": "landscape",
171
  "size": "medium"
172
  }
173
-
174
  response = requests.get(
175
  "https://api.pexels.com/videos/search",
176
  headers=headers,
@@ -178,20 +164,19 @@ def buscar_videos_pexels(query, api_key, per_page=5):
178
  timeout=20
179
  )
180
  response.raise_for_status()
181
-
182
  data = response.json()
183
  videos = data.get('videos', [])
184
  logger.info(f"Pexels: {len(videos)} videos encontrados para '{query}'")
185
  return videos
186
-
187
  except requests.exceptions.RequestException as e:
188
  logger.error(f"Error de conexión Pexels para '{query}': {str(e)}")
 
189
  except json.JSONDecodeError:
190
- logger.error(f"Pexels: JSON inválido recibido | Status: {response.status_code} | Respuesta: {response.text[:200]}...")
 
191
  except Exception as e:
192
- logger.error(f"Error inesperado Pexels para '{query}': {str(e)}", exc_info=True)
193
-
194
- return []
195
 
196
  def generate_script(prompt, max_length=150):
197
  logger.info(f"Generando guión | Prompt: '{prompt[:50]}...' | Longitud máxima: {max_length}")
@@ -207,7 +192,6 @@ def generate_script(prompt, max_length=150):
207
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
208
  model.to(device)
209
  inputs = {k: v.to(device) for k, v in inputs.items()}
210
-
211
  outputs = model.generate(
212
  **inputs,
213
  max_length=max_length + inputs[list(inputs.keys())[0]].size(1),
@@ -220,247 +204,147 @@ def generate_script(prompt, max_length=150):
220
  eos_token_id=tokenizer.eos_token_id,
221
  no_repeat_ngram_size=3
222
  )
223
-
224
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
225
-
226
- cleaned_text = text.strip()
227
- # Limpieza mejorada de la frase de instrucción
228
- try:
229
- # Buscar el índice de inicio del prompt original dentro del texto generado
230
- prompt_in_output_idx = text.lower().find(prompt.lower())
231
- if prompt_in_output_idx != -1:
232
- # Tomar todo el texto DESPUÉS del prompt original
233
- cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
234
- logger.debug("Texto limpiado tomando parte después del prompt original.")
235
  else:
236
- # Fallback si el prompt original no está exacto en la salida: buscar la frase de instrucción base
237
- instruction_start_idx = text.find(instruction_phrase_start)
238
- if instruction_start_idx != -1:
239
- # Tomar texto después de la frase base (puede incluir el prompt)
240
- cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
241
- logger.debug("Texto limpiado tomando parte después de la frase de instrucción base.")
242
- else:
243
- # Si ni la frase de instrucción ni el prompt se encuentran, usar el texto original
244
- logger.warning("No se pudo identificar el inicio del guión generado. Usando texto generado completo.")
245
- cleaned_text = text.strip() # Limpieza básica
246
-
247
-
248
- except Exception as e:
249
- logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
250
- cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Limpieza básica como fallback
251
-
252
- # Asegurarse de que el texto resultante no sea solo la instrucción o vacío
253
- if not cleaned_text or len(cleaned_text) < 10: # Umbral de longitud mínima
254
- logger.warning("El guión generado parece muy corto o vacío después de la limpieza heurística. Usando el texto generado original (sin limpieza adicional).")
255
- cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Fallback al texto original limpio
256
-
257
- # Limpieza final de caracteres especiales y espacios sobrantes
258
  cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
259
- cleaned_text = cleaned_text.lstrip(':').strip() # Quitar posibles ':' al inicio
260
- cleaned_text = cleaned_text.lstrip('.').strip() # Quitar posibles '.' al inicio
261
-
262
-
263
- # Intentar obtener al menos una oración completa si es posible para un inicio más limpio
264
  sentences = cleaned_text.split('.')
265
  if sentences and sentences[0].strip():
266
  final_text = sentences[0].strip() + '.'
267
- # Añadir la segunda oración si existe y es razonable
268
- if len(sentences) > 1 and sentences[1].strip() and len(final_text.split()) < max_length * 0.7: # Usar un 70% de max_length como umbral
269
- final_text += " " + sentences[1].strip() + "."
270
- final_text = final_text.replace("..", ".") # Limpiar doble punto
271
-
272
  logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
273
  return final_text.strip()
274
-
275
- logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
276
- return cleaned_text.strip() # Si no se puede formar una oración, devolver el texto limpio tal cual
277
-
278
  except Exception as e:
279
- logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
280
- logger.warning("Usando prompt original como guion debido al error de generación.")
281
  return prompt.strip()
282
 
283
- # Función TTS ahora recibe la voz a usar
284
  async def text_to_speech(text, output_path, voice):
285
- logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
286
  if not text or not text.strip():
287
  logger.warning("Texto vacío para TTS")
288
  return False
289
-
290
  try:
291
  communicate = edge_tts.Communicate(text, voice)
292
  await communicate.save(output_path)
293
-
294
  if os.path.exists(output_path) and os.path.getsize(output_path) > 100:
295
- logger.info(f"Audio guardado exitosamente en: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
296
  return True
297
- else:
298
- logger.error(f"TTS guardó un archivo pequeño o vacío en: {output_path}")
299
- return False
300
-
301
  except Exception as e:
302
- logger.error(f"Error en TTS con voz '{voice}': {str(e)}", exc_info=True)
303
  return False
304
 
305
  def download_video_file(url, temp_dir):
306
  if not url:
307
- logger.warning("URL de video no proporcionada para descargar")
308
  return None
309
-
310
  try:
311
  logger.info(f"Descargando video desde: {url[:80]}...")
312
  os.makedirs(temp_dir, exist_ok=True)
313
  file_name = f"video_dl_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.mp4"
314
  output_path = os.path.join(temp_dir, file_name)
315
-
316
  with requests.get(url, stream=True, timeout=60) as r:
317
  r.raise_for_status()
318
  with open(output_path, 'wb') as f:
319
  for chunk in r.iter_content(chunk_size=8192):
320
  f.write(chunk)
321
-
322
  if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
323
- logger.info(f"Video descargado exitosamente: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
324
- return output_path
325
- else:
326
- 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")
327
- if os.path.exists(output_path):
328
- os.remove(output_path)
329
- return None
330
-
331
  except requests.exceptions.RequestException as e:
332
- logger.error(f"Error de descarga para {url[:80]}... : {str(e)}")
 
333
  except Exception as e:
334
- logger.error(f"Error inesperado descargando {url[:80]}... : {str(e)}", exc_info=True)
335
-
336
- return None
337
 
338
  def loop_audio_to_length(audio_clip, target_duration):
339
  logger.debug(f"Ajustando audio | Duración actual: {audio_clip.duration:.2f}s | Objetivo: {target_duration:.2f}s")
340
-
341
  if audio_clip is None or audio_clip.duration is None or audio_clip.duration <= 0:
342
- logger.warning("Input audio clip is invalid (None or zero duration), cannot loop.")
343
- try:
344
- sr = getattr(audio_clip, 'fps', 44100) if audio_clip else 44100
345
- return AudioClip(lambda t: 0, duration=target_duration, sr=sr)
346
- except Exception as e:
347
- logger.error(f"Could not create silence clip: {e}", exc_info=True)
348
- return AudioFileClip(filename="")
349
-
350
  if audio_clip.duration >= target_duration:
351
- logger.debug("Audio clip already longer or equal to target. Trimming.")
352
- trimmed_clip = audio_clip.subclip(0, target_duration)
353
- if trimmed_clip.duration is None or trimmed_clip.duration <= 0:
354
- logger.error("Trimmed audio clip is invalid.")
355
- try: trimmed_clip.close()
356
- except: pass
357
- return AudioFileClip(filename="")
358
- return trimmed_clip
359
-
360
  loops = math.ceil(target_duration / audio_clip.duration)
361
  logger.debug(f"Creando {loops} loops de audio")
362
-
363
- audio_segments = [audio_clip] * loops
364
- looped_audio = None
365
- final_looped_audio = None
366
  try:
367
- looped_audio = concatenate_audioclips(audio_segments)
368
-
369
- if looped_audio.duration is None or looped_audio.duration <= 0:
370
- logger.error("Concatenated audio clip is invalid (None or zero duration).")
371
- raise ValueError("Invalid concatenated audio.")
372
-
373
- final_looped_audio = looped_audio.subclip(0, target_duration)
374
-
375
- if final_looped_audio.duration is None or final_looped_audio.duration <= 0:
376
- logger.error("Final subclipped audio clip is invalid (None or zero duration).")
377
- raise ValueError("Invalid final subclipped audio.")
378
-
379
- return final_looped_audio
380
-
381
  except Exception as e:
382
- logger.error(f"Error concatenating/subclipping audio clips for looping: {str(e)}", exc_info=True)
383
- try:
384
- if audio_clip.duration is not None and audio_clip.duration > 0:
385
- logger.warning("Returning original audio clip (may be too short).")
386
- return audio_clip.subclip(0, min(audio_clip.duration, target_duration))
387
- except:
388
- pass
389
- logger.error("Fallback to original audio clip failed.")
390
- return AudioFileClip(filename="")
391
-
392
- finally:
393
- if looped_audio is not None and looped_audio is not final_looped_audio:
394
- try: looped_audio.close()
395
- except: pass
396
-
397
 
398
  def extract_visual_keywords_from_script(script_text):
399
  logger.info("Extrayendo palabras clave del guion")
400
  if not script_text or not script_text.strip():
401
- logger.warning("Guion vacío, no se pueden extraer palabras clave.")
402
  return ["naturaleza", "ciudad", "paisaje"]
403
-
404
  clean_text = re.sub(r'[^\w\sáéíóúñÁÉÍÓÚÑ]', '', script_text)
405
- keywords_list = []
406
-
407
  if kw_model:
408
  try:
409
- logger.debug("Intentando extracción con KeyBERT...")
410
  keywords1 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(1, 1), stop_words='spanish', top_n=5)
411
  keywords2 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(2, 2), stop_words='spanish', top_n=3)
412
-
413
  all_keywords = keywords1 + keywords2
414
  all_keywords.sort(key=lambda item: item[1], reverse=True)
415
-
416
  seen_keywords = set()
417
- for keyword, score in all_keywords:
418
  formatted_keyword = keyword.lower().replace(" ", "+")
419
  if formatted_keyword and formatted_keyword not in seen_keywords:
420
  keywords_list.append(formatted_keyword)
421
  seen_keywords.add(formatted_keyword)
422
  if len(keywords_list) >= 5:
423
  break
424
-
425
  if keywords_list:
426
  logger.debug(f"Palabras clave extraídas por KeyBERT: {keywords_list}")
427
  return keywords_list
428
-
429
  except Exception as e:
430
- logger.warning(f"KeyBERT falló: {str(e)}. Intentando método simple.")
431
-
432
  logger.debug("Extrayendo palabras clave con método simple...")
433
  words = clean_text.lower().split()
434
- 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",
435
- "nuestro", "vuestro", "este", "ese", "aquel", "esta", "esa", "aquella", "esto", "eso", "aquello", "mis", "tus",
436
- "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á"}
437
-
438
  valid_words = [word for word in words if len(word) > 3 and word not in stop_words]
439
-
440
  if not valid_words:
441
- logger.warning("No se encontraron palabras clave válidas con método simple. Usando palabras clave predeterminadas.")
442
- return ["espiritual", "terror", "matrix", "arcontes", "galaxia", "creepy", "magia", "gangstalking","conspiracy",]
443
-
444
  word_counts = Counter(valid_words)
445
  top_keywords = [word.replace(" ", "+") for word, _ in word_counts.most_common(5)]
446
-
447
- if not top_keywords:
448
- logger.warning("El método simple no produjo keywords. Usando palabras clave predeterminadas.")
449
- return ["espiritual", "terror", "matrix", "arcontes", "galaxia", "creepy", "magia", "gangstalking","conspiracy",]
450
-
451
  logger.info(f"Palabras clave finales: {top_keywords}")
452
  return top_keywords
453
 
454
- # crear_video ahora recibe la voz seleccionada
455
  def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
456
  logger.info("="*80)
457
  logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
458
  logger.debug(f"Input: '{input_text[:100]}...'")
459
  logger.info(f"Voz seleccionada: {selected_voice}")
460
-
461
  start_time = datetime.now()
462
- temp_dir_intermediate = None
463
-
 
464
  audio_tts_original = None
465
  musica_audio_original = None
466
  audio_tts = None
@@ -472,403 +356,147 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
472
 
473
  try:
474
  # 1. Generar o usar guion
475
- if prompt_type == "Generar Guion con IA":
476
- guion = generate_script(input_text)
477
- else:
478
- guion = input_text.strip()
479
-
480
  logger.info(f"Guion final ({len(guion)} chars): '{guion[:100]}...'")
481
-
482
  if not guion.strip():
483
- logger.error("El guion resultante está vacío o solo contiene espacios.")
484
  raise ValueError("El guion está vacío.")
485
 
486
- temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
487
- logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
488
- temp_intermediate_files = []
489
-
490
- # 2. Generar audio de voz usando la voz seleccionada, con reintentos si falla
491
- logger.info("Generando audio de voz...")
492
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
493
-
494
- tts_voices_to_try = [selected_voice]
495
- fallback_juan = "es-ES-JuanNeural"
496
- fallback_elvira = "es-ES-ElviraNeural"
497
-
498
- if fallback_juan and fallback_juan != selected_voice and fallback_juan not in tts_voices_to_try:
499
- tts_voices_to_try.append(fallback_juan)
500
- if fallback_elvira and fallback_elvira != selected_voice and fallback_elvira not in tts_voices_to_try:
501
- tts_voices_to_try.append(fallback_elvira)
502
-
503
  tts_success = False
504
  tried_voices = set()
505
 
506
  for current_voice in tts_voices_to_try:
507
- if not current_voice or current_voice in tried_voices: continue
 
508
  tried_voices.add(current_voice)
509
-
510
  logger.info(f"Intentando TTS con voz: {current_voice}...")
511
- try:
512
- tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=current_voice))
513
- if tts_success:
514
- logger.info(f"TTS exitoso con voz '{current_voice}'.")
515
- break
516
- except Exception as e:
517
- logger.warning(f"Fallo al generar TTS con voz '{current_voice}': {str(e)}", exc_info=True)
518
- pass
519
 
520
  if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
521
- 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.")
522
- raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
523
 
524
  temp_intermediate_files.append(voz_path)
525
-
526
  audio_tts_original = AudioFileClip(voz_path)
527
-
528
- if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
529
- logger.critical("Clip de audio TTS inicial es inválido (reader is None o duración <= 0) *después* de crear AudioFileClip.")
530
- try: audio_tts_original.close()
531
- except: pass
532
- audio_tts_original = None
533
- if os.path.exists(voz_path):
534
- try: os.remove(voz_path)
535
- except: pass
536
- if voz_path in temp_intermediate_files:
537
- temp_intermediate_files.remove(voz_path)
538
-
539
- raise ValueError("Audio de voz generado es inválido después de procesamiento inicial.")
540
-
541
  audio_tts = audio_tts_original
542
  audio_duration = audio_tts_original.duration
543
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
544
-
545
  if audio_duration < 1.0:
546
- logger.error(f"Duración audio voz ({audio_duration:.2f}s) es muy corta.")
547
- raise ValueError("Generated voice audio is too short (min 1 second required).")
548
- # 3. Extraer palabras clave
549
- logger.info("Extrayendo palabras clave...")
550
- try:
551
- keywords = extract_visual_keywords_from_script(guion)
552
- logger.info(f"Palabras clave identificadas: {keywords}")
553
- except Exception as e:
554
- logger.error(f"Error extrayendo keywords: {str(e)}", exc_info=True)
555
- keywords = ["naturaleza", "paisaje"]
556
 
 
 
557
  if not keywords:
558
- keywords = ["video", "background"]
 
559
 
560
  # 4. Buscar y descargar videos
561
- logger.info("Buscando videos en Pexels...")
562
  videos_data = []
563
  total_desired_videos = 10
564
  per_page_per_keyword = max(1, total_desired_videos // len(keywords))
565
-
566
  for keyword in keywords:
567
- if len(videos_data) >= total_desired_videos: break
568
- try:
569
- videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=per_page_per_keyword)
570
- if videos:
571
- videos_data.extend(videos)
572
- logger.info(f"Encontrados {len(videos)} videos para '{keyword}'. Total data: {len(videos_data)}")
573
- except Exception as e:
574
- logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
575
 
576
  if len(videos_data) < total_desired_videos / 2:
577
- logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
578
- generic_keywords = ["mystery", "alien", "ufo", "conspiracy", "paranormal", "supernatural", "horror", "fear", "suspense", "secret", "government", "cover_up", "simulation", "matrix", "apocalypse", "dystopian", "shadow", "occult", "unexplained", "creepy", "extraterrestrial", "abduction", "experiment", "secret_society", "illuminati", "new_world_order", "ancient_aliens", "ufo_sighting", "cryptid", "bigfoot", "loch_ness", "ghost", "haunting", "spirit", "demon", "possession", "exorcism", "witchcraft", "ritual", "cursed", "urban_legend", "myth", "legend", "folklore", "scary", "terror", "panic", "anxiety", "dread", "nightmare", "dark", "gloomy", "fog", "haunted", "cemetery", "asylum", "abandoned", "ruins", "underground", "tunnel", "bunker", "lab", "experiment", "government_secret", "mind_control", "brainwash", "propaganda", "surveillance", "spy", "whistleblower", "leak", "anonymous", "hack", "cyber", "virtual_reality", "ai", "artificial_intelligence", "robot", "cyborg", "apocalyptic", "post_apocalyptic", "zombie", "outbreak", "pandemic", "contagion", "biohazard", "radiation", "nuclear", "doomsday", "armageddon", "revelation", "prophecy", "symbolism", "hidden_meaning", "enigma", "puzzle", "code", "cipher", "mysterious", "unidentified", "anomaly", "glitch", "time_travel", "parallel_universe", "dimension", "portal"]
579
  for keyword in generic_keywords:
580
- if len(videos_data) >= total_desired_videos: break
581
- try:
582
- videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=2)
583
- if videos:
584
- videos_data.extend(videos)
585
- logger.info(f"Encontrados {len(videos)} videos para '{keyword}' (genérico). Total data: {len(videos_data)}")
586
- except Exception as e:
587
- logger.warning(f"Error buscando videos genéricos para '{keyword}': {str(e)}")
588
 
589
  if not videos_data:
590
- logger.error("No se encontraron videos en Pexels para ninguna palabra clave.")
591
- raise ValueError("No se encontraron videos adecuados en Pexels.")
592
 
593
  video_paths = []
594
- logger.info(f"Intentando descargar {len(videos_data)} videos encontrados...")
595
  for video in videos_data:
596
  if 'video_files' not in video or not video['video_files']:
597
- logger.debug(f"Saltando video sin archivos de video: {video.get('id')}")
598
  continue
 
 
 
 
 
 
599
 
600
- try:
601
- best_quality = None
602
- for vf in sorted(video['video_files'], key=lambda x: x.get('width', 0) * x.get('height', 0), reverse=True):
603
- if 'link' in vf:
604
- best_quality = vf
605
- break
606
-
607
- if best_quality and 'link' in best_quality:
608
- path = download_video_file(best_quality['link'], temp_dir_intermediate)
609
- if path:
610
- video_paths.append(path)
611
- temp_intermediate_files.append(path)
612
- logger.info(f"Video descargado OK desde {best_quality['link'][:50]}...")
613
- else:
614
- logger.warning(f"No se pudo descargar video desde {best_quality['link'][:50]}...")
615
- else:
616
- logger.warning(f"No se encontró enlace de descarga válido para video {video.get('id')}.")
617
-
618
- except Exception as e:
619
- logger.warning(f"Error procesando/descargando video {video.get('id')}: {str(e)}")
620
-
621
- logger.info(f"Descargados {len(video_paths)} archivos de video utilizables.")
622
  if not video_paths:
623
- logger.error("No se pudo descargar ningún archivo de video utilizable.")
624
- raise ValueError("No se pudo descargar ningún video utilizable de Pexels.")
625
 
626
  # 5. Procesar y concatenar clips de video
627
- logger.info("Procesando y concatenando videos descargados...")
628
  current_duration = 0
629
  min_clip_duration = 0.5
630
  max_clip_segment = 10.0
631
-
632
  for i, path in enumerate(video_paths):
633
  if current_duration >= audio_duration + max_clip_segment:
634
- 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.")
635
  break
636
-
637
- clip = None
638
  try:
639
- logger.debug(f"[{i+1}/{len(video_paths)}] Abriendo clip: {path}")
640
  clip = VideoFileClip(path)
641
  source_clips.append(clip)
642
-
643
- if clip.reader is None or clip.duration is None or clip.duration <= 0:
644
- logger.warning(f"[{i+1}/{len(video_paths)}] Clip fuente {path} parece inválido (reader is None o duración <= 0). Saltando.")
645
  continue
646
-
647
  remaining_needed = audio_duration - current_duration
648
- potential_use_duration = min(clip.duration, max_clip_segment)
649
-
650
- if remaining_needed > 0:
651
- segment_duration = min(potential_use_duration, remaining_needed + min_clip_duration)
652
- segment_duration = max(min_clip_duration, segment_duration)
653
- segment_duration = min(segment_duration, clip.duration)
654
-
655
- if segment_duration >= min_clip_duration:
656
- try:
657
- sub = clip.subclip(0, segment_duration)
658
- if sub.reader is None or sub.duration is None or sub.duration <= 0:
659
- logger.warning(f"[{i+1}/{len(video_paths)}] Subclip generado de {path} es inválido. Saltando.")
660
- try: sub.close()
661
- except: pass
662
- continue
663
-
664
- clips_to_concatenate.append(sub)
665
- current_duration += sub.duration
666
- logger.debug(f"[{i+1}/{len(video_paths)}] Segmento añadido: {sub.duration:.1f}s (total video: {current_duration:.1f}/{audio_duration:.1f}s)")
667
-
668
- except Exception as sub_e:
669
- logger.warning(f"[{i+1}/{len(video_paths)}] Error creando subclip de {path} ({segment_duration:.1f}s): {str(sub_e)}")
670
- continue
671
- else:
672
- 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.")
673
- else:
674
- logger.debug(f"[{i+1}/{len(video_paths)}] Duración de video base ya alcanzada. Saltando clip.")
675
-
676
  except Exception as e:
677
- logger.warning(f"[{i+1}/{len(video_paths)}] Error procesando video {path}: {str(e)}", exc_info=True)
678
- continue
679
-
680
- logger.info(f"Procesamiento de clips fuente finalizado. Se obtuvieron {len(clips_to_concatenate)} segmentos válidos.")
681
 
682
  if not clips_to_concatenate:
683
- logger.error("No hay segmentos de video válidos disponibles para crear la secuencia.")
684
- raise ValueError("No hay segmentos de video válidos disponibles para crear el video.")
685
 
686
- logger.info(f"Concatenando {len(clips_to_concatenate)} segmentos de video.")
687
- concatenated_base = None
688
- try:
689
- concatenated_base = concatenate_videoclips(clips_to_concatenate, method="chain")
690
- logger.info(f"Duración video base después de concatenación inicial: {concatenated_base.duration:.2f}s")
691
-
692
- if concatenated_base is None or concatenated_base.duration is None or concatenated_base.duration <= 0:
693
- logger.critical("Video base concatenado es inválido después de la primera concatenación (None o duración cero).")
694
- raise ValueError("Fallo al crear video base válido a partir de segmentos.")
695
-
696
- except Exception as e:
697
- logger.critical(f"Error durante la concatenación inicial: {str(e)}", exc_info=True)
698
- raise ValueError("Fallo durante la concatenación de video inicial.")
699
- finally:
700
- for clip_segment in clips_to_concatenate:
701
- try: clip_segment.close()
702
- except: pass
703
- clips_to_concatenate = []
704
-
705
- video_base = concatenated_base
706
-
707
- final_video_base = video_base
708
 
709
- if final_video_base.duration < audio_duration:
710
- logger.info(f"Video base ({final_video_base.duration:.2f}s) es más corto que el audio ({audio_duration:.2f}s). Repitiendo...")
711
-
712
- num_full_repeats = int(audio_duration // final_video_base.duration)
713
- remaining_duration = audio_duration % final_video_base.duration
714
-
715
- repeated_clips_list = [final_video_base] * num_full_repeats
716
  if remaining_duration > 0:
717
- try:
718
- remaining_clip = final_video_base.subclip(0, remaining_duration)
719
- if remaining_clip is None or remaining_clip.duration is None or remaining_clip.duration <= 0:
720
- logger.warning(f"Subclip generado para duración restante {remaining_duration:.2f}s es inválido. Saltando.")
721
- try: remaining_clip.close()
722
- except: pass
723
- else:
724
- repeated_clips_list.append(remaining_clip)
725
- logger.debug(f"Añadiendo segmento restante: {remaining_duration:.2f}s")
726
-
727
- except Exception as e:
728
- logger.warning(f"Error creando subclip para duración restante {remaining_duration:.2f}s: {str(e)}")
729
-
730
- if repeated_clips_list:
731
- logger.info(f"Concatenando {len(repeated_clips_list)} partes para repetición.")
732
- video_base_repeated = None
733
- try:
734
- video_base_repeated = concatenate_videoclips(repeated_clips_list, method="chain")
735
- logger.info(f"Duración del video base repetido: {video_base_repeated.duration:.2f}s")
736
-
737
- if video_base_repeated is None or video_base_repeated.duration is None or video_base_repeated.duration <= 0:
738
- logger.critical("Video base repetido concatenado es inválido.")
739
- raise ValueError("Fallo al crear video base repetido válido.")
740
-
741
- if final_video_base is not video_base_repeated:
742
- try: final_video_base.close()
743
- except: pass
744
-
745
- final_video_base = video_base_repeated
746
-
747
- except Exception as e:
748
- logger.critical(f"Error durante la concatenación de repetición: {str(e)}", exc_info=True)
749
- raise ValueError("Fallo durante la repetición de video.")
750
- finally:
751
- if 'repeated_clips_list' in locals():
752
- for clip in repeated_clips_list:
753
- if clip is not final_video_base:
754
- try: clip.close()
755
- except: pass
756
-
757
-
758
- if final_video_base.duration > audio_duration:
759
- 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).")
760
- trimmed_video_base = None
761
- try:
762
- trimmed_video_base = final_video_base.subclip(0, audio_duration)
763
- if trimmed_video_base is None or trimmed_video_base.duration is None or trimmed_video_base.duration <= 0:
764
- logger.critical("Video base recortado es inválido.")
765
- raise ValueError("Fallo al crear video base recortado válido.")
766
-
767
- if final_video_base is not trimmed_video_base:
768
- try: final_video_base.close()
769
- except: pass
770
-
771
- final_video_base = trimmed_video_base
772
-
773
- except Exception as e:
774
- logger.critical(f"Error durante el recorte: {str(e)}", exc_info=True)
775
- raise ValueError("Fallo durante el recorte de video.")
776
-
777
-
778
- if final_video_base is None or final_video_base.duration is None or final_video_base.duration <= 0:
779
- logger.critical("Video base final es inválido antes de audio/escritura (None o duración cero).")
780
- raise ValueError("Video base final es inválido.")
781
-
782
- if final_video_base.size is None or final_video_base.size[0] <= 0 or final_video_base.size[1] <= 0:
783
- logger.critical(f"Video base final tiene tamaño inválido: {final_video_base.size}. No se puede escribir video.")
784
- raise ValueError("Video base final tiene tamaño inválido antes de escribir.")
785
-
786
- video_base = final_video_base
787
 
788
  # 6. Manejar música de fondo
789
- logger.info("Procesando audio...")
790
-
791
- final_audio = audio_tts_original
792
-
793
- musica_audio_looped = None
794
-
795
  if musica_file:
796
- musica_audio_original = None
797
  try:
798
  music_path = os.path.join(temp_dir_intermediate, "musica_bg.mp3")
799
  shutil.copyfile(musica_file, music_path)
800
  temp_intermediate_files.append(music_path)
801
- logger.info(f"Música de fondo copiada a: {music_path}")
802
-
803
  musica_audio_original = AudioFileClip(music_path)
804
-
805
- if musica_audio_original.reader is None or musica_audio_original.duration is None or musica_audio_original.duration <= 0:
806
- logger.warning("Clip de música de fondo parece inválido o tiene duración cero. Saltando música.")
807
- try: musica_audio_original.close()
808
- except: pass
809
- musica_audio_original = None
810
- else:
811
- musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
812
- logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
813
-
814
- if musica_audio_looped is None or musica_audio_looped.duration is None or musica_audio_looped.duration <= 0:
815
- logger.warning("Clip de música de fondo loopeado es inválido. Saltando música.")
816
- try: musica_audio_looped.close()
817
- except: pass
818
- musica_audio_looped = None
819
-
820
-
821
- if musica_audio_looped:
822
- composite_audio = CompositeAudioClip([
823
- musica_audio_looped.volumex(0.2), # Volumen 20% para música
824
- audio_tts_original.volumex(1.0) # Volumen 100% para voz
825
  ])
826
-
827
- if composite_audio.duration is None or composite_audio.duration <= 0:
828
- logger.warning("Clip de audio compuesto es inválido (None o duración cero). Usando solo audio de voz.")
829
- try: composite_audio.close()
830
- except: pass
831
- final_audio = audio_tts_original
832
- else:
833
- logger.info("Mezcla de audio completada (voz + música).")
834
- final_audio = composite_audio
835
- musica_audio = musica_audio_looped # Asignar para limpieza
836
-
837
- except Exception as e:
838
- logger.warning(f"Error procesando música de fondo: {str(e)}", exc_info=True)
839
- final_audio = audio_tts_original
840
- musica_audio = None
841
- logger.warning("Usando solo audio de voz debido a un error con la música.")
842
-
843
-
844
- if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
845
- 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.")
846
- try:
847
- if final_audio.duration > video_base.duration:
848
- trimmed_final_audio = final_audio.subclip(0, video_base.duration)
849
- if trimmed_final_audio is None or trimmed_final_audio.duration <= 0:
850
- logger.warning("Audio final recortado es inválido. Usando audio final original.")
851
- try: trimmed_final_audio.close()
852
- except: pass
853
- else:
854
- if final_audio is not trimmed_final_audio:
855
- try: final_audio.close()
856
- except: pass
857
- final_audio = trimmed_final_audio
858
- logger.warning("Audio final recortado para que coincida con la duración del video.")
859
  except Exception as e:
860
- logger.warning(f"Error ajustando duración del audio final: {str(e)}")
 
861
 
862
- try:
863
- # ... (todo el código previo de crear_video permanece igual)
864
 
865
- # 7. Crear video final (INDENTACIÓN ORIGINAL)
866
- # Definir variables aquí para que estén disponibles en finally
867
- output_filename = f"video_{int(time.time())}.mp4" # Nombre único con timestamp
868
  output_path = os.path.join(temp_dir_intermediate, output_filename)
869
  permanent_path = f"/tmp/{output_filename}"
870
-
871
- # Escribir el video
872
  video_final.write_videofile(
873
  output_path,
874
  fps=24,
@@ -876,172 +504,95 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
876
  codec="libx264",
877
  audio_codec="aac",
878
  preset="medium",
879
- ffmpeg_params=[
880
- '-vf', 'scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:-1:-1:color=black',
881
- '-crf', '23'
882
- ],
883
  logger='bar'
884
  )
885
 
886
- # Mover a ubicación permanente en /tmp
887
- try:
888
- shutil.copy(output_path, permanent_path) # Usamos copy() en lugar de move()
889
- logger.info(f"Video guardado permanentemente en: {permanent_path}")
890
- except Exception as move_error:
891
- logger.error(f"Error moviendo archivo: {str(move_error)}. Usando path original.")
892
- permanent_path = output_path
893
-
894
- # Cierra los clips para liberar memoria
895
- try:
896
- video_final.close()
897
- if 'video_base' in locals() and video_base is not None and video_base is not video_final:
898
- video_base.close()
899
- except Exception as close_error:
900
- logger.error(f"Error cerrando clips: {str(close_error)}")
901
-
902
  total_time = (datetime.now() - start_time).total_seconds()
903
- logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {permanent_path} | Tiempo total: {total_time:.2f}s")
904
-
905
  return permanent_path
906
 
907
  except ValueError as ve:
908
- logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
909
- raise ve
910
  except Exception as e:
911
- logger.critical(f"ERROR CRÍTICO NO CONTROLADO en crear_video: {str(e)}", exc_info=True)
912
- raise e
913
  finally:
914
- logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
915
-
916
- for clip in source_clips:
917
  try:
918
  clip.close()
919
- except Exception as e:
920
- logger.warning(f"Error cerrando clip de video fuente en finally: {str(e)}")
921
-
922
- for clip_segment in clips_to_concatenate:
923
- try:
924
- clip_segment.close()
925
- except Exception as e:
926
- logger.warning(f"Error cerrando segmento de video en finally: {str(e)}")
927
-
928
- if musica_audio is not None:
929
  try:
930
  musica_audio.close()
931
- except Exception as e:
932
- logger.warning(f"Error cerrando musica_audio (procesada) en finally: {str(e)}")
933
-
934
- if musica_audio_original is not None and musica_audio_original is not musica_audio:
935
- try:
936
- musica_audio_original.close()
937
- except Exception as e:
938
- logger.warning(f"Error cerrando musica_audio_original en finally: {str(e)}")
939
-
940
- if audio_tts is not None and audio_tts is not audio_tts_original:
941
- try:
942
- audio_tts.close()
943
- except Exception as e:
944
- logger.warning(f"Error cerrando audio_tts (procesada) en finally: {str(e)}")
945
-
946
- if audio_tts_original is not None:
947
- try:
948
- audio_tts_original.close()
949
- except Exception as e:
950
- logger.warning(f"Error cerrando audio_tts_original en finally: {str(e)}")
951
-
952
- if video_final is not None:
953
  try:
954
  video_final.close()
955
- except Exception as e:
956
- logger.warning(f"Error cerrando video_final en finally: {str(e)}")
957
- elif video_base is not None and video_base is not video_final:
958
- try:
959
- video_base.close()
960
- except Exception as e:
961
- logger.warning(f"Error cerrando video_base en finally: {str(e)}")
962
-
963
- if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
964
- final_output_in_temp = os.path.join(temp_dir_intermediate, output_filename)
965
-
966
- for path in temp_intermediate_files:
967
- try:
968
- if os.path.isfile(path) and path != final_output_in_temp and path != permanent_path:
969
- logger.debug(f"Eliminando archivo temporal intermedio: {path}")
970
- os.remove(path)
971
- elif os.path.isfile(path) and (path == final_output_in_temp or path == permanent_path):
972
- logger.debug(f"Saltando eliminación del archivo de video final: {path}")
973
- except Exception as e:
974
- logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
975
-
976
- logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
977
 
978
- # run_app ahora recibe todos los inputs, incluyendo la voz seleccionada
979
- def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice): # <-- Recibe el valor del Dropdown
980
  logger.info("="*80)
981
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
982
-
983
- # Elegir el texto de entrada basado en el prompt_type
984
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
985
-
986
  output_video = None
987
  output_file = None
988
- status_msg = gr.update(value="⏳ Procesando...", interactive=False)
989
 
990
  if not input_text or not input_text.strip():
991
  logger.warning("Texto de entrada vacío.")
992
- # Retornar None para video y archivo, actualizar estado con mensaje de error
993
- return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
994
 
995
- # Validar la voz seleccionada. Si no es válida, usar la por defecto.
996
- # AVAILABLE_VOICES se obtiene al inicio. Hay que buscar si el voice_id existe en la lista de pares (nombre, id)
997
  voice_ids_disponibles = [v[1] for v in AVAILABLE_VOICES]
998
  if selected_voice not in voice_ids_disponibles:
999
- logger.warning(f"Voz seleccionada inválida o no encontrada en la lista: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE_ID}.")
1000
- selected_voice = DEFAULT_VOICE_ID # <-- Usar el ID de la voz por defecto
1001
- else:
1002
- logger.info(f"Voz seleccionada validada: {selected_voice}")
1003
-
1004
-
1005
- logger.info(f"Tipo de entrada: {prompt_type}")
1006
- logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
1007
- if musica_file:
1008
- logger.info(f"Archivo de música recibido: {musica_file}")
1009
- else:
1010
- logger.info("No se proporcionó archivo de música.")
1011
- logger.info(f"Voz final a usar (ID): {selected_voice}") # Loguear el ID de la voz final
1012
 
1013
  try:
1014
- logger.info("Llamando a crear_video...")
1015
- # Pasar el input_text elegido, la voz seleccionada (el ID) y el archivo de música a crear_video
1016
- video_path = crear_video(prompt_type, input_text, selected_voice, musica_file) # <-- PASAR selected_voice (ID) a crear_video
1017
-
1018
  if video_path and os.path.exists(video_path):
1019
- logger.info(f"crear_video retornó path: {video_path}")
1020
- logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
1021
- output_video = video_path # Establecer valor del componente de video
1022
- output_file = video_path # Establecer valor del componente de archivo para descarga
1023
- status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
1024
  else:
1025
- logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
1026
- status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
1027
-
1028
  except ValueError as ve:
1029
- logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
1030
- status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
1031
  except Exception as e:
1032
- logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
1033
- status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
1034
- finally:
1035
- logger.info("Fin del handler run_app.")
1036
- return output_video, output_file, status_msg
1037
-
1038
 
1039
  # Interfaz de Gradio
1040
- with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
1041
- .gradio-container {max-width: 800px; margin: auto;}
1042
- h1 {text-align: center;}
1043
- """) as app:
1044
-
1045
  gr.Markdown("# 🎬 Generador Automático de Videos con IA")
1046
  gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
1047
 
@@ -1052,44 +603,32 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1052
  label="Método de Entrada",
1053
  value="Generar Guion con IA"
1054
  )
1055
-
1056
- # Contenedores para los campos de texto para controlar la visibilidad
1057
  with gr.Column(visible=True) as ia_guion_column:
1058
  prompt_ia = gr.Textbox(
1059
  label="Tema para IA",
1060
  lines=2,
1061
- placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer, mostrando la belleza de la naturaleza...",
1062
- max_lines=4,
1063
- value=""
1064
  )
1065
-
1066
  with gr.Column(visible=False) as manual_guion_column:
1067
  prompt_manual = gr.Textbox(
1068
  label="Tu Guion Completo",
1069
  lines=5,
1070
- 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!",
1071
- max_lines=10,
1072
- value=""
1073
  )
1074
-
1075
  musica_input = gr.Audio(
1076
  label="Música de fondo (opcional)",
1077
  type="filepath",
1078
- interactive=True,
1079
- value=None
1080
  )
1081
-
1082
- # --- COMPONENTE: Selección de Voz ---
1083
  voice_dropdown = gr.Dropdown(
1084
  label="Seleccionar Voz para Guion",
1085
  choices=AVAILABLE_VOICES,
1086
  value=DEFAULT_VOICE_ID,
1087
  interactive=True
1088
  )
1089
- # --- FIN COMPONENTE ---
1090
-
1091
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
1092
-
1093
  with gr.Column():
1094
  video_output = gr.Video(
1095
  label="Previsualización del Video Generado",
@@ -1104,24 +643,19 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1104
  status_output = gr.Textbox(
1105
  label="Estado",
1106
  interactive=False,
1107
- show_label=False,
1108
  placeholder="Esperando acción...",
1109
  value="Esperando entrada..."
1110
  )
1111
 
1112
- # Evento para mostrar/ocultar los campos de texto según el tipo de prompt
1113
  prompt_type.change(
1114
- lambda x: (gr.update(visible=x == "Generar Guion con IA"),
1115
- gr.update(visible=x == "Usar Mi Guion")),
1116
  inputs=prompt_type,
1117
  outputs=[ia_guion_column, manual_guion_column]
1118
  )
1119
 
1120
- # Evento click del botón de generar video
1121
  generate_btn.click(
1122
- lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
1123
- outputs=[video_output, file_output, status_output],
1124
- queue=True,
1125
  ).then(
1126
  run_app,
1127
  inputs=[prompt_type, prompt_ia, prompt_manual, musica_input, voice_dropdown],
@@ -1134,37 +668,29 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1134
 
1135
  gr.Markdown("### Instrucciones:")
1136
  gr.Markdown("""
1137
- 1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
1138
- 2. **Selecciona el tipo de entrada**: "Generar Guion con IA" o "Usar Mi Guion".
1139
- 3. **Sube música** (opcional): Selecciona un archio de audio (MP3, WAV, etc.).
1140
- 4. **Selecciona la voz** deseada del desplegable.
1141
- 5. **Haz clic en "✨ Generar Video"**.
1142
- 6. Espera a que se procese el video. Verás el estado.
1143
- 7. La previsualización aparecerá si es posible, y siempre un enlace **Descargar Archivo de Video** se mostrará si la generación fue exitosa.
1144
- 8. Revisa `video_generator_full.log` para detalles si hay errores.
1145
  """)
1146
- gr.Markdown("---")
1147
- gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
1148
 
1149
  if __name__ == "__main__":
1150
- logger.info("Verificando dependencias críticas...")
1151
  try:
1152
  from moviepy.editor import ColorClip
1153
- try:
1154
- temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
1155
- temp_clip.close()
1156
- logger.info("Clips base de MoviePy creados y cerrados exitosamente. FFmpeg parece accesible.")
1157
- except Exception as e:
1158
- logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
1159
  except Exception as e:
1160
- logger.critical(f"Fallo al importar MoviePy. Asegúrate de que está instalado. Error: {e}", exc_info=True)
1161
-
1162
- # Solución para el timeout de Gradio
1163
- os.environ['GRADIO_SERVER_TIMEOUT'] = '6000' # 600 segundos = 10 minutos
1164
-
1165
  logger.info("Iniciando aplicación Gradio...")
1166
  try:
1167
  app.launch(server_name="0.0.0.0", server_port=7860, share=False)
1168
  except Exception as e:
1169
- logger.critical(f"No se pudo iniciar la app: {str(e)}", exc_info=True)
1170
  raise
 
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
 
31
  logger.info("="*80)
32
 
33
  # Diccionario de voces TTS disponibles organizadas por idioma
 
34
  VOCES_DISPONIBLES = {
35
  "Español (España)": {
36
  "es-ES-JuanNeural": "Juan (España) - Masculino",
 
98
  choices = []
99
  for region, voices in VOCES_DISPONIBLES.items():
100
  for voice_id, voice_name in voices.items():
 
101
  choices.append((f"{voice_name} ({region})", voice_id))
102
  return choices
103
 
104
  # Obtener las voces al inicio del script
105
+ AVAILABLE_VOICES = get_voice_choices()
106
+ DEFAULT_VOICE_ID = "es-ES-JuanNeural"
 
 
 
 
 
 
107
  DEFAULT_VOICE_NAME = DEFAULT_VOICE_ID
108
  for text, voice_id in AVAILABLE_VOICES:
109
  if voice_id == DEFAULT_VOICE_ID:
110
  DEFAULT_VOICE_NAME = text
111
  break
 
112
  if DEFAULT_VOICE_ID not in [v[1] for v in AVAILABLE_VOICES]:
113
  DEFAULT_VOICE_ID = AVAILABLE_VOICES[0][1] if AVAILABLE_VOICES else "en-US-AriaNeural"
114
+ DEFAULT_VOICE_NAME = AVAILABLE_VOICES[0][0] if AVAILABLE_VOICES else "Aria (United States) - Female"
 
115
  logger.info(f"Voz por defecto seleccionada (ID): {DEFAULT_VOICE_ID}")
116
 
 
117
  # Clave API de Pexels
118
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
119
  if not PEXELS_API_KEY:
120
  logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
 
121
 
122
  # Inicialización de modelos
123
  MODEL_NAME = "datificate/gpt2-small-spanish"
 
157
  "orientation": "landscape",
158
  "size": "medium"
159
  }
 
160
  response = requests.get(
161
  "https://api.pexels.com/videos/search",
162
  headers=headers,
 
164
  timeout=20
165
  )
166
  response.raise_for_status()
 
167
  data = response.json()
168
  videos = data.get('videos', [])
169
  logger.info(f"Pexels: {len(videos)} videos encontrados para '{query}'")
170
  return videos
 
171
  except requests.exceptions.RequestException as e:
172
  logger.error(f"Error de conexión Pexels para '{query}': {str(e)}")
173
+ return []
174
  except json.JSONDecodeError:
175
+ logger.error(f"Pexels: JSON inválido recibido | Status: {response.status_code}")
176
+ return []
177
  except Exception as e:
178
+ logger.error(f"Error inesperado Pexels para '{query}': {str(e)}")
179
+ return []
 
180
 
181
  def generate_script(prompt, max_length=150):
182
  logger.info(f"Generando guión | Prompt: '{prompt[:50]}...' | Longitud máxima: {max_length}")
 
192
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
193
  model.to(device)
194
  inputs = {k: v.to(device) for k, v in inputs.items()}
 
195
  outputs = model.generate(
196
  **inputs,
197
  max_length=max_length + inputs[list(inputs.keys())[0]].size(1),
 
204
  eos_token_id=tokenizer.eos_token_id,
205
  no_repeat_ngram_size=3
206
  )
 
207
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
208
+ prompt_in_output_idx = text.lower().find(prompt.lower())
209
+ if prompt_in_output_idx != -1:
210
+ cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
211
+ logger.debug("Texto limpiado tomando parte después del prompt original.")
212
+ else:
213
+ instruction_start_idx = text.find(instruction_phrase_start)
214
+ if instruction_start_idx != -1:
215
+ cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
216
+ logger.debug("Texto limpiado tomando parte después de la frase de instrucción base.")
 
217
  else:
218
+ logger.warning("No se pudo identificar el inicio del guión generado.")
219
+ cleaned_text = text.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
221
+ cleaned_text = cleaned_text.lstrip(':').lstrip('.').strip()
 
 
 
 
222
  sentences = cleaned_text.split('.')
223
  if sentences and sentences[0].strip():
224
  final_text = sentences[0].strip() + '.'
225
+ if len(sentences) > 1 and sentences[1].strip() and len(final_text.split()) < max_length * 0.7:
226
+ final_text += " " + sentences[1].strip() + "."
227
+ final_text = final_text.replace("..", ".")
 
 
228
  logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
229
  return final_text.strip()
230
+ logger.info(f"Guion generado final (sin oraciones completas detectadas): '{cleaned_text[:100]}...'")
231
+ return cleaned_text.strip()
 
 
232
  except Exception as e:
233
+ logger.error(f"Error generando guion con GPT-2: {str(e)}")
 
234
  return prompt.strip()
235
 
 
236
  async def text_to_speech(text, output_path, voice):
237
+ logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice}")
238
  if not text or not text.strip():
239
  logger.warning("Texto vacío para TTS")
240
  return False
 
241
  try:
242
  communicate = edge_tts.Communicate(text, voice)
243
  await communicate.save(output_path)
 
244
  if os.path.exists(output_path) and os.path.getsize(output_path) > 100:
245
+ logger.info(f"Audio guardado exitosamente en: {output_path}")
246
  return True
247
+ logger.error(f"TTS guardó un archivo pequeño o vacío en: {output_path}")
248
+ return False
 
 
249
  except Exception as e:
250
+ logger.error(f"Error en TTS con voz '{voice}': {str(e)}")
251
  return False
252
 
253
  def download_video_file(url, temp_dir):
254
  if not url:
255
+ logger.warning("URL de video no proporcionada")
256
  return None
 
257
  try:
258
  logger.info(f"Descargando video desde: {url[:80]}...")
259
  os.makedirs(temp_dir, exist_ok=True)
260
  file_name = f"video_dl_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.mp4"
261
  output_path = os.path.join(temp_dir, file_name)
 
262
  with requests.get(url, stream=True, timeout=60) as r:
263
  r.raise_for_status()
264
  with open(output_path, 'wb') as f:
265
  for chunk in r.iter_content(chunk_size=8192):
266
  f.write(chunk)
 
267
  if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
268
+ logger.info(f"Video descargado exitosamente: {output_path}")
269
+ return output_path
270
+ logger.warning(f"Descarga parece incompleta o vacía: {output_path}")
271
+ if os.path.exists(output_path):
272
+ os.remove(output_path)
273
+ return None
 
 
274
  except requests.exceptions.RequestException as e:
275
+ logger.error(f"Error de descarga para {url[:80]}...: {str(e)}")
276
+ return None
277
  except Exception as e:
278
+ logger.error(f"Error inesperado descargando {url[:80]}...: {str(e)}")
279
+ return None
 
280
 
281
  def loop_audio_to_length(audio_clip, target_duration):
282
  logger.debug(f"Ajustando audio | Duración actual: {audio_clip.duration:.2f}s | Objetivo: {target_duration:.2f}s")
 
283
  if audio_clip is None or audio_clip.duration is None or audio_clip.duration <= 0:
284
+ logger.warning("Input audio clip is invalid")
285
+ sr = getattr(audio_clip, 'fps', 44100) if audio_clip else 44100
286
+ return AudioClip(lambda t: 0, duration=target_duration, fps=sr)
 
 
 
 
 
287
  if audio_clip.duration >= target_duration:
288
+ logger.debug("Audio clip ya es suficientemente largo. Recortando.")
289
+ return audio_clip.subclip(0, target_duration)
 
 
 
 
 
 
 
290
  loops = math.ceil(target_duration / audio_clip.duration)
291
  logger.debug(f"Creando {loops} loops de audio")
 
 
 
 
292
  try:
293
+ looped_audio = concatenate_audioclips([audio_clip] * loops)
294
+ final_looped_audio = looped_audio.subclip(0, target_duration)
295
+ looped_audio.close()
296
+ return final_looped_audio
 
 
 
 
 
 
 
 
 
 
297
  except Exception as e:
298
+ logger.error(f"Error concatenando audio: {str(e)}")
299
+ return audio_clip.subclip(0, min(audio_clip.duration, target_duration))
 
 
 
 
 
 
 
 
 
 
 
 
 
300
 
301
  def extract_visual_keywords_from_script(script_text):
302
  logger.info("Extrayendo palabras clave del guion")
303
  if not script_text or not script_text.strip():
304
+ logger.warning("Guion vacío")
305
  return ["naturaleza", "ciudad", "paisaje"]
 
306
  clean_text = re.sub(r'[^\w\sáéíóúñÁÉÍÓÚÑ]', '', script_text)
 
 
307
  if kw_model:
308
  try:
 
309
  keywords1 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(1, 1), stop_words='spanish', top_n=5)
310
  keywords2 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(2, 2), stop_words='spanish', top_n=3)
 
311
  all_keywords = keywords1 + keywords2
312
  all_keywords.sort(key=lambda item: item[1], reverse=True)
313
+ keywords_list = []
314
  seen_keywords = set()
315
+ for keyword, _ in all_keywords:
316
  formatted_keyword = keyword.lower().replace(" ", "+")
317
  if formatted_keyword and formatted_keyword not in seen_keywords:
318
  keywords_list.append(formatted_keyword)
319
  seen_keywords.add(formatted_keyword)
320
  if len(keywords_list) >= 5:
321
  break
 
322
  if keywords_list:
323
  logger.debug(f"Palabras clave extraídas por KeyBERT: {keywords_list}")
324
  return keywords_list
 
325
  except Exception as e:
326
+ logger.warning(f"KeyBERT falló: {str(e)}. Usando método simple.")
 
327
  logger.debug("Extrayendo palabras clave con método simple...")
328
  words = clean_text.lower().split()
329
+ 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"}
 
 
 
330
  valid_words = [word for word in words if len(word) > 3 and word not in stop_words]
 
331
  if not valid_words:
332
+ logger.warning("No se encontraron palabras clave válidas.")
333
+ return ["espiritual", "terror", "matrix", "arcontes", "galaxia"]
 
334
  word_counts = Counter(valid_words)
335
  top_keywords = [word.replace(" ", "+") for word, _ in word_counts.most_common(5)]
 
 
 
 
 
336
  logger.info(f"Palabras clave finales: {top_keywords}")
337
  return top_keywords
338
 
 
339
  def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
340
  logger.info("="*80)
341
  logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
342
  logger.debug(f"Input: '{input_text[:100]}...'")
343
  logger.info(f"Voz seleccionada: {selected_voice}")
 
344
  start_time = datetime.now()
345
+ temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
346
+ logger.info(f"Directorio temporal creado: {temp_dir_intermediate}")
347
+ temp_intermediate_files = []
348
  audio_tts_original = None
349
  musica_audio_original = None
350
  audio_tts = None
 
356
 
357
  try:
358
  # 1. Generar o usar guion
359
+ guion = generate_script(input_text) if prompt_type == "Generar Guion con IA" else input_text.strip()
 
 
 
 
360
  logger.info(f"Guion final ({len(guion)} chars): '{guion[:100]}...'")
 
361
  if not guion.strip():
 
362
  raise ValueError("El guion está vacío.")
363
 
364
+ # 2. Generar audio de voz
 
 
 
 
 
365
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
366
+ tts_voices_to_try = [selected_voice, "es-ES-JuanNeural", "es-ES-ElviraNeural"]
 
 
 
 
 
 
 
 
 
367
  tts_success = False
368
  tried_voices = set()
369
 
370
  for current_voice in tts_voices_to_try:
371
+ if current_voice in tried_voices or not current_voice:
372
+ continue
373
  tried_voices.add(current_voice)
 
374
  logger.info(f"Intentando TTS con voz: {current_voice}...")
375
+ tts_success = asyncio.run(text_to_speech(guion, voz_path, current_voice))
376
+ if tts_success:
377
+ break
 
 
 
 
 
378
 
379
  if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
380
+ raise ValueError("Error generando voz a partir del guion.")
 
381
 
382
  temp_intermediate_files.append(voz_path)
 
383
  audio_tts_original = AudioFileClip(voz_path)
384
+ if audio_tts_original.duration is None or audio_tts_original.duration <= 0:
385
+ raise ValueError("Audio de voz generado es inválido.")
 
 
 
 
 
 
 
 
 
 
 
 
386
  audio_tts = audio_tts_original
387
  audio_duration = audio_tts_original.duration
388
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
 
389
  if audio_duration < 1.0:
390
+ raise ValueError("Audio de voz demasiado corto.")
 
 
 
 
 
 
 
 
 
391
 
392
+ # 3. Extraer palabras clave
393
+ keywords = extract_visual_keywords_from_script(guion)
394
  if not keywords:
395
+ keywords = ["video", "background"]
396
+ logger.info(f"Palabras clave: {keywords}")
397
 
398
  # 4. Buscar y descargar videos
 
399
  videos_data = []
400
  total_desired_videos = 10
401
  per_page_per_keyword = max(1, total_desired_videos // len(keywords))
 
402
  for keyword in keywords:
403
+ if len(videos_data) >= total_desired_videos:
404
+ break
405
+ videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=per_page_per_keyword)
406
+ videos_data.extend(videos)
 
 
 
 
407
 
408
  if len(videos_data) < total_desired_videos / 2:
409
+ generic_keywords = ["mystery", "alien", "ufo", "conspiracy", "paranormal"]
 
410
  for keyword in generic_keywords:
411
+ if len(videos_data) >= total_desired_videos:
412
+ break
413
+ videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=2)
414
+ videos_data.extend(videos)
 
 
 
 
415
 
416
  if not videos_data:
417
+ raise ValueError("No se encontraron videos en Pexels.")
 
418
 
419
  video_paths = []
 
420
  for video in videos_data:
421
  if 'video_files' not in video or not video['video_files']:
 
422
  continue
423
+ best_quality = max(video['video_files'], key=lambda x: x.get('width', 0) * x.get('height', 0), default=None)
424
+ if best_quality and 'link' in best_quality:
425
+ path = download_video_file(best_quality['link'], temp_dir_intermediate)
426
+ if path:
427
+ video_paths.append(path)
428
+ temp_intermediate_files.append(path)
429
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  if not video_paths:
431
+ raise ValueError("No se descargaron videos utilizables.")
 
432
 
433
  # 5. Procesar y concatenar clips de video
 
434
  current_duration = 0
435
  min_clip_duration = 0.5
436
  max_clip_segment = 10.0
 
437
  for i, path in enumerate(video_paths):
438
  if current_duration >= audio_duration + max_clip_segment:
 
439
  break
 
 
440
  try:
 
441
  clip = VideoFileClip(path)
442
  source_clips.append(clip)
443
+ if clip.duration is None or clip.duration <= 0:
 
 
444
  continue
 
445
  remaining_needed = audio_duration - current_duration
446
+ segment_duration = min(clip.duration, max_clip_segment, remaining_needed + min_clip_duration)
447
+ if segment_duration >= min_clip_duration:
448
+ sub = clip.subclip(0, segment_duration)
449
+ clips_to_concatenate.append(sub)
450
+ current_duration += sub.duration
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  except Exception as e:
452
+ logger.warning(f"Error procesando video {path}: {str(e)}")
 
 
 
453
 
454
  if not clips_to_concatenate:
455
+ raise ValueError("No hay segmentos de video válidos.")
 
456
 
457
+ video_base = concatenate_videoclips(clips_to_concatenate, method="chain")
458
+ if video_base.duration is None or video_base.duration <= 0:
459
+ raise ValueError("Video base inválido.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
 
461
+ # Ajustar duración del video
462
+ if video_base.duration < audio_duration:
463
+ num_full_repeats = int(audio_duration // video_base.duration)
464
+ remaining_duration = audio_duration % video_base.duration
465
+ repeated_clips_list = [video_base] * num_full_repeats
 
 
466
  if remaining_duration > 0:
467
+ remaining_clip = video_base.subclip(0, remaining_duration)
468
+ repeated_clips_list.append(remaining_clip)
469
+ video_base = concatenate_videoclips(repeated_clips_list, method="chain")
470
+ elif video_base.duration > audio_duration:
471
+ video_base = video_base.subclip(0, audio_duration)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
 
473
  # 6. Manejar música de fondo
474
+ final_audio = audio_tts
 
 
 
 
 
475
  if musica_file:
 
476
  try:
477
  music_path = os.path.join(temp_dir_intermediate, "musica_bg.mp3")
478
  shutil.copyfile(musica_file, music_path)
479
  temp_intermediate_files.append(music_path)
 
 
480
  musica_audio_original = AudioFileClip(music_path)
481
+ if musica_audio_original.duration > 0:
482
+ musica_audio = loop_audio_to_length(musica_audio_original, video_base.duration)
483
+ final_audio = CompositeAudioClip([
484
+ musica_audio.volumex(0.2),
485
+ audio_tts.volumex(1.0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
  ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  except Exception as e:
488
+ logger.warning(f"Error procesando música: {str(e)}")
489
+ final_audio = audio_tts
490
 
491
+ if abs(final_audio.duration - video_base.duration) > 0.2:
492
+ final_audio = final_audio.subclip(0, video_base.duration)
493
 
494
+ # 7. Combinar audio y video
495
+ video_final = video_base.set_audio(final_audio)
496
+ output_filename = f"video_{int(datetime.now().timestamp())}.mp4"
497
  output_path = os.path.join(temp_dir_intermediate, output_filename)
498
  permanent_path = f"/tmp/{output_filename}"
499
+
 
500
  video_final.write_videofile(
501
  output_path,
502
  fps=24,
 
504
  codec="libx264",
505
  audio_codec="aac",
506
  preset="medium",
507
+ ffmpeg_params=['-vf', 'scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:-1:-1:color=black', '-crf', '23'],
 
 
 
508
  logger='bar'
509
  )
510
 
511
+ shutil.copy(output_path, permanent_path)
512
+ logger.info(f"Video guardado en: {permanent_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
  total_time = (datetime.now() - start_time).total_seconds()
514
+ logger.info(f"Video generado en {total_time:.2f}s")
 
515
  return permanent_path
516
 
517
  except ValueError as ve:
518
+ logger.error(f"Error controlado: {str(ve)}")
519
+ raise
520
  except Exception as e:
521
+ logger.critical(f"Error crítico: {str(e)}")
522
+ raise
523
  finally:
524
+ for clip in source_clips + clips_to_concatenate:
 
 
525
  try:
526
  clip.close()
527
+ except:
528
+ pass
529
+ if audio_tts_original:
530
+ try:
531
+ audio_tts_original.close()
532
+ except:
533
+ pass
534
+ if musica_audio:
 
 
535
  try:
536
  musica_audio.close()
537
+ except:
538
+ pass
539
+ if musica_audio_original:
540
+ try:
541
+ musica_audio_original.close()
542
+ except:
543
+ pass
544
+ if video_base:
545
+ try:
546
+ video_base.close()
547
+ except:
548
+ pass
549
+ if video_final:
 
 
 
 
 
 
 
 
 
550
  try:
551
  video_final.close()
552
+ except:
553
+ pass
554
+ for path in temp_intermediate_files:
555
+ if os.path.isfile(path) and path != permanent_path:
556
+ try:
557
+ os.remove(path)
558
+ except:
559
+ logger.warning(f"No se pudo eliminar {path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
 
561
+ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
 
562
  logger.info("="*80)
563
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
 
 
564
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
 
565
  output_video = None
566
  output_file = None
567
+ status_msg = gr.update(value="⏳ Procesando...")
568
 
569
  if not input_text or not input_text.strip():
570
  logger.warning("Texto de entrada vacío.")
571
+ return None, None, gr.update(value="⚠️ Ingresa texto para el guion o tema.")
 
572
 
 
 
573
  voice_ids_disponibles = [v[1] for v in AVAILABLE_VOICES]
574
  if selected_voice not in voice_ids_disponibles:
575
+ logger.warning(f"Voz inválida: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE_ID}")
576
+ selected_voice = DEFAULT_VOICE_ID
 
 
 
 
 
 
 
 
 
 
 
577
 
578
  try:
579
+ video_path = crear_video(prompt_type, input_text, selected_voice, musica_file)
 
 
 
580
  if video_path and os.path.exists(video_path):
581
+ output_video = video_path
582
+ output_file = video_path
583
+ status_msg = gr.update(value="✅ Video generado exitosamente.")
 
 
584
  else:
585
+ status_msg = gr.update(value=" Error: Falló la generación del video.")
 
 
586
  except ValueError as ve:
587
+ logger.warning(f"Error de validación: {str(ve)}")
588
+ status_msg = gr.update(value=f"⚠️ Error: {str(ve)}")
589
  except Exception as e:
590
+ logger.critical(f"Error crítico: {str(e)}")
591
+ status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}")
592
+ return output_video, output_file, status_msg
 
 
 
593
 
594
  # Interfaz de Gradio
595
+ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft()) as app:
 
 
 
 
596
  gr.Markdown("# 🎬 Generador Automático de Videos con IA")
597
  gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
598
 
 
603
  label="Método de Entrada",
604
  value="Generar Guion con IA"
605
  )
 
 
606
  with gr.Column(visible=True) as ia_guion_column:
607
  prompt_ia = gr.Textbox(
608
  label="Tema para IA",
609
  lines=2,
610
+ placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer...",
611
+ max_lines=4
 
612
  )
 
613
  with gr.Column(visible=False) as manual_guion_column:
614
  prompt_manual = gr.Textbox(
615
  label="Tu Guion Completo",
616
  lines=5,
617
+ placeholder="Ej: En este video exploraremos los misterios del océano...",
618
+ max_lines=10
 
619
  )
 
620
  musica_input = gr.Audio(
621
  label="Música de fondo (opcional)",
622
  type="filepath",
623
+ interactive=True
 
624
  )
 
 
625
  voice_dropdown = gr.Dropdown(
626
  label="Seleccionar Voz para Guion",
627
  choices=AVAILABLE_VOICES,
628
  value=DEFAULT_VOICE_ID,
629
  interactive=True
630
  )
 
 
631
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
 
632
  with gr.Column():
633
  video_output = gr.Video(
634
  label="Previsualización del Video Generado",
 
643
  status_output = gr.Textbox(
644
  label="Estado",
645
  interactive=False,
 
646
  placeholder="Esperando acción...",
647
  value="Esperando entrada..."
648
  )
649
 
 
650
  prompt_type.change(
651
+ lambda x: (gr.update(visible=x == "Generar Guion con IA"), gr.update(visible=x == "Usar Mi Guion")),
 
652
  inputs=prompt_type,
653
  outputs=[ia_guion_column, manual_guion_column]
654
  )
655
 
 
656
  generate_btn.click(
657
+ lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.")),
658
+ outputs=[video_output, file_output, status_output]
 
659
  ).then(
660
  run_app,
661
  inputs=[prompt_type, prompt_ia, prompt_manual, musica_input, voice_dropdown],
 
668
 
669
  gr.Markdown("### Instrucciones:")
670
  gr.Markdown("""
671
+ 1. Configura la variable de entorno `PEXELS_API_KEY`.
672
+ 2. Selecciona el tipo de entrada: "Generar Guion con IA" o "Usar Mi Guion".
673
+ 3. Sube música (opcional).
674
+ 4. Selecciona la voz.
675
+ 5. Haz clic en "✨ Generar Video".
676
+ 6. Revisa el estado y descarga el video generado.
677
+ 7. Consulta `video_generator_full.log` para errores.
 
678
  """)
 
 
679
 
680
  if __name__ == "__main__":
681
+ logger.info("Verificando dependencias...")
682
  try:
683
  from moviepy.editor import ColorClip
684
+ temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
685
+ temp_clip.close()
686
+ logger.info("MoviePy y FFmpeg accesibles.")
 
 
 
687
  except Exception as e:
688
+ logger.critical(f"Fallo en dependencias: {e}")
689
+ raise
690
+ os.environ['GRADIO_SERVER_TIMEOUT'] = '6000'
 
 
691
  logger.info("Iniciando aplicación Gradio...")
692
  try:
693
  app.launch(server_name="0.0.0.0", server_port=7860, share=False)
694
  except Exception as e:
695
+ logger.critical(f"No se pudo iniciar la app: {str(e)}")
696
  raise