gnosticdev commited on
Commit
1d525bc
·
verified ·
1 Parent(s): 7d01868

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +227 -130
app.py CHANGED
@@ -11,21 +11,21 @@ from transformers import GPT2Tokenizer, GPT2LMHeadModel
11
  import torch
12
 
13
  # --- Importar la librería de Pexels ---
14
- from pexels_api import API # ¡Asegúrate de instalarla: pip install pexels-api!
 
 
 
15
 
16
  # --- Configuración de Logging ---
17
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
18
  logger = logging.getLogger(__name__)
19
 
20
  # --- Clave API de Pexels ---
21
- # IMPORTANTE: No uses tu clave API directamente aquí en un repositorio público.
22
- # Lo ideal es cargarla desde una variable de entorno o un archivo de configuración SEGURO.
23
- # Para Hugging Face Spaces, puedes añadirla como un "Secret" llamado PEXELS_API_KEY.
24
- PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY", "TU_CLAVE_API_PEXELS_AQUI") # Reemplaza "TU_CLAVE_API_PEXELS_AQUI" con tu clave si pruebas localmente sin variables de entorno
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)
31
  model = GPT2LMHeadModel.from_pretrained(MODEL_NAME).eval()
@@ -37,9 +37,19 @@ except Exception as e:
37
  tokenizer = None
38
  model = None
39
 
 
 
 
 
 
 
 
 
 
 
40
  # --- Funciones de Utilidad ---
41
 
42
- def generate_script(prompt, max_length=250):
43
  if not tokenizer or not model:
44
  logger.error("Modelo GPT-2 no disponible para generar guion.")
45
  return "Lo siento, el generador de guiones no está disponible en este momento."
