gnosticdev commited on
Commit
cc24506
·
verified ·
1 Parent(s): 43865d4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +287 -445
app.py CHANGED
@@ -7,21 +7,16 @@ from datetime import datetime
7
  import edge_tts
8
  import gradio as gr
9
  import torch
10
- # Corregir typo: GPT2Tokenizer en lugar de GPTizer
11
  from transformers import GPT2Tokenizer, GPT2LMHeadModel
12
  from keybert import KeyBERT
13
- # Importar concatenate_audioclips específicamente si no está en editor por defecto
14
  from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips
15
  import re
16
  import math
17
- # Pydub ya no parece usarse directamente en las funciones principales,
18
- # pero se mantiene por si acaso o por si se usó en intentos previos.
19
- # from pydub import AudioSegment # Comentado porque no se usa en el flujo principal corregido
20
- from collections import Counter
21
  import shutil
22
  import json
 
23
 
24
- # Configuración de logging MEJORADA
25
  logging.basicConfig(
26
  level=logging.DEBUG,
27
  format='%(asctime)s - %(levelname)s - %(message)s',
@@ -32,52 +27,46 @@ logging.basicConfig(
32
  )
33
  logger = logging.getLogger(__name__)
34
  logger.info("="*80)
35
- logger.info("INICIO DE EJECUCIÓN - GENERADOR DE VIDEOS")
36
  logger.info("="*80)
37
 
38
- # Clave API de Pexels (configuración segura)
39
- # Asegúrate de que esta variable de entorno esté configurada en tu entorno.
40
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
41
  if not PEXELS_API_KEY:
42
- logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
43
- # Permite que continúe en desarrollo/test pero registra el problema
44
- # raise ValueError("API key de Pexels no configurada") # Descomentar para forzar fallo si no está
45
- logger.warning("Continuando sin PEXELS_API_KEY. La búsqueda de videos fallará.")
46
- else:
47
- logger.info("API key de Pexels configurada correctamente")
48
-
49
- # Inicialización de modelos CON LOGS DETALLADOS
50
  MODEL_NAME = "datificate/gpt2-small-spanish"
51
- logger.info(f"Inicializando modelo GPT-2: {MODEL_NAME}")
52
- tokenizer = None # Inicializar a None por si falla
53
- model = None # Inicializar a None por si falla
54
  try:
55
- # CORRECCIÓN: Usar GPT2Tokenizer
56
  tokenizer = GPT2Tokenizer.from_pretrained(MODEL_NAME)
57
  model = GPT2LMHeadModel.from_pretrained(MODEL_NAME).eval()
58
  if tokenizer.pad_token is None:
59
  tokenizer.pad_token = tokenizer.eos_token
60
- logger.info(f"Modelo GPT-2 cargado | Vocabulario: {len(tokenizer)} tokens")
61
  except Exception as e:
62
- logger.error(f"FALLA CRÍTICA al cargar GPT-2: {str(e)}", exc_info=True)
63
- tokenizer = model = None # Asegurar que queden como None
64
 
65
- logger.info("Cargando modelo KeyBERT...")
66
- kw_model = None # Inicializar a None por si falla
67
  try:
68
  kw_model = KeyBERT('distilbert-base-multilingual-cased')
69
- logger.info("KeyBERT inicializado correctamente")
70
  except Exception as e:
71
- logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
72
- kw_model = None # Asegurar que quede como None
73
 
74
- # [FUNCIÓN BUSCAR_VIDEOS_PEXELS ORIGINAL CON LOGS AÑADIDOS]
75
  def buscar_videos_pexels(query, api_key, per_page=5):
76
  if not api_key:
77
- logger.warning("No se puede buscar en Pexels: API Key no configurada.")
78
  return []
79
 
80
- logger.debug(f"Buscando en Pexels: '{query}' | Resultados: {per_page}")
81
  headers = {"Authorization": api_key}
82
  try:
83
  params = {
@@ -87,368 +76,301 @@ def buscar_videos_pexels(query, api_key, per_page=5):
87
  "size": "medium"
88
  }
89
 
90
- logger.debug(f"Params: {params}")
91
  response = requests.get(
92
  "https://api.pexels.com/videos/search",
93
  headers=headers,
94
  params=params,
95
  timeout=20
96
  )
97
- response.raise_for_status() # Lanza una excepción para códigos de estado de error HTTP
98
-
99
- try:
100
- data = response.json()
101
- videos = data.get('videos', [])
102
- logger.info(f"Pexels: {len(videos)} videos encontrados para '{query}'")
103
- return videos
104
- except json.JSONDecodeError:
105
- logger.error(f"Pexels: JSON inválido recibido | Status: {response.status_code} | Respuesta: {response.text[:200]}...")
106
- return []
107
 
108
  except requests.exceptions.RequestException as e:
109
- logger.error(f"Error de conexión Pexels para '{query}': {str(e)}")
 
 
110
  except Exception as e:
111
- logger.error(f"Error inesperado Pexels para '{query}': {str(e)}", exc_info=True)
112
 
113
  return []
114
 
115
- # [FUNCIÓN GENERATE_SCRIPT ORIGINAL CON LOGS]
116
  def generate_script(prompt, max_length=150):
117
- logger.info(f"Generando guión | Prompt: '{prompt[:50]}...' | Longitud máxima: {max_length}")
118
  if not tokenizer or not model:
119
- logger.warning("Modelos GPT-2 no disponibles - Usando prompt original como guion.")
120
  return prompt
121
 
122
  try:
123
- # Intenta mejorar el prompt para la IA si se detecta que es muy corto o no es una pregunta
124
- # (Esto es una heurística simple, podrías mejorarlo)
125
- if len(prompt.split()) < 5 or not re.search(r'[¿?.]', prompt):
126
- enhanced_prompt = f"Escribe un guion corto, interesante y coherente sobre: {prompt}"
127
- else:
128
- enhanced_prompt = prompt # Si parece ya un guion o pregunta, úsalo directamente
129
- logger.debug("Usando prompt original como base para la generación.")
130
-
131
  inputs = tokenizer(enhanced_prompt, return_tensors="pt", truncation=True, max_length=512)
132
 
133
- logger.debug("Generando texto con GPT-2...")
134
- # Asegurar que el modelo esté en modo evaluación y en la CPU si no hay GPU
135
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
136
  model.to(device)
137
  inputs = {k: v.to(device) for k, v in inputs.items()}
138
 
139
-
140
  outputs = model.generate(
141
  **inputs,
142
  max_length=max_length,
143
- do_sample=True, # Permite creatividad
144
  top_p=0.9,
145
  top_k=40,
146
  temperature=0.7,
147
- repetition_penalty=1.2, # Reducido un poco para menos repetición agresiva
148
  pad_token_id=tokenizer.pad_token_id,
149
  eos_token_id=tokenizer.eos_token_id,
150
- no_repeat_ngram_size=3 # Ayuda a evitar repeticiones de frases cortas
151
  )
152
 
153
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
154
 
155
- # Limpieza del texto
156
- text = re.sub(r'<[^>]+>', '', text) # Elimina posibles tags HTML/XML si los hay
157
  text = text.strip()
158
 
159
- # Intenta obtener al menos una oración completa si es posible
160
  sentences = text.split('.')
161
  if sentences and sentences[0].strip():
162
  final_text = sentences[0].strip() + '.'
163
- # Añadir la segunda oración si existe y es razonable
164
- if len(sentences) > 1 and sentences[1].strip() and len(final_text.split()) < max_length * 0.5: # Si la primera es corta y el guion no muy largo aún
165
  final_text += " " + sentences[1].strip() + "."
166
- final_text = final_text.replace("..", ".") # Limpiar doble punto
167
 
168
- logger.info(f"Guion generado (Truncado a 100 chars): '{final_text[:100]}...'")
169
- return final_text.strip() # Asegurar que no haya espacios al inicio/fin
170
 
171
- logger.info(f"Guion generado (sin oraciones completas detectadas): '{text[:100]}...'")
172
- return text.strip() # Si no se puede formar una oración, devolver el texto tal cual
 
173
  except Exception as e:
174
- logger.error(f"Error generando guion con GPT-2: {str(e)}", exc_info=True)
175
- logger.warning("Usando prompt original como guion debido al error de generación.")
176
- return prompt.strip() # En caso de error, devolver el prompt original limpio
177
 
178
- # [FUNCIÓN TEXT_TO_SPEECH ORIGINAL CON LOGS]
179
  async def text_to_speech(text, output_path, voice="es-ES-ElviraNeural"):
180
- logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
181
  if not text or not text.strip():
182
- logger.warning("Texto vacío para TTS")
183
  return False
184
 
185
  try:
186
  communicate = edge_tts.Communicate(text, voice)
187
  await communicate.save(output_path)
188
 
189
- if os.path.exists(output_path) and os.path.getsize(output_path) > 100: # Check for minimum size
190
- logger.info(f"Audio guardado exitosamente en: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
191
  return True
192
  else:
193
- logger.error(f"TTS guardó un archivo pequeño o vacío en: {output_path}")
194
  return False
195
 
196
  except Exception as e:
197
- logger.error(f"Error en TTS: {str(e)}", exc_info=True)
198
  return False
199
 
200
- # [FUNCIÓN DOWNLOAD_VIDEO_FILE ORIGINAL CON LOGS]
201
  def download_video_file(url, temp_dir):
202
  if not url:
203
- logger.warning("URL de video no proporcionada para descargar")
204
  return None
205
 
206
  try:
207
- logger.info(f"Descargando video desde: {url[:80]}...")
208
- # Usar un nombre más robusto y asegurar que el directorio exista
209
  os.makedirs(temp_dir, exist_ok=True)
210
  file_name = f"video_dl_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.mp4"
211
  output_path = os.path.join(temp_dir, file_name)
212
 
213
- # Aumentar timeout para descargas grandes
214
  with requests.get(url, stream=True, timeout=60) as r:
215
  r.raise_for_status()
216
- total_size = int(r.headers.get('content-length', 0))
217
- downloaded_size = 0
218
- logger.debug(f"Tamaño esperado: {total_size} bytes")
219
-
220
  with open(output_path, 'wb') as f:
221
  for chunk in r.iter_content(chunk_size=8192):
222
  f.write(chunk)
223
- downloaded_size += len(chunk)
224
- # Opcional: loguear progreso si es muy lento
225
 
226
- # Verificar si el archivo descargado tiene un tamaño razonable
227
- if os.path.exists(output_path) and os.path.getsize(output_path) > 1000: # Mínimo 1KB
228
- logger.info(f"Video descargado exitosamente: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
229
  return output_path
230
  else:
231
- logger.warning(f"Descarga parece incompleta o vacía para {url[:80]}... Archivo: {output_path} Tamaño: {os.path.getsize(output_path) if os.path.exists(output_path) else 'N/A'} bytes")
232
  if os.path.exists(output_path):
233
- os.remove(output_path) # Eliminar archivo inválido
234
  return None
235
 
236
  except requests.exceptions.RequestException as e:
237
- logger.error(f"Error de descarga para {url[:80]}... : {str(e)}")
238
  except Exception as e:
239
- logger.error(f"Error inesperado descargando {url[:80]}... : {str(e)}", exc_info=True)
240
 
241
  return None
242
 
243
- # [FUNCIÓN LOOP_AUDIO_TO_LENGTH ORIGINAL CON LOGS]
244
  def loop_audio_to_length(audio_clip, target_duration):
245
- logger.debug(f"Ajustando audio | Duración actual: {audio_clip.duration:.2f}s | Objetivo: {target_duration:.2f}s")
246
  if audio_clip.duration <= 0:
247
- logger.warning("Clip de audio de duración cero o negativa, no se puede ajustar.")
248
- return AudioFileClip(filename="") # Devuelve clip de audio vacío
249
 
250
  if audio_clip.duration >= target_duration:
251
- logger.debug("Clip de audio ya es más largo o igual al objetivo.")
252
  return audio_clip.subclip(0, target_duration)
253
 
254
- # CORRECCIÓN: Usar concatenate_audioclips para audio
255
  loops = math.ceil(target_duration / audio_clip.duration)
256
- logger.debug(f"Creando {loops} loops de audio")
257
 
258
- # Crear una lista de clips de audio
259
  audio_segments = [audio_clip] * loops
260
-
261
- # Concatenar los clips de audio
262
  looped_audio = concatenate_audioclips(audio_segments)
263
 
264
- # Recortar al target_duration exacto
265
  return looped_audio.subclip(0, target_duration)
266
 
267
- # [FUNCIÓN EXTRACT_VISUAL_KEYWORDS_FROM_SCRIPT ORIGINAL CON LOGS]
268
  def extract_visual_keywords_from_script(script_text):
269
- logger.info("Extrayendo palabras clave del guion")
270
  if not script_text or not script_text.strip():
271
- logger.warning("Guion vacío, no se pueden extraer palabras clave.")
272
- return ["naturaleza", "ciudad", "paisaje"] # Keywords por defecto
273
 
274
- # Limpieza de texto más robusta, manteniendo espacios para keyphrases
275
  clean_text = re.sub(r'[^\w\sáéíóúñÁÉÍÓÚÑ]', '', script_text)
276
-
277
  keywords_list = []
278
 
279
  if kw_model:
280
  try:
281
- logger.debug("Intentando extraer keywords con KeyBERT...")
282
- # Intentar con ngrams 1 y 2
283
- keywords1 = kw_model.extract_keywords(
284
- clean_text,
285
- keyphrase_ngram_range=(1, 1),
286
- stop_words='spanish',
287
- top_n=5
288
- )
289
- keywords2 = kw_model.extract_keywords(
290
- clean_text,
291
- keyphrase_ngram_range=(2, 2),
292
- stop_words='spanish',
293
- top_n=3
294
- )
295
 
296
- # Combinar y priorizar palabras clave (ngram 1) y frases clave (ngram 2)
297
  all_keywords = keywords1 + keywords2
298
- # Ordenar por score descendente
299
  all_keywords.sort(key=lambda item: item[1], reverse=True)
300
 
301
- # Tomar los top N únicos
302
  seen_keywords = set()
303
  for keyword, score in all_keywords:
304
- # Convertir a minúsculas y reemplazar espacios por + para URL
305
  formatted_keyword = keyword.lower().replace(" ", "+")
306
- if formatted_keyword not in seen_keywords:
307
  keywords_list.append(formatted_keyword)
308
  seen_keywords.add(formatted_keyword)
309
- if len(keywords_list) >= 5: # Limitar el número total de keywords
310
  break
311
 
312
  if keywords_list:
313
- logger.debug(f"KeyBERT keywords extraídas: {keywords_list}")
314
  return keywords_list
315
 
316
  except Exception as e:
317
- logger.warning(f"KeyBERT falló durante la extracción: {str(e)}. Intentando método simple.")
318
- # Continúa al método simple si KeyBERT falla
319
 
320
- # Método simple de extracción si KeyBERT no está disponible o falló
321
- logger.debug("Extrayendo keywords con método simple...")
322
  words = clean_text.lower().split()
323
- # Conjunto de stop words más amplio y eficiente
324
- stop_words = {"el", "la", "los", "las", "de", "en", "y", "a", "que", "es", "un", "una", "con", "para", "del", "al", "por", "su", "sus", "se", "lo", "le", "me", "te", "nos", "os", "les", "mi", "tu", "nuestro", "vuestro", "este", "ese", "aquel", "esta", "esa", "aquella", "esto", "eso", "aquello", "mis", "tus", "nuestros", "vuestros", "estas", "esas", "aquellas", "si", "no", "más", "menos", "sin", "sobre", "bajo", "entre", "hasta", "desde", "durante", "mediante", "según", "versus", "via", "cada", "todo", "todos", "toda", "todas", "poco", "pocos", "poca", "pocas", "mucho", "muchos", "mucha", "muchas", "varios", "varias", "otro", "otros", "otra", "otras", "mismo", "misma", "mismos", "mismas", "tan", "tanto", "tanta", "tantos", "tantas", "tal", "tales", "cual", "cuales", "cuyo", "cuya", "cuyos", "cuyas", "quien", "quienes", "cuan", "cuanto", "cuanta", "cuantos", "cuantas", "como", "donde", "cuando", "que", "porque", "si", "aunque", "mientras", "cuando", "como", "donde", "siempre", "nunca", "jamás", "muy", "más", "menos", "tan", "tanto", "casi", "solo", "solamente", "incluso", "apenas", "quizás", "tal vez", "acaso", "claro", "cierto", "obvio", "evidentemente", "realmente", "simplemente", "generalmente", "especialmente", "principalmente", "posiblemente", "probablemente", "difícilmente", "fácilmente", "rápidamente", "lentamente", "bien", "mal", "mejor", "peor", "arriba", "abajo", "adelante", "atrás", "cerca", "lejos", "dentro", "fuera", "encima", "debajo", "frente", "detrás", "antes", "después", "luego", "pronto", "tarde", "todavía", "ya", "aun", "aún", "siempre", "nunca", "jamás", "quizá", "acaso", "tal vez"}
325
 
326
- # Filtrar palabras, eliminar stop words y palabras cortas
327
  valid_words = [word for word in words if len(word) > 3 and word not in stop_words]
328
 
329
  if not valid_words:
330
- logger.warning("No se encontraron palabras clave válidas con método simple. Usando palabras clave predeterminadas.")
331
  return ["naturaleza", "ciudad", "paisaje"]
332
 
333
- # Contar frecuencia y tomar las más comunes
334
  word_counts = Counter(valid_words)
335
- # Tomar hasta 5 palabras clave más comunes
336
  top_keywords = [word.replace(" ", "+") for word, _ in word_counts.most_common(5)]
337
 
338
  if not top_keywords:
339
- logger.warning("El método simple no produjo keywords. Usando palabras clave predeterminadas.")
340
  return ["naturaleza", "ciudad", "paisaje"]
341
 
342
- logger.info(f"Palabras clave finales: {top_keywords}")
343
  return top_keywords
344
 
345
- # [FUNCIÓN CREAR_VIDEO CORREGIDA]
346
  def crear_video(prompt_type, input_text, musica_file=None):
347
  logger.info("="*80)
348
- logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
349
  logger.debug(f"Input: '{input_text[:100]}...'")
350
 
351
  start_time = datetime.now()
352
- temp_dir_intermediate = None # Directorio para archivos temporales intermedios
353
- output_video_path = None # Path para el video final fuera del directorio intermedio
354
-
355
  try:
356
- # 1. Generar o usar guion
357
  if prompt_type == "Generar Guion con IA":
358
- logger.info("Generando guion con IA...")
359
  guion = generate_script(input_text)
360
  else:
361
- logger.info("Usando guion proporcionado")
362
- guion = input_text.strip() # Asegurar que el guion manual no tenga espacios al inicio/fin
363
 
364
- logger.info(f"Guion final ({len(guion)} caracteres): '{guion[:100]}...'")
365
 
366
  if not guion.strip():
367
- logger.error("El guion resultante está vacío o solo contiene espacios.")
368
- raise ValueError("El guion está vacío.")
369
 
370
- # Crear directorio temporal para archivos intermedios (audio TTS, videos descargados)
371
  temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
372
- logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
373
- temp_intermediate_files = [] # Lista para seguir los archivos que hay que limpiar
374
 
375
- # 2. Generar audio de voz
376
- logger.info("Generando audio de voz...")
377
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
378
- # No añadimos voz_path a temp_intermediate_files *antes* de la generación,
379
- # solo si la generación es exitosa.
380
  if not asyncio.run(text_to_speech(guion, voz_path)):
381
- logger.error("Fallo en generación de voz.")
382
- raise ValueError("Error generando voz a partir del guion.")
383
- temp_intermediate_files.append(voz_path) # Añadir para limpieza si la generación tuvo éxito
384
 
385
  audio_tts = AudioFileClip(voz_path)
386
  audio_duration = audio_tts.duration
387
- logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
388
 
389
  if audio_duration < 1.0:
390
- logger.warning(f"Duración del audio de voz ({audio_duration:.2f}s) es muy corta. El video podría ser muy breve o fallar.")
391
- # Podrías añadir un mínimo o generar un guion/voz alternativo
392
 
393
- # 3. Extraer palabras clave
394
- logger.info("Extrayendo palabras clave...")
395
  try:
396
  keywords = extract_visual_keywords_from_script(guion)
397
- logger.info(f"Palabras clave identificadas: {keywords}")
398
  except Exception as e:
399
- logger.error(f"Error extrayendo keywords: {str(e)}", exc_info=True)
400
- keywords = ["naturaleza", "paisaje"] # Fallback si la extracción falla
401
 
402
  if not keywords:
403
- logger.warning("Lista de palabras clave vacía después de extracción y fallback.")
404
- keywords = ["video", "background"] # Fallback final
405
 
406
- # 4. Buscar y descargar videos
407
- logger.info("Buscando videos en Pexels...")
408
  videos_data = []
409
- # Buscar con todas las keywords, intentando obtener más resultados al principio
410
- total_desired_videos = 10 # Intentar descargar hasta 10 videos en total
411
- per_page_per_keyword = max(1, total_desired_videos // len(keywords)) # Distribuir la búsqueda
412
 
413
  for keyword in keywords:
414
- if len(videos_data) >= total_desired_videos: break # Dejar de buscar si ya tenemos suficientes
415
  try:
416
  videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=per_page_per_keyword)
417
  if videos:
418
  videos_data.extend(videos)
419
- logger.info(f"Encontrados {len(videos)} videos para '{keyword}'. Total data: {len(videos_data)}")
420
  except Exception as e:
421
- logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
422
 
423
- # Si no se encontraron suficientes videos con keywords específicas, intentar genéricas
424
- if len(videos_data) < total_desired_videos / 2: # Si tenemos menos de la mitad de los deseados
425
- logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
426
- generic_keywords = ["nature", "city", "background", "abstract"] # Palabras clave genéricas en inglés para Pexels
427
  for keyword in generic_keywords:
428
  if len(videos_data) >= total_desired_videos: break
429
  try:
430
- # Buscar menos por cada genérica para no saturar
431
- videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=2)
432
  if videos:
433
  videos_data.extend(videos)
434
- logger.info(f"Encontrados {len(videos)} videos para '{keyword}' (genérico). Total data: {len(videos_data)}")
435
  except Exception as e:
436
- logger.warning(f"Error buscando videos genéricos para '{keyword}': {str(e)}")
437
 
438
 
439
  if not videos_data:
440
- logger.error("No se encontraron videos en Pexels para ninguna palabra clave.")
441
- raise ValueError("No se encontraron videos adecuados en Pexels.")
442
 
443
  video_paths = []
444
- logger.info(f"Intentando descargar {len(videos_data)} videos encontrados...")
445
  for video in videos_data:
446
  if 'video_files' not in video or not video['video_files']:
447
- logger.debug(f"Saltando video sin archivos de video: {video.get('id')}")
448
  continue
449
 
450
  try:
451
- # Encontrar la mejor calidad disponible con un enlace de descarga
452
  best_quality = None
453
  for vf in sorted(video['video_files'], key=lambda x: x.get('width', 0) * x.get('height', 0), reverse=True):
454
  if 'link' in vf:
@@ -459,341 +381,287 @@ def crear_video(prompt_type, input_text, musica_file=None):
459
  path = download_video_file(best_quality['link'], temp_dir_intermediate)
460
  if path:
461
  video_paths.append(path)
462
- temp_intermediate_files.append(path) # Añadir para limpieza
463
- logger.info(f"Video descargado OK desde {best_quality['link'][:50]}...")
464
  else:
465
- logger.warning(f"No se pudo descargar video desde {best_quality['link'][:50]}...")
466
  else:
467
- logger.warning(f"No se encontró enlace de descarga válido para video {video.get('id')}.")
468
 
469
  except Exception as e:
470
- logger.warning(f"Error procesando/descargando video {video.get('id')}: {str(e)}")
471
 
472
- logger.info(f"Descargados {len(video_paths)} archivos de video utilizables.")
473
  if not video_paths:
474
- logger.error("No se pudo descargar ningún archivo de video utilizable.")
475
- raise ValueError("No se pudo descargar ningún video utilizable de Pexels.")
476
 
477
- # 5. Procesar y concatenar clips de video
478
- logger.info("Procesando y concatenando videos descargados...")
479
  clips = []
480
  current_duration = 0
481
- min_clip_duration = 1.0 # Duración mínima para un clip
482
- max_clip_segment = 8.0 # Segmento máximo a tomar de un clip fuente
483
-
484
- # Ordenar clips descargados por tamaño o duración si se desea, o usar el orden de descarga
485
- # Por ahora, usamos el orden de descarga
486
 
487
- for path in video_paths:
488
- # Dejar de añadir clips si ya tenemos suficiente duración o estamos cerca
489
- # Añadimos un buffer para no cortar bruscamente el último clip
490
- if current_duration >= audio_duration + max_clip_segment:
491
  break
492
 
 
493
  try:
494
- logger.debug(f"Abriendo clip: {path}")
495
  clip = VideoFileClip(path)
496
-
497
- # Calcular duración útil: min(duración_restante_necesaria, duración_total_clip, segmento_máximo)
 
 
 
 
 
498
  remaining_needed = audio_duration - current_duration
499
- usable_duration = min(clip.duration, max_clip_segment)
500
-
501
- # Si necesitamos más duración, tomamos hasta el final del clip o el max_clip_segment
502
  if remaining_needed > 0:
503
- usable_duration = min(usable_duration, remaining_needed + min_clip_duration) # Añadir un poco extra si el último clip nos deja justos
504
- usable_duration = max(min_clip_duration, usable_duration) # Asegurar duración mínima si es posible
505
-
506
- if usable_duration > min_clip_duration:
507
- # Asegurar que no excedemos la duración del clip fuente
508
- segment_duration = min(usable_duration, clip.duration)
509
- if segment_duration > min_clip_duration:
510
- clips.append(clip.subclip(0, segment_duration))
511
- current_duration += segment_duration
512
- logger.debug(f"Clip añadido: {segment_duration:.1f}s (total video: {current_duration:.1f}/{audio_duration:.1f}s)")
513
- else:
514
- logger.debug(f"Clip {path} es demasiado corto ({clip.duration:.1f}s) para añadir un segmento útil.")
 
 
 
515
  else:
516
- logger.debug(f"Clip {path} demasiado corto ({clip.duration:.1f}s) o no necesita más duración.")
517
-
518
- # Cerrar clip para liberar recursos
519
- clip.close()
520
 
521
  except Exception as e:
522
- logger.warning(f"Error procesando video clip {path}: {str(e)}", exc_info=True)
523
- # Continuar con el siguiente clip si hay un error con este
524
  continue
525
 
 
 
 
 
 
 
 
 
 
 
 
526
  if not clips:
527
- logger.error("No hay clips de video válidos para crear la secuencia.")
528
- raise ValueError("No hay clips de video válidos para crear el video.")
529
 
530
- logger.info(f"Concatenando {len(clips)} clips de video.")
531
  video_base = concatenate_videoclips(clips, method="compose")
532
- logger.info(f"Duración base del video concatenado: {video_base.duration:.2f}s")
533
 
534
- # Asegurar que el video base tenga al menos la duración del audio
535
  if video_base.duration < audio_duration:
536
- # Calcular cuántas veces se necesita repetir el video base o parte de él
537
- # Un método simple es repetir el video base completo
538
  num_repeats = math.ceil(audio_duration / video_base.duration)
539
- logger.info(f"Repitiendo video base ({video_base.duration:.2f}s) {num_repeats} veces para alcanzar {audio_duration:.2f}s.")
540
  repeated_clips = [video_base] * num_repeats
541
  video_base = concatenate_videoclips(repeated_clips, method="compose").subclip(0, audio_duration)
542
- logger.info(f"Duración del video base ajustada: {video_base.duration:.2f}s")
543
 
544
- # Si el video base es más largo que el audio (puede pasar si el último clip añadido fue largo)
545
- # Recortarlo exactamente a la duración del audio
546
  if video_base.duration > audio_duration:
547
- logger.info(f"Recortando video base ({video_base.duration:.2f}s) a la duración del audio ({audio_duration:.2f}s).")
548
  video_base = video_base.subclip(0, audio_duration)
549
- logger.info(f"Duración final del video base: {video_base.duration:.2f}s")
550
 
551
 
552
- # 6. Manejar música de fondo
553
- logger.info("Procesando audio...")
554
- final_audio = audio_tts # El audio base es la voz TTS
555
 
556
  if musica_file:
557
  try:
558
- # Copiar archivo de música a un lugar temporal seguro
559
  music_path = os.path.join(temp_dir_intermediate, "musica_bg.mp3")
560
  shutil.copyfile(musica_file, music_path)
561
- temp_intermediate_files.append(music_path) # Añadir para limpieza
562
- logger.info(f"Música de fondo copiada a: {music_path}")
563
 
564
  musica_audio = AudioFileClip(music_path)
565
- logger.debug(f"Duración música original: {musica_audio.duration:.2f}s")
566
 
567
- # Ajustar duración de la música al video final (que ya tiene la duración del audio TTS)
568
  musica_audio = loop_audio_to_length(musica_audio, video_base.duration)
569
- logger.debug(f"Música ajustada a duración del video: {musica_audio.duration:.2f}s")
570
 
571
- # Mezclar voz y música. Ajusta los volúmenes según prefieras.
572
  final_audio = CompositeAudioClip([
573
- musica_audio.volumex(0.2), # Volumen bajo para la música
574
- audio_tts.volumex(1.0) # Volumen normal para la voz
575
  ])
576
- logger.info("Mezcla de audio completada (voz + música).")
577
  except Exception as e:
578
- logger.warning(f"Error procesando música de fondo: {str(e)}", exc_info=True)
579
- # Si falla la música, simplemente usamos el audio TTS
580
  final_audio = audio_tts
581
- logger.warning("Usando solo audio de voz debido a un error con la música.")
582
 
583
- # Asegurar que el audio final tenga exactamente la misma duración que el video base
584
  if final_audio.duration > video_base.duration:
585
  final_audio = final_audio.subclip(0, video_base.duration)
586
- elif final_audio.duration < video_base.duration:
587
- # Esto no debería pasar si loop_audio_to_length funciona, pero como seguridad
588
- logger.warning("La duración del audio final es menor que la del video base. Intentando ajustar.")
589
- # Podría re-ajustar o logear error más severo
590
- # Para evitar errores en write_videofile, es mejor que el audio no sea más corto
591
- # Si es más corto, moviepy suele extender el último frame de audio, lo cual es feo pero funciona.
592
- # No intentaremos extenderlo aquí automáticamente para evitar complejidad, MoviePy lo manejará.
593
- pass # MoviePy manejará la diferencia de duración
594
-
595
- # 7. Crear video final
596
- logger.info("Renderizando video final...")
597
  video_final = video_base.set_audio(final_audio)
598
 
599
- # Gradio maneja directorios temporales para los outputs.
600
- # Podemos escribir directamente en un archivo temporal dentro de la carpeta temporal de Gradio.
601
- # Si no estamos en un entorno Gradio, podríamos usar un tempfile normal.
602
- # Por simplicidad y compatibilidad con Gradio, devolveremos el path absoluto.
603
- # El directorio temporal creado por tempfile.mkdtemp() EXISTIRÁ cuando la función retorne
604
- # porque hemos eliminado la línea que lo borraba. Gradio debería poder leerlo.
605
- # Si queremos un archivo temporal más "formal" de Gradio, necesitaríamos que Gradio
606
- # nos pasara un path temporal. La forma actual de retornar el path debería funcionar
607
- # ahora que no borramos el directorio.
608
-
609
- # Asegurar que el directorio para el output existe si no es temp_dir_intermediate
610
- # output_filename = f"final_video_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4"
611
- # # Si queremos que el archivo no esté en temp_dir_intermediate, podríamos usar otro temp_dir
612
- # # o un path fijo para prueba. Para Gradio, devolver el path en temp_dir_intermediate
613
- # # FUNCIONARÁ si temp_dir_intermediate NO SE BORRA.
614
- # output_path = os.path.join(temp_dir_intermediate, output_filename)
615
-
616
- # Dejar el nombre y path original dentro del temp_dir_intermediate,
617
- # pero ASEGURARSE de que el rmtree del finally está COMENTADO/ELIMINADO.
618
  output_filename = "final_video.mp4"
619
  output_path = os.path.join(temp_dir_intermediate, output_filename)
620
- logger.info(f"Escribiendo video final a: {output_path}")
621
 
622
  video_final.write_videofile(
623
  output_path,
624
  fps=24,
625
- threads=4, # Usa un número razonable de hilos
626
- codec="libx264", # Codec estándar compatible
627
- audio_codec="aac", # Codec de audio estándar
628
- preset="medium", # Balance entre velocidad y tamaño/calidad
629
- # CORRECCIÓN: Mostrar log de MoviePy para debugging
630
- logger='bar' # Muestra una barra de progreso y mensajes de FFmpeg
631
- # logger='log' # Muestra mensajes detallados de FFmpeg
632
  )
633
 
634
  total_time = (datetime.now() - start_time).total_seconds()
635
- logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {output_path} | Tiempo total: {total_time:.2f}s")
636
 
637
- # Cerrar clips para liberar recursos
638
- video_base.close()
639
- audio_tts.close()
640
- if musica_file: musica_audio.close()
641
- video_final.close()
 
 
 
642
 
643
- return output_path # Retornar el path del archivo generado
644
 
 
 
645
  except ValueError as ve:
646
- logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
647
- # Asegurar que se cierran los clips abiertos antes de relanzar
648
- # (Esto es mejor hacerlo en el finally, pero aquí es una capa extra)
649
- try:
650
- if 'video_base' in locals() and video_base is not None: video_base.close()
651
- if 'audio_tts' in locals() and audio_tts is not None: audio_tts.close()
652
- if 'musica_audio' in locals() and musica_audio is not None: musica_audio.close()
653
- if 'video_final' in locals() and video_final is not None: video_final.close()
654
- except: pass # Ignore errors during cleanup before re-raise
655
- raise ve # Rellenar la excepción para que la atrape run_app
656
-
657
  except Exception as e:
658
- logger.critical(f"ERROR CRÍTICO NO CONTROLADO en crear_video: {str(e)}", exc_info=True)
659
- # Asegurar que se cierran los clips abiertos antes de relanzar
660
- try:
661
- if 'video_base' in locals() and video_base is not None: video_base.close()
662
- if 'audio_tts' in locals() and audio_tts is not None: audio_tts.close()
663
- if 'musica_audio' in locals() and musica_audio is not None: musica_audio.close()
664
- if 'video_final' in locals() and video_final is not None: video_final.close()
665
- except: pass # Ignore errors during cleanup before re-raise
666
- raise e # Rellenar la excepción
667
-
668
  finally:
669
- logger.info("Iniciando limpieza de archivos temporales intermedios...")
670
- # Limpiar solo los archivos listados como temporales intermedios
671
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
672
- # Asegurarse de que el archivo de video final *no* sea borrado si está en esta lista por error
673
  final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4")
674
 
675
  for path in temp_intermediate_files:
676
  try:
677
- if os.path.isfile(path) and path != final_output_in_temp: # No borrar el archivo final
678
- logger.debug(f"Eliminando archivo temporal: {path}")
679
  os.remove(path)
680
- elif os.path.isdir(path): # Limpieza defensiva, aunque no deberíamos tener directorios aquí
681
- logger.warning(f"Intentando eliminar directorio listado como archivo temporal: {path}")
682
- shutil.rmtree(path, ignore_errors=True)
683
  except Exception as e:
684
- logger.warning(f"No se pudo eliminar archivo temporal {path}: {str(e)}")
685
 
686
- # CORRECCIÓN CRÍTICA: NO eliminar el directorio temporal completo con rmtree.
687
- # Este directorio contiene el video final que Gradio necesita leer.
688
- # El sistema operativo se encargará de limpiar los directorios temporales abandonados
689
- # con el tiempo, o puedes implementar una limpieza más sofisticada si es necesario
690
- # para archivos *muy* viejos, pero NO lo hagas inmediatamente después de crear el archivo.
691
- # shutil.rmtree(temp_dir_intermediate, ignore_errors=True) # COMENTADO/ELIMINADO intencionadamente
692
- logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
693
 
694
 
695
- # [FUNCIÓN RUN_APP ORIGINAL CON LOGS]
696
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
697
  logger.info("="*80)
698
- logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
699
 
700
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
701
 
702
  if not input_text or not input_text.strip():
703
- logger.warning("Texto de entrada vacío.")
704
- # Asegurarse de devolver None para el video y un mensaje de error
705
  return None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.")
706
 
707
- # Gradio actualiza la interfaz, no necesitamos un primer return para "Procesando..." aquí,
708
- # el .then() encadenado en la interfaz ya maneja eso.
709
- logger.info(f"Tipo de entrada: {prompt_type}")
710
- logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
711
  if musica_file:
712
- logger.info(f"Archivo de música recibido: {musica_file}")
713
  else:
714
- logger.info("No se proporcionó archivo de música.")
715
 
716
  try:
717
- logger.info("Llamando a crear_video...")
718
  video_path = crear_video(prompt_type, input_text, musica_file)
719
 
720
  if video_path and os.path.exists(video_path):
721
- logger.info(f"Función crear_video retornó path: {video_path}")
722
- logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
723
  return video_path, gr.update(value="✅ Video generado exitosamente.", interactive=False)
724
  else:
725
- logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
726
  return None, gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
727
 
728
  except ValueError as ve:
729
- logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
730
  return None, gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
731
  except Exception as e:
732
- logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
733
  return None, gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
734
  finally:
735
- logger.info("Fin del handler run_app.")
736
 
737
 
738
- # [INTERFAZ DE GRADIO ORIGINAL COMPLETA]
739
  with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
740
  .gradio-container {max-width: 800px; margin: auto;}
741
  h1 {text-align: center;}
742
  """) as app:
743
 
744
- gr.Markdown("# 🎬 Generador Automático de Videos con IA")
745
- gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
746
 
747
  with gr.Row():
748
  with gr.Column():
749
  prompt_type = gr.Radio(
750
- ["Generar Guion con IA", "Usar Mi Guion"],
751
- label="Método de Entrada",
752
- value="Generar Guion con IA" # Valor por defecto
753
  )
754
 
755
  with gr.Column(visible=True) as ia_guion_column:
756
  prompt_ia = gr.Textbox(
757
- label="Tema para IA",
758
  lines=2,
759
- placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer, mostrando la belleza de la naturaleza...",
760
  max_lines=4,
761
- value="" # Inicializar vacío
762
  )
763
 
764
  with gr.Column(visible=False) as manual_guion_column:
765
  prompt_manual = gr.Textbox(
766
- label="Tu Guion Completo",
767
  lines=5,
768
- placeholder="Ej: En este video exploraremos los misterios del océano. Veremos la vida marina fascinante y los arrecifes de coral vibrantes. ¡Acompáñanos en esta aventura subacuática!",
769
  max_lines=10,
770
- value="" # Inicializar vacío
771
  )
772
 
773
  musica_input = gr.Audio(
774
- label="Música de fondo (opcional)",
775
  type="filepath",
776
  interactive=True,
777
- value=None # Inicializar vacío
778
  )
779
 
780
- generate_btn = gr.Button("✨ Generar Video", variant="primary")
781
 
782
  with gr.Column():
783
  video_output = gr.Video(
784
- label="Video Generado",
785
- interactive=False, # No interactivo para el usuario
786
- height=400 # Altura fija
787
  )
788
  status_output = gr.Textbox(
789
- label="Estado",
790
- interactive=False, # Solo lectura
791
- show_label=False, # No mostrar la etiqueta "Estado"
792
- placeholder="Esperando acción...",
793
- value="Esperando entrada..." # Estado inicial
794
  )
795
 
796
- # Lógica para mostrar/ocultar campos de texto según el tipo de prompt
797
  prompt_type.change(
798
  lambda x: (gr.update(visible=x == "Generar Guion con IA"),
799
  gr.update(visible=x == "Usar Mi Guion")),
@@ -801,67 +669,41 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
801
  outputs=[ia_guion_column, manual_guion_column]
802
  )
803
 
804
- # Lógica para el botón de generar video
805
- # Primero, actualizar el estado a "Procesando..."
806
- # Luego, llamar a la función run_app
807
  generate_btn.click(
808
- # La primera acción (sincrona) actualiza el estado y limpia el video anterior
809
- lambda: (None, gr.update(value="⏳ Procesando... Esto puede tomar 2-5 minutos dependiendo de la longitud y recursos.", interactive=False)),
810
  outputs=[video_output, status_output],
811
- queue=True, # Poner en cola si hay varias peticiones
812
- # preprocess=False, # No pre-procesar para esta primera acción
813
  ).then(
814
- # La segunda acción (asincrona) ejecuta la función principal de generación
815
  run_app,
816
  inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
817
  outputs=[video_output, status_output]
818
  )
819
 
820
- gr.Markdown("### Instrucciones:")
821
  gr.Markdown("""
822
- 1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
823
- 2. **Selecciona el tipo de entrada**:
824
- - "Generar Guion con IA": Describe brevemente un tema (ej. "La belleza de las montañas"). La IA generará un guion corto.
825
- - "Usar Mi Guion": Escribe el guion completo que quieres para el video.
826
- 3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.) para usar como música de fondo.
827
- 4. **Haz clic en "✨ Generar Video"**.
828
- 5. Espera a que se procese el video. El tiempo de espera puede variar. Verás el estado en el cuadro de texto.
829
- 6. Si hay errores, revisa el log `video_generator_full.log` para más detalles.
830
  """)
831
  gr.Markdown("---")
832
- gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]") # Añade tu nombre o un alias si quieres
833
 
834
  if __name__ == "__main__":
835
- # Verificar si FFmpeg está instalado (MoviePy lo requiere)
836
- # Puedes añadir esta verificación aquí
837
- logger.info("Verificando dependencias críticas...")
838
  try:
839
- # moviepy intenta encontrar ffmpeg, si falla, lanzar error
840
- # from moviepy.config import get_setting
841
- # ffmpeg_path = get_setting("FFMPEG_BINARY")
842
- # if not os.path.exists(ffmpeg_path):
843
- # logger.critical(f"FFmpeg no encontrado en {ffmpeg_path}. Instálalo y/o configura la variable FFMPEG_BINARY.")
844
- # # raise RuntimeError("FFmpeg no está instalado o configurado correctamente.")
845
- # else:
846
- # logger.info(f"FFmpeg encontrado en {ffmpeg_path}")
847
-
848
- # Una forma más simple es intentar importar algo que use FFmpeg
849
- try:
850
- from moviepy.editor import VideoFileClip
851
- # No cargamos un archivo real, solo verificamos que el import no falle por FFmpeg
852
- logger.info("MoviePy importado correctamente. FFmpeg parece accesible.")
853
- except Exception as e:
854
- logger.critical(f"Fallo al importar MoviePy, a menudo indica problemas con FFmpeg. Asegúrate de tenerlo instalado y en el PATH. Error: {e}")
855
- # raise RuntimeError("MoviePy/FFmpeg dependency issue.")
856
-
857
  except Exception as e:
858
- logger.warning(f"No se pudo verificar FFmpeg automáticamente: {e}") # Esto no debería detener el script, solo advertir
859
 
860
- logger.info("Iniciando aplicación Gradio...")
861
  try:
862
- # Usa share=True para obtener un enlace público si lo necesitas para probar fuera de localhost
863
- # No uses share=True en producción sin medidas de seguridad adecuadas
864
  app.launch(server_name="0.0.0.0", server_port=7860, share=False)
865
  except Exception as e:
866
- logger.critical(f"No se pudo iniciar la app: {str(e)}", exc_info=True)
867
- raise # Relanzar la excepción para ver el traceback completo si falla al iniciar
 
7
  import edge_tts
8
  import gradio as gr
9
  import torch
 
10
  from transformers import GPT2Tokenizer, GPT2LMHeadModel
11
  from keybert import KeyBERT
 
12
  from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips
13
  import re
14
  import math
 
 
 
 
15
  import shutil
16
  import json
17
+ from collections import Counter
18
 
19
+ # Logging configuration
20
  logging.basicConfig(
21
  level=logging.DEBUG,
22
  format='%(asctime)s - %(levelname)s - %(message)s',
 
27
  )
28
  logger = logging.getLogger(__name__)
29
  logger.info("="*80)
30
+ logger.info("STARTING VIDEO GENERATOR EXECUTION")
31
  logger.info("="*80)
32
 
33
+ # Pexels API Key
 
34
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
35
  if not PEXELS_API_KEY:
36
+ logger.critical("PEXELS_API_KEY environment variable not found.")
37
+ # logger.warning("Continuing without PEXELS_API_KEY. Video search will fail.")
38
+ # raise ValueError("Pexels API key not configured") # Uncomment to force fail if not set
39
+
40
+ # Model Initialization
 
 
 
41
  MODEL_NAME = "datificate/gpt2-small-spanish"
42
+ logger.info(f"Initializing GPT-2 model: {MODEL_NAME}")
43
+ tokenizer = None
44
+ model = None
45
  try:
 
46
  tokenizer = GPT2Tokenizer.from_pretrained(MODEL_NAME)
47
  model = GPT2LMHeadModel.from_pretrained(MODEL_NAME).eval()
48
  if tokenizer.pad_token is None:
49
  tokenizer.pad_token = tokenizer.eos_token
50
+ logger.info(f"GPT-2 model loaded | Vocab size: {len(tokenizer)}")
51
  except Exception as e:
52
+ logger.error(f"CRITICAL FAILURE loading GPT-2: {str(e)}", exc_info=True)
53
+ tokenizer = model = None
54
 
55
+ logger.info("Loading KeyBERT model...")
56
+ kw_model = None
57
  try:
58
  kw_model = KeyBERT('distilbert-base-multilingual-cased')
59
+ logger.info("KeyBERT initialized successfully")
60
  except Exception as e:
61
+ logger.error(f"FAILURE loading KeyBERT: {str(e)}", exc_info=True)
62
+ kw_model = None
63
 
 
64
  def buscar_videos_pexels(query, api_key, per_page=5):
65
  if not api_key:
66
+ logger.warning("Cannot search Pexels: API Key not configured.")
67
  return []
68
 
69
+ logger.debug(f"Searching Pexels: '{query}' | Results per page: {per_page}")
70
  headers = {"Authorization": api_key}
71
  try:
72
  params = {
 
76
  "size": "medium"
77
  }
78
 
 
79
  response = requests.get(
80
  "https://api.pexels.com/videos/search",
81
  headers=headers,
82
  params=params,
83
  timeout=20
84
  )
85
+ response.raise_for_status()
86
+
87
+ data = response.json()
88
+ videos = data.get('videos', [])
89
+ logger.info(f"Pexels: Found {len(videos)} videos for '{query}'")
90
+ return videos
 
 
 
 
91
 
92
  except requests.exceptions.RequestException as e:
93
+ logger.error(f"Pexels connection error for '{query}': {str(e)}")
94
+ except json.JSONDecodeError:
95
+ logger.error(f"Pexels: Invalid JSON received | Status: {response.status_code} | Response: {response.text[:200]}...")
96
  except Exception as e:
97
+ logger.error(f"Unexpected Pexels error for '{query}': {str(e)}", exc_info=True)
98
 
99
  return []
100
 
 
101
  def generate_script(prompt, max_length=150):
102
+ logger.info(f"Generating script | Prompt: '{prompt[:50]}...' | Max length: {max_length}")
103
  if not tokenizer or not model:
104
+ logger.warning("GPT-2 models not available - Using original prompt as script.")
105
  return prompt
106
 
107
  try:
108
+ enhanced_prompt = f"Escribe un guion corto, interesante y coherente sobre: {prompt}"
 
 
 
 
 
 
 
109
  inputs = tokenizer(enhanced_prompt, return_tensors="pt", truncation=True, max_length=512)
110
 
 
 
111
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
112
  model.to(device)
113
  inputs = {k: v.to(device) for k, v in inputs.items()}
114
 
 
115
  outputs = model.generate(
116
  **inputs,
117
  max_length=max_length,
118
+ do_sample=True,
119
  top_p=0.9,
120
  top_k=40,
121
  temperature=0.7,
122
+ repetition_penalty=1.2,
123
  pad_token_id=tokenizer.pad_token_id,
124
  eos_token_id=tokenizer.eos_token_id,
125
+ no_repeat_ngram_size=3
126
  )
127
 
128
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
129
 
130
+ text = re.sub(r'<[^>]+>', '', text)
 
131
  text = text.strip()
132
 
 
133
  sentences = text.split('.')
134
  if sentences and sentences[0].strip():
135
  final_text = sentences[0].strip() + '.'
136
+ if len(sentences) > 1 and sentences[1].strip() and len(final_text.split()) < max_length * 0.5:
 
137
  final_text += " " + sentences[1].strip() + "."
138
+ final_text = final_text.replace("..", ".")
139
 
140
+ logger.info(f"Generated script (Truncated): '{final_text[:100]}...'")
141
+ return final_text.strip()
142
 
143
+ logger.info(f"Generated script (no full sentences detected): '{text[:100]}...'")
144
+ return text.strip()
145
+
146
  except Exception as e:
147
+ logger.error(f"Error generating script with GPT-2: {str(e)}", exc_info=True)
148
+ logger.warning("Using original prompt as script due to generation error.")
149
+ return prompt.strip()
150
 
 
151
  async def text_to_speech(text, output_path, voice="es-ES-ElviraNeural"):
152
+ logger.info(f"Converting text to speech | Chars: {len(text)} | Voice: {voice} | Output: {output_path}")
153
  if not text or not text.strip():
154
+ logger.warning("Empty text for TTS")
155
  return False
156
 
157
  try:
158
  communicate = edge_tts.Communicate(text, voice)
159
  await communicate.save(output_path)
160
 
161
+ if os.path.exists(output_path) and os.path.getsize(output_path) > 100:
162
+ logger.info(f"Audio saved successfully to: {output_path} | Size: {os.path.getsize(output_path)} bytes")
163
  return True
164
  else:
165
+ logger.error(f"TTS saved small or empty file to: {output_path}")
166
  return False
167
 
168
  except Exception as e:
169
+ logger.error(f"Error in TTS: {str(e)}", exc_info=True)
170
  return False
171
 
 
172
  def download_video_file(url, temp_dir):
173
  if not url:
174
+ logger.warning("Video URL not provided for download")
175
  return None
176
 
177
  try:
178
+ logger.info(f"Downloading video from: {url[:80]}...")
 
179
  os.makedirs(temp_dir, exist_ok=True)
180
  file_name = f"video_dl_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.mp4"
181
  output_path = os.path.join(temp_dir, file_name)
182
 
 
183
  with requests.get(url, stream=True, timeout=60) as r:
184
  r.raise_for_status()
185
+ # total_size = int(r.headers.get('content-length', 0)) # Uncomment for progress logging
186
+ # downloaded_size = 0
 
 
187
  with open(output_path, 'wb') as f:
188
  for chunk in r.iter_content(chunk_size=8192):
189
  f.write(chunk)
190
+ # downloaded_size += len(chunk) # Uncomment for progress logging
 
191
 
192
+ if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
193
+ logger.info(f"Video downloaded successfully: {output_path} | Size: {os.path.getsize(output_path)} bytes")
 
194
  return output_path
195
  else:
196
+ logger.warning(f"Download seems incomplete or empty for {url[:80]}... File: {output_path} Size: {os.path.getsize(output_path) if os.path.exists(output_path) else 'N/A'} bytes")
197
  if os.path.exists(output_path):
198
+ os.remove(output_path)
199
  return None
200
 
201
  except requests.exceptions.RequestException as e:
202
+ logger.error(f"Download error for {url[:80]}... : {str(e)}")
203
  except Exception as e:
204
+ logger.error(f"Unexpected error downloading {url[:80]}... : {str(e)}", exc_info=True)
205
 
206
  return None
207
 
 
208
  def loop_audio_to_length(audio_clip, target_duration):
209
+ logger.debug(f"Adjusting audio | Current duration: {audio_clip.duration:.2f}s | Target: {target_duration:.2f}s")
210
  if audio_clip.duration <= 0:
211
+ logger.warning("Audio clip has zero or negative duration, cannot loop.")
212
+ return AudioFileClip(filename="")
213
 
214
  if audio_clip.duration >= target_duration:
215
+ logger.debug("Audio clip already longer or equal to target.")
216
  return audio_clip.subclip(0, target_duration)
217
 
 
218
  loops = math.ceil(target_duration / audio_clip.duration)
219
+ logger.debug(f"Creating {loops} audio loops")
220
 
 
221
  audio_segments = [audio_clip] * loops
 
 
222
  looped_audio = concatenate_audioclips(audio_segments)
223
 
 
224
  return looped_audio.subclip(0, target_duration)
225
 
 
226
  def extract_visual_keywords_from_script(script_text):
227
+ logger.info("Extracting keywords from script")
228
  if not script_text or not script_text.strip():
229
+ logger.warning("Empty script, cannot extract keywords.")
230
+ return ["naturaleza", "ciudad", "paisaje"]
231
 
 
232
  clean_text = re.sub(r'[^\w\sáéíóúñÁÉÍÓÚÑ]', '', script_text)
 
233
  keywords_list = []
234
 
235
  if kw_model:
236
  try:
237
+ logger.debug("Attempting KeyBERT extraction...")
238
+ keywords1 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(1, 1), stop_words='spanish', top_n=5)
239
+ keywords2 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(2, 2), stop_words='spanish', top_n=3)
 
 
 
 
 
 
 
 
 
 
 
240
 
 
241
  all_keywords = keywords1 + keywords2
 
242
  all_keywords.sort(key=lambda item: item[1], reverse=True)
243
 
 
244
  seen_keywords = set()
245
  for keyword, score in all_keywords:
 
246
  formatted_keyword = keyword.lower().replace(" ", "+")
247
+ if formatted_keyword and formatted_keyword not in seen_keywords: # Ensure keyword is not empty
248
  keywords_list.append(formatted_keyword)
249
  seen_keywords.add(formatted_keyword)
250
+ if len(keywords_list) >= 5:
251
  break
252
 
253
  if keywords_list:
254
+ logger.debug(f"KeyBERT extracted keywords: {keywords_list}")
255
  return keywords_list
256
 
257
  except Exception as e:
258
+ logger.warning(f"KeyBERT failed: {str(e)}. Trying simple method.")
 
259
 
260
+ logger.debug("Extracting keywords with simple method...")
 
261
  words = clean_text.lower().split()
262
+ stop_words = {"el", "la", "los", "las", "de", "en", "y", "a", "que", "es", "un", "una", "con", "para", "del", "al", "por", "su", "sus", "se", "lo", "le", "me", "te", "nos", "os", "les", "mi", "tu", "nuestro", "vuestro", "este", "ese", "aquel", "esta", "esa", "aquella", "esto", "eso", "aquello", "mis", "tus", "nuestros", "vuestros", "estas", "esas", "aquellas", "si", "no", "más", "menos", "sin", "sobre", "bajo", "entre", "hasta", "desde", "durante", "mediante", "según", "versus", "via", "cada", "todo", "todos", "toda", "todas", "poco", "pocos", "poca", "pocas", "mucho", "muchos", "mucha", "muchas", "varios", "varias", "otro", "otros", "otra", "otras", "mismo", "misma", "mismos", "mismas", "tan", "tanto", "tanta", "tantos", "tantas", "tal", "tales", "cual", "cuales", "cuyo", "cuya", "cuyos", "cuyas", "quien", "quienes", "cuan", "cuanto", "cuanta", "cuantos", "cuantas", "como", "donde", "cuando", "porque", "aunque", "mientras", "siempre", "nunca", "jamás", "muy", "casi", "solo", "solamente", "incluso", "apenas", "quizás", "tal vez", "acaso", "claro", "cierto", "obvio", "evidentemente", "realmente", "simplemente", "generalmente", "especialmente", "principalmente", "posiblemente", "probablemente", "difícilmente", "fácilmente", "rápidamente", "lentamente", "bien", "mal", "mejor", "peor", "arriba", "abajo", "adelante", "atrás", "cerca", "lejos", "dentro", "fuera", "encima", "debajo", "frente", "detrás", "antes", "después", "luego", "pronto", "tarde", "todavía", "ya", "aun", "aún", "quizá"}
 
263
 
 
264
  valid_words = [word for word in words if len(word) > 3 and word not in stop_words]
265
 
266
  if not valid_words:
267
+ logger.warning("No valid keywords found with simple method. Using default keywords.")
268
  return ["naturaleza", "ciudad", "paisaje"]
269
 
 
270
  word_counts = Counter(valid_words)
 
271
  top_keywords = [word.replace(" ", "+") for word, _ in word_counts.most_common(5)]
272
 
273
  if not top_keywords:
274
+ logger.warning("Simple method produced no keywords. Using default keywords.")
275
  return ["naturaleza", "ciudad", "paisaje"]
276
 
277
+ logger.info(f"Final keywords: {top_keywords}")
278
  return top_keywords
279
 
 
280
  def crear_video(prompt_type, input_text, musica_file=None):
281
  logger.info("="*80)
282
+ logger.info(f"STARTING VIDEO CREATION | Type: {prompt_type}")
283
  logger.debug(f"Input: '{input_text[:100]}...'")
284
 
285
  start_time = datetime.now()
286
+ temp_dir_intermediate = None
287
+
 
288
  try:
289
+ # 1. Generate or use script
290
  if prompt_type == "Generar Guion con IA":
 
291
  guion = generate_script(input_text)
292
  else:
293
+ guion = input_text.strip()
 
294
 
295
+ logger.info(f"Final script ({len(guion)} chars): '{guion[:100]}...'")
296
 
297
  if not guion.strip():
298
+ logger.error("Resulting script is empty.")
299
+ raise ValueError("The script is empty.")
300
 
 
301
  temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
302
+ logger.info(f"Intermediate temporary directory created: {temp_dir_intermediate}")
303
+ temp_intermediate_files = []
304
 
305
+ # 2. Generate voice audio
306
+ logger.info("Generating voice audio...")
307
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
 
 
308
  if not asyncio.run(text_to_speech(guion, voz_path)):
309
+ logger.error("Failed to generate voice audio.")
310
+ raise ValueError("Error generating voice audio.")
311
+ temp_intermediate_files.append(voz_path)
312
 
313
  audio_tts = AudioFileClip(voz_path)
314
  audio_duration = audio_tts.duration
315
+ logger.info(f"Voice audio duration: {audio_duration:.2f} seconds")
316
 
317
  if audio_duration < 1.0:
318
+ logger.warning(f"Voice audio duration ({audio_duration:.2f}s) is very short.")
 
319
 
320
+ # 3. Extract keywords
321
+ logger.info("Extracting keywords...")
322
  try:
323
  keywords = extract_visual_keywords_from_script(guion)
324
+ logger.info(f"Identified keywords: {keywords}")
325
  except Exception as e:
326
+ logger.error(f"Error extracting keywords: {str(e)}", exc_info=True)
327
+ keywords = ["naturaleza", "paisaje"]
328
 
329
  if not keywords:
330
+ keywords = ["video", "background"]
 
331
 
332
+ # 4. Search and download videos
333
+ logger.info("Searching videos on Pexels...")
334
  videos_data = []
335
+ total_desired_videos = 10
336
+ per_page_per_keyword = max(1, total_desired_videos // len(keywords))
 
337
 
338
  for keyword in keywords:
339
+ if len(videos_data) >= total_desired_videos: break
340
  try:
341
  videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=per_page_per_keyword)
342
  if videos:
343
  videos_data.extend(videos)
344
+ logger.info(f"Found {len(videos)} videos for '{keyword}'. Total data: {len(videos_data)}")
345
  except Exception as e:
346
+ logger.warning(f"Error searching videos for '{keyword}': {str(e)}")
347
 
348
+ if len(videos_data) < total_desired_videos / 2:
349
+ logger.warning(f"Few videos found ({len(videos_data)}). Trying generic keywords.")
350
+ generic_keywords = ["nature", "city", "background", "abstract"]
 
351
  for keyword in generic_keywords:
352
  if len(videos_data) >= total_desired_videos: break
353
  try:
354
+ videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=2)
 
355
  if videos:
356
  videos_data.extend(videos)
357
+ logger.info(f"Found {len(videos)} videos for '{keyword}' (generic). Total data: {len(videos_data)}")
358
  except Exception as e:
359
+ logger.warning(f"Error searching generic videos for '{keyword}': {str(e)}")
360
 
361
 
362
  if not videos_data:
363
+ logger.error("No videos found on Pexels for any keyword.")
364
+ raise ValueError("No suitable videos found on Pexels.")
365
 
366
  video_paths = []
367
+ logger.info(f"Attempting to download {len(videos_data)} found videos...")
368
  for video in videos_data:
369
  if 'video_files' not in video or not video['video_files']:
370
+ logger.debug(f"Skipping video without video files: {video.get('id')}")
371
  continue
372
 
373
  try:
 
374
  best_quality = None
375
  for vf in sorted(video['video_files'], key=lambda x: x.get('width', 0) * x.get('height', 0), reverse=True):
376
  if 'link' in vf:
 
381
  path = download_video_file(best_quality['link'], temp_dir_intermediate)
382
  if path:
383
  video_paths.append(path)
384
+ temp_intermediate_files.append(path)
385
+ logger.info(f"Video downloaded OK from {best_quality['link'][:50]}...")
386
  else:
387
+ logger.warning(f"Could not download video from {best_quality['link'][:50]}...")
388
  else:
389
+ logger.warning(f"No valid download link found for video {video.get('id')}.")
390
 
391
  except Exception as e:
392
+ logger.warning(f"Error processing/downloading video {video.get('id')}: {str(e)}")
393
 
394
+ logger.info(f"Downloaded {len(video_paths)} usable video files.")
395
  if not video_paths:
396
+ logger.error("Could not download any usable video file.")
397
+ raise ValueError("Could not download any usable video from Pexels.")
398
 
399
+ # 5. Process and concatenate video clips
400
+ logger.info("Processing and concatenating downloaded videos...")
401
  clips = []
402
  current_duration = 0
403
+ min_clip_duration = 1.0
404
+ max_clip_segment = 8.0
 
 
 
405
 
406
+ for i, path in enumerate(video_paths):
407
+ if current_duration >= audio_duration + max_clip_segment:
408
+ logger.debug(f"Video base sufficient ({current_duration:.1f}s >= {audio_duration:.1f}s + {max_clip_segment:.1f}s buffer). Stopping processing remaining source clips.")
 
409
  break
410
 
411
+ clip = None
412
  try:
413
+ logger.debug(f"[{i+1}/{len(video_paths)}] Opening clip: {path}")
414
  clip = VideoFileClip(path)
415
+
416
+ # Check clip validity after opening
417
+ if clip.reader is None or clip.duration is None or clip.duration <= 0:
418
+ logger.warning(f"[{i+1}/{len(video_paths)}] Clip {path} seems invalid (reader is None or duration <= 0). Skipping.")
419
+ continue
420
+
421
+ # Calculate how much to take from this clip
422
  remaining_needed = audio_duration - current_duration
423
+ potential_use_duration = min(clip.duration, max_clip_segment)
424
+
 
425
  if remaining_needed > 0:
426
+ segment_duration = min(potential_use_duration, remaining_needed + min_clip_duration)
427
+ segment_duration = max(min_clip_duration, segment_duration)
428
+ segment_duration = min(segment_duration, clip.duration)
429
+
430
+ if segment_duration >= min_clip_duration:
431
+ try:
432
+ sub = clip.subclip(0, segment_duration)
433
+ clips.append(sub)
434
+ current_duration += sub.duration
435
+ logger.debug(f"[{i+1}/{len(video_paths)}] Segment added: {sub.duration:.1f}s (total video: {current_duration:.1f}/{audio_duration:.1f}s)")
436
+ except Exception as sub_e:
437
+ logger.warning(f"[{i+1}/{len(video_paths)}] Error creating subclip from {path} ({segment_duration:.1f}s): {str(sub_e)}")
438
+ continue
439
+ else:
440
+ logger.debug(f"[{i+1}/{len(video_paths)}] Clip {path} ({clip.duration:.1f}s) doesn't contribute sufficient segment ({segment_duration:.1f}s needed from it). Skipping.")
441
  else:
442
+ logger.debug(f"[{i+1}/{len(video_paths)}] Video base duration already reached. Skipping clip.")
 
 
 
443
 
444
  except Exception as e:
445
+ logger.warning(f"[{i+1}/{len(video_paths)}] Error processing video {path}: {str(e)}", exc_info=True)
 
446
  continue
447
 
448
+ finally:
449
+ if clip is not None:
450
+ try:
451
+ clip.close()
452
+ logger.debug(f"[{i+1}/{len(video_paths)}] Clip {path} closed.")
453
+ except Exception as close_e:
454
+ logger.warning(f"[{i+1}/{len(video_paths)}] Error closing clip {path}: {str(close_e)}")
455
+
456
+
457
+ logger.info(f"Source clip processing finished. Obtained {len(clips)} valid clips.")
458
+
459
  if not clips:
460
+ logger.error("No valid video clips available to create the sequence.")
461
+ raise ValueError("No valid video clips available to create the video.")
462
 
463
+ logger.info(f"Concatenating {len(clips)} video clips.")
464
  video_base = concatenate_videoclips(clips, method="compose")
465
+ logger.info(f"Base video duration: {video_base.duration:.2f}s")
466
 
 
467
  if video_base.duration < audio_duration:
 
 
468
  num_repeats = math.ceil(audio_duration / video_base.duration)
469
+ logger.info(f"Repeating base video ({video_base.duration:.2f}s) {num_repeats} times to reach {audio_duration:.2f}s.")
470
  repeated_clips = [video_base] * num_repeats
471
  video_base = concatenate_videoclips(repeated_clips, method="compose").subclip(0, audio_duration)
472
+ logger.info(f"Adjusted base video duration: {video_base.duration:.2f}s")
473
 
 
 
474
  if video_base.duration > audio_duration:
475
+ logger.info(f"Trimming base video ({video_base.duration:.2f}s) to match audio duration ({audio_duration:.2f}s).")
476
  video_base = video_base.subclip(0, audio_duration)
477
+ logger.info(f"Final base video duration: {video_base.duration:.2f}s")
478
 
479
 
480
+ # 6. Handle background music
481
+ logger.info("Processing audio...")
482
+ final_audio = audio_tts
483
 
484
  if musica_file:
485
  try:
 
486
  music_path = os.path.join(temp_dir_intermediate, "musica_bg.mp3")
487
  shutil.copyfile(musica_file, music_path)
488
+ temp_intermediate_files.append(music_path)
489
+ logger.info(f"Background music copied to: {music_path}")
490
 
491
  musica_audio = AudioFileClip(music_path)
492
+ logger.debug(f"Original music duration: {musica_audio.duration:.2f}s")
493
 
 
494
  musica_audio = loop_audio_to_length(musica_audio, video_base.duration)
495
+ logger.debug(f"Music adjusted to video duration: {musica_audio.duration:.2f}s")
496
 
 
497
  final_audio = CompositeAudioClip([
498
+ musica_audio.volumex(0.2),
499
+ audio_tts.volumex(1.0)
500
  ])
501
+ logger.info("Audio mix completed (voice + music).")
502
  except Exception as e:
503
+ logger.warning(f"Error processing background music: {str(e)}", exc_info=True)
 
504
  final_audio = audio_tts
505
+ logger.warning("Using voice audio only due to music processing error.")
506
 
 
507
  if final_audio.duration > video_base.duration:
508
  final_audio = final_audio.subclip(0, video_base.duration)
509
+
510
+
511
+ # 7. Create final video
512
+ logger.info("Rendering final video...")
 
 
 
 
 
 
 
513
  video_final = video_base.set_audio(final_audio)
514
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
  output_filename = "final_video.mp4"
516
  output_path = os.path.join(temp_dir_intermediate, output_filename)
517
+ logger.info(f"Writing final video to: {output_path}")
518
 
519
  video_final.write_videofile(
520
  output_path,
521
  fps=24,
522
+ threads=4,
523
+ codec="libx264",
524
+ audio_codec="aac",
525
+ preset="medium",
526
+ logger='bar' # Show MoviePy progress bar
 
 
527
  )
528
 
529
  total_time = (datetime.now() - start_time).total_seconds()
530
+ logger.info(f"VIDEO PROCESS FINISHED | Output: {output_path} | Total time: {total_time:.2f}s")
531
 
532
+ # Close main clips
533
+ try:
534
+ video_base.close()
535
+ audio_tts.close()
536
+ if 'musica_audio' in locals() and musica_audio is not None: musica_audio.close()
537
+ video_final.close()
538
+ except Exception as e:
539
+ logger.warning(f"Error closing final clips: {str(e)}")
540
 
 
541
 
542
+ return output_path
543
+
544
  except ValueError as ve:
545
+ logger.error(f"CONTROLLED ERROR in crear_video: {str(ve)}")
546
+ raise ve
 
 
 
 
 
 
 
 
 
547
  except Exception as e:
548
+ logger.critical(f"CRITICAL UNHANDLED ERROR in crear_video: {str(e)}", exc_info=True)
549
+ raise e
 
 
 
 
 
 
 
 
550
  finally:
551
+ logger.info("Starting cleanup of intermediate temporary files...")
 
552
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
 
553
  final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4")
554
 
555
  for path in temp_intermediate_files:
556
  try:
557
+ if os.path.isfile(path) and path != final_output_in_temp:
558
+ logger.debug(f"Deleting temporary file: {path}")
559
  os.remove(path)
 
 
 
560
  except Exception as e:
561
+ logger.warning(f"Could not delete temporary file {path}: {str(e)}")
562
 
563
+ # IMPORTANT: DO NOT remove the temp_dir_intermediate itself.
564
+ # It contains the final video file needed by Gradio.
565
+ logger.info(f"Intermediate temporary directory {temp_dir_intermediate} will persist for Gradio to read the final video.")
 
 
 
 
566
 
567
 
 
568
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
569
  logger.info("="*80)
570
+ logger.info("REQUEST RECEIVED IN INTERFACE")
571
 
572
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
573
 
574
  if not input_text or not input_text.strip():
575
+ logger.warning("Empty input text.")
 
576
  return None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.")
577
 
578
+ logger.info(f"Input Type: {prompt_type}")
579
+ logger.debug(f"Input Text: '{input_text[:100]}...'")
 
 
580
  if musica_file:
581
+ logger.info(f"Music file received: {musica_file}")
582
  else:
583
+ logger.info("No music file provided.")
584
 
585
  try:
586
+ logger.info("Calling crear_video...")
587
  video_path = crear_video(prompt_type, input_text, musica_file)
588
 
589
  if video_path and os.path.exists(video_path):
590
+ logger.info(f"crear_video returned path: {video_path}")
591
+ logger.info(f"Size of returned video file: {os.path.getsize(video_path)} bytes")
592
  return video_path, gr.update(value="✅ Video generado exitosamente.", interactive=False)
593
  else:
594
+ logger.error(f"crear_video did not return a valid path or file does not exist: {video_path}")
595
  return None, gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
596
 
597
  except ValueError as ve:
598
+ logger.warning(f"Validation error during video creation: {str(ve)}")
599
  return None, gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
600
  except Exception as e:
601
+ logger.critical(f"Critical error during video creation: {str(e)}", exc_info=True)
602
  return None, gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
603
  finally:
604
+ logger.info("End of run_app handler.")
605
 
606
 
607
+ # Gradio Interface
608
  with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
609
  .gradio-container {max-width: 800px; margin: auto;}
610
  h1 {text-align: center;}
611
  """) as app:
