gnosticdev commited on
Commit
df09931
·
verified ·
1 Parent(s): bfc799a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +598 -141
app.py CHANGED
@@ -14,15 +14,6 @@ import math
14
  import shutil
15
  import json
16
  from collections import Counter
17
- import threading
18
- import time
19
- from PIL import Image
20
-
21
- # *** CAMBIO 1 (CORRECCIÓN): Parche para la compatibilidad de Pillow >= 10.0 ***
22
- # Las versiones nuevas de Pillow eliminaron 'ANTIALIAS'. MoviePy aún lo usa.
23
- # Este código restaura la compatibilidad haciendo que ANTIALIAS apunte a LANCZOS.
24
- if not hasattr(Image, 'ANTIALIAS'):
25
- Image.ANTIALIAS = Image.LANCZOS
26
 
27
  # Variable global para TTS
28
  tts_model = None
@@ -70,20 +61,6 @@ except Exception as e:
70
  logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
71
  kw_model = None
72
 
73
- # *** CAMBIO 3 (AÑADIDO): Función para eliminar directorios temporalmente ***
74
- def schedule_deletion(directory_path, delay_seconds):
75
- """Programa la eliminación de un directorio después de un cierto tiempo."""
76
- logger.info(f"PROGRAMADA eliminación del directorio '{directory_path}' en {delay_seconds / 3600:.1f} horas.")
77
- time.sleep(delay_seconds)
78
- try:
79
- if os.path.isdir(directory_path):
80
- shutil.rmtree(directory_path)
81
- logger.info(f"Directorio temporal '{directory_path}' eliminado exitosamente.")
82
- else:
83
- logger.warning(f"No se pudo eliminar: '{directory_path}' no es un directorio válido o ya fue eliminado.")
84
- except Exception as e:
85
- logger.error(f"Error durante la eliminación programada de '{directory_path}': {str(e)}")
86
-
87
  def buscar_videos_pexels(query, api_key, per_page=5):
88
  if not api_key:
89
  logger.warning("No se puede buscar en Pexels: API Key no configurada.")
@@ -205,15 +182,20 @@ def text_to_speech(text, output_path, voice=None):
205
  return False
206
 
207
  try:
 
208
  tts = TTS(model_name="tts_models/es/css10/vits", progress_bar=False, gpu=False)
 
 
209
  text = text.replace("na hora", "A la hora")
210
  text = re.sub(r'[^\w\s.,!?áéíóúñÁÉÍÓÚÑ]', '', text)
211
  if len(text) > 500:
212
  logger.warning("Texto demasiado largo, truncando a 500 caracteres")
213
  text = text[:500]
214
 
 
215
  tts.tts_to_file(text=text, file_path=output_path)
216
 
 
217
  if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
218
  logger.info(f"Audio creado: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
219
  return True
@@ -246,7 +228,7 @@ def download_video_file(url, temp_dir):
246
  logger.info(f"Video descargado exitosamente: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
247
  return output_path
248
  else:
249
- logger.warning(f"Descarga parece incompleta o vacía para {url[:80]}...")
250
  if os.path.exists(output_path):
251
  os.remove(output_path)
252
  return None
@@ -263,21 +245,59 @@ def loop_audio_to_length(audio_clip, target_duration):
263
 
264
  if audio_clip is None or audio_clip.duration is None or audio_clip.duration <= 0:
265
  logger.warning("Input audio clip is invalid (None or zero duration), cannot loop.")
266
- return None
 
 
 
 
 
267
 
268
  if audio_clip.duration >= target_duration:
269
  logger.debug("Audio clip already longer or equal to target. Trimming.")
270
- return audio_clip.subclip(0, target_duration)
 
 
 
 
 
 
271
 
272
  loops = math.ceil(target_duration / audio_clip.duration)
273
  logger.debug(f"Creando {loops} loops de audio")
274
 
 
 
 
275
  try:
276
- looped_audio = concatenate_audioclips([audio_clip] * loops)
277
- return looped_audio.subclip(0, target_duration)
 
 
 
 
 
 
 
 
 
 
 
 
278
  except Exception as e:
279
  logger.error(f"Error concatenating/subclipping audio clips for looping: {str(e)}", exc_info=True)
280
- return None
 
 
 
 
 
 
 
 
 
 
 
 
281
 
282
  def extract_visual_keywords_from_script(script_text):
283
  logger.info("Extrayendo palabras clave del guion")
@@ -290,27 +310,48 @@ def extract_visual_keywords_from_script(script_text):
290
 
291
  if kw_model:
292
  try:
 
293
  keywords1 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(1, 1), stop_words='spanish', top_n=5)
294
  keywords2 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(2, 2), stop_words='spanish', top_n=3)
295
- all_keywords = sorted(keywords1 + keywords2, key=lambda item: item[1], reverse=True)
 
 
 
296
  seen_keywords = set()
297
  for keyword, score in all_keywords:
298
  formatted_keyword = keyword.lower().replace(" ", "+")
299
  if formatted_keyword and formatted_keyword not in seen_keywords:
300
  keywords_list.append(formatted_keyword)
301
  seen_keywords.add(formatted_keyword)
302
- if len(keywords_list) >= 5: break
 
 
303
  if keywords_list:
304
  logger.debug(f"Palabras clave extraídas por KeyBERT: {keywords_list}")
305
  return keywords_list
 
306
  except Exception as e:
307
  logger.warning(f"KeyBERT falló: {str(e)}. Intentando método simple.")
308
 
