gnosticdev commited on
Commit
335fcdc
·
verified ·
1 Parent(s): 5a9da67

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +31 -93
app.py CHANGED
@@ -10,21 +10,15 @@ import gradio as gr
10
  from transformers import GPT2Tokenizer, GPT2LMHeadModel
11
  import torch
12
 
13
- # --- Importar la librería de Pexels ---
14
  from pexels_api import API
 
15
 
16
- # --- Importar KeyBERT para extracción de palabras clave ---
17
- from keybert import KeyBERT # Asegúrate de tener 'keybert' y 'sentence-transformers' en requirements.txt
18
-
19
- # --- Configuración de Logging ---
20
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
21
  logger = logging.getLogger(__name__)
22
 
23
- # --- Clave API de Pexels ---
24
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
25
  pexels_api = API(PEXELS_API_KEY)
26
 
27
- # --- Inicialización de Tokenizer y Modelo GPT-2 ---
28
  MODEL_NAME = "gpt2-small"
29
  try:
30
  tokenizer = GPT2Tokenizer.from_pretrained(MODEL_NAME)
@@ -37,18 +31,13 @@ except Exception as e:
37
  tokenizer = None
38
  model = None
39
 
40
- # --- Inicialización del modelo KeyBERT ---
41
  try:
42
- # Se descarga la primera vez. 'multi-qa-MiniLM-L6-cos-v1' es un buen modelo multilingüe pequeño.
43
- # Considera 'distiluse-base-multilingual-cased-v2' para mejor calidad multilingüe si hay suficiente RAM.
44
  kw_model = KeyBERT('multi-qa-MiniLM-L6-cos-v1')
45
  logger.info("Modelo KeyBERT cargado exitosamente.")
46
  except Exception as e:
47
  logger.error(f"Error al cargar el modelo KeyBERT: {e}. La búsqueda de videos será menos precisa.", exc_info=True)
48
  kw_model = None
49
 
50
- # --- Funciones de Utilidad ---
51
-
52
  def generate_script(prompt, max_length=250):
53
  if not tokenizer or not model:
54
  logger.error("Modelo GPT-2 no disponible para generar guion.")
