gnosticdev commited on
Commit
3d41bc8
·
verified ·
1 Parent(s): 506f1df

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +82 -52
app.py CHANGED
@@ -9,7 +9,7 @@ import gradio as gr
9
  import torch
10
  from transformers import GPT2Tokenizer, GPT2LMHeadModel
11
  from keybert import KeyBERT
12
- # Importación correcta: Solo 'concatenate_videoclips'
13
  from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
14
  import re
15
  import math
@@ -61,6 +61,34 @@ except Exception as e:
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.")
@@ -175,7 +203,7 @@ def generate_script(prompt, max_length=150):
175
  logger.warning("Usando prompt original como guion debido al error de generación.")
176
  return prompt.strip()
177
 
178
- # Función TTS con voz especificada
179
  async def text_to_speech(text, output_path, voice):
180
  logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
181
  if not text or not text.strip():
@@ -346,10 +374,11 @@ def extract_visual_keywords_from_script(script_text):
346
  logger.info(f"Palabras clave finales: {top_keywords}")
347
  return top_keywords
348
 
349
- def crear_video(prompt_type, input_text, musica_file=None):
350
  logger.info("="*80)
351
  logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
352
  logger.debug(f"Input: '{input_text[:100]}...'")
 
353
 
354
  start_time = datetime.now()
355
  temp_dir_intermediate = None
@@ -380,35 +409,31 @@ def crear_video(prompt_type, input_text, musica_file=None):
380
  logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
381
  temp_intermediate_files = []
382
 
383
- # 2. Generar audio de voz con reintentos y voz de respaldo
384
  logger.info("Generando audio de voz...")
385
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
386
 
387
- primary_voice = "es-ES-JuanNeural"
388
- fallback_voice = "es-ES-ElviraNeural" # Otra voz en español
389
- tts_success = False
390
- retries = 3
391
 
392
- for attempt in range(retries):
393
- current_voice = primary_voice if attempt == 0 else fallback_voice
394
- if attempt > 0: logger.warning(f"Reintentando TTS ({attempt+1}/{retries})...")
395
- logger.info(f"Intentando TTS con voz: {current_voice}")
396
- try:
397
- tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=current_voice))
398
- if tts_success:
399
- logger.info(f"TTS exitoso en intento {attempt + 1} con voz {current_voice}.")
400
- break
401
- except Exception as e:
402
- pass
403
 
404
- if not tts_success and attempt == 0 and primary_voice != fallback_voice:
405
- logger.warning(f"Fallo con voz {primary_voice}, intentando voz de respaldo: {fallback_voice}")
406
- elif not tts_success and attempt < retries - 1:
407
- logger.warning(f"Fallo con voz {current_voice}, reintentando...")
 
408
 
409
 
 
410
  if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
411
- logger.error(f"Fallo en la generación de voz después de {retries} intentos. Archivo de audio no creado o es muy pequeño.")
412
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
413
 
414
  temp_intermediate_files.append(voz_path)
@@ -843,12 +868,11 @@ def crear_video(prompt_type, input_text, musica_file=None):
843
  logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
844
 
845
 
846
- # La función run_app ahora recibe todos los inputs de texto y el archivo de música
847
- def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
848
  logger.info("="*80)
849
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
850
 
851
- # Elegir el texto de entrada basado en el prompt_type
852
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
853
 
854
  output_video = None
@@ -857,26 +881,30 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
857
 
858
  if not input_text or not input_text.strip():
859
  logger.warning("Texto de entrada vacío.")
860
- # Retornar None para video y archivo, actualizar estado con mensaje de error
861
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
862
 
 
 
 
 
863
  logger.info(f"Tipo de entrada: {prompt_type}")
864
  logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
865
  if musica_file:
866
  logger.info(f"Archivo de música recibido: {musica_file}")
867
  else:
868
  logger.info("No se proporcionó archivo de música.")
 
869
 
870
  try:
871
  logger.info("Llamando a crear_video...")
872
- # Pasar el input_text elegido y el archivo de música a crear_video
873
- video_path = crear_video(prompt_type, input_text, musica_file)
874
 
875
  if video_path and os.path.exists(video_path):
876
  logger.info(f"crear_video retornó path: {video_path}")
877
  logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
878
- output_video = video_path # Establecer valor del componente de video
879
- output_file = video_path # Establecer valor del componente de archivo para descarga
880
  status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
881
  else:
882
  logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