309
- stop_words = set(["el", "la", "los", "las", "de", "en", "y", "a", "que", "es", "un", "una", "con", "para", "del", "al", "por", "su", "sus"])
310
- words = [word for word in clean_text.lower().split() if len(word) > 3 and word not in stop_words]
311
- if not words: return ["naturaleza", "ciudad", "paisaje"]
312
-
313
- top_keywords = [word.replace(" ", "+") for word, _ in Counter(words).most_common(5)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  logger.info(f"Palabras clave finales: {top_keywords}")
315
  return top_keywords
316
 
@@ -321,7 +362,6 @@ def crear_video(prompt_type, input_text, musica_file=None):
321
 
322
  start_time = datetime.now()
323
  temp_dir_intermediate = None
324
- TARGET_RESOLUTION = (1280, 720) # *** CAMBIO 2 (AÑADIDO): Resolución 720p ***
325
 
326
  audio_tts_original = None
327
  musica_audio_original = None
@@ -334,160 +374,511 @@ def crear_video(prompt_type, input_text, musica_file=None):
334
 
335
  try:
336
  # 1. Generar o usar guion
337
- guion = generate_script(input_text) if prompt_type == "Generar Guion con IA" else input_text.strip()
338
- if not guion.strip(): raise ValueError("El guion está vacío.")
 
 
 
 
 
 
 
 
 
 
339
  guion = guion.replace("na hora", "A la hora")
340
 
341
  temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
 
342
  temp_intermediate_files = []
343
 
344
  # 2. Generar audio de voz
 
345
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
346
- if not text_to_speech(guion, voz_path) or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 1000:
 
 
 
 
 
347
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
 
348
  temp_intermediate_files.append(voz_path)
 
349
  audio_tts_original = AudioFileClip(voz_path)
350
- if audio_tts_original.duration is None or audio_tts_original.duration < 1.0:
351
- raise ValueError("Generated voice audio is too short (min 1 second required).")
 
 
 
 
 
 
352
  audio_tts = audio_tts_original
353
  audio_duration = audio_tts_original.duration
 
354
 
355
- # 3. Extraer palabras clave y buscar videos
356
- keywords = extract_visual_keywords_from_script(guion)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  videos_data = []
358
- for keyword in keywords + ["nature", "city", "background", "abstract"]:
359
- if len(videos_data) >= 10: break
360
- videos_data.extend(buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=3))
361
- if not videos_data: raise ValueError("No se encontraron videos adecuados en Pexels.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
 
363
- # 4. Descargar videos
364
  video_paths = []
 
365
  for video in videos_data:
366
- best_quality = next((vf for vf in sorted(video.get('video_files', []), key=lambda x: x.get('width', 0), reverse=True) if 'link' in vf), None)
367
- if best_quality:
368
- path = download_video_file(best_quality['link'], temp_dir_intermediate)
369
- if path:
370
- video_paths.append(path)
371
- temp_intermediate_files.append(path)
372
- if not video_paths: raise ValueError("No se pudo descargar ningún video utilizable de Pexels.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
 
374
  # 5. Procesar y concatenar clips de video
 
375
  current_duration = 0
 
 
 
376
  for i, path in enumerate(video_paths):
377
- if current_duration >= audio_duration: break
 
 
 
378
  clip = None
379
  try:
 
380
  clip = VideoFileClip(path)
381
  source_clips.append(clip)
382
- if clip.duration is None or clip.duration <= 0.5: continue
383
-
384
- segment_duration = min(clip.duration, audio_duration - current_duration, 10.0)
385
- if segment_duration >= 0.5:
386
- sub_raw = clip.subclip(0, segment_duration)
387
-
388
- # *** CAMBIO 2 (AÑADIDO): Redimensionar y recortar CADA clip a 720p ***
389
- sub_resized = sub_raw.resize(height=TARGET_RESOLUTION[1]).crop(x_center='center', y_center='center', width=TARGET_RESOLUTION[0], height=TARGET_RESOLUTION[1])
390
- sub_raw.close() # Liberar memoria del clip intermedio sin redimensionar
391
-
392
- if sub_resized.duration is not None and sub_resized.duration > 0:
393
- clips_to_concatenate.append(sub_resized)
394
- current_duration += sub_resized.duration
395
- logger.debug(f"Segmento añadido: {sub_resized.duration:.1f}s (total: {current_duration:.1f}/{audio_duration:.1f}s)")
396
- else:
397
- sub_resized.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
 
399
  except Exception as e:
400
- logger.warning(f"Error procesando video {path}: {str(e)}")
401
-
 
 
 
402
  if not clips_to_concatenate:
 
403
  raise ValueError("No hay segmentos de video válidos disponibles para crear el video.")
404
 
405
- video_base = concatenate_videoclips(clips_to_concatenate, method="chain")
406
- for seg in clips_to_concatenate: seg.close() # Limpieza de los clips en la lista
407
- clips_to_concatenate = []
 
 
 
 
 
 
408
 
409
- if video_base.duration < audio_duration:
410
- video_base = video_base.loop(duration=audio_duration)
411
- if video_base.duration > audio_duration:
412
- video_base = video_base.subclip(0, audio_duration)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
 
414
  # 6. Manejar música de fondo
415
- final_audio = audio_tts
 
 
 
 
 
416
  if musica_file:
 
417
  try:
418
  music_path = os.path.join(temp_dir_intermediate, "musica_bg.mp3")
419
  shutil.copyfile(musica_file, music_path)
420
  temp_intermediate_files.append(music_path)
 
 
421
  musica_audio_original = AudioFileClip(music_path)
422
- if musica_audio_original.duration > 0:
423
- musica_audio = loop_audio_to_length(musica_audio_original, video_base.duration)
424
- if musica_audio:
425
- final_audio = CompositeAudioClip([musica_audio.volumex(0.2), audio_tts.volumex(1.0)])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  except Exception as e:
427
- logger.warning(f"Error procesando música: {str(e)}")
428
 
429
- # 7. Crear video final
 
430
  video_final = video_base.set_audio(final_audio)
431
- output_path = os.path.join(temp_dir_intermediate, "final_video.mp4")
432
- video_final.write_videofile(
433
- filename=output_path, fps=24, threads=4, codec="libx264",
434
- audio_codec="aac", preset="medium", logger='bar'
435
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
 
437
  total_time = (datetime.now() - start_time).total_seconds()
438
  logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {output_path} | Tiempo total: {total_time:.2f}s")
 
439
  return output_path
440
 
 
 
 
441
  except Exception as e:
442
- logger.critical(f"ERROR CRÍTICO en crear_video: {str(e)}", exc_info=True)
443
- raise
444
  finally:
445
- # Limpieza de todos los recursos de MoviePy
446
- all_clips = [audio_tts_original, musica_audio_original, audio_tts, musica_audio, video_base, video_final] + source_clips + clips_to_concatenate
447
- for clip_resource in all_clips:
448
- if clip_resource:
449
- try: clip_resource.close()
450
- except Exception as close_e: logger.warning(f"Error menor cerrando un clip: {close_e}")
451
-
452
- # Limpieza de archivos temporales
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
454
- final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4")
455
- for path in temp_intermediate_files:
456
- if os.path.isfile(path) and path != final_output_in_temp:
457
- try:
458
- os.remove(path)
459
- logger.debug(f"Eliminando archivo temporal intermedio: {path}")
460
- except Exception as rm_e: logger.warning(f"No se pudo eliminar archivo temporal {path}: {rm_e}")
461
- logger.info(f"Directorio temporal {temp_dir_intermediate} persistirá para Gradio.")
 
 
 
 
 
462
 
463
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
464
  logger.info("="*80)
465
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
466
 
467
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
468
- output_video, output_file, status_msg = None, gr.update(value=None, visible=False), gr.update(value="⏳ Procesando...", interactive=False)
 
 
 
469
 
470
  if not input_text or not input_text.strip():
471
- return output_video, output_file, gr.update(value="⚠️ Por favor, ingresa un guion o tema.", interactive=False)
 
 
 
 
 
 
 
 
 
472
 
473
  try:
 
474
  video_path = crear_video(prompt_type, input_text, musica_file)
 
475
  if video_path and os.path.exists(video_path):
 
 
476
  output_video = video_path
477
  output_file = gr.update(value=video_path, visible=True)
478
  status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
479
-
480
- # *** CAMBIO 3 (AÑADIDO): Programar la eliminación automática del directorio del video ***
481
- temp_dir_to_delete = os.path.dirname(video_path)
482
- deletion_thread = threading.Thread(
483
- target=schedule_deletion,
484
- args=(temp_dir_to_delete, 3 * 3600) # 3 horas en segundos
485
- )
486
- deletion_thread.daemon = True # Permite que el programa principal termine aunque el hilo esté esperando
487
- deletion_thread.start()
488
  else:
489
- status_msg = gr.update(value=" Error: La generación del video falló.", interactive=False)
 
 
 
 
 
490
  except Exception as e:
 
491
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
492
  finally:
493
  logger.info("Fin del handler run_app.")
@@ -503,38 +894,104 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
503
 
504
  with gr.Row():
505
  with gr.Column():
506
- prompt_type = gr.Radio(["Generar Guion con IA", "Usar Mi Guion"], label="Método de Entrada", value="Generar Guion con IA")
 
 
 
 
 
507
  with gr.Column(visible=True) as ia_guion_column:
508
- prompt_ia = gr.Textbox(label="Tema para IA", lines=2, placeholder="Ej: Un paisaje natural con montañas y ríos...")
 
 
 
 
 
 
 
509
  with gr.Column(visible=False) as manual_guion_column:
510
- prompt_manual = gr.Textbox(label="Tu Guion Completo", lines=5, placeholder="Ej: En este video exploraremos los misterios del océano...")
511
- musica_input = gr.Audio(label="Música de fondo (opcional)", type="filepath")
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
513
 
514
  with gr.Column():
515
- video_output = gr.Video(label="Previsualización del Video Generado", interactive=False, height=400)
516
- file_output = gr.File(label="Descargar Archivo de Video", interactive=False, visible=False)
517
- status_output = gr.Textbox(label="Estado", interactive=False, show_label=False, placeholder="Esperando acción...")
518
-
519
- prompt_type.change(lambda x: (gr.update(visible=x == "Generar Guion con IA"), gr.update(visible=x == "Usar Mi Guion")), inputs=prompt_type, outputs=[ia_guion_column, manual_guion_column])
520
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  generate_btn.click(
522
  lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
523
- outputs=[video_output, file_output, status_output], queue=True
 
524
  ).then(
525
- run_app, inputs=[prompt_type, prompt_ia, prompt_manual, musica_input], outputs=[video_output, file_output, status_output]
 
 
526
  )
527
 
528
  gr.Markdown("### Instrucciones:")
529
- gr.Markdown("1. **Clave API de Pexels:** Asegúrate de tener la variable de entorno `PEXELS_API_KEY`.\n"
530
- "2. **Selecciona el método** y escribe tu tema o guion.\n"
531
- "3. **Sube música** (opcional).\n"
532
- "4. Haz clic en **Generar Video** y espera.\n"
533
- "5. El video generado se eliminará automáticamente del servidor después de 3 horas.")
 
 
 
 
 
 
534
  gr.Markdown("---")
535
  gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
536
 
537
  if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  logger.info("Iniciando aplicación Gradio...")
539
  try:
540
  app.launch(server_name="0.0.0.0", server_port=7860, share=False)
 
14
  import shutil
15
  import json
16
  from collections import Counter
 
 
 
 
 
 
 
 
 
17
 
18
  # Variable global para TTS
19
  tts_model = None
 
61
  logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
62
  kw_model = None
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  def buscar_videos_pexels(query, api_key, per_page=5):
65
  if not api_key:
66
  logger.warning("No se puede buscar en Pexels: API Key no configurada.")
 
182
  return False
183
 
184
  try:
185
+ # Usar modelo específico para español, sin GPU
186
  tts = TTS(model_name="tts_models/es/css10/vits", progress_bar=False, gpu=False)
187
+
188
+ # Limpiar y truncar texto
189
  text = text.replace("na hora", "A la hora")
190
  text = re.sub(r'[^\w\s.,!?áéíóúñÁÉÍÓÚÑ]', '', text)
191
  if len(text) > 500:
192
  logger.warning("Texto demasiado largo, truncando a 500 caracteres")
193
  text = text[:500]
194
 
195
+ # Generar audio sin especificar idioma
196
  tts.tts_to_file(text=text, file_path=output_path)
197
 
198
+ # Verificar archivo generado
199
  if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
200
  logger.info(f"Audio creado: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
201
  return True
 
228
  logger.info(f"Video descargado exitosamente: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
229
  return output_path
230
  else:
231
+ 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")
232
  if os.path.exists(output_path):
233
  os.remove(output_path)
234
  return None
 
245
 
246
  if audio_clip is None or audio_clip.duration is None or audio_clip.duration <= 0:
247
  logger.warning("Input audio clip is invalid (None or zero duration), cannot loop.")
248
+ try:
249
+ sr = getattr(audio_clip, 'fps', 44100) if audio_clip else 44100
250
+ return AudioClip(lambda t: 0, duration=target_duration, sr=sr)
251
+ except Exception as e:
252
+ logger.error(f"Could not create silence clip: {e}", exc_info=True)
253
+ return AudioFileClip(filename="")
254
 
255
  if audio_clip.duration >= target_duration:
256
  logger.debug("Audio clip already longer or equal to target. Trimming.")
257
+ trimmed_clip = audio_clip.subclip(0, target_duration)
258
+ if trimmed_clip.duration is None or trimmed_clip.duration <= 0:
259
+ logger.error("Trimmed audio clip is invalid.")
260
+ try: trimmed_clip.close()
261
+ except: pass
262
+ return AudioFileClip(filename="")
263
+ return trimmed_clip
264
 
265
  loops = math.ceil(target_duration / audio_clip.duration)
266
  logger.debug(f"Creando {loops} loops de audio")
267
 
268
+ audio_segments = [audio_clip] * loops
269
+ looped_audio = None
270
+ final_looped_audio = None
271
  try:
272
+ looped_audio = concatenate_audioclips(audio_segments)
273
+
274
+ if looped_audio.duration is None or looped_audio.duration <= 0:
275
+ logger.error("Concatenated audio clip is invalid (None or zero duration).")
276
+ raise ValueError("Invalid concatenated audio.")
277
+
278
+ final_looped_audio = looped_audio.subclip(0, target_duration)
279
+
280
+ if final_looped_audio.duration is None or final_looped_audio.duration <= 0:
281
+ logger.error("Final subclipped audio clip is invalid (None or zero duration).")
282
+ raise ValueError("Invalid final subclipped audio.")
283
+
284
+ return final_looped_audio
285
+
286
  except Exception as e:
287
  logger.error(f"Error concatenating/subclipping audio clips for looping: {str(e)}", exc_info=True)
288
+ try:
289
+ if audio_clip.duration is not None and audio_clip.duration > 0:
290
+ logger.warning("Returning original audio clip (may be too short).")
291
+ return audio_clip.subclip(0, min(audio_clip.duration, target_duration))
292
+ except:
293
+ pass
294
+ logger.error("Fallback to original audio clip failed.")
295
+ return AudioFileClip(filename="")
296
+
297
+ finally:
298
+ if looped_audio is not None and looped_audio is not final_looped_audio:
299
+ try: looped_audio.close()
300
+ except: pass
301
 
302
  def extract_visual_keywords_from_script(script_text):
303
  logger.info("Extrayendo palabras clave del guion")
 
310
 
311
  if kw_model:
312
  try:
313
+ logger.debug("Intentando extracción con KeyBERT...")
314
  keywords1 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(1, 1), stop_words='spanish', top_n=5)
315
  keywords2 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(2, 2), stop_words='spanish', top_n=3)
316
+
317
+ all_keywords = keywords1 + keywords2
318
+ all_keywords.sort(key=lambda item: item[1], reverse=True)
319
+
320
  seen_keywords = set()
321
  for keyword, score in all_keywords:
322
  formatted_keyword = keyword.lower().replace(" ", "+")
323
  if formatted_keyword and formatted_keyword not in seen_keywords:
324
  keywords_list.append(formatted_keyword)
325
  seen_keywords.add(formatted_keyword)
326
+ if len(keywords_list) >= 5:
327
+ break
328
+
329
  if keywords_list:
330
  logger.debug(f"Palabras clave extraídas por KeyBERT: {keywords_list}")
331
  return keywords_list
332
+
333
  except Exception as e:
334
  logger.warning(f"KeyBERT falló: {str(e)}. Intentando método simple.")
335
 
336
+ logger.debug("Extrayendo palabras clave con método simple...")
337
+ words = clean_text.lower().split()
338
+ 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",
339
+ "nuestro", "vuestro", "este", "ese", "aquel", "esta", "esa", "aquella", "esto", "eso", "aquello", "mis", "tus",
340
+ "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á"}
341
+
342
+ valid_words = [word for word in words if len(word) > 3 and word not in stop_words]
343
+
344
+ if not valid_words:
345
+ logger.warning("No se encontraron palabras clave válidas con método simple. Usando palabras clave predeterminadas.")
346
+ return ["naturaleza", "ciudad", "paisaje"]
347
+
348
+ word_counts = Counter(valid_words)
349
+ top_keywords = [word.replace(" ", "+") for word, _ in word_counts.most_common(5)]
350
+
351
+ if not top_keywords:
352
+ logger.warning("El método simple no produjo keywords. Usando palabras clave predeterminadas.")
353
+ return ["naturaleza", "ciudad", "paisaje"]
354
+
355
  logger.info(f"Palabras clave finales: {top_keywords}")
356
  return top_keywords
357
 
 
362
 
363
  start_time = datetime.now()
364
  temp_dir_intermediate = None
 
365
 
366
  audio_tts_original = None
367
  musica_audio_original = None
 
374
 
375
  try:
376
  # 1. Generar o usar guion
377
+ if prompt_type == "Generar Guion con IA":
378
+ guion = generate_script(input_text)
379
+ else:
380
+ guion = input_text.strip()
381
+
382
+ logger.info(f"Guion final ({len(guion)} chars): '{guion[:100]}...'")
383
+
384
+ if not guion.strip():
385
+ logger.error("El guion resultante está vacío o solo contiene espacios.")
386
+ raise ValueError("El guion está vacío.")
387
+
388
+ # Corregir error tipográfico en el guion
389
  guion = guion.replace("na hora", "A la hora")
390
 
391
  temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
392
+ logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
393
  temp_intermediate_files = []
394
 
395
  # 2. Generar audio de voz
396
+ logger.info("Generando audio de voz...")
397
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
398
+
399
+ # Llamar a text_to_speech directamente
400
+ tts_success = text_to_speech(guion, voz_path)
401
+
402
+ if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 1000:
403
+ logger.error(f"Fallo en la generación de voz. Archivo de audio no creado o es muy pequeño: {voz_path}")
404
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
405
+
406
  temp_intermediate_files.append(voz_path)
407
+
408
  audio_tts_original = AudioFileClip(voz_path)
409
+
410
+ if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
411
+ logger.critical("Clip de audio TTS inicial es inválido (reader is None o duración <= 0) *después* de crear AudioFileClip.")
412
+ try: audio_tts_original.close()
413
+ except: pass
414
+ audio_tts_original = None
415
+ raise ValueError("Audio de voz generado es inválido después de procesamiento inicial.")
416
+
417
  audio_tts = audio_tts_original
418
  audio_duration = audio_tts_original.duration
419
+ logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
420
 
421
+ if audio_duration < 1.0:
422
+ logger.error(f"Duración audio voz ({audio_duration:.2f}s) es muy corta.")
423
+ raise ValueError("Generated voice audio is too short (min 1 second required).")
424
+
425
+ # 3. Extraer palabras clave
426
+ logger.info("Extrayendo palabras clave...")
427
+ try:
428
+ keywords = extract_visual_keywords_from_script(guion)
429
+ logger.info(f"Palabras clave identificadas: {keywords}")
430
+ except Exception as e:
431
+ logger.error(f"Error extrayendo keywords: {str(e)}", exc_info=True)
432
+ keywords = ["naturaleza", "paisaje"]
433
+
434
+ if not keywords:
435
+ keywords = ["video", "background"]
436
+
437
+ # 4. Buscar y descargar videos
438
+ logger.info("Buscando videos en Pexels...")
439
  videos_data = []
440
+ total_desired_videos = 10
441
+ per_page_per_keyword = max(1, total_desired_videos // len(keywords))
442
+
443
+ for keyword in keywords:
444
+ if len(videos_data) >= total_desired_videos: break
445
+ try:
446
+ videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=per_page_per_keyword)
447
+ if videos:
448
+ videos_data.extend(videos)
449
+ logger.info(f"Encontrados {len(videos)} videos para '{keyword}'. Total data: {len(videos_data)}")
450
+ except Exception as e:
451
+ logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
452
+
453
+ if len(videos_data) < total_desired_videos / 2:
454
+ logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
455
+ generic_keywords = ["nature", "city", "background", "abstract"]
456
+ for keyword in generic_keywords:
457
+ if len(videos_data) >= total_desired_videos: break
458
+ try:
459
+ videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=2)
460
+ if videos:
461
+ videos_data.extend(videos)
462
+ logger.info(f"Encontrados {len(videos)} videos para '{keyword}' (genérico). Total data: {len(videos_data)}")
463
+ except Exception as e:
464
+ logger.warning(f"Error buscando videos genéricos para '{keyword}': {str(e)}")
465
+
466
+ if not videos_data:
467
+ logger.error("No se encontraron videos en Pexels para ninguna palabra clave.")
468
+ raise ValueError("No se encontraron videos adecuados en Pexels.")
469
 
 
470
  video_paths = []
471
+ logger.info(f"Intentando descargar {len(videos_data)} videos encontrados...")
472
  for video in videos_data:
473
+ if 'video_files' not in video or not video['video_files']:
474
+ logger.debug(f"Saltando video sin archivos de video: {video.get('id')}")
475
+ continue
476
+
477
+ try:
478
+ best_quality = None
479
+ for vf in sorted(video['video_files'], key=lambda x: x.get('width', 0) * x.get('height', 0), reverse=True):
480
+ if 'link' in vf:
481
+ best_quality = vf
482
+ break
483
+
484
+ if best_quality and 'link' in best_quality:
485
+ path = download_video_file(best_quality['link'], temp_dir_intermediate)
486
+ if path:
487
+ video_paths.append(path)
488
+ temp_intermediate_files.append(path)
489
+ logger.info(f"Video descargado OK desde {best_quality['link'][:50]}...")
490
+ else:
491
+ logger.warning(f"No se pudo descargar video desde {best_quality['link'][:50]}...")
492
+ else:
493
+ logger.warning(f"No se encontró enlace de descarga válido para video {video.get('id')}.")
494
+
495
+ except Exception as e:
496
+ logger.warning(f"Error procesando/descargando video {video.get('id')}: {str(e)}")
497
+
498
+ logger.info(f"Descargados {len(video_paths)} archivos de video utilizables.")
499
+ if not video_paths:
500
+ logger.error("No se pudo descargar ningún archivo de video utilizable.")
501
+ raise ValueError("No se pudo descargar ningún video utilizable de Pexels.")
502
 
503
  # 5. Procesar y concatenar clips de video
504
+ logger.info("Procesando y concatenando videos descargados...")
505
  current_duration = 0
506
+ min_clip_duration = 0.5
507
+ max_clip_segment = 10.0
508
+
509
  for i, path in enumerate(video_paths):
510
+ if current_duration >= audio_duration + max_clip_segment:
511
+ 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.")
512
+ break
513
+
514
  clip = None
515
  try:
516
+ logger.debug(f"[{i+1}/{len(video_paths)}] Abriendo clip: {path}")
517
  clip = VideoFileClip(path)
518
  source_clips.append(clip)
519
+
520
+ if clip.reader is None or clip.duration is None or clip.duration <= 0:
521
+ logger.warning(f"[{i+1}/{len(video_paths)}] Clip fuente {path} parece inválido (reader is None o duración <= 0). Saltando.")
522
+ continue
523
+
524
+ remaining_needed = audio_duration - current_duration
525
+ potential_use_duration = min(clip.duration, max_clip_segment)
526
+
527
+ if remaining_needed > 0:
528
+ segment_duration = min(potential_use_duration, remaining_needed + min_clip_duration)
529
+ segment_duration = max(min_clip_duration, segment_duration)
530
+ segment_duration = min(segment_duration, clip.duration)
531
+
532
+ if segment_duration >= min_clip_duration:
533
+ try:
534
+ sub = clip.subclip(0, segment_duration)
535
+ if sub.reader is None or sub.duration is None or sub.duration <= 0:
536
+ logger.warning(f"[{i+1}/{len(video_paths)}] Subclip generado de {path} es inválido. Saltando.")
537
+ try: sub.close()
538
+ except: pass
539
+ continue
540
+
541
+ clips_to_concatenate.append(sub)
542
+ current_duration += sub.duration
543
+ logger.debug(f"[{i+1}/{len(video_paths)}] Segmento añadido: {sub.duration:.1f}s (total video: {current_duration:.1f}/{audio_duration:.1f}s)")
544
+
545
+ except Exception as sub_e:
546
+ logger.warning(f"[{i+1}/{len(video_paths)}] Error creando subclip de {path} ({segment_duration:.1f}s): {str(sub_e)}")
547
+ continue
548
+ else:
549
+ 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.")
550
+ else:
551
+ logger.debug(f"[{i+1}/{len(video_paths)}] Duración de video base ya alcanzada. Saltando clip.")
552
 
553
  except Exception as e:
554
+ logger.warning(f"[{i+1}/{len(video_paths)}] Error procesando video {path}: {str(e)}", exc_info=True)
555
+ continue
556
+
557
+ logger.info(f"Procesamiento de clips fuente finalizado. Se obtuvieron {len(clips_to_concatenate)} segmentos válidos.")
558
+
559
  if not clips_to_concatenate:
560
+ logger.error("No hay segmentos de video válidos disponibles para crear la secuencia.")
561
  raise ValueError("No hay segmentos de video válidos disponibles para crear el video.")
562
 
563
+ logger.info(f"Concatenando {len(clips_to_concatenate)} segmentos de video.")
564
+ concatenated_base = None
565
+ try:
566
+ concatenated_base = concatenate_videoclips(clips_to_concatenate, method="chain")
567
+ logger.info(f"Duración video base después de concatenación inicial: {concatenated_base.duration:.2f}s")
568
+
569
+ if concatenated_base is None or concatenated_base.duration is None or concatenated_base.duration <= 0:
570
+ logger.critical("Video base concatenado es inválido después de la primera concatenación (None o duración cero).")
571
+ raise ValueError("Fallo al crear video base válido a partir de segmentos.")
572
 
573
+ except Exception as e:
574
+ logger.critical(f"Error durante la concatenación inicial: {str(e)}", exc_info=True)
575
+ raise ValueError("Fallo durante la concatenación de video inicial.")
576
+ finally:
577
+ for clip_segment in clips_to_concatenate:
578
+ try: clip_segment.close()
579
+ except: pass
580
+ clips_to_concatenate = []
581
+
582
+ video_base = concatenated_base
583
+
584
+ final_video_base = video_base
585
+
586
+ if final_video_base.duration < audio_duration:
587
+ logger.info(f"Video base ({final_video_base.duration:.2f}s) es más corto que el audio ({audio_duration:.2f}s). Repitiendo...")
588
+
589
+ num_full_repeats = int(audio_duration // final_video_base.duration)
590
+ remaining_duration = audio_duration % final_video_base.duration
591
+
592
+ repeated_clips_list = [final_video_base] * num_full_repeats
593
+ if remaining_duration > 0:
594
+ try:
595
+ remaining_clip = final_video_base.subclip(0, remaining_duration)
596
+ if remaining_clip is None or remaining_clip.duration is None or remaining_clip.duration <= 0:
597
+ logger.warning(f"Subclip generado para duración restante {remaining_duration:.2f}s es inválido. Saltando.")
598
+ try: remaining_clip.close()
599
+ except: pass
600
+ else:
601
+ repeated_clips_list.append(remaining_clip)
602
+ logger.debug(f"Añadiendo segmento restante: {remaining_duration:.2f}s")
603
+
604
+ except Exception as e:
605
+ logger.warning(f"Error creando subclip para duración restante {remaining_duration:.2f}s: {str(e)}")
606
+
607
+ if repeated_clips_list:
608
+ logger.info(f"Concatenando {len(repeated_clips_list)} partes para repetición.")
609
+ video_base_repeated = None
610
+ try:
611
+ video_base_repeated = concatenate_videoclips(repeated_clips_list, method="chain")
612
+ logger.info(f"Duración del video base repetido: {video_base_repeated.duration:.2f}s")
613
+
614
+ if video_base_repeated is None or video_base_repeated.duration is None or video_base_repeated.duration <= 0:
615
+ logger.critical("Video base repetido concatenado es inválido.")
616
+ raise ValueError("Fallo al crear video base repetido válido.")
617
+
618
+ if final_video_base is not video_base_repeated:
619
+ try: final_video_base.close()
620
+ except: pass
621
+
622
+ final_video_base = video_base_repeated
623
+
624
+ except Exception as e:
625
+ logger.critical(f"Error durante la concatenación de repetición: {str(e)}", exc_info=True)
626
+ raise ValueError("Fallo durante la repetición de video.")
627
+ finally:
628
+ if 'repeated_clips_list' in locals():
629
+ for clip in repeated_clips_list:
630
+ if clip is not final_video_base:
631
+ try: clip.close()
632
+ except: pass
633
+
634
+ if final_video_base.duration > audio_duration:
635
+ 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).")
636
+ trimmed_video_base = None
637
+ try:
638
+ trimmed_video_base = final_video_base.subclip(0, audio_duration)
639
+ if trimmed_video_base is None or trimmed_video_base.duration is None or trimmed_video_base.duration <= 0:
640
+ logger.critical("Video base recortado es inválido.")
641
+ raise ValueError("Fallo al crear video base recortado válido.")
642
+
643
+ if final_video_base is not trimmed_video_base:
644
+ try: final_video_base.close()
645
+ except: pass
646
+
647
+ final_video_base = trimmed_video_base
648
+
649
+ except Exception as e:
650
+ logger.critical(f"Error durante el recorte: {str(e)}", exc_info=True)
651
+ raise ValueError("Fallo durante el recorte de video.")
652
+
653
+ if final_video_base is None or final_video_base.duration is None or final_video_base.duration <= 0:
654
+ logger.critical("Video base final es inválido antes de audio/escritura (None o duración cero).")
655
+ raise ValueError("Video base final es inválido.")
656
+
657
+ if final_video_base.size is None or final_video_base.size[0] <= 0 or final_video_base.size[1] <= 0:
658
+ logger.critical(f"Video base final tiene tamaño inválido: {final_video_base.size}. No se puede escribir video.")
659
+ raise ValueError("Video base final tiene tamaño inválido antes de escribir.")
660
+
661
+ video_base = final_video_base
662
 
663
  # 6. Manejar música de fondo
664
+ logger.info("Procesando audio...")
665
+
666
+ final_audio = audio_tts_original
667
+
668
+ musica_audio_looped = None
669
+
670
  if musica_file:
671
+ musica_audio_original = None
672
  try:
673
  music_path = os.path.join(temp_dir_intermediate, "musica_bg.mp3")
674
  shutil.copyfile(musica_file, music_path)
675
  temp_intermediate_files.append(music_path)
676
+ logger.info(f"Música de fondo copiada a: {music_path}")
677
+
678
  musica_audio_original = AudioFileClip(music_path)
679
+
680
+ if musica_audio_original.reader is None or musica_audio_original.duration is None or musica_audio_original.duration <= 0:
681
+ logger.warning("Clip de música de fondo parece inválido o tiene duración cero. Saltando música.")
682
+ try: musica_audio_original.close()
683
+ except: pass
684
+ musica_audio_original = None
685
+ else:
686
+ musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
687
+ logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
688
+
689
+ if musica_audio_looped is None or musica_audio_looped.duration is None or musica_audio_looped.duration <= 0:
690
+ logger.warning("Clip de música de fondo loopeado es inválido. Saltando música.")
691
+ try: musica_audio_looped.close()
692
+ except: pass
693
+ musica_audio_looped = None
694
+
695
+ if musica_audio_looped:
696
+ composite_audio = CompositeAudioClip([
697
+ musica_audio_looped.volumex(0.2), # Volumen 20% para música
698
+ audio_tts_original.volumex(1.0) # Volumen 100% para voz
699
+ ])
700
+
701
+ if composite_audio.duration is None or composite_audio.duration <= 0:
702
+ logger.warning("Clip de audio compuesto es inválido (None o duración cero). Usando solo audio de voz.")
703
+ try: composite_audio.close()
704
+ except: pass
705
+ final_audio = audio_tts_original
706
+ else:
707
+ logger.info("Mezcla de audio completada (voz + música).")
708
+ final_audio = composite_audio
709
+ musica_audio = musica_audio_looped # Asignar para limpieza
710
+
711
+ except Exception as e:
712
+ logger.warning(f"Error procesando música de fondo: {str(e)}", exc_info=True)
713
+ final_audio = audio_tts_original
714
+ musica_audio = None
715
+ logger.warning("Usando solo audio de voz debido a un error con la música.")
716
+
717
+ if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
718
+ 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.")
719
+ try:
720
+ if final_audio.duration > video_base.duration:
721
+ trimmed_final_audio = final_audio.subclip(0, video_base.duration)
722
+ if trimmed_final_audio is None or trimmed_final_audio.duration <= 0:
723
+ logger.warning("Audio final recortado es inválido. Usando audio final original.")
724
+ try: trimmed_final_audio.close()
725
+ except: pass
726
+ else:
727
+ if final_audio is not trimmed_final_audio:
728
+ try: final_audio.close()
729
+ except: pass
730
+ final_audio = trimmed_final_audio
731
+ logger.warning("Audio final recortado para que coincida con la duración del video.")
732
  except Exception as e:
733
+ logger.warning(f"Error ajustando duración del audio final: {str(e)}")
734
 
735
+ # 7. Crear video final
736
+ logger.info("Renderizando video final...")
737
  video_final = video_base.set_audio(final_audio)
738
+
739
+ if video_final is None or video_final.duration is None or video_final.duration <= 0:
740
+ logger.critical("Clip de video final (con audio) es inválido antes de escribir (None o duración cero).")
741
+ raise ValueError("Clip de video final es inválido antes de escribir.")
742
+
743
+ output_filename = "final_video.mp4"
744
+ output_path = os.path.join(temp_dir_intermediate, output_filename)
745
+ logger.info(f"Escribiendo video final a: {output_path}")
746
+
747
+ if not output_path or not isinstance(output_path, str):
748
+ logger.critical(f"output_path no es válido: {output_path}")
749
+ raise ValueError("El nombre del archivo de salida no es válido.")
750
+
751
+ try:
752
+ video_final.write_videofile(
753
+ filename=output_path,
754
+ fps=24,
755
+ threads=4,
756
+ codec="libx264",
757
+ audio_codec="aac",
758
+ preset="medium",
759
+ logger='bar'
760
+ )
761
+ except Exception as e:
762
+ logger.critical(f"Error al escribir el video final: {str(e)}", exc_info=True)
763
+ raise ValueError(f"Fallo al escribir el video final: {str(e)}")
764
 
765
  total_time = (datetime.now() - start_time).total_seconds()
766
  logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {output_path} | Tiempo total: {total_time:.2f}s")
767
+
768
  return output_path
769
 
770
+ except ValueError as ve:
771
+ logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
772
+ raise ve
773
  except Exception as e:
774
+ logger.critical(f"ERROR CRÍTICO NO CONTROLADO en crear_video: {str(e)}", exc_info=True)
775
+ raise e
776
  finally:
777
+ logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
778
+
779
+ for clip in source_clips:
780
+ try:
781
+ clip.close()
782
+ except Exception as e:
783
+ logger.warning(f"Error cerrando clip de video fuente en finally: {str(e)}")
784
+
785
+ for clip_segment in clips_to_concatenate:
786
+ try:
787
+ clip_segment.close()
788
+ except Exception as e:
789
+ logger.warning(f"Error cerrando segmento de video en finally: {str(e)}")
790
+
791
+ if musica_audio is not None:
792
+ try:
793
+ musica_audio.close()
794
+ except Exception as e:
795
+ logger.warning(f"Error cerrando musica_audio (procesada) en finally: {str(e)}")
796
+
797
+ if musica_audio_original is not None and musica_audio_original is not musica_audio:
798
+ try:
799
+ musica_audio_original.close()
800
+ except Exception as e:
801
+ logger.warning(f"Error cerrando musica_audio_original en finally: {str(e)}")
802
+
803
+ if audio_tts is not None and audio_tts is not audio_tts_original:
804
+ try:
805
+ audio_tts.close()
806
+ except Exception as e:
807
+ logger.warning(f"Error cerrando audio_tts (procesada) en finally: {str(e)}")
808
+
809
+ if audio_tts_original is not None:
810
+ try:
811
+ audio_tts_original.close()
812
+ except Exception as e:
813
+ logger.warning(f"Error cerrando audio_tts_original en finally: {str(e)}")
814
+
815
+ if video_final is not None:
816
+ try:
817
+ video_final.close()
818
+ except Exception as e:
819
+ logger.warning(f"Error cerrando video_final en finally: {str(e)}")
820
+ elif video_base is not None and video_base is not video_final:
821
+ try:
822
+ video_base.close()
823
+ except Exception as e:
824
+ logger.warning(f"Error cerrando video_base en finally: {str(e)}")
825
+
826
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
827
+ final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4")
828
+
829
+ for path in temp_intermediate_files:
830
+ try:
831
+ if os.path.isfile(path) and path != final_output_in_temp:
832
+ logger.debug(f"Eliminando archivo temporal intermedio: {path}")
833
+ os.remove(path)
834
+ elif os.path.isfile(path) and path == final_output_in_temp:
835
+ logger.debug(f"Saltando eliminación del archivo de video final: {path}")
836
+ except Exception as e:
837
+ logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
838
+
839
+ logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
840
 
841
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
842
  logger.info("="*80)
843
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
844
 
845
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
846
+
847
+ output_video = None
848
+ output_file = gr.update(value=None, visible=False)
849
+ status_msg = gr.update(value="⏳ Procesando...", interactive=False)
850
 
851
  if not input_text or not input_text.strip():
852
+ logger.warning("Texto de entrada vacío.")
853
+ status_msg = gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
854
+ return output_video, output_file, status_msg
855
+
856
+ logger.info(f"Tipo de entrada: {prompt_type}")
857
+ logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
858
+ if musica_file:
859
+ logger.info(f"Archivo de música recibido: {musica_file}")
860
+ else:
861
+ logger.info("No se proporcionó archivo de música.")
862
 
863
  try:
864
+ logger.info("Llamando a crear_video...")
865
  video_path = crear_video(prompt_type, input_text, musica_file)
866
+
867
  if video_path and os.path.exists(video_path):
868
+ logger.info(f"crear_video retornó path: {video_path}")
869
+ logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
870
  output_video = video_path
871
  output_file = gr.update(value=video_path, visible=True)
872
  status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
 
 
 
 
 
 
 
 
 
873
  else:
874
+ logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
875
+ status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
876
+
877
+ except ValueError as ve:
878
+ logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
879
+ status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
880
  except Exception as e:
881
+ logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
882
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
883
  finally:
884
  logger.info("Fin del handler run_app.")
 
894
 
895
  with gr.Row():
896
  with gr.Column():
897
+ prompt_type = gr.Radio(
898
+ ["Generar Guion con IA", "Usar Mi Guion"],
899
+ label="Método de Entrada",
900
+ value="Generar Guion con IA"
901
+ )
902
+
903
  with gr.Column(visible=True) as ia_guion_column:
904
+ prompt_ia = gr.Textbox(
905
+ label="Tema para IA",
906
+ lines=2,
907
+ placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer, mostrando la belleza de la naturaleza...",
908
+ max_lines=4,
909
+ value=""
910
+ )
911
+
912
  with gr.Column(visible=False) as manual_guion_column:
913
+ prompt_manual = gr.Textbox(
914
+ label="Tu Guion Completo",
915
+ lines=5,
916
+ 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!",
917
+ max_lines=10,
918
+ value=""
919
+ )
920
+
921
+ musica_input = gr.Audio(
922
+ label="Música de fondo (opcional)",
923
+ type="filepath",
924
+ interactive=True,
925
+ value=None
926
+ )
927
+
928
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
929
 
930
  with gr.Column():
931
+ video_output = gr.Video(
932
+ label="Previsualización del Video Generado",
933
+ interactive=False,
934
+ height=400
935
+ )
936
+ file_output = gr.File(
937
+ label="Descargar Archivo de Video",
938
+ interactive=False,
939
+ visible=False
940
+ )
941
+ status_output = gr.Textbox(
942
+ label="Estado",
943
+ interactive=False,
944
+ show_label=False,
945
+ placeholder="Esperando acción...",
946
+ value="Esperando entrada..."
947
+ )
948
+
949
+ prompt_type.change(
950
+ lambda x: (gr.update(visible=x == "Generar Guion con IA"),
951
+ gr.update(visible=x == "Usar Mi Guion")),
952
+ inputs=prompt_type,
953
+ outputs=[ia_guion_column, manual_guion_column]
954
+ )
955
+
956
  generate_btn.click(
957
  lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
958
+ outputs=[video_output, file_output, status_output],
959
+ queue=True,
960
  ).then(
961
+ run_app,
962
+ inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
963
+ outputs=[video_output, file_output, status_output]
964
  )
965
 
966
  gr.Markdown("### Instrucciones:")
967
+ gr.Markdown("""
968
+ 1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
969
+ 2. **Selecciona el tipo de entrada**:
970
+ - "Generar Guion con IA": Describe brevemente un tema (ej. "La belleza de las montañas"). La IA generará un guion corto.
971
+ - "Usar Mi Guion": Escribe el guion completo que quieres para el video.
972
+ 3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.) para usar como música de fondo.
973
+ 4. **Haz clic en "✨ Generar Video"**.
974
+ 5. Espera a que se procese el video. El tiempo de espera puede variar. Verás el estado en el cuadro de texto.
975
+ 6. La previsualización del video aparecerá arriba (puede fallar para archivos grandes), y un enlace **Descargar Archivo de Video** se mostrará si la generación fue exitosa.
976
+ 7. Si hay errores, revisa el log `video_generator_full.log` para más detalles.
977
+ """)
978
  gr.Markdown("---")
979
  gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
980
 
981
  if __name__ == "__main__":
982
+ logger.info("Verificando dependencias críticas...")
983
+ try:
984
+ from moviepy.editor import ColorClip
985
+ try:
986
+ temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
987
+ temp_clip.close()
988
+ logger.info("Clips base de MoviePy (como ColorClip) creados y cerrados exitosamente. FFmpeg parece accesible.")
989
+ except Exception as e:
990
+ logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
991
+
992
+ except Exception as e:
993
+ logger.critical(f"Fallo al importar MoviePy. Asegúrate de que está instalado. Error: {e}", exc_info=True)
994
+
995
  logger.info("Iniciando aplicación Gradio...")
996
  try:
997
  app.launch(server_name="0.0.0.0", server_port=7860, share=False)