@@ -79,31 +89,36 @@ async def text_to_speech(text, voice="es-ES-ElviraNeural", output_path="voz.mp3"
79
  logger.error(f"Error al generar audio TTS: {e}")
80
  raise
81
 
82
- def download_video_sample(url):
83
  if not url:
84
  return None
85
- logger.info(f"Intentando descargar video de: {url}")
86
- tmp = None
 
 
 
 
 
 
87
  try:
88
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
89
- response = requests.get(url, stream=True, timeout=20) # Aumentar timeout para videos más grandes
90
  response.raise_for_status()
91
 
92
- for chunk in response.iter_content(chunk_size=8192):
93
- if chunk:
94
- tmp.write(chunk)
95
- tmp.close()
96
- logger.info(f"Video descargado a: {tmp.name}")
97
- return tmp.name
98
  except requests.exceptions.RequestException as e:
99
  logger.error(f"Error de red/HTTP al descargar el video {url}: {e}")
100
- if tmp and os.path.exists(tmp.name):
101
- os.remove(tmp.name)
102
  return None
103
  except Exception as e:
104
  logger.error(f"Error inesperado al descargar video {url}: {e}")
105
- if tmp and os.path.exists(tmp.name):
106
- os.remove(tmp.name)
107
  return None
108
 
109
  def loop_audio_to_length(audio_clip, target_duration):
@@ -115,60 +130,105 @@ def loop_audio_to_length(audio_clip, target_duration):
115
  concatenated = concatenate_videoclips(audios)
116
  return concatenated.subclip(0, target_duration)
117
 
118
- # --- NUEVA FUNCIÓN: Búsqueda de videos en Pexels ---
119
- def search_pexels_videos(query, num_videos=3, min_duration_sec=5):
120
  """
121
- Busca videos en Pexels basados en una query y devuelve una lista de URLs de descarga.
 
122
  """
123
- if not PEXELS_API_KEY or PEXELS_API_KEY == "TU_CLAVE_API_PEXELS_AQUI":
124
- logger.warning("PEXELS_API_KEY no configurada. No se buscarán videos en Pexels.")
125
- return []
 
126
 
127
- logger.info(f"Buscando {num_videos} videos en Pexels para la query: '{query}'")
128
- video_urls = []
129
- try:
130
- # Pexels API para videos: search_videos(query, orientation, size, per_page, page)
131
- # La API de Pexels no permite filtrar directamente por duración mínima.
132
- # Tendremos que filtrar después de obtener los resultados.
133
- pexels_api.search_videos(query, per_page=num_videos * 3) # Pedimos más por si algunos no cumplen duración
134
- videos = pexels_api.get_videos()
135
-
136
- if not videos:
137
- logger.warning(f"No se encontraron videos en Pexels para la query: '{query}'")
138
- return []
139
-
140
- for video in videos:
141
- # Pexels devuelve diferentes archivos de video (quality). Buscamos la más adecuada (p.ej., medium o large).
142
- # También verificamos la duración si está disponible en los metadatos de Pexels.
143
- if video.get('duration') and video['duration'] < min_duration_sec:
144
- logger.info(f"Saltando video {video['id']} de Pexels por duración ({video['duration']}s < {min_duration_sec}s).")
145
- continue
146
-
147
- # Buscar la URL de descarga del video de mayor calidad disponible o una calidad aceptable.
148
- # Los archivos vienen en una lista 'video_files'.
149
- best_quality_url = None
150
- for file in video.get('video_files', []):
151
- # Preferir calidades como 'hd', 'sd', 'medium', 'large'
152
- if file.get('quality') in ['hd', 'sd', 'medium', 'large'] and file.get('link'):
153
- best_quality_url = file['link']
154
- break # Tomar el primero que cumpla
155
- elif file.get('link'): # Si no hay calidad específica, tomar cualquier enlace válido
156
- best_quality_url = file['link']
157
-
158
- if best_quality_url:
159
- video_urls.append(best_quality_url)
160
- if len(video_urls) >= num_videos:
161
- break # Suficientes videos
162
- else:
163
- logger.warning(f"No se encontró URL de descarga válida para el video Pexels ID: {video.get('id')}")
164
 
165
- logger.info(f"Encontrados {len(video_urls)} URLs de video de Pexels para '{query}'.")
166
- return video_urls
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
- except Exception as e:
169
- logger.error(f"Error al buscar videos en Pexels para '{query}': {e}")
170
  return []
171
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  # --- Función Principal de Creación de Video ---
173
 
174
  def crear_video(prompt_type, input_text, musica_url=None):
@@ -177,8 +237,8 @@ def crear_video(prompt_type, input_text, musica_url=None):
177
  if prompt_type == "Generar Guion con IA":
178
  guion = generate_script(input_text)
179
  if not guion or guion == "No se pudo generar el guion. Intenta con otro prompt o un guion propio.":
180
- raise ValueError(guion)
181
- else: # prompt_type == "Usar Mi Guion"
182
  guion = input_text
183
  if not guion.strip():
184
  raise ValueError("Por favor, introduce tu guion.")
@@ -186,8 +246,13 @@ def crear_video(prompt_type, input_text, musica_url=None):
186
  if not guion.strip():
187
  raise ValueError("El guion está vacío. No se puede proceder.")
188
 
189
- temp_files = []
190
- clips = []
 
 
 
 
 
191
 
192
  try:
193
  # 1. Generar audio TTS
@@ -196,61 +261,82 @@ def crear_video(prompt_type, input_text, musica_url=None):
196
  asyncio.run(text_to_speech(guion, output_path=voz_archivo))
197
  audio_tts = AudioFileClip(voz_archivo)
198
 
199
- # 2. Búsqueda y descarga de videos con Pexels
200
- # Usamos el guion o el prompt inicial para la búsqueda en Pexels
201
- search_query = input_text if prompt_type == "Generar Guion con IA" else guion.split('.')[0] # Usar primera frase del guion
202
- pexels_video_urls = search_pexels_videos(search_query, num_videos=4, min_duration_sec=7) # Pedimos 4, min 7s
203
-
204
- # Si Pexels no devuelve nada o falla, podemos tener unas URLs de fallback
205
- if not pexels_video_urls:
206
- logger.warning("Pexels no devolvió videos. Usando videos de ejemplo de fallback.")
207
- pexels_video_urls = [
208
- "https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4",
209
- "https://file-examples.com/storage/fe2c91b5c46522c0734a74a/2017/04/file_example_MP4_480_1_5MG.mp4",
210
- "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4",
211
- "https://test-videos.co.uk/vids/bigbuckbunny/mp4/720/big_buck_bunny_720p_1mb.mp4"
212
- ]
213
-
214
- valid_clip_found = False
215
- for url in pexels_video_urls:
216
- video_path = download_video_sample(url)
 
 
217
  if video_path:
218
- temp_files.append(video_path)
219
- try:
220
- clip = VideoFileClip(video_path).subclip(0, min(15, VideoFileClip(video_path).duration))
221
- if clip.duration > 1:
222
- clips.append(clip)
223
- valid_clip_found = True
224
- else:
225
- logger.warning(f"Clip de video muy corto ({clip.duration:.2f}s) de {url}, omitiendo.")
226
- clip.close()
227
- except Exception as e:
228
- logger.warning(f"No se pudo cargar o procesar el clip de video {video_path} de {url}: {e}")
229
  else:
230
- logger.warning(f"No se pudo descargar el video de la URL: {url}")
231
 
232
- if not valid_clip_found or not clips:
233
- logger.error("No se pudieron obtener clips de video válidos. Abortando creación de video.")
234
- raise ValueError("No se encontraron clips de video válidos. Asegúrate de que las URLs de Pexels o fallback sean correctas y accesibles.")
235
 
236
- # 3. Concatenar videos
237
- video_base = concatenate_videoclips(clips, method="compose")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  logger.info(f"Video base concatenado, duración: {video_base.duration:.2f}s")
239
 
 
 
240
  if video_base.duration < audio_tts.duration:
241
  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.")
242
  num_repeats = int(audio_tts.duration / video_base.duration) + 1
243
  repeated_clips = [video_base] * num_repeats
244
- video_base = concatenate_videoclips(repeated_clips, method="compose")
245
-
 
246
  final_video_duration = audio_tts.duration
247
 
248
- # 4. Música de fondo en loop
249
  mezcla_audio = audio_tts
250
  if musica_url and musica_url.strip():
251
- musica_path = download_video_sample(musica_url)
252
  if musica_path:
253
- temp_files.append(musica_path)
254
  try:
255
  musica_audio = AudioFileClip(musica_path)
256
  musica_loop = loop_audio_to_length(musica_audio, final_video_duration)
@@ -261,11 +347,11 @@ def crear_video(prompt_type, input_text, musica_url=None):
261
  else:
262
  logger.warning(f"No se pudo descargar la música de {musica_url}. Se usará solo la voz.")
263
 
264
- # 5. Asignar audio al video y ajustar duración
265
  video_final = video_base.set_audio(mezcla_audio).subclip(0, final_video_duration)
266
  logger.info(f"Video final configurado con audio. Duración final: {video_final.duration:.2f}s")
267
 
268
- # 6. Guardar video final
269
  output_dir = "generated_videos"
270
  os.makedirs(output_dir, exist_ok=True)
271
  output_path = os.path.join(output_dir, f"video_output_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
@@ -279,23 +365,34 @@ def crear_video(prompt_type, input_text, musica_url=None):
279
  logger.error(f"Error general en la creación del video: {e}", exc_info=True)
280
  raise e
281
  finally:
 
282
  for f in temp_files:
283
  if os.path.exists(f):
284
- try:
285
- os.remove(f)
286
- logger.info(f"Archivo temporal eliminado: {f}")
287
- except Exception as e:
288
- logger.warning(f"No se pudo eliminar el archivo temporal {f}: {e}")
289
- for clip in clips:
290
- if clip:
 
 
 
 
 
 
 
 
 
 
291
  clip.close()
292
- if 'audio_tts' in locals() and audio_tts:
293
  audio_tts.close()
294
- if 'musica_audio' in locals() and musica_audio:
295
  musica_audio.close()
296
- if 'video_final' in locals() and video_final:
297
  video_final.close()
298
- if 'video_base' in locals() and video_base:
299
  video_base.close()
300
 
301
 
@@ -319,8 +416,8 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_url):
319
  return video_path, gr.update(value="¡Video generado exitosamente!")
320
  else:
321
  raise gr.Error("Hubo un problema desconocido al generar el video. Revisa los logs.")
322
- except ValueError as ve:
323
- logger.error(f"Error de validación: {ve}")
324
  return None, gr.update(value=f"Error: {ve}", text_color="red")
325
  except Exception as e:
326
  logger.error(f"Error inesperado al ejecutar la aplicación: {e}", exc_info=True)
@@ -330,7 +427,7 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_url):
330
  with gr.Blocks() as app:
