gnosticdev commited on
Commit
55d8544
·
verified ·
1 Parent(s): b8bd6c3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +195 -121
app.py CHANGED
@@ -7,7 +7,6 @@ from datetime import datetime
7
  from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip
8
  import edge_tts
9
  import gradio as gr
10
-
11
  from transformers import GPT2Tokenizer, GPT2LMHeadModel
12
  import torch
13
 
@@ -17,77 +16,96 @@ logger = logging.getLogger(__name__)
17
 
18
  # --- Inicialización de Tokenizer y Modelo GPT-2 ---
19
  # Especificamos un modelo más pequeño para una generación más rápida y menos exigente en recursos
20
- # Puedes cambiarlo a "gpt2" si tienes suficiente RAM y GPU
21
- MODEL_NAME = "gpt2-small" # O "gpt2"
22
  try:
23
  tokenizer = GPT2Tokenizer.from_pretrained(MODEL_NAME)
24
  model = GPT2LMHeadModel.from_pretrained(MODEL_NAME).eval()
25
- # Añadir un token de padding si el modelo no tiene uno (común en GPT-2)
26
  if tokenizer.pad_token is None:
27
  tokenizer.pad_token = tokenizer.eos_token
 
28
  except Exception as e:
29
- logger.error(f"Error al cargar el modelo GPT-2: {e}")
30
- # Considerar una salida de emergencia o un mensaje al usuario si esto falla
 
 
31
 
32
  # --- Funciones de Utilidad ---
33
 
34
- def generate_script(prompt, max_length=300):
35
  """
36
- Genera un guion usando el modelo GPT-2 basado en un prompt dado.
37
- Ajustamos truncation a True y añadimos un token de padding.
38
  """
 
 
 
 
39
  logger.info("Generando guion con GPT-2...")
40
- inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=512) # Truncar para evitar errores
41
- with torch.no_grad():
42
- outputs = model.generate(
43
- **inputs,
44
- max_length=max_length,
45
- do_sample=True,
46
- top_p=0.95,
47
- top_k=60,
48
- temperature=0.9,
49
- pad_token_id=tokenizer.pad_token_id, # Usar el token de padding
50
- eos_token_id=tokenizer.eos_token_id
51
- )
52
- text = tokenizer.decode(outputs[0], skip_special_tokens=True)
53
- logger.info(f"Guion generado (longitud: {len(text)} caracteres): {text[:200]}...") # Mostrar solo los primeros 200 chars
54
- return text
 
 
 
 
 
 
 
 
 
55
 
56
  async def text_to_speech(text, voice="es-ES-ElviraNeural", output_path="voz.mp3"):
57
  """
58
  Convierte texto a voz usando Edge TTS.
59
  """
60
- logger.info(f"Generando audio TTS para: {text[:100]}...")
61
  try:
62
  communicate = edge_tts.Communicate(text, voice)
63
  await communicate.save(output_path)
64
- logger.info(f"Audio guardado en {output_path}")
65
  except Exception as e:
66
  logger.error(f"Error al generar audio TTS: {e}")
67
- raise # Volver a lanzar la excepción para que el llamador la maneje
68
 
69
  def download_video_sample(url):
70
  """
71
  Descarga un archivo de video desde una URL a un archivo temporal.
72
  """
73
- logger.info(f"Descargando video de ejemplo desde: {url}")
 
 
 
74
  try:
75
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
76
- response = requests.get(url, stream=True, timeout=10) # Añadir timeout
77
  response.raise_for_status() # Lanza un error para códigos de estado HTTP malos
78
- for chunk in response.iter_content(chunk_size=1024 * 1024):
79
- tmp.write(chunk)
 
 
 
80
  tmp.close()
81
  logger.info(f"Video descargado a: {tmp.name}")
82
  return tmp.name
83
  except requests.exceptions.RequestException as e:
84
- logger.error(f"Error al descargar el video {url}: {e}")
85
- if os.path.exists(tmp.name):
86
  os.remove(tmp.name)
87
- return None # Retorna None si la descarga falla
88
  except Exception as e:
89
  logger.error(f"Error inesperado al descargar video {url}: {e}")
90
- if os.path.exists(tmp.name):
91
  os.remove(tmp.name)
92
  return None
93
 
