Spaces:
Runtime error
Runtime error
Update app.py
Browse files
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
|
13 |
from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
|
14 |
import re
|
15 |
import math
|
@@ -35,7 +35,7 @@ logger.info("="*80)
|
|
35 |
PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
|
36 |
if not PEXELS_API_KEY:
|
37 |
logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
|
38 |
-
# raise ValueError("API key de Pexels no configurada")
|
39 |
|
40 |
# Inicialización de modelos
|
41 |
MODEL_NAME = "datificate/gpt2-small-spanish"
|
@@ -66,14 +66,16 @@ 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 |
-
#
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
|
|
|
|
77 |
|
78 |
except Exception as e:
|
79 |
logger.error(f"Error obteniendo voces de Edge TTS: {str(e)}", exc_info=True)
|
@@ -157,46 +159,56 @@ def generate_script(prompt, max_length=150):
|
|
157 |
text = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
158 |
|
159 |
cleaned_text = text.strip()
|
|
|
160 |
try:
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
|
|
|
|
165 |
else:
|
|
|
166 |
instruction_start_idx = text.find(instruction_phrase_start)
|
167 |
if instruction_start_idx != -1:
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
|
|
175 |
|
176 |
except Exception as e:
|
177 |
logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
|
178 |
-
cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
|
179 |
|
180 |
-
|
181 |
-
|
182 |
-
|
|
|
183 |
|
|
|
184 |
cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
|
185 |
-
cleaned_text = cleaned_text.lstrip(':').strip()
|
186 |
-
cleaned_text = cleaned_text.lstrip('.').strip()
|
187 |
|
|
|
|
|
188 |
sentences = cleaned_text.split('.')
|
189 |
if sentences and sentences[0].strip():
|
190 |
final_text = sentences[0].strip() + '.'
|
191 |
-
|
|
|
192 |
final_text += " " + sentences[1].strip() + "."
|
193 |
-
final_text = final_text.replace("..", ".")
|
194 |
|
195 |
logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
|
196 |
return final_text.strip()
|
197 |
|
198 |
logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
|
199 |
-
return cleaned_text.strip()
|
200 |
|
201 |
except Exception as e:
|
202 |
logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
|
@@ -374,11 +386,12 @@ def extract_visual_keywords_from_script(script_text):
|
|
374 |
logger.info(f"Palabras clave finales: {top_keywords}")
|
375 |
return top_keywords
|
376 |
|
377 |
-
|
|
|
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}")
|
382 |
|
383 |
start_time = datetime.now()
|
384 |
temp_dir_intermediate = None
|
@@ -409,31 +422,37 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=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 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
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(
|
437 |
raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
|
438 |
|
439 |
temp_intermediate_files.append(voz_path)
|
@@ -869,10 +888,11 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None): # <-
|
|
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): # <--
|
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
|
@@ -883,9 +903,14 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
|
|
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 |
-
|
887 |
-
|
888 |
-
|
|
|
|
|
|
|
|
|
|
|
889 |
|
890 |
logger.info(f"Tipo de entrada: {prompt_type}")
|
891 |
logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
|
@@ -893,18 +918,18 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
|
|
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
|
897 |
|
898 |
try:
|
899 |
logger.info("Llamando a crear_video...")
|
900 |
-
# Pasar la voz seleccionada
|
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}")
|
@@ -943,7 +968,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
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,15 +988,15 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
963 |
interactive=True,
|
964 |
value=None
|
965 |
)
|
966 |
-
|
967 |
-
# ---
|
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
|
975 |
|
976 |
|
977 |
generate_btn = gr.Button("✨ Generar Video", variant="primary")
|
@@ -985,7 +1010,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
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",
|
@@ -1000,7 +1025,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
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
|
@@ -1008,16 +1033,17 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
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
|
1014 |
run_app,
|
1015 |
-
# PASAR TODOS LOS INPUTS
|
1016 |
-
inputs=[prompt_type, prompt_ia, prompt_manual, musica_input, 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
|
|
|
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]
|
@@ -1027,15 +1053,13 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
|
|
1027 |
gr.Markdown("### Instrucciones:")
|
1028 |
gr.Markdown("""
|
1029 |
1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
|
1030 |
-
2. **Selecciona el tipo de entrada**:
|
1031 |
-
|
1032 |
-
|
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.
|
1037 |
-
7. La previsualización
|
1038 |
-
8.
|
1039 |
""")
|
1040 |
gr.Markdown("---")
|
1041 |
gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
|
@@ -1047,7 +1071,7 @@ if __name__ == "__main__":
|
|
1047 |
try:
|
1048 |
temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
|
1049 |
temp_clip.close()
|
1050 |
-
logger.info("Clips base de MoviePy
|
1051 |
except Exception as e:
|
1052 |
logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
|
1053 |
|
|
|
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
|
|
|
35 |
PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
|
36 |
if not PEXELS_API_KEY:
|
37 |
logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
|
38 |
+
# raise ValueError("API key de Pexels no configurada") # Descomentar para forzar fallo si no está
|
39 |
|
40 |
# Inicialización de modelos
|
41 |
MODEL_NAME = "datificate/gpt2-small-spanish"
|
|
|
66 |
logger.info("Obteniendo lista de voces disponibles de Edge TTS...")
|
67 |
try:
|
68 |
voices = await edge_tts.VoicesManager.create()
|
69 |
+
# Retornar solo voces en español si prefieres
|
70 |
+
es_voices = [voice.Name for voice in voices.Voices if voice.Locale.startswith('es-')]
|
71 |
+
if es_voices:
|
72 |
+
logger.info(f"Encontradas {len(es_voices)} voces en español.")
|
73 |
+
return es_voices
|
74 |
+
else:
|
75 |
+
# Si no hay español, retornar todas las voces
|
76 |
+
all_voices = [voice.Name for voice in voices.Voices]
|
77 |
+
logger.warning(f"No se encontraron voces en español. Retornando {len(all_voices)} voces en todos los idiomas.")
|
78 |
+
return all_voices if all_voices else ["en-US-AriaNeural"] # Fallback si no hay ninguna
|
79 |
|
80 |
except Exception as e:
|
81 |
logger.error(f"Error obteniendo voces de Edge TTS: {str(e)}", exc_info=True)
|
|
|
159 |
text = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
160 |
|
161 |
cleaned_text = text.strip()
|
162 |
+
# Limpieza mejorada de la frase de instrucción
|
163 |
try:
|
164 |
+
# Buscar el índice de inicio del prompt original dentro del texto generado
|
165 |
+
prompt_in_output_idx = text.lower().find(prompt.lower())
|
166 |
+
if prompt_in_output_idx != -1:
|
167 |
+
# Tomar todo el texto DESPUÉS del prompt original
|
168 |
+
cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
|
169 |
+
logger.debug("Texto limpiado tomando parte después del prompt original.")
|
170 |
else:
|
171 |
+
# Fallback si el prompt original no está exacto en la salida: buscar la frase de instrucción base
|
172 |
instruction_start_idx = text.find(instruction_phrase_start)
|
173 |
if instruction_start_idx != -1:
|
174 |
+
# Tomar texto después de la frase base (puede incluir el prompt)
|
175 |
+
cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
|
176 |
+
logger.debug("Texto limpiado tomando parte después de la frase de instrucción base.")
|
177 |
+
else:
|
178 |
+
# Si ni la frase de instrucción ni el prompt se encuentran, usar el texto original
|
179 |
+
logger.warning("No se pudo identificar el inicio del guión generado. Usando texto generado completo.")
|
180 |
+
cleaned_text = text.strip() # Limpieza básica
|
181 |
+
|
182 |
|
183 |
except Exception as e:
|
184 |
logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
|
185 |
+
cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Limpieza básica como fallback
|
186 |
|
187 |
+
# Asegurarse de que el texto resultante no sea solo la instrucción o vacío
|
188 |
+
if not cleaned_text or len(cleaned_text) < 10: # Umbral de longitud mínima
|
189 |
+
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).")
|
190 |
+
cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Fallback al texto original limpio
|
191 |
|
192 |
+
# Limpieza final de caracteres especiales y espacios sobrantes
|
193 |
cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
|
194 |
+
cleaned_text = cleaned_text.lstrip(':').strip() # Quitar posibles ':' al inicio
|
195 |
+
cleaned_text = cleaned_text.lstrip('.').strip() # Quitar posibles '.' al inicio
|
196 |
|
197 |
+
|
198 |
+
# Intentar obtener al menos una oración completa si es posible para un inicio más limpio
|
199 |
sentences = cleaned_text.split('.')
|
200 |
if sentences and sentences[0].strip():
|
201 |
final_text = sentences[0].strip() + '.'
|
202 |
+
# Añadir la segunda oración si existe y es razonable
|
203 |
+
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
|
204 |
final_text += " " + sentences[1].strip() + "."
|
205 |
+
final_text = final_text.replace("..", ".") # Limpiar doble punto
|
206 |
|
207 |
logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
|
208 |
return final_text.strip()
|
209 |
|
210 |
logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
|
211 |
+
return cleaned_text.strip() # Si no se puede formar una oración, devolver el texto limpio tal cual
|
212 |
|
213 |
except Exception as e:
|
214 |
logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
|
|
|
386 |
logger.info(f"Palabras clave finales: {top_keywords}")
|
387 |
return top_keywords
|
388 |
|
389 |
+
# crear_video ahora recibe la voz seleccionada
|
390 |
+
def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
|
391 |
logger.info("="*80)
|
392 |
logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
|
393 |
logger.debug(f"Input: '{input_text[:100]}...'")
|
394 |
+
logger.info(f"Voz seleccionada para TTS: {selected_voice}")
|
395 |
|
396 |
start_time = datetime.now()
|
397 |
temp_dir_intermediate = None
|
|
|
422 |
logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
|
423 |
temp_intermediate_files = []
|
424 |
|
425 |
+
# 2. Generar audio de voz usando la voz seleccionada, con reintentos si falla
|
426 |
logger.info("Generando audio de voz...")
|
427 |
voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
|
428 |
|
429 |
+
tts_voices_to_try = [selected_voice] # Intentar primero la voz seleccionada
|
430 |
+
# Añadir voces de respaldo si son diferentes a la seleccionada
|
431 |
+
if "es-ES-JuanNeural" not in tts_voices_to_try: tts_voices_to_try.append("es-ES-JuanNeural")
|
432 |
+
if "es-ES-ElviraNeural" not in tts_voices_to_try: tts_voices_to_try.append("es-ES-ElviraNeural")
|
433 |
+
# Si la lista de voces disponibles es conocida y contiene otras voces españolas, añadirlas también
|
434 |
+
# Opcional: si AVAILABLE_VOICES es fiable, podrías usar un subconjunto ordenado para reintentos
|
435 |
+
# Ejemplo: for voice_id in [selected_voice] + [v for v in AVAILABLE_VOICES if v != selected_voice][:2]:
|
436 |
+
|
437 |
+
tts_success = False
|
438 |
+
tried_voices = []
|
439 |
+
|
440 |
+
for current_voice in tts_voices_to_try:
|
441 |
+
if current_voice in tried_voices: continue # Evitar intentar la misma voz dos veces si está duplicada
|
442 |
+
tried_voices.append(current_voice)
|
443 |
+
logger.info(f"Intentando TTS con voz: {current_voice}...")
|
444 |
+
try:
|
445 |
+
tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=current_voice))
|
446 |
+
if tts_success:
|
447 |
+
logger.info(f"TTS exitoso con voz '{current_voice}'.")
|
448 |
+
break # Salir del bucle de reintentos si tiene éxito
|
449 |
+
except Exception as e:
|
450 |
+
logger.warning(f"Fallo al generar TTS con voz '{current_voice}': {str(e)}", exc_info=True)
|
451 |
+
pass # Continuar al siguiente intento
|
452 |
|
453 |
+
# Verificar si el archivo fue creado después de todos los intentos
|
454 |
if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
|
455 |
+
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.")
|
456 |
raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
|
457 |
|
458 |
temp_intermediate_files.append(voz_path)
|
|
|
888 |
|
889 |
|
890 |
# run_app ahora recibe todos los inputs, incluyendo la voz seleccionada
|
891 |
+
def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice): # <-- Recibe el valor del Dropdown
|
892 |
logger.info("="*80)
|
893 |
logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
|
894 |
|
895 |
+
# Elegir el texto de entrada basado en el prompt_type
|
896 |
input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
|
897 |
|
898 |
output_video = None
|
|
|
903 |
logger.warning("Texto de entrada vacío.")
|
904 |
return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
|
905 |
|
906 |
+
# Validar la voz seleccionada. Si no es válida, usar la por defecto.
|
907 |
+
# AVAILABLE_VOICES se obtiene al inicio.
|
908 |
+
if selected_voice not in AVAILABLE_VOICES:
|
909 |
+
logger.warning(f"Voz seleccionada inválida o no encontrada en la lista: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE}.")
|
910 |
+
selected_voice = DEFAULT_VOICE
|
911 |
+
else:
|
912 |
+
logger.info(f"Voz seleccionada validada: {selected_voice}")
|
913 |
+
|
914 |
|
915 |
logger.info(f"Tipo de entrada: {prompt_type}")
|
916 |
logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
|
|
|
918 |
logger.info(f"Archivo de música recibido: {musica_file}")
|
919 |
else:
|
920 |
logger.info("No se proporcionó archivo de música.")
|
921 |
+
logger.info(f"Voz final a usar: {selected_voice}") # Loguear la voz final que se usará
|
922 |
|
923 |
try:
|
924 |
logger.info("Llamando a crear_video...")
|
925 |
+
# Pasar el input_text elegido, la voz seleccionada y el archivo de música
|
926 |
+
video_path = crear_video(prompt_type, input_text, selected_voice, musica_file) # <-- PASAR selected_voice a crear_video
|
927 |
|
928 |
if video_path and os.path.exists(video_path):
|
929 |
logger.info(f"crear_video retornó path: {video_path}")
|
930 |
logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
|
931 |
+
output_video = video_path # Establecer valor del componente de video
|
932 |
+
output_file = video_path # Establecer valor del componente de archivo para descarga
|
933 |
status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
|
934 |
else:
|
935 |
logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
|
|
|
968 |
prompt_ia = gr.Textbox(
|
969 |
label="Tema para IA",
|
970 |
lines=2,
|
971 |
+
placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer, mostrando la belleza de la naturaleza...",
|
972 |
max_lines=4,
|
973 |
value=""
|
974 |
)
|
|
|
988 |
interactive=True,
|
989 |
value=None
|
990 |
)
|
991 |
+
|
992 |
+
# --- COMPONENTE: Selección de Voz ---
|
993 |
voice_dropdown = gr.Dropdown(
|
994 |
label="Seleccionar Voz para Guion",
|
995 |
choices=AVAILABLE_VOICES, # Usar la lista obtenida al inicio
|
996 |
value=DEFAULT_VOICE, # Usar la voz por defecto calculada
|
997 |
interactive=True
|
998 |
)
|
999 |
+
# --- FIN COMPONENTE ---
|
1000 |
|
1001 |
|
1002 |
generate_btn = gr.Button("✨ Generar Video", variant="primary")
|
|
|
1010 |
file_output = gr.File(
|
1011 |
label="Descargar Archivo de Video",
|
1012 |
interactive=False,
|
1013 |
+
visible=False # Ocultar inicialmente
|
1014 |
)
|
1015 |
status_output = gr.Textbox(
|
1016 |
label="Estado",
|
|
|
1025 |
lambda x: (gr.update(visible=x == "Generar Guion con IA"),
|
1026 |
gr.update(visible=x == "Usar Mi Guion")),
|
1027 |
inputs=prompt_type,
|
1028 |
+
outputs=[ia_guion_column, manual_guion_column] # Apuntar a las Columnas
|
1029 |
)
|
1030 |
|
1031 |
# Evento click del botón de generar video
|
|
|
1033 |
# Acción 1 (síncrona): Resetear salidas y establecer estado
|
1034 |
lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
|
1035 |
outputs=[video_output, file_output, status_output],
|
1036 |
+
queue=True, # Usar la cola de Gradio
|
1037 |
).then(
|
1038 |
+
# Acción 2 (asíncrona): Llamar a la función principal
|
1039 |
run_app,
|
1040 |
+
# PASAR TODOS LOS INPUTS RELEVANTES
|
1041 |
+
inputs=[prompt_type, prompt_ia, prompt_manual, musica_input, voice_dropdown], # <-- PASAR el dropdown de voz
|
1042 |
# run_app retornará los 3 outputs esperados
|
1043 |
outputs=[video_output, file_output, status_output]
|
1044 |
).then(
|
1045 |
+
# Acción 3 (síncrona): Hacer visible el enlace de descarga
|
1046 |
+
# Recibe las salidas de la Acción 2
|
1047 |
lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
|
1048 |
inputs=[video_output, file_output, status_output],
|
1049 |
outputs=[file_output]
|
|
|
1053 |
gr.Markdown("### Instrucciones:")
|
1054 |
gr.Markdown("""
|
1055 |
1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
|
1056 |
+
2. **Selecciona el tipo de entrada**: "Generar Guion con IA" o "Usar Mi Guion".
|
1057 |
+
3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.).
|
1058 |
+
4. **Selecciona la voz** deseada del desplegable.
|
|
|
|
|
1059 |
5. **Haz clic en "✨ Generar Video"**.
|
1060 |
+
6. Espera a que se procese el video. Verás el estado.
|
1061 |
+
7. La previsualización aparecerá si es posible, y siempre un enlace **Descargar Archivo de Video** si la generación fue exitosa.
|
1062 |
+
8. Revisa `video_generator_full.log` para detalles si hay errores.
|
1063 |
""")
|
1064 |
gr.Markdown("---")
|
1065 |
gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
|
|
|
1071 |
try:
|
1072 |
temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
|
1073 |
temp_clip.close()
|
1074 |
+
logger.info("Clips base de MoviePy creados y cerrados exitosamente. FFmpeg parece accesible.")
|
1075 |
except Exception as e:
|
1076 |
logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
|
1077 |
|