331
  gr.Markdown("""
332
  ### 🎬 Generador de Video Inteligente con Pexels 🚀
333
- Crea videos con guiones generados por IA o propios, voz automática y videos de stock relevantes de Pexels.
334
  """)
335
 
336
  with gr.Tab("Generar Video"):
 
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)
31
  model = GPT2LMHeadModel.from_pretrained(MODEL_NAME).eval()
 
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.")
55
  return "Lo siento, el generador de guiones no está disponible en este momento."
 
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:
108
+ for chunk in response.iter_content(chunk_size=8192):
109
+ if chunk:
110
+ f.write(chunk)
111
+ logger.info(f"Video descargado a: {output_path}")
112
+ return output_path
113
  except requests.exceptions.RequestException as e:
114
  logger.error(f"Error de red/HTTP al descargar el video {url}: {e}")
115
+ if os.path.exists(output_path):
116
+ os.remove(output_path)
117
  return None
118
  except Exception as e:
119
  logger.error(f"Error inesperado al descargar video {url}: {e}")
120
+ if os.path.exists(output_path):
121
+ os.remove(output_path)
122
  return None
123
 
124
  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.")
187
 
188
+ if not query_list:
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
+
201
+ if not videos:
202
+ logger.info(f"No se encontraron videos en Pexels para la query: '{query}'")
203
+ continue
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
211
+ for file in video.get('video_files', []):
212
+ if file.get('quality') in ['hd', 'sd', 'medium', 'large'] and file.get('link'):
213
+ best_quality_url = file['link']
214
+ break
215
+ elif file.get('link'):
216
+ best_quality_url = file['link']
217
+
218
+ if best_quality_url:
219
+ all_video_urls.add(best_quality_url)
220
+ else:
221
+ logger.warning(f"No se encontró URL de descarga válida para el video Pexels ID: {video.get('id')}")
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):
 