@@ -97,72 +115,89 @@ def loop_audio_to_length(audio_clip, target_duration):
97
  """
98
  if audio_clip.duration >= target_duration:
99
  return audio_clip.subclip(0, target_duration)
100
- loops = int(target_duration // audio_clip.duration) + 1
 
101
  audios = [audio_clip] * loops
102
- concatenated = concatenate_videoclips(audios) # No necesitas method="compose" para audio
103
  return concatenated.subclip(0, target_duration)
104
 
105
  # --- Función Principal de Creación de Video ---
106
 
107
- def crear_video(prompt, musica_url=None):
108
  """
109
- Crea un video combinando un guion generado, voz TTS, clips de video y música de fondo.
110
  """
111
- logger.info(f"Iniciando creación de video para el prompt: '{prompt}'")
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  temp_files = [] # Para llevar un registro de archivos temporales a limpiar
 
113
 
114
  try:
115
- # 1. Generar guion
116
- guion = generate_script(prompt, max_length=200) # Ajusta max_length si quieres guiones más largos
117
-
118
- # 2. Generar audio TTS
119
- voz_archivo = os.path.join(tempfile.gettempdir(), "voz_temp.mp3")
120
  temp_files.append(voz_archivo)
121
  asyncio.run(text_to_speech(guion, output_path=voz_archivo))
122
  audio_tts = AudioFileClip(voz_archivo)
123
 
124
- # 3. Descargar videos de ejemplo
125
- # ¡IMPORTANTE! Aquí debes reemplazar estas URLs con URLs de videos reales y variados
126
- # o implementar una lógica para buscar y descargar videos relevantes.
127
- # Por ahora, usaré URLs de ejemplo distintas para simular variedad.
128
  video_urls = [
129
  "https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4",
130
  "https://file-examples.com/storage/fe2c91b5c46522c0734a74a/2017/04/file_example_MP4_480_1_5MG.mp4",
131
- "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4"
 
132
  ]
133
 
134
- clips = []
135
- video_download_success = False
136
- for i, url in enumerate(video_urls):
137
  video_path = download_video_sample(url)
138
  if video_path:
139
  temp_files.append(video_path)
140
  try:
141
- clip = VideoFileClip(video_path).subclip(0, min(10, VideoFileClip(video_path).duration)) # máximo 10 segundos o duración real
142
- clips.append(clip)
143
- video_download_success = True
 
 
 
 
 
144
  except Exception as e:
145
- logger.warning(f"No se pudo cargar el clip de video {video_path}: {e}")
146
  else:
147
  logger.warning(f"No se pudo descargar el video de la URL: {url}")
148
 
149
- if not video_download_success or not clips:
150
  logger.error("No se pudieron obtener clips de video válidos. Abortando creación de video.")
151
- raise ValueError("No hay clips de video disponibles para crear el video.")
152
 
153
- # 4. Concatenar videos
154
- video_final = concatenate_videoclips(clips, method="compose")
155
-
156
- # Asegurarse de que el video sea al menos tan largo como el audio TTS si es posible
157
- if video_final.duration < audio_tts.duration:
158
- logger.info("Duración del video es menor que la del audio. Intentando extender el video.")
159
- # Repetir el video si es necesario, o al menos el último clip
160
- num_repeats = int(audio_tts.duration / video_final.duration) + 1
161
- repeated_clips = [video_final] * num_repeats
162
- video_final = concatenate_videoclips(repeated_clips, method="compose").subclip(0, audio_tts.duration)
163
 
 
 
164
 
165
- # 5. Música de fondo en loop si está definida
166
  mezcla_audio = audio_tts # Por defecto, solo la voz
167
  if musica_url and musica_url.strip():
168
  musica_path = download_video_sample(musica_url)
@@ -170,35 +205,36 @@ def crear_video(prompt, musica_url=None):
170
  temp_files.append(musica_path)
171
  try:
172
  musica_audio = AudioFileClip(musica_path)
173
- # Loop música a duración del video final
174
- musica_loop = loop_audio_to_length(musica_audio, video_final.duration)
175
  # Mezclar audio TTS y música (TTS al 100%, música al 30%)
176
- mezcla_audio = CompositeAudioClip([musica_loop.volumex(0.3), audio_tts.set_duration(video_final.duration).volumex(1.0)])
 
 
177
  except Exception as e:
178
- logger.warning(f"No se pudo procesar la música de fondo: {e}. Se usará solo la voz.")
179
  else:
180
- logger.warning("No se pudo descargar la música. Se usará solo la voz.")
181
 
182
- # 6. Asignar audio al video y ajustar duración del video a la duración del audio TTS
183
- # Es crucial que el video tenga al menos la misma duración que el audio combinado.
184
- video_final = video_final.set_audio(mezcla_audio).subclip(0, mezcla_audio.duration)
185
 
186
-
187
- # 7. Guardar video final
188
  output_dir = "generated_videos"
189
  os.makedirs(output_dir, exist_ok=True)
190
  output_path = os.path.join(output_dir, f"video_output_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
191
  logger.info(f"Escribiendo video final a: {output_path}")
192
- video_final.write_videofile(output_path, fps=24, threads=4, logger=None, preset="medium") # Mejorar preset para calidad
193
 
194
  logger.info(f"Video generado exitosamente en: {output_path}")
195
  return output_path
196
 
197
  except Exception as e:
198
  logger.error(f"Error general en la creación del video: {e}", exc_info=True)
199
- return None
200
  finally:
201
- # 8. Limpiar archivos temporales
202
  for f in temp_files:
203
  if os.path.exists(f):
204
  try:
@@ -216,67 +252,105 @@ def crear_video(prompt, musica_url=None):
216
  musica_audio.close()
217
  if 'video_final' in locals() and video_final:
218
  video_final.close()
 
 
219
 
220
 
221
- def run_app(prompt, musica_url):
222
  """