@@ -890,7 +918,6 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
890
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
891
  finally:
892
  logger.info("Fin del handler run_app.")
893
- # Retornar las tres salidas esperadas por el evento click
894
  return output_video, output_file, status_msg
895
 
896
 
@@ -912,12 +939,11 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
912
  )
913
 
914
  # Contenedores para los campos de texto para controlar la visibilidad
915
- # Nombrados para que coincidan con los outputs del evento change
916
  with gr.Column(visible=True) as ia_guion_column:
917
  prompt_ia = gr.Textbox(
918
  label="Tema para IA",
919
  lines=2,
920
- placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer, mostrando la belleza de la naturaleza...",
921
  max_lines=4,
922
  value=""
923
  )
@@ -937,6 +963,16 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
937
  interactive=True,
938
  value=None
939
  )
 
 
 
 
 
 
 
 
 
 
940
 
941
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
942
 
@@ -949,7 +985,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
949
  file_output = gr.File(
950
  label="Descargar Archivo de Video",
951
  interactive=False,
952
- visible=False # Ocultar inicialmente
953
  )
954
  status_output = gr.Textbox(
955
  label="Estado",
@@ -960,37 +996,30 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
960
  )
961
 
962
  # Evento para mostrar/ocultar los campos de texto según el tipo de prompt
963
- # Apuntar a los componentes Column padre para controlar la visibilidad
964
  prompt_type.change(
965
  lambda x: (gr.update(visible=x == "Generar Guion con IA"),
966
  gr.update(visible=x == "Usar Mi Guion")),
967
  inputs=prompt_type,
968
- # Pasar los componentes Column
969
  outputs=[ia_guion_column, manual_guion_column]
970
  )
971
 
972
  # Evento click del botón de generar video
973
  generate_btn.click(
974
- # Acción 1 (síncrona): Resetear salidas y establecer estado a procesando
975
- # Retorna None para los 3 outputs iniciales
976
  lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
977
  outputs=[video_output, file_output, status_output],
978
- queue=True, # Usar la cola de Gradio para tareas largas
979
  ).then(
980
  # Acción 2 (asíncrona): Llamar a la función principal de procesamiento
981
  run_app,
982
- # PASAR TODOS LOS INPUTS DE LA INTERFAZ que run_app espera
983
- inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
984
- # run_app retornará los 3 outputs esperados aquí
985
  outputs=[video_output, file_output, status_output]
986
  ).then(
987
  # Acción 3 (síncrona): Hacer visible el enlace de descarga si se retornó un archivo
988
- # Esta función recibe las salidas de la Acción 2 (video_path, file_path, status_msg)
989
- # Solo necesitamos video_path o file_path para decidir si mostrar el enlace
990
  lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
991
- # Inputs son las salidas de la función .then() anterior
992
  inputs=[video_output, file_output, status_output],
993
- # Actualizamos la visibilidad del componente file_output
994
  outputs=[file_output]
995
  )
996
 
@@ -1002,10 +1031,11 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1002
  - "Generar Guion con IA": Describe brevemente un tema (ej. "La belleza de las montañas"). La IA generará un guion corto.
1003
  - "Usar Mi Guion": Escribe el guion completo que quieres para el video.
1004
  3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.) para usar como música de fondo.
1005
- 4. **Haz clic en "✨ Generar Video"**.
1006
- 5. Espera a que se procese el video. El tiempo de espera puede variar. Verás el estado en el cuadro de texto.
1007
- 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.
1008
- 7. Si hay errores, revisa el log `video_generator_full.log` para más detalles.
 