@@ -89,19 +78,18 @@ async def text_to_speech(text, voice="es-ES-ElviraNeural", output_path="voz.mp3"
89
  logger.error(f"Error al generar audio TTS: {e}")
90
  raise
91
 
92
- def download_video_file(url, temp_dir): # Renombrada y mejorada para descargar a un dir temporal
93
  if not url:
94
  return None
95
 
96
- # Intentar obtener el nombre del archivo de la URL o generar uno
97
  file_name = url.split('/')[-1].split('?')[0]
98
- if not file_name.endswith('.mp4'): # Asegurar extensión correcta
99
  file_name = f"video_temp_{os.getpid()}_{datetime.now().strftime('%f')}.mp4"
100
 
101
  output_path = os.path.join(temp_dir, file_name)
102
  logger.info(f"Intentando descargar video de: {url} a {output_path}")
103
  try:
104
- response = requests.get(url, stream=True, timeout=30) # Aumentar timeout por videos grandes
105
  response.raise_for_status()
106
 
107
  with open(output_path, 'wb') as f:
@@ -130,57 +118,40 @@ def loop_audio_to_length(audio_clip, target_duration):
130
  concatenated = concatenate_videoclips(audios)
131
  return concatenated.subclip(0, target_duration)
132
 
133
- # --- NUEVA LÓGICA: Extracción de palabras clave visuales del guion ---
134
  def extract_visual_keywords_from_script(script_text, max_keywords_per_segment=2):
135
- """
136
- Extrae palabras clave visuales de un script dividiéndolo en segmentos.
137
- Retorna una lista de términos de búsqueda únicos.
138
- """
139
  if not kw_model:
140
  logger.warning("Modelo KeyBERT no disponible. La extracción de palabras clave será limitada.")
141
- # Fallback si KeyBERT no carga: usar las primeras palabras del guion
142
  return [script_text.split('.')[0].strip().replace(" ", "+")] if script_text.strip() else []
143
 
144
  logger.info("Extrayendo palabras clave visuales del guion.")
145
 
146
- # Dividir el guion en frases o párrafos para un mejor análisis contextual
147
- # Puedes ajustar esto, por ejemplo, split('.') si prefieres por frases.
148
  segments = [s.strip() for s in script_text.split('\n') if s.strip()]
149
- if not segments: # Si no se puede dividir, usar el texto completo
150
  segments = [script_text]
151
 
152
- all_keywords = set() # Usar un set para evitar duplicados
153
 
154
  for segment in segments:
155
  if not segment: continue
156
  try:
157
- # keyphrase_ngram_range=(1,2) busca palabras sueltas o frases de dos palabras
158
- # top_n=max_keywords_per_segment es cuántas palabras clave por segmento
159
- # use_mmr y diversity ayudan a obtener palabras clave más variadas.
160
  keywords_with_scores = kw_model.extract_keywords(
161
  segment,
162
  keyphrase_ngram_range=(1, 2),
163
- stop_words='spanish', # ¡IMPORTANTE! Usar stop words en español
164
  top_n=max_keywords_per_segment,
165
  use_mmr=True,
166
  diversity=0.7
167
  )
168
  for kw, score in keywords_with_scores:
169
- # Convertir a formato de búsqueda de URL (espacios por +)
170
  all_keywords.add(kw.replace(" ", "+"))
171
  except Exception as e:
172
  logger.warning(f"Error al extraer palabras clave de un segmento: {e}")
173
- # En caso de error en un segmento, intentar con el segmento completo como palabra clave simple
174
  all_keywords.add(segment.split(' ')[0].strip().replace(" ", "+"))
175
 
176
  logger.info(f"Palabras clave visuales extraídas del guion: {list(all_keywords)}")
177
- return list(all_keywords) # Devolver como lista para iterar
178
 
179
- # --- Búsqueda de videos en Pexels ---
180
  def search_pexels_videos(query_list, num_videos_per_query=5, min_duration_sec=7):
181
- """
182
- Busca videos en Pexels basados en una lista de queries y devuelve URLs de descarga.
183
- """
184
  if not PEXELS_API_KEY:
185
  logger.error("ERROR: PEXELS_API_KEY no está configurada. No se pueden buscar videos en Pexels.")
186
  raise ValueError("PEXELS_API_KEY no está configurada. Por favor, configúrala como un secreto en Hugging Face Spaces.")
@@ -189,12 +160,11 @@ def search_pexels_videos(query_list, num_videos_per_query=5, min_duration_sec=7)
189
  logger.warning("Lista de queries para Pexels vacía. No se buscarán videos.")
190
  return []
191
 
192
- all_video_urls = set() # Usar un set para evitar URLs duplicadas
193
 
194
  for query in query_list:
195
  logger.info(f"Buscando {num_videos_per_query} videos en Pexels para la query: '{query}'")
196
  try:
197
- # Pedimos más de los que necesitamos por query por si algunos no cumplen duración
198
  pexels_api.search_videos(query, per_page=num_videos_per_query * 2)
199
  videos = pexels_api.get_videos()
200
 
@@ -204,7 +174,6 @@ def search_pexels_videos(query_list, num_videos_per_query=5, min_duration_sec=7)
204
 
205
  for video in videos:
206
  if video.get('duration') and video['duration'] < min_duration_sec:
207
- # logger.info(f"Saltando video {video['id']} de Pexels por duración ({video['duration']}s < {min_duration_sec}s).")
208
  continue
209
 
210
  best_quality_url = None
@@ -222,15 +191,11 @@ def search_pexels_videos(query_list, num_videos_per_query=5, min_duration_sec=7)
222
 
223
  except Exception as e:
224
  logger.error(f"Error al buscar videos en Pexels para '{query}': {e}", exc_info=True)
225
- # No lanzamos la excepción aquí, para que otras queries puedan seguir funcionando
226
- # Pero el error se registra.
227
 
228
  final_urls = list(all_video_urls)
229
  logger.info(f"Total de {len(final_urls)} URLs de video únicas obtenidas de Pexels.")
230
  return final_urls
231
 
232
- # --- Función Principal de Creación de Video ---
233
-
234
  def crear_video(prompt_type, input_text, musica_url=None):
235
  logger.info(f"Iniciando creación de video. Tipo de prompt: {prompt_type}")
236
  guion = ""
@@ -247,40 +212,32 @@ def crear_video(prompt_type, input_text, musica_url=None):
247
  raise ValueError("El guion está vacío. No se puede proceder.")
248
 
249
  temp_files = []
250
- downloaded_clip_paths = [] # Para los paths de los videos descargados
251
- final_clips = [] # Para los objetos VideoFileClip ya procesados
252
 
253
- # Directorio temporal para descargar videos
254
  temp_video_dir = tempfile.mkdtemp()
255
- temp_files.append(temp_video_dir) # Añadir el directorio temporal para su limpieza
256
 
257
  try:
258
- # 1. Generar audio TTS
259
  voz_archivo = os.path.join(tempfile.gettempdir(), f"voz_temp_{os.getpid()}.mp3")
260
  temp_files.append(voz_archivo)
261
  asyncio.run(text_to_speech(guion, output_path=voz_archivo))
262
  audio_tts = AudioFileClip(voz_archivo)
263
 
264
- # 2. Extracción de palabras clave y búsqueda masiva en Pexels
265
- # Dividimos el guion en segmentos y extraemos palabras clave de cada uno
266
- # Esto genera una lista de queries para Pexels, como ['misterios+china', 'muralla+china', ...]
267
  search_queries_for_pexels = extract_visual_keywords_from_script(guion, max_keywords_per_segment=2)
268
 
269
- # Si no se pudo extraer ninguna palabra clave útil, lanzar error.
270
  if not search_queries_for_pexels:
271
  raise ValueError("No se pudieron extraer palabras clave visuales del guion para buscar videos.")
272
 
273
- # Buscar y obtener URLs de muchos videos de Pexels (e.g., 5 videos por cada palabra clave)
274
  pexels_video_urls_found = search_pexels_videos(search_queries_for_pexels, num_videos_per_query=5, min_duration_sec=7)
275
 
276
  if not pexels_video_urls_found:
277
  logger.error("Pexels no devolvió ningún video para las palabras clave extraídas.")
278
  raise ValueError(f"Pexels no encontró videos adecuados para el guion. Intenta con otro tema o guion más descriptivo. Palabras clave usadas: {search_queries_for_pexels}")
279
 
280
- # 3. Descargar todos los videos encontrados de Pexels
281
  logger.info(f"Descargando {len(pexels_video_urls_found)} videos de Pexels...")
282
  for url in pexels_video_urls_found:
283
- video_path = download_video_file(url, temp_video_dir) # Descargar al directorio temporal
284
  if video_path:
285
  downloaded_clip_paths.append(video_path)
286
  else:
@@ -290,53 +247,45 @@ def crear_video(prompt_type, input_text, musica_url=None):
290
  logger.error("No se pudo descargar ningún video válido de Pexels.")
291
  raise ValueError("No se pudo descargar ningún video válido de Pexels. Revisa la conexión o la calidad de las URLs.")
292
 
293
- # 4. Cargar y procesar los clips descargados
294
- total_desired_video_duration = audio_tts.duration * 1.2 # Intentar tener 20% más de video que audio
295
  current_video_clips_duration = 0
296
 
297
  for path in downloaded_clip_paths:
298
  try:
299
  clip = VideoFileClip(path)
300
- # Opcional: Si quieres clips cortos y variados para transiciones rápidas
301
- clip_duration = min(clip.duration, 10) # Limitar a un máximo de 10 segundos por clip
302
- if clip_duration > 1: # Asegurar que el clip sea lo suficientemente largo
303
  final_clips.append(clip.subclip(0, clip_duration))
304
  current_video_clips_duration += clip_duration
305
  if current_video_clips_duration >= total_desired_video_duration:
306
- break # Ya tenemos suficiente metraje
307
  else:
308
  logger.warning(f"Clip de video muy corto ({clip_duration:.2f}s) de {path}, omitiendo.")
309
  clip.close()
310
  except Exception as e:
311
  logger.warning(f"No se pudo cargar o procesar el clip de video {path}: {e}. Omitiendo.")
312
- if 'clip' in locals() and clip: clip.close() # Asegurarse de cerrar si hay un error
313
 
314
  if not final_clips:
315
  logger.error("No se pudo obtener ningún clip de video usable después de la descarga y procesamiento.")
316
  raise ValueError("No se pudieron procesar los videos descargados de Pexels.")
317
 
318
-
319
- # 5. Concatenar videos y ajustar duración
320
  video_base = concatenate_videoclips(final_clips, method="compose")
321
  logger.info(f"Video base concatenado, duración: {video_base.duration:.2f}s")
322
 
323
- # Asegurarse de que el video base sea al menos tan largo como el audio TTS
324
- # Si es más corto, loopearlo.
325
  if video_base.duration < audio_tts.duration:
326
  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.")
327
  num_repeats = int(audio_tts.duration / video_base.duration) + 1
328
  repeated_clips = [video_base] * num_repeats
329
  video_base = concatenate_videoclips(repeated_clips, method="compose")
330
 
331
- # El video final siempre tendrá la duración del audio TTS (o la mezcla de audio)
332
  final_video_duration = audio_tts.duration
333
 
334
- # 6. Música de fondo en loop
335
  mezcla_audio = audio_tts
336
  if musica_url and musica_url.strip():
337
- musica_path = download_video_file(musica_url, temp_video_dir) # Usar la misma función de descarga
338
  if musica_path:
339
- temp_files.append(musica_path) # Añadir el path del archivo de música a limpiar
340
  try:
341
  musica_audio = AudioFileClip(musica_path)
342
  musica_loop = loop_audio_to_length(musica_audio, final_video_duration)
@@ -347,11 +296,9 @@ def crear_video(prompt_type, input_text, musica_url=None):
347
  else:
348
  logger.warning(f"No se pudo descargar la música de {musica_url}. Se usará solo la voz.")
349
 
350
- # 7. Asignar audio al video y ajustar duración
351
  video_final = video_base.set_audio(mezcla_audio).subclip(0, final_video_duration)
352
  logger.info(f"Video final configurado con audio. Duración final: {video_final.duration:.2f}s")
353
 
354
- # 8. Guardar video final
355
  output_dir = "generated_videos"
356
  os.makedirs(output_dir, exist_ok=True)
357
  output_path = os.path.join(output_dir, f"video_output_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
@@ -365,34 +312,26 @@ def crear_video(prompt_type, input_text, musica_url=None):
365
  logger.error(f"Error general en la creación del video: {e}", exc_info=True)
366
  raise e
367
  finally:
368
- # 9. Limpiar archivos y directorios temporales
369
  for f in temp_files:
370
  if os.path.exists(f):
371
  if os.path.isdir(f):
372
- try:
373
- import shutil
374
- shutil.rmtree(f)
375
- logger.info(f"Directorio temporal eliminado: {f}")
376
- except Exception as e:
377
- logger.warning(f"No se pudo eliminar el directorio temporal {f}: {e}")
378
  else:
379
- try:
380
- os.remove(f)
381
- logger.info(f"Archivo temporal eliminado: {f}")
382
- except Exception as e:
383
- logger.warning(f"No se pudo eliminar el archivo temporal {f}: {e}")
384
 
385
- # Asegurarse de cerrar todos los objetos MoviePy VideoFileClip y AudioFileClip
386
- for clip in final_clips: # Iterar sobre los clips finales que se usaron
387
- if clip and not clip.is_playing: # Solo cerrar si no está ya cerrado
388
  clip.close()
389
- if 'audio_tts' in locals() and audio_tts and not audio_tts.is_playing:
390
  audio_tts.close()
391
- if 'musica_audio' in locals() and musica_audio and not musica_audio.is_playing:
392
  musica_audio.close()
393
- if 'video_final' in locals() and video_final and not video_final.is_playing:
394
  video_final.close()
395
- if 'video_base' in locals() and video_base and not video_base.is_playing:
396
  video_base.close()
397
 
398
 
@@ -423,7 +362,6 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_url):
423
  logger.error(f"Error inesperado al ejecutar la aplicación: {e}", exc_info=True)
424
  return None, gr.update(value=f"Ocurrió un error grave: {e}. Por favor, inténtalo de nuevo.", text_color="red")
425
 
426
- # --- Interfaz de Gradio ---
427
  with gr.Blocks() as app:
428
  gr.Markdown("""
429
  ### 🎬 Generador de Video Inteligente con Pexels 🚀
 
10
  from transformers import GPT2Tokenizer, GPT2LMHeadModel
11
  import torch
12
 
 
13
  from pexels_api import API
14
+ from keybert import KeyBERT
15
 
 
 
 
 
16
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
17
  logger = logging.getLogger(__name__)
18
 
 
19
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
20
  pexels_api = API(PEXELS_API_KEY)
21
 
 
22
  MODEL_NAME = "gpt2-small"
23
  try:
24
  tokenizer = GPT2Tokenizer.from_pretrained(MODEL_NAME)
 
31
  tokenizer = None
32
  model = None
33
 
 
34
  try:
 
 
35
  kw_model = KeyBERT('multi-qa-MiniLM-L6-cos-v1')
36
  logger.info("Modelo KeyBERT cargado exitosamente.")
37
  except Exception as e:
38
  logger.error(f"Error al cargar el modelo KeyBERT: {e}. La búsqueda de videos será menos precisa.", exc_info=True)
39
  kw_model = None
40
 
 
 
41
  def generate_script(prompt, max_length=250):
42
  if not tokenizer or not model:
43
  logger.error("Modelo GPT-2 no disponible para generar guion.")
 
78
  logger.error(f"Error al generar audio TTS: {e}")
79
  raise
80
 
81
+ def download_video_file(url, temp_dir):
82
  if not url:
83
  return None
84
 
 
85
  file_name = url.split('/')[-1].split('?')[0]
86
+ if not file_name.endswith('.mp4'):
87
  file_name = f"video_temp_{os.getpid()}_{datetime.now().strftime('%f')}.mp4"
88
 
89
  output_path = os.path.join(temp_dir, file_name)
90
  logger.info(f"Intentando descargar video de: {url} a {output_path}")
91
  try:
92
+ response = requests.get(url, stream=True, timeout=30)
93
  response.raise_for_status()
94
 
95
  with open(output_path, 'wb') as f:
 
118
  concatenated = concatenate_videoclips(audios)
119
  return concatenated.subclip(0, target_duration)
120
 
 
121
  def extract_visual_keywords_from_script(script_text, max_keywords_per_segment=2):
 
 
 
 
122
  if not kw_model:
123
  logger.warning("Modelo KeyBERT no disponible. La extracción de palabras clave será limitada.")
 
124
  return [script_text.split('.')[0].strip().replace(" ", "+")] if script_text.strip() else []
125
 
126
  logger.info("Extrayendo palabras clave visuales del guion.")
127
 
 
 
128
  segments = [s.strip() for s in script_text.split('\n') if s.strip()]
129
+ if not segments:
130
  segments = [script_text]
131
 
132
+ all_keywords = set()
133
 
134
  for segment in segments:
135
  if not segment: continue
136
  try:
 
 
 
137
  keywords_with_scores = kw_model.extract_keywords(
138
  segment,
139
  keyphrase_ngram_range=(1, 2),
140
+ stop_words='spanish',
141
  top_n=max_keywords_per_segment,
142
  use_mmr=True,
143
  diversity=0.7
144
  )
145
  for kw, score in keywords_with_scores:
 
146
  all_keywords.add(kw.replace(" ", "+"))
147
  except Exception as e:
148
  logger.warning(f"Error al extraer palabras clave de un segmento: {e}")
 
149
  all_keywords.add(segment.split(' ')[0].strip().replace(" ", "+"))
150
 
151
  logger.info(f"Palabras clave visuales extraídas del guion: {list(all_keywords)}")
152
+ return list(all_keywords)
153
 
 
154
  def search_pexels_videos(query_list, num_videos_per_query=5, min_duration_sec=7):
 
 
 
155
  if not PEXELS_API_KEY:
156
  logger.error("ERROR: PEXELS_API_KEY no está configurada. No se pueden buscar videos en Pexels.")
157
  raise ValueError("PEXELS_API_KEY no está configurada. Por favor, configúrala como un secreto en Hugging Face Spaces.")
 
160
  logger.warning("Lista de queries para Pexels vacía. No se buscarán videos.")
161
  return []
162
 
163
+ all_video_urls = set()
164
 
165
  for query in query_list:
166
  logger.info(f"Buscando {num_videos_per_query} videos en Pexels para la query: '{query}'")
167
  try:
 
168
  pexels_api.search_videos(query, per_page=num_videos_per_query * 2)
169
  videos = pexels_api.get_videos()
170
 
 
174
 
175
  for video in videos:
176
  if video.get('duration') and video['duration'] < min_duration_sec:
 
177
  continue
178
 
179
  best_quality_url = None
 
191
 
192
  except Exception as e:
193
  logger.error(f"Error al buscar videos en Pexels para '{query}': {e}", exc_info=True)
 
 
194
 
195
  final_urls = list(all_video_urls)
196
  logger.info(f"Total de {len(final_urls)} URLs de video únicas obtenidas de Pexels.")
197
  return final_urls
198
 
 
 
199
  def crear_video(prompt_type, input_text, musica_url=None):
200
  logger.info(f"Iniciando creación de video. Tipo de prompt: {prompt_type}")
201
  guion = ""
 
212
  raise ValueError("El guion está vacío. No se puede proceder.")
213
 
214
  temp_files = []
215
+ downloaded_clip_paths = []
216
+ final_clips = []
217
 
 
218
  temp_video_dir = tempfile.mkdtemp()
219
+ temp_files.append(temp_video_dir)
220
 
221
  try:
 
222
  voz_archivo = os.path.join(tempfile.gettempdir(), f"voz_temp_{os.getpid()}.mp3")
223
  temp_files.append(voz_archivo)
224
  asyncio.run(text_to_speech(guion, output_path=voz_archivo))
225
  audio_tts = AudioFileClip(voz_archivo)
226
 
 
 
 
227
  search_queries_for_pexels = extract_visual_keywords_from_script(guion, max_keywords_per_segment=2)
228
 
 
229
  if not search_queries_for_pexels:
230
  raise ValueError("No se pudieron extraer palabras clave visuales del guion para buscar videos.")
231
 
 
232
  pexels_video_urls_found = search_pexels_videos(search_queries_for_pexels, num_videos_per_query=5, min_duration_sec=7)
233
 
234
  if not pexels_video_urls_found:
235
  logger.error("Pexels no devolvió ningún video para las palabras clave extraídas.")
236
  raise ValueError(f"Pexels no encontró videos adecuados para el guion. Intenta con otro tema o guion más descriptivo. Palabras clave usadas: {search_queries_for_pexels}")
237
 
 
238
  logger.info(f"Descargando {len(pexels_video_urls_found)} videos de Pexels...")
239
  for url in pexels_video_urls_found:
240
+ video_path = download_video_file(url, temp_video_dir)
241
  if video_path:
242
  downloaded_clip_paths.append(video_path)
243
  else:
 
247
  logger.error("No se pudo descargar ningún video válido de Pexels.")
248
  raise ValueError("No se pudo descargar ningún video válido de Pexels. Revisa la conexión o la calidad de las URLs.")
249
 
250
+ total_desired_video_duration = audio_tts.duration * 1.2
 
251
  current_video_clips_duration = 0
252
 
253
  for path in downloaded_clip_paths:
254
  try:
255
  clip = VideoFileClip(path)
256
+ clip_duration = min(clip.duration, 10)
257
+ if clip_duration > 1:
 
258
  final_clips.append(clip.subclip(0, clip_duration))
259
  current_video_clips_duration += clip_duration
260
  if current_video_clips_duration >= total_desired_video_duration:
261
+ break
262
  else:
263
  logger.warning(f"Clip de video muy corto ({clip_duration:.2f}s) de {path}, omitiendo.")
264
  clip.close()
265
  except Exception as e:
266
  logger.warning(f"No se pudo cargar o procesar el clip de video {path}: {e}. Omitiendo.")
267
+ if 'clip' in locals() and clip: clip.close()
268
 
269
  if not final_clips:
270
  logger.error("No se pudo obtener ningún clip de video usable después de la descarga y procesamiento.")
271
  raise ValueError("No se pudieron procesar los videos descargados de Pexels.")
272
 
 
 
273
  video_base = concatenate_videoclips(final_clips, method="compose")
274
  logger.info(f"Video base concatenado, duración: {video_base.duration:.2f}s")
275
 
 
 
276
  if video_base.duration < audio_tts.duration:
277
  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.")
278
  num_repeats = int(audio_tts.duration / video_base.duration) + 1
279
  repeated_clips = [video_base] * num_repeats
280
  video_base = concatenate_videoclips(repeated_clips, method="compose")
281
 
 
282
  final_video_duration = audio_tts.duration
283
 
 
284
  mezcla_audio = audio_tts
285
  if musica_url and musica_url.strip():
286
+ musica_path = download_video_file(musica_url, temp_video_dir)
287
  if musica_path:
288
+ temp_files.append(musica_path)
289
  try:
290
  musica_audio = AudioFileClip(musica_path)
291
  musica_loop = loop_audio_to_length(musica_audio, final_video_duration)
 
296
  else:
297
  logger.warning(f"No se pudo descargar la música de {musica_url}. Se usará solo la voz.")
298
 
 
299
  video_final = video_base.set_audio(mezcla_audio).subclip(0, final_video_duration)
300
  logger.info(f"Video final configurado con audio. Duración final: {video_final.duration:.2f}s")
301
 
 
302
  output_dir = "generated_videos"
303
  os.makedirs(output_dir, exist_ok=True)
304
  output_path = os.path.join(output_dir, f"video_output_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
 
312
  logger.error(f"Error general en la creación del video: {e}", exc_info=True)
313
  raise e
314
  finally:
 
315
  for f in temp_files:
316
  if os.path.exists(f):
317
  if os.path.isdir(f):
318
+ import shutil
319
+ shutil.rmtree(f)
320
+ logger.info(f"Directorio temporal eliminado: {f}")
 
 
 
321
  else:
322
+ os.remove(f)
323
+ logger.info(f"Archivo temporal eliminado: {f}")
 
 
 
324
 
325
+ for clip in final_clips:
326
+ if clip and hasattr(clip, 'close') and not clip.is_playing:
 
327
  clip.close()
328
+ if 'audio_tts' in locals() and audio_tts and hasattr(audio_tts, 'close') and not audio_tts.is_playing:
329
  audio_tts.close()
330
+ if 'musica_audio' in locals() and musica_audio and hasattr(musica_audio, 'close') and not musica_audio.is_playing:
331
  musica_audio.close()
332
+ if 'video_final' in locals() and video_final and hasattr(video_final, 'close') and not video_final.is_playing:
333
  video_final.close()
334
+ if 'video_base' in locals() and video_base and hasattr(video_base, 'close') and not video_base.is_playing:
335
  video_base.close()
336
 
337
 
 
362
  logger.error(f"Error inesperado al ejecutar la aplicación: {e}", exc_info=True)
363
  return None, gr.update(value=f"Ocurrió un error grave: {e}. Por favor, inténtalo de nuevo.", text_color="red")
364
 
 
365
  with gr.Blocks() as app:
366
  gr.Markdown("""
367
  ### 🎬 Generador de Video Inteligente con Pexels 🚀