223
  Función envoltorio para Gradio que maneja la ejecución y los errores.
224
  """
225
- logger.info(f"Solicitud recibida en run_app con prompt: '{prompt}', musica_url: '{musica_url}'")
226
- if not prompt.strip():
227
- gr.Warning("Por favor, introduce un tema para generar el guion.")
228
- return None
 
 
 
 
 
 
 
229
 
230
  try:
231
- video_path = crear_video(prompt, musica_url if musica_url.strip() else None)
232
  if video_path:
233
  logger.info(f"Proceso completado. Video disponible en: {video_path}")
234
- return video_path
235
  else:
236
- gr.Error("Hubo un problema al generar el video. Revisa los logs para más detalles.")
237
- return None
 
 
 
238
  except Exception as e:
239
  logger.error(f"Error inesperado al ejecutar la aplicación: {e}", exc_info=True)
240
- gr.Error(f"Ocurrió un error: {e}. Por favor, inténtalo de nuevo.")
241
- return None
242
 
243
  # --- Interfaz de Gradio ---
244
  with gr.Blocks() as app:
245
  gr.Markdown("""
246
- ### 🎬 Generador de Video con IA
247
- Introduce un tema, y la IA generará un guion, lo convertirá a voz, le añadirá videos de ejemplo y música de fondo.
248
  """)
249
- with gr.Row():
250
- prompt_input = gr.Textbox(label="Introduce el tema para generar el guion", lines=3, placeholder="Ej: Las maravillas de la inteligencia artificial y su futuro.")
251
- musica_input = gr.Textbox(label="URL de Música de Fondo (opcional)", placeholder="Ej: https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3")
252
 
253
- boton = gr.Button("🚀 Generar Video")
254
-
255
- # Usamos gr.Column para organizar mejor la salida y mensajes
256
- with gr.Column():
257
- salida_video = gr.Video(label="Video Generado", interactive=False)
258
- estado_mensaje = gr.Textbox(label="Estado", interactive=False, visible=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
  # Conectar el botón a la función run_app
261
- # Añadimos un paso intermedio para actualizar el estado mientras se procesa
262
  boton.click(
263
- fn=lambda: gr.update(value="Generando video... Por favor, espera."),
264
- outputs=estado_mensaje,
265
- queue=False
266
  ).then(
267
  fn=run_app,
268
- inputs=[prompt_input, musica_input],
269
- outputs=salida_video
270
- ).then(
271
- fn=lambda: gr.update(value="¡Video generado exitosamente!"),
272
- outputs=estado_mensaje,
273
- queue=False
274
  )
275
- # Resetear el mensaje de estado si el usuario cambia el prompt
276
- prompt_input.change(fn=lambda: gr.update(value=""), outputs=estado_mensaje, queue=False)
277
- musica_input.change(fn=lambda: gr.update(value=""), outputs=estado_mensaje, queue=False)
278
-
279
 
 
 
 
 
 
280
  if __name__ == "__main__":
281
- logger.info("Iniciando aplicación Gradio...")
282
- app.launch(server_name="0.0.0.0", server_port=7860, share=False) # share=True si quieres un enlace público
 
7
  from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip
8
  import edge_tts
9
  import gradio as gr
 
10
  from transformers import GPT2Tokenizer, GPT2LMHeadModel
11
  import torch
12
 
 
16
 
17
  # --- Inicialización de Tokenizer y Modelo GPT-2 ---
18
  # Especificamos un modelo más pequeño para una generación más rápida y menos exigente en recursos
19
+ MODEL_NAME = "gpt2-small" # Puedes cambiar a "gpt2" si tienes más RAM/GPU.
 
20
  try:
21
  tokenizer = GPT2Tokenizer.from_pretrained(MODEL_NAME)
22
  model = GPT2LMHeadModel.from_pretrained(MODEL_NAME).eval()
 
23
  if tokenizer.pad_token is None:
24
  tokenizer.pad_token = tokenizer.eos_token
25
+ logger.info(f"Modelo GPT-2 '{MODEL_NAME}' cargado exitosamente.")
26
  except Exception as e:
27
+ logger.error(f"Error al cargar el modelo GPT-2 '{MODEL_NAME}': {e}")
28
+ # En un entorno de producción, podrías querer un fallback o salir aquí.
29
+ tokenizer = None
30
+ model = None
31
 
32
  # --- Funciones de Utilidad ---
33
 
34
+ def generate_script(prompt, max_length=250): # Max_length ajustado ligeramente
35
  """
