gnosticdev commited on
Commit
80b1aba
·
verified ·
1 Parent(s): 7b02e0f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +14 -37
app.py CHANGED
@@ -26,14 +26,13 @@ except ImportError:
26
  except Exception as e:
27
  logger.critical(f"Fallo al instalar moviepy: {str(e)}")
28
  logger.info("Continuando con placeholder para pruebas...")
29
- moviepy = None # Placeholder para evitar errores
30
  import re
31
  import math
32
  import shutil
33
  import json
34
  from collections import Counter
35
 
36
- # Configuración de logging
37
  logging.basicConfig(
38
  level=logging.DEBUG,
39
  format='%(asctime)s - %(levelname)s - %(message)s',
@@ -46,7 +45,6 @@ logger.info("="*80)
46
  logger.info("INICIO DE EJECUCIÓN - GENERADOR DE VIDEOS")
47
  logger.info("="*80)
48
 
49
- # Diccionario de voces TTS disponibles organizadas por idioma
50
  VOCES_DISPONIBLES = {
51
  "Español (España)": {
52
  "es-ES-JuanNeural": "Juan (España) - Masculino",
@@ -109,7 +107,6 @@ VOCES_DISPONIBLES = {
109
  }
110
  }
111
 
112
- # Función para obtener lista plana de voces para el dropdown
113
  def get_voice_choices():
114
  choices = []
115
  for region, voices in VOCES_DISPONIBLES.items():
@@ -117,9 +114,8 @@ def get_voice_choices():
117
  choices.append((f"{voice_name} ({region})", voice_id))
118
  return choices
119
 
120
- # Obtener las voces al inicio del script
121
  AVAILABLE_VOICES = get_voice_choices()
122
- DEFAULT_VOICE_ID = "es-MX-DaliaNeural" # Voz más estable
123
  DEFAULT_VOICE_NAME = DEFAULT_VOICE_ID
124
  for text, voice_id in AVAILABLE_VOICES:
125
  if voice_id == DEFAULT_VOICE_ID:
@@ -130,12 +126,10 @@ if DEFAULT_VOICE_ID not in [v[1] for v in AVAILABLE_VOICES]:
130
  DEFAULT_VOICE_NAME = AVAILABLE_VOICES[0][0] if AVAILABLE_VOICES else "Dalia (México) - Femenino"
131
  logger.info(f"Voz por defecto seleccionada (ID): {DEFAULT_VOICE_ID}")
132
 
133
- # Clave API de Pexels
134
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
135
  if not PEXELS_API_KEY:
136
  logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
137
 
138
- # Inicialización de modelos
139
  MODEL_NAME = "datificate/gpt2-small-spanish"
140
  logger.info(f"Inicializando modelo GPT-2: {MODEL_NAME}")
141
  tokenizer = None
@@ -163,7 +157,6 @@ def buscar_videos_pexels(query, api_key, per_page=5):
163
  if not api_key:
164
  logger.warning("No se puede buscar en Pexels: API Key no configurada.")
165
  return []
166
-
167
  logger.debug(f"Buscando en Pexels: '{query}' | Resultados: {per_page}")
168
  headers = {"Authorization": api_key}
169
  try:
@@ -199,10 +192,8 @@ def generate_script(prompt, max_length=150):
199
  if not tokenizer or not model:
200
  logger.warning("Modelos GPT-2 no disponibles - Usando prompt original como guion.")
201
  return prompt.strip()
202
-
203
  instruction_phrase_start = "Escribe un guion corto, interesante y coherente sobre:"
204
  ai_prompt = f"{instruction_phrase_start} {prompt}"
205
-
206
  try:
207
  inputs = tokenizer(ai_prompt, return_tensors="pt", truncation=True, max_length=512)
208
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
@@ -263,7 +254,6 @@ async def text_to_speech(text, output_path, voice):
263
  logger.warning(f"edge_tts falló, intentando gTTS...")
264
  except Exception as e:
265
  logger.error(f"Error en edge_tts con voz '{voice}': {str(e)}")
266
-
267
  try:
268
  tts = gTTS(text=text, lang='es')
269
  tts.save(output_path)
@@ -382,15 +372,14 @@ async def crear_video_async(prompt_type, input_text, selected_voice, musica_file
382
  video_final = None
383
  source_clips = []
384
  clips_to_concatenate = []
 
385
 
386
  try:
387
- # 1. Generar o usar guion
388
  guion = generate_script(input_text) if prompt_type == "Generar Guion con IA" else input_text.strip()
389
  logger.info(f"Guion final ({len(guion)} chars): '{guion[:100]}...'")
390
  if not guion.strip():
391
  raise ValueError("El guion está vacío.")
392
 
393
- # 2. Generar audio de voz
394
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
395
  tts_voices_to_try = [selected_voice, "es-MX-DaliaNeural"]
396
  tts_success = False
@@ -444,18 +433,16 @@ async def crear_video_async(prompt_type, input_text, selected_voice, musica_file
444
  audio_duration = audio_tts_original.duration
445
  else:
446
  logger.warning("MoviePy no disponible, asumiendo duración mínima para audio")
447
- audio_duration = 1.0 # Valor placeholder
448
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
449
  if audio_duration < 1.0:
450
  raise ValueError("Audio de voz demasiado corto.")
451
 
452
- # 3. Extraer palabras clave
453
  keywords = extract_visual_keywords_from_script(guion)
454
  if not keywords:
455
  keywords = ["video", "background"]
456
  logger.info(f"Palabras clave: {keywords}")
457
 
458
- # 4. Buscar y descargar videos
459
  videos_data = []
460
  total_desired_videos = 10
461
  per_page_per_keyword = max(1, total_desired_videos // len(keywords))
@@ -490,14 +477,13 @@ async def crear_video_async(prompt_type, input_text, selected_voice, musica_file
490
  if not video_paths:
491
  raise ValueError("No se descargaron videos utilizables.")
492
 
493
- # 5. Procesar y concatenar clips de video
494
  if not moviepy:
495
  logger.warning("MoviePy no disponible, retornando placeholder...")
496
  output_filename = f"video_{int(datetime.now().timestamp())}.mp4"
497
- persistent_path = os.path.join(temp_dir_intermediate, output_filename)
498
- open(persistent_path, 'a').close() # Crea archivo vacío como placeholder
499
- download_url = f"https://gnosticdev-invideo-basic.hf.space/file={persistent_path}"
500
- return persistent_path, download_url
501
 
502
  current_duration = 0
503
  min_clip_duration = 0.5
@@ -526,7 +512,6 @@ async def crear_video_async(prompt_type, input_text, selected_voice, musica_file
526
  if video_base.duration is None or video_base.duration <= 0:
527
  raise ValueError("Video base inválido.")
528
 
529
- # Ajustar duración del video
530
  if video_base.duration < audio_duration:
531
  num_full_repeats = int(audio_duration // video_base.duration)
532
  remaining_duration = audio_duration % video_base.duration
@@ -538,7 +523,6 @@ async def crear_video_async(prompt_type, input_text, selected_voice, musica_file
538
  elif video_base.duration > audio_duration:
539
  video_base = video_base.subclip(0, audio_duration)
540
 
541
- # 6. Manejar música de fondo
542
  final_audio = audio_tts
543
  if musica_file:
544
  try:
@@ -559,7 +543,6 @@ async def crear_video_async(prompt_type, input_text, selected_voice, musica_file
559
  if abs(final_audio.duration - video_base.duration) > 0.2:
560
  final_audio = final_audio.subclip(0, video_base.duration)
561
 
562
- # 7. Combinar audio y video
563
  video_final = video_base.set_audio(final_audio)
564
  output_filename = f"video_{int(datetime.now().timestamp())}.mp4"
565
  output_path = os.path.join(temp_dir_intermediate, output_filename)
@@ -620,11 +603,16 @@ async def crear_video_async(prompt_type, input_text, selected_voice, musica_file
620
  except:
621
  pass
622
  for path in temp_intermediate_files:
623
- if os.path.isfile(path) and path != output_path:
624
  try:
625
  os.remove(path)
626
  except:
627
  logger.warning(f"No se pudo eliminar {path}")
 
 
 
 
 
628
 
629
  async def run_app_async(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
630
  logger.info("="*80)
@@ -644,10 +632,7 @@ async def run_app_async(prompt_type, prompt_ia, prompt_manual, musica_file, sele
644
  selected_voice = DEFAULT_VOICE_ID
645
 
646
  try:
647
- # Crear tarea para generar video
648
  task = asyncio.create_task(crear_video_async(prompt_type, input_text, selected_voice, musica_file))
649
-
650
- # Verificar progreso cada 5 segundos durante 10 minutos (600s)
651
  timeout = 600
652
  interval = 5
653
  elapsed = 0
@@ -668,8 +653,6 @@ async def run_app_async(prompt_type, prompt_ia, prompt_manual, musica_file, sele
668
  elapsed += interval
669
  status_msg = gr.update(value=f"⏳ Procesando... Tiempo transcurrido: {elapsed}s")
670
  logger.debug(f"Esperando video, tiempo transcurrido: {elapsed}s")
671
-
672
- # Si se excede el timeout
673
  logger.error("Tiempo de espera excedido para la generación del video.")
674
  status_msg = gr.update(value="❌ Error: Tiempo de espera excedido (10 minutos).")
675
  task.cancel()
@@ -678,7 +661,6 @@ async def run_app_async(prompt_type, prompt_ia, prompt_manual, musica_file, sele
678
  except asyncio.CancelledError:
679
  pass
680
  return None, None, status_msg
681
-
682
  except ValueError as ve:
683
  logger.warning(f"Error de validación: {str(ve)}")
684
  status_msg = gr.update(value=f"⚠️ Error: {str(ve)}")
@@ -691,11 +673,9 @@ async def run_app_async(prompt_type, prompt_ia, prompt_manual, musica_file, sele
691
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
692
  return asyncio.run(run_app_async(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice))
693
 
694
- # Interfaz de Gradio
695
  with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft()) as app:
696
  gr.Markdown("# 🎬 Generador Automático de Videos con IA")
697
  gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
698
-
699
  with gr.Row():
700
  with gr.Column():
701
  prompt_type = gr.Radio(
@@ -746,13 +726,11 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft()) as ap
746
  placeholder="Esperando acción...",
747
  value="Esperando entrada..."
748
  )
749
-
750
  prompt_type.change(
751
  fn=lambda x: (gr.update(visible=x == "Generar Guion con IA"), gr.update(visible=x == "Usar Mi Guion")),
752
  inputs=prompt_type,
753
  outputs=[ia_guion_column, manual_guion_column]
754
  )
755
-
756
  generate_btn.click(
757
  fn=lambda: (None, None, gr.update(value="⏳ Iniciando generación de video...")),
758
  outputs=[video_output, file_output, status_output]
@@ -766,7 +744,6 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft()) as ap
766
  inputs=[video_output, file_output, status_output],
767
  outputs=[file_output]
768
  )
769
-
770
  gr.Markdown("### Instrucciones:")
771
  gr.Markdown("""
772
  1. Configura la variable de entorno `PEXELS_API_KEY`.
 
26
  except Exception as e:
27
  logger.critical(f"Fallo al instalar moviepy: {str(e)}")
28
  logger.info("Continuando con placeholder para pruebas...")
29
+ moviepy = None
30
  import re
31
  import math
32
  import shutil
33
  import json
34
  from collections import Counter
35
 
 
36
  logging.basicConfig(
37
  level=logging.DEBUG,
38
  format='%(asctime)s - %(levelname)s - %(message)s',
 
45
  logger.info("INICIO DE EJECUCIÓN - GENERADOR DE VIDEOS")
46
  logger.info("="*80)
47
 
 
48
  VOCES_DISPONIBLES = {
49
  "Español (España)": {
50
  "es-ES-JuanNeural": "Juan (España) - Masculino",
 
107
  }
108
  }
109
 
 
110
  def get_voice_choices():
111
  choices = []
112
  for region, voices in VOCES_DISPONIBLES.items():
 
114
  choices.append((f"{voice_name} ({region})", voice_id))
115
  return choices
116
 
 
117
  AVAILABLE_VOICES = get_voice_choices()
118
+ DEFAULT_VOICE_ID = "es-MX-DaliaNeural"
119
  DEFAULT_VOICE_NAME = DEFAULT_VOICE_ID
120
  for text, voice_id in AVAILABLE_VOICES:
121
  if voice_id == DEFAULT_VOICE_ID:
 
126
  DEFAULT_VOICE_NAME = AVAILABLE_VOICES[0][0] if AVAILABLE_VOICES else "Dalia (México) - Femenino"
127
  logger.info(f"Voz por defecto seleccionada (ID): {DEFAULT_VOICE_ID}")
128
 
 
129
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
130
  if not PEXELS_API_KEY:
131
  logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
132
 
 
133
  MODEL_NAME = "datificate/gpt2-small-spanish"
134
  logger.info(f"Inicializando modelo GPT-2: {MODEL_NAME}")
135
  tokenizer = None
 
157
  if not api_key:
158
  logger.warning("No se puede buscar en Pexels: API Key no configurada.")
159
  return []
 
160
  logger.debug(f"Buscando en Pexels: '{query}' | Resultados: {per_page}")
161
  headers = {"Authorization": api_key}
162
  try:
 
192
  if not tokenizer or not model:
193
  logger.warning("Modelos GPT-2 no disponibles - Usando prompt original como guion.")
194
  return prompt.strip()
 
195
  instruction_phrase_start = "Escribe un guion corto, interesante y coherente sobre:"
196
  ai_prompt = f"{instruction_phrase_start} {prompt}"
 
197
  try:
198
  inputs = tokenizer(ai_prompt, return_tensors="pt", truncation=True, max_length=512)
199
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
254
  logger.warning(f"edge_tts falló, intentando gTTS...")
255
  except Exception as e:
256
  logger.error(f"Error en edge_tts con voz '{voice}': {str(e)}")
 
257
  try:
258
  tts = gTTS(text=text, lang='es')
259
  tts.save(output_path)
 
372
  video_final = None
373
  source_clips = []
374
  clips_to_concatenate = []
375
+ output_path = None
376
 
377
  try:
 
378
  guion = generate_script(input_text) if prompt_type == "Generar Guion con IA" else input_text.strip()
379
  logger.info(f"Guion final ({len(guion)} chars): '{guion[:100]}...'")
380
  if not guion.strip():
381
  raise ValueError("El guion está vacío.")
382
 
 
383
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
384
  tts_voices_to_try = [selected_voice, "es-MX-DaliaNeural"]
385
  tts_success = False
 
433
  audio_duration = audio_tts_original.duration
434
  else:
435
  logger.warning("MoviePy no disponible, asumiendo duración mínima para audio")
436
+ audio_duration = 1.0
437
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
438
  if audio_duration < 1.0:
439
  raise ValueError("Audio de voz demasiado corto.")
440
 
 
441
  keywords = extract_visual_keywords_from_script(guion)
442
  if not keywords:
443
  keywords = ["video", "background"]
444
  logger.info(f"Palabras clave: {keywords}")
445
 
 
446
  videos_data = []
447
  total_desired_videos = 10
448
  per_page_per_keyword = max(1, total_desired_videos // len(keywords))
 
477
  if not video_paths:
478
  raise ValueError("No se descargaron videos utilizables.")
479
 
 
480
  if not moviepy:
481
  logger.warning("MoviePy no disponible, retornando placeholder...")
482
  output_filename = f"video_{int(datetime.now().timestamp())}.mp4"
483
+ output_path = os.path.join(temp_dir_intermediate, output_filename)
484
+ open(output_path, 'a').close()
485
+ download_url = f"https://gnosticdev-invideo-basic.hf.space/file={output_path}"
486
+ return output_path, download_url
487
 
488
  current_duration = 0
489
  min_clip_duration = 0.5
 
512
  if video_base.duration is None or video_base.duration <= 0:
513
  raise ValueError("Video base inválido.")
514
 
 
515
  if video_base.duration < audio_duration:
516
  num_full_repeats = int(audio_duration // video_base.duration)
517
  remaining_duration = audio_duration % video_base.duration
 
523
  elif video_base.duration > audio_duration:
524
  video_base = video_base.subclip(0, audio_duration)
525
 
 
526
  final_audio = audio_tts
527
  if musica_file:
528
  try:
 
543
  if abs(final_audio.duration - video_base.duration) > 0.2:
544
  final_audio = final_audio.subclip(0, video_base.duration)
545
 
 
546
  video_final = video_base.set_audio(final_audio)
547
  output_filename = f"video_{int(datetime.now().timestamp())}.mp4"
548
  output_path = os.path.join(temp_dir_intermediate, output_filename)
 
603
  except:
604
  pass
605
  for path in temp_intermediate_files:
606
+ if os.path.isfile(path) and (output_path is None or path != output_path):
607
  try:
608
  os.remove(path)
609
  except:
610
  logger.warning(f"No se pudo eliminar {path}")
611
+ try:
612
+ if os.path.exists(temp_dir_intermediate) and (output_path is None or not output_path.startswith(temp_dir_intermediate)):
613
+ shutil.rmtree(temp_dir_intermediate)
614
+ except:
615
+ logger.warning(f"No se pudo eliminar directorio temporal {temp_dir_intermediate}")
616
 
617
  async def run_app_async(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
618
  logger.info("="*80)
 
632
  selected_voice = DEFAULT_VOICE_ID
633
 
634
  try:
 
635
  task = asyncio.create_task(crear_video_async(prompt_type, input_text, selected_voice, musica_file))
 
 
636
  timeout = 600
637
  interval = 5
638
  elapsed = 0
 
653
  elapsed += interval
654
  status_msg = gr.update(value=f"⏳ Procesando... Tiempo transcurrido: {elapsed}s")
655
  logger.debug(f"Esperando video, tiempo transcurrido: {elapsed}s")
 
 
656
  logger.error("Tiempo de espera excedido para la generación del video.")
657
  status_msg = gr.update(value="❌ Error: Tiempo de espera excedido (10 minutos).")
658
  task.cancel()
 
661
  except asyncio.CancelledError:
662
  pass
663
  return None, None, status_msg
 
664
  except ValueError as ve:
665
  logger.warning(f"Error de validación: {str(ve)}")
666
  status_msg = gr.update(value=f"⚠️ Error: {str(ve)}")
 
673
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
674
  return asyncio.run(run_app_async(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice))
675
 
 
676
  with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft()) as app:
677
  gr.Markdown("# 🎬 Generador Automático de Videos con IA")
678
  gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
 
679
  with gr.Row():
680
  with gr.Column():
681
  prompt_type = gr.Radio(
 
726
  placeholder="Esperando acción...",
727
  value="Esperando entrada..."
728
  )
 
729
  prompt_type.change(
730
  fn=lambda x: (gr.update(visible=x == "Generar Guion con IA"), gr.update(visible=x == "Usar Mi Guion")),
731
  inputs=prompt_type,
732
  outputs=[ia_guion_column, manual_guion_column]
733
  )
 
734
  generate_btn.click(
735
  fn=lambda: (None, None, gr.update(value="⏳ Iniciando generación de video...")),
736
  outputs=[video_output, file_output, status_output]
 
744
  inputs=[video_output, file_output, status_output],
745
  outputs=[file_output]
746
  )
 
747
  gr.Markdown("### Instrucciones:")
748
  gr.Markdown("""
749
  1. Configura la variable de entorno `PEXELS_API_KEY`.