612
 
613
+ gr.Markdown("# 🎬 Automatic AI Video Generator")
614
+ gr.Markdown("Generate short videos from a topic or script, using stock footage from Pexels and generated voice.")
615
 
616
  with gr.Row():
617
  with gr.Column():
618
  prompt_type = gr.Radio(
619
+ ["Generar Guion con IA", "Usar Mi Guion"],
620
+ label="Input Method",
621
+ value="Generar Guion con IA"
622
  )
623
 
624
  with gr.Column(visible=True) as ia_guion_column:
625
  prompt_ia = gr.Textbox(
626
+ label="Topic for AI",
627
  lines=2,
628
+ placeholder="Ex: A natural landscape with mountains and rivers at sunrise, showing the beauty of nature...",
629
  max_lines=4,
630
+ value=""
631
  )
632
 
633
  with gr.Column(visible=False) as manual_guion_column:
634
  prompt_manual = gr.Textbox(
635
+ label="Your Full Script",
636
  lines=5,
637
+ placeholder="Ex: In this video, we will explore the mysteries of the ocean. We will see fascinating marine life and vibrant coral reefs. Join us on this underwater adventure!",
638
  max_lines=10,
639
+ value=""
640
  )
641
 
642
  musica_input = gr.Audio(
643
+ label="Background Music (optional)",
644
  type="filepath",
645
  interactive=True,
646
+ value=None
647
  )