36
+ Genera un guion usando el modelo GPT-2.
 
37
  """
38
+ if not tokenizer or not model:
39
+ logger.error("Modelo GPT-2 no disponible para generar guion.")
40
+ return "Lo siento, el generador de guiones no está disponible en este momento."
41
+
42
  logger.info("Generando guion con GPT-2...")
43
+ try:
44
+ inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=512)
45
+ # Mover a GPU si está disponible
46
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
47
+ inputs = {k: v.to(device) for k, v in inputs.items()}
48
+ model.to(device)
49
+
50
+ with torch.no_grad():
51
+ outputs = model.generate(
52
+ **inputs,
53
+ max_length=max_length,
54
+ do_sample=True,
55
+ top_p=0.95,
56
+ top_k=60,
57
+ temperature=0.9,
58
+ pad_token_id=tokenizer.pad_token_id,
59
+ eos_token_id=tokenizer.eos_token_id
60
+ )
61
+ text = tokenizer.decode(outputs[0], skip_special_tokens=True)
62
+ logger.info(f"Guion generado (longitud: {len(text)} caracteres): {text[:200]}...")
63
+ return text
64
+ except Exception as e:
65
+ logger.error(f"Error durante la generación del guion: {e}")
66
+ return "No se pudo generar el guion. Intenta con otro prompt o un guion propio."
67
 
68
  async def text_to_speech(text, voice="es-ES-ElviraNeural", output_path="voz.mp3"):
69
  """
70
  Convierte texto a voz usando Edge TTS.
71
  """
72
+ logger.info(f"Generando audio TTS para: '{text[:100]}...'")
73
  try:
74
  communicate = edge_tts.Communicate(text, voice)
75
  await communicate.save(output_path)
76
+ logger.info(f"Audio TTS guardado en {output_path}")
77
  except Exception as e:
78
  logger.error(f"Error al generar audio TTS: {e}")
79
+ raise # Relanzar la excepción para manejo en la función principal
80
 
81
  def download_video_sample(url):
82
  """
83
  Descarga un archivo de video desde una URL a un archivo temporal.