1009
  """)
1010
  gr.Markdown("---")
1011
  gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
 
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
 
61
  logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
62
  kw_model = None
63
 
64
+ # --- NUEVA FUNCIÓN: Obtener voces de Edge TTS ---
65
+ async def get_available_voices():
66
+ logger.info("Obteniendo lista de voces disponibles de Edge TTS...")
67
+ try:
68
+ voices = await edge_tts.VoicesManager.create()
69
+ # Filtrar solo voces en español si prefieres, o dejar todas
70
+ # es_voices = [voice.Name for voice in voices.Voices if voice.Locale.startswith('es-')]
71
+ # return es_voices if es_voices else [voice.Name for voice in voices.Voices]
72
+
73
+ # O simplemente retornar todas las voces
74
+ all_voices = [voice.Name for voice in voices.Voices]
75
+ logger.info(f"Encontradas {len(all_voices)} voces de Edge TTS.")
76
+ return all_voices
77
+
78
+ except Exception as e:
79
+ logger.error(f"Error obteniendo voces de Edge TTS: {str(e)}", exc_info=True)
80
+ # Retornar una lista de voces por defecto si falla la API de Edge TTS
81
+ logger.warning("No se pudieron obtener voces de Edge TTS. Usando lista de voces por defecto.")
82
+ return ["es-ES-JuanNeural", "es-ES-ElviraNeural", "en-US-AriaNeural"]
83
+
84
+ # Obtener las voces al inicio del script (esto puede tardar un poco)
85
+ logger.info("Inicializando lista de voces disponibles...")
86
+ AVAILABLE_VOICES = asyncio.run(get_available_voices())
87
+ # Establecer una voz por defecto inicial
88
+ DEFAULT_VOICE = "es-ES-JuanNeural" if "es-ES-JuanNeural" in AVAILABLE_VOICES else (AVAILABLE_VOICES[0] if AVAILABLE_VOICES else "en-US-AriaNeural")
89
+ logger.info(f"Voz por defecto seleccionada: {DEFAULT_VOICE}")
90
+
91
+
92
  def buscar_videos_pexels(query, api_key, per_page=5):
93
  if not api_key:
94
  logger.warning("No se puede buscar en Pexels: API Key no configurada.")
 
203
  logger.warning("Usando prompt original como guion debido al error de generación.")
204
  return prompt.strip()
205
 
206
+ # Función TTS ahora recibe la voz a usar
207
  async def text_to_speech(text, output_path, voice):
208
  logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
209
  if not text or not text.strip():
 
374
  logger.info(f"Palabras clave finales: {top_keywords}")
375
  return top_keywords
376
 
377
+ def crear_video(prompt_type, input_text, selected_voice, musica_file=None): # <-- AHORA RECIBE selected_voice
378
  logger.info("="*80)
379
  logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
380
  logger.debug(f"Input: '{input_text[:100]}...'")
381
+ logger.info(f"Voz seleccionada para TTS: {selected_voice}") # <-- LOGUEAR la voz seleccionada
382
 
383
  start_time = datetime.now()
384
  temp_dir_intermediate = None
 
409
  logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
410
  temp_intermediate_files = []
411
 
412
+ # 2. Generar audio de voz usando la voz seleccionada
413
  logger.info("Generando audio de voz...")
414
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
415
 
416
+ # Ya no necesitamos reintentos/fallback aquí, la voz viene seleccionada
417
+ tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=selected_voice))
 
 
418
 
419
+ # Si falla la generación con la voz seleccionada, intentar con una voz de respaldo
420
+ if not tts_success:
421
+ logger.warning(f"La generación de TTS falló con la voz seleccionada '{selected_voice}'. Intentando con voz de respaldo 'es-ES-ElviraNeural'.")
422
+ fallback_voice = "es-ES-ElviraNeural"
423
+ if selected_voice == fallback_voice: # Evitar reintentar con la misma voz fallida
424
+ fallback_voice = "en-US-AriaNeural" # O alguna otra conocida
425
+ logger.warning(f"La voz de respaldo era la misma que falló. Intentando otra voz de respaldo: {fallback_voice}")
 
 
 
 
426
 
427
+ tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=fallback_voice))
428
+ if tts_success:
429
+ logger.info(f"TTS exitoso con voz de respaldo: {fallback_voice}.")
430
+ else:
431
+ logger.error(f"La generación de TTS falló también con la voz de respaldo.")
432
 
433
 
434
+ # Verificar si el archivo fue creado después de los intentos
435
  if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
436
+ logger.error(f"Fallo en la generación de voz. Archivo de audio no creado o es muy pequeño.")
437
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
438
 
439
  temp_intermediate_files.append(voz_path)
 
868
  logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
869
 
870
 
871
+ # run_app ahora recibe todos los inputs, incluyendo la voz seleccionada
872
+ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice): # <-- AHORA RECIBE selected_voice
873
  logger.info("="*80)
874
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
875
 
 
876
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
877
 
878
  output_video = None
 
881
 
882
  if not input_text or not input_text.strip():
883
  logger.warning("Texto de entrada vacío.")
 
884
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
885
 
886
+ if not selected_voice or selected_voice not in AVAILABLE_VOICES:
887
+ logger.warning(f"Voz seleccionada inválida o vacía: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE}.")
888
+ selected_voice = DEFAULT_VOICE # Usar voz por defecto si la seleccionada es inválida
889
+
890
  logger.info(f"Tipo de entrada: {prompt_type}")
891
  logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
892
  if musica_file:
893
  logger.info(f"Archivo de música recibido: {musica_file}")
894
  else:
895
  logger.info("No se proporcionó archivo de música.")
896
+ logger.info(f"Voz seleccionada (validada): {selected_voice}") # Loguear la voz validada
897
 
898
  try:
899
  logger.info("Llamando a crear_video...")
900
+ # Pasar la voz seleccionada a crear_video
901
+ video_path = crear_video(prompt_type, input_text, selected_voice, musica_file) # <-- PASAR selected_voice
902
 
903
  if video_path and os.path.exists(video_path):
904
  logger.info(f"crear_video retornó path: {video_path}")
905
  logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
906
+ output_video = video_path
907
+ output_file = video_path
908
  status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
909
  else:
910
  logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
 
918
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
919
  finally:
920
  logger.info("Fin del handler run_app.")
 
921
  return output_video, output_file, status_msg
922
 
923
 
 
939
  )
940
 
941
  # Contenedores para los campos de texto para controlar la visibilidad
 
942
  with gr.Column(visible=True) as ia_guion_column:
943
  prompt_ia = gr.Textbox(
944
  label="Tema para IA",
945
  lines=2,
946
+ placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer...",
947
  max_lines=4,
948
  value=""
949
  )
 
963
  interactive=True,
964
  value=None
965
  )
966
+
967
+ # --- NUEVO COMPONENTE: Selección de Voz ---
968
+ voice_dropdown = gr.Dropdown(
969
+ label="Seleccionar Voz para Guion",
970
+ choices=AVAILABLE_VOICES, # Usar la lista obtenida al inicio
971
+ value=DEFAULT_VOICE, # Usar la voz por defecto calculada
972
+ interactive=True
973
+ )
974
+ # --- FIN NUEVO COMPONENTE ---
975
+
976
 
977
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
978
 
 
985
  file_output = gr.File(
986
  label="Descargar Archivo de Video",
987
  interactive=False,
988
+ visible=False
989
  )
990
  status_output = gr.Textbox(
991
  label="Estado",
 
996
  )
997
 
998
  # Evento para mostrar/ocultar los campos de texto según el tipo de prompt
 
999
  prompt_type.change(
1000
  lambda x: (gr.update(visible=x == "Generar Guion con IA"),
1001
  gr.update(visible=x == "Usar Mi Guion")),
1002
  inputs=prompt_type,
 
1003
  outputs=[ia_guion_column, manual_guion_column]
1004
  )
1005
 
1006
  # Evento click del botón de generar video
1007
  generate_btn.click(
1008
+ # Acción 1 (síncrona): Resetear salidas y establecer estado
 
1009
  lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
1010
  outputs=[video_output, file_output, status_output],
1011
+ queue=True,
1012
  ).then(
1013
  # Acción 2 (asíncrona): Llamar a la función principal de procesamiento
1014
  run_app,
1015
+ # PASAR TODOS LOS INPUTS DE LA INTERFAZ a run_app
1016
+ inputs=[prompt_type, prompt_ia, prompt_manual, musica_input, voice_dropdown], # <-- AHORA PASAMOS voice_dropdown
1017
+ # run_app retornará los 3 outputs esperados
1018
  outputs=[video_output, file_output, status_output]
1019
  ).then(
1020
  # Acción 3 (síncrona): Hacer visible el enlace de descarga si se retornó un archivo
 
 
1021
  lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
 
1022
  inputs=[video_output, file_output, status_output],
 
1023
  outputs=[file_output]
1024
  )
1025
 
 
1031
  - "Generar Guion con IA": Describe brevemente un tema (ej. "La belleza de las montañas"). La IA generará un guion corto.
1032
  - "Usar Mi Guion": Escribe el guion completo que quieres para el video.
1033
  3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.) para usar como música de fondo.
1034
+ 4. **Selecciona la voz** para el guion.
1035
+ 5. **Haz clic en "✨ Generar Video"**.
1036
+ 6. Espera a que se procese el video. El tiempo de espera puede variar. Verás el estado en el cuadro de texto.
1037
+ 7. 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.
1038
+ 8. Si hay errores, revisa el log `video_generator_full.log` para más detalles.
1039
  """)
1040
  gr.Markdown("---")
1041
  gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")