648
 
649
+ generate_btn = gr.Button("✨ Generate Video", variant="primary")
650
 
651
  with gr.Column():
652
  video_output = gr.Video(
653
+ label="Generated Video",
654
+ interactive=False,
655
+ height=400
656
  )
657
  status_output = gr.Textbox(
658
+ label="Status",
659
+ interactive=False,
660
+ show_label=False,
661
+ placeholder="Waiting for action...",
662
+ value="Waiting for input..."
663
  )
664
 
 
665
  prompt_type.change(
666
  lambda x: (gr.update(visible=x == "Generar Guion con IA"),
667
  gr.update(visible=x == "Usar Mi Guion")),
 
669
  outputs=[ia_guion_column, manual_guion_column]
670
  )
671
 
 
 
 
672
  generate_btn.click(
673
+ lambda: (None, gr.update(value="⏳ Processing... This can take 2-5 minutes depending on length and resources.", interactive=False)),
 
674
  outputs=[video_output, status_output],
675
+ queue=True,
 
676
  ).then(
 
677
  run_app,
678
  inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
679
  outputs=[video_output, status_output]
680
  )
681
 
682
+ gr.Markdown("### Instructions:")
683
  gr.Markdown("""
684
+ 1. **Pexels API Key:** Ensure you have set the `PEXELS_API_KEY` environment variable.
685
+ 2. **Select Input Method**:
686
+ - "Generate Script with AI": Describe a topic (e.g., "The beauty of mountains"). AI will generate a short script.
687
+ - "Use My Script": Write the full script for your video.
688
+ 3. **Upload Music** (optional): Select an audio file (MP3, WAV, etc.) for background music.
689
+ 4. **Click "✨ Generate Video"**.
690
+ 5. Wait for the video to process. Processing time may vary. Check the status box.
691
+ 6. If there are errors, check the `video_generator_full.log` file for details.
692
  """)
693
  gr.Markdown("---")
694
+ gr.Markdown("Developed by [Your Name/Company/Alias - Optional]")
695
 
696
  if __name__ == "__main__":
697
+ logger.info("Verifying critical dependencies...")
 
 
698
  try:
699
+ from moviepy.editor import VideoFileClip
700
+ logger.info("MoviePy imported correctly. FFmpeg seems accessible.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
701
  except Exception as e:
702
+ logger.critical(f"Failed to import MoviePy, often indicates FFmpeg issues. Ensure it is installed and in PATH. Error: {e}")
703
 
704
+ logger.info("Starting Gradio app...")
705
  try:
 
 
706
  app.launch(server_name="0.0.0.0", server_port=7860, share=False)
707
  except Exception as e:
708
+ logger.critical(f"Could not launch app: {str(e)}", exc_info=True)
709
+ raise