237
  if prompt_type == "Generar Guion con IA":
238
  guion = generate_script(input_text)
239
  if not guion or guion == "No se pudo generar el guion. Intenta con otro prompt o un guion propio.":
240
+ raise ValueError(guion)
241
+ else:
242
  guion = input_text
243
  if not guion.strip():
244
  raise ValueError("Por favor, introduce tu guion.")
 
246
  if not guion.strip():
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
 
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:
287
+ logger.warning(f"No se pudo descargar video de Pexels: {url}")
288
 
289
+ if not downloaded_clip_paths:
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
  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
  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
 
 
416
  return video_path, gr.update(value="¡Video generado exitosamente!")
417
  else:
418
  raise gr.Error("Hubo un problema desconocido al generar el video. Revisa los logs.")
419
+ except ValueError as ve:
420
+ logger.error(f"Error de validación o búsqueda de Pexels: {ve}")
421
  return None, gr.update(value=f"Error: {ve}", text_color="red")
422
  except Exception as e:
423
  logger.error(f"Error inesperado al ejecutar la aplicación: {e}", exc_info=True)
 
427
  with gr.Blocks() as app:
428
  gr.Markdown("""
429
  ### 🎬 Generador de Video Inteligente con Pexels 🚀
430
+ Crea videos con guiones generados por IA o propios, voz automática y **videos de stock relevantes de Pexels (sin videos de ejemplo)**.
431
  """)
432
 
433
  with gr.Tab("Generar Video"):