84
  """
85
+ if not url: # Manejar URL vacía
86
+ return None
87
+ logger.info(f"Intentando descargar video de ejemplo desde: {url}")
88
+ tmp = None # Inicializar tmp para asegurar que exista en caso de error
89
  try:
90
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
91
+ response = requests.get(url, stream=True, timeout=15) # Aumentar timeout
92
  response.raise_for_status() # Lanza un error para códigos de estado HTTP malos
93
+
94
+ # Guardar el contenido en chunks
95
+ for chunk in response.iter_content(chunk_size=8192): # Chunk más pequeño
96
+ if chunk: # Filtrar chunks vacíos
97
+ tmp.write(chunk)
98
  tmp.close()
99
  logger.info(f"Video descargado a: {tmp.name}")
100
  return tmp.name
101
  except requests.exceptions.RequestException as e:
102
+ logger.error(f"Error de red/HTTP al descargar el video {url}: {e}")
103
+ if tmp and os.path.exists(tmp.name):
104
  os.remove(tmp.name)
105
+ return None
106
  except Exception as e:
107
  logger.error(f"Error inesperado al descargar video {url}: {e}")
108
+ if tmp and os.path.exists(tmp.name):
109
  os.remove(tmp.name)
110
  return None
111
 
 
115
  """
116
  if audio_clip.duration >= target_duration:
117
  return audio_clip.subclip(0, target_duration)
118
+
119
+ loops = int(target_duration / audio_clip.duration) + 1
120
  audios = [audio_clip] * loops
121
+ concatenated = concatenate_videoclips(audios)
122
  return concatenated.subclip(0, target_duration)
123
 
124
  # --- Función Principal de Creación de Video ---
125
 
126
+ def crear_video(prompt_type, input_text, musica_url=None):
127
  """
128
+ Crea un video combinando un guion (generado o provisto), voz TTS, clips de video y música de fondo.
129
  """
130
+ logger.info(f"Iniciando creación de video. Tipo de prompt: {prompt_type}")
131
+ guion = ""
132
+ if prompt_type == "Generar Guion con IA":
133
+ guion = generate_script(input_text)
134
+ if not guion or guion == "No se pudo generar el guion. Intenta con otro prompt o un guion propio.":
135
+ raise ValueError(guion) # Propagar el error para Gradio
136
+ else: # prompt_type == "Usar Mi Guion"
137
+ guion = input_text
138
+ if not guion.strip():
139
+ raise ValueError("Por favor, introduce tu guion.")
140
+
141
+ if not guion.strip():
142
+ raise ValueError("El guion está vacío. No se puede proceder.")
143
+
144
  temp_files = [] # Para llevar un registro de archivos temporales a limpiar
145
+ clips = [] # Lista para almacenar los objetos VideoFileClip
146
 
147
  try:
148
+ # 1. Generar audio TTS
149
+ voz_archivo = os.path.join(tempfile.gettempdir(), f"voz_temp_{os.getpid()}.mp3")
 
 
 
150
  temp_files.append(voz_archivo)
151
  asyncio.run(text_to_speech(guion, output_path=voz_archivo))
152
  audio_tts = AudioFileClip(voz_archivo)
153
 
154
+ # 2. Descargar videos de ejemplo (¡REEMPLAZA ESTAS URLs CON TUS FUENTES REALES!)
155
+ # Sugerencia: Busca videos de stock gratuitos o crea un sistema para buscar por palabras clave
 
 
156
  video_urls = [
157
  "https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4",
158
  "https://file-examples.com/storage/fe2c91b5c46522c0734a74a/2017/04/file_example_MP4_480_1_5MG.mp4",
159
+ "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4",
160
+ "https://test-videos.co.uk/vids/bigbuckbunny/mp4/720/big_buck_bunny_720p_1mb.mp4" # Otra URL de ejemplo
161
  ]
162
 
163
+ valid_clip_found = False
164
+ for url in video_urls:
 
165
  video_path = download_video_sample(url)
166
  if video_path:
167
  temp_files.append(video_path)
168
  try:
169
+ # Limitar duración del clip individual para que no sea excesivo
170
+ clip = VideoFileClip(video_path).subclip(0, min(15, VideoFileClip(video_path).duration))
171
+ if clip.duration > 1: # Asegurarse de que el clip tenga una duración mínima
172
+ clips.append(clip)
173
+ valid_clip_found = True
174
+ else:
175
+ logger.warning(f"Clip de video muy corto ({clip.duration:.2f}s) de {url}, omitiendo.")
176
+ clip.close() # Cerrar clip si no se va a usar
177
  except Exception as e:
178
+ logger.warning(f"No se pudo cargar o procesar el clip de video {video_path} de {url}: {e}")
179
  else:
180
  logger.warning(f"No se pudo descargar el video de la URL: {url}")
181
 
182
+ if not valid_clip_found or not clips:
183
  logger.error("No se pudieron obtener clips de video válidos. Abortando creación de video.")
184
+ raise ValueError("No se encontraron clips de video válidos. Asegúrate de que las URLs sean correctas y accesibles.")
185
 
186
+ # 3. Concatenar videos
187
+ video_base = concatenate_videoclips(clips, method="compose")
188
+ logger.info(f"Video base concatenado, duración: {video_base.duration:.2f}s")
189
+
190
+ # Asegurarse de que el video base sea al menos tan largo como el audio TTS
191
+ if video_base.duration < audio_tts.duration:
192
+ logger.info(f"Duración del video ({video_base.duration:.2f}s) es menor que la del audio TTS ({audio_tts.duration:.2f}s). Repitiendo video.")
193
+ num_repeats = int(audio_tts.duration / video_base.duration) + 1
194
+ repeated_clips = [video_base] * num_repeats
195
+ video_base = concatenate_videoclips(repeated_clips, method="compose") # No subclip aquí aún
196
 
197
+ # El video final tendrá la duración del audio combinado
198
+ final_video_duration = audio_tts.duration
199
 
200
+ # 4. Música de fondo en loop si está definida
201
  mezcla_audio = audio_tts # Por defecto, solo la voz
202
  if musica_url and musica_url.strip():
203
  musica_path = download_video_sample(musica_url)
 
205
  temp_files.append(musica_path)
206
  try:
207
  musica_audio = AudioFileClip(musica_path)
208
+ # Loop música hasta la duración final del video
209
+ musica_loop = loop_audio_to_length(musica_audio, final_video_duration)
210
  # Mezclar audio TTS y música (TTS al 100%, música al 30%)
211
+ # Asegurarse de que el audio TTS tenga la duración correcta antes de mezclar
212
+ mezcla_audio = CompositeAudioClip([musica_loop.volumex(0.3), audio_tts.set_duration(final_video_duration).volumex(1.0)])
213
+ logger.info("Música de fondo añadida y mezclada.")
214
  except Exception as e:
215
+ logger.warning(f"No se pudo procesar la música de fondo de {musica_url}: {e}. Se usará solo la voz.")
216
  else:
217
+ logger.warning(f"No se pudo descargar la música de {musica_url}. Se usará solo la voz.")
218
 
219
+ # 5. Asignar audio al video y ajustar duración del video final
220
+ video_final = video_base.set_audio(mezcla_audio).subclip(0, final_video_duration)
221
+ logger.info(f"Video final configurado con audio. Duración final: {video_final.duration:.2f}s")
222
 
223
+ # 6. Guardar video final
 
224
  output_dir = "generated_videos"
225
  os.makedirs(output_dir, exist_ok=True)
226
  output_path = os.path.join(output_dir, f"video_output_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
227
  logger.info(f"Escribiendo video final a: {output_path}")
228
+ video_final.write_videofile(output_path, fps=24, threads=4, logger=None, preset="medium", codec="libx264")
229
 
230
  logger.info(f"Video generado exitosamente en: {output_path}")
231
  return output_path
232
 
233
  except Exception as e:
234
  logger.error(f"Error general en la creación del video: {e}", exc_info=True)
235
+ raise e # Re-lanzar para que Gradio lo muestre
236
  finally:
237
+ # 7. Limpiar archivos temporales
238
  for f in temp_files:
239
  if os.path.exists(f):
240
  try:
 
252
  musica_audio.close()
253
  if 'video_final' in locals() and video_final:
254
  video_final.close()
255
+ if 'video_base' in locals() and video_base:
256
+ video_base.close()
257
 
258
 
259
+ def run_app(prompt_type, prompt_ia, prompt_manual, musica_url):
260
  """
261
  Función envoltorio para Gradio que maneja la ejecución y los errores.
262
  """
263
+ input_text = ""
264
+ if prompt_type == "Generar Guion con IA":
265
+ input_text = prompt_ia
266
+ if not input_text.strip():
267
+ raise gr.Error("Por favor, introduce un tema para generar el guion.")
268
+ else: # Usar Mi Guion
269
+ input_text = prompt_manual
270
+ if not input_text.strip():
271
+ raise gr.Error("Por favor, introduce tu guion.")
272
+
273
+ logger.info(f"Solicitud recibida: Tipo='{prompt_type}', Input='{input_text[:50]}...', Música='{musica_url}'")
274
 
275
  try:
276
+ video_path = crear_video(prompt_type, input_text, musica_url if musica_url.strip() else None)
277
  if video_path:
278
  logger.info(f"Proceso completado. Video disponible en: {video_path}")
279
+ return video_path, gr.update(value="¡Video generado exitosamente!")
280
  else:
281
+ # Esto se manejará mejor por las excepciones lanzadas en crear_video
282
+ raise gr.Error("Hubo un problema desconocido al generar el video. Revisa los logs.")
283
+ except ValueError as ve:
284
+ logger.error(f"Error de validación: {ve}")
285
+ return None, gr.update(value=f"Error: {ve}", text_color="red")
286
  except Exception as e:
287
  logger.error(f"Error inesperado al ejecutar la aplicación: {e}", exc_info=True)
288
+ return None, gr.update(value=f"Ocurrió un error grave: {e}. Por favor, inténtalo de nuevo.", text_color="red")
 
289
 
290
  # --- Interfaz de Gradio ---
291
  with gr.Blocks() as app:
292
  gr.Markdown("""
293
+ ### 🎬 Generador de Video Inteligente 🚀
294
+ Crea videos con guiones generados por IA o propios, voz automática y música de fondo.
295
  """)
 
 
 
296
 
297
+ with gr.Tab("Generar Video"):
298
+ with gr.Row():
299
+ prompt_type = gr.Radio(
300
+ ["Generar Guion con IA", "Usar Mi Guion"],
301
+ label="Método de Guion",
302
+ value="Generar Guion con IA"
303
+ )
304
+
305
+ with gr.Column(visible=True) as ia_guion_column:
306
+ prompt_ia = gr.Textbox(
307
+ label="Tema para Generar Guion (con IA)",
308
+ lines=2,
309
+ placeholder="Ej: Las maravillas del universo y las estrellas."
310
+ )
311
+
312
+ with gr.Column(visible=False) as manual_guion_column:
313
+ prompt_manual = gr.Textbox(
314
+ label="Introduce Tu Guion Propio",
315
+ lines=5,
316
+ placeholder="Ej: Hola a todos, hoy hablaremos de un tema fascinante..."
317
+ )
318
+
319
+ musica_input = gr.Textbox(
320
+ label="URL de Música de Fondo (opcional, MP3 recomendado)",
321
+ placeholder="Ej: https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"
322
+ )
323
+
324
+ boton = gr.Button("✨ Generar Video")
325
+
326
+ with gr.Column():
327
+ salida_video = gr.Video(label="Video Generado", interactive=False)
328
+ estado_mensaje = gr.Textbox(label="Estado del Proceso", interactive=False, value="")
329
+
330
+ # Lógica para mostrar/ocultar columnas según el tipo de prompt
331
+ prompt_type.change(
332
+ fn=lambda value: (gr.update(visible=value == "Generar Guion con IA"),
333
+ gr.update(visible=value == "Usar Mi Guion")),
334
+ inputs=prompt_type,
335
+ outputs=[ia_guion_column, manual_guion_column]
336
+ )
337
 
338
  # Conectar el botón a la función run_app
 
339
  boton.click(
340
+ fn=lambda: (None, gr.update(value="Iniciando generación... Por favor, espera, esto puede tardar un minuto o más.")),
341
+ outputs=[salida_video, estado_mensaje],
342
+ queue=False # Desactivar cola para feedback inmediato
343
  ).then(
344
  fn=run_app,
345
+ inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
346
+ outputs=[salida_video, estado_mensaje]
 
 
 
 
347
  )
 
 
 
 
348
 
349
+ # Limpiar el mensaje de estado cuando el usuario cambia las entradas
350
+ prompt_ia.change(fn=lambda: gr.update(value=""), outputs=estado_mensaje, queue=False)
351
+ prompt_manual.change(fn=lambda: gr.update(value=""), outputs=estado_mensaje, queue=False)
352
+ musica_input.change(fn=lambda: gr.update(value=""), outputs=estado_mensaje, queue=False)
353
+
354
  if __name__ == "__main__":
355
+ logger.info("Iniciando aplicación Gradio para Hugging Face Spaces...")
356
+ app.launch(server_name="0.0.0.0", server_port=7860, share=False) # 'share=True' para un enlace público temporal (solo para pruebas)