gnosticdev commited on
Commit
932aa73
·
verified ·
1 Parent(s): 1908eee

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +24 -161
app.py CHANGED
@@ -1,26 +1,3 @@
1
- import os
2
- import subprocess
3
- import sys
4
- import importlib
5
-
6
- # Instalar dependencias faltantes
7
- required_packages = [
8
- 'moviepy',
9
- 'edge-tts',
10
- 'gradio',
11
- 'torch',
12
- 'transformers',
13
- 'keybert',
14
- 'requests'
15
- ]
16
-
17
- for package in required_packages:
18
- try:
19
- importlib.import_module(package.replace('-', '_'))
20
- except ImportError:
21
- print(f"Instalando {package}...")
22
- subprocess.check_call([sys.executable, "-m", "pip", "install", package])
23
-
24
  import asyncio
25
  import logging
26
  import tempfile
@@ -38,7 +15,6 @@ import shutil
38
  import json
39
  from collections import Counter
40
  import time
41
-
42
  logging.basicConfig(
43
  level=logging.DEBUG,
44
  format='%(asctime)s - %(levelname)s - %(message)s',
@@ -51,7 +27,6 @@ logger = logging.getLogger(__name__)
51
  logger.info("="*80)
52
  logger.info("INICIO DE EJECUCIÓN - GENERADOR DE VIDEOS")
53
  logger.info("="*80)
54
-
55
  VOCES_DISPONIBLES = {
56
  "Español (España)": {
57
  "es-ES-JuanNeural": "Juan (España) - Masculino",
@@ -113,14 +88,12 @@ VOCES_DISPONIBLES = {
113
  "es-US-PalomaNeural": "Paloma (Estados Unidos) - Femenino"
114
  }
115
  }
116
-
117
  def get_voice_choices():
118
  choices = []
119
  for region, voices in VOCES_DISPONIBLES.items():
120
  for voice_id, voice_name in voices.items():
121
  choices.append((f"{voice_name} ({region})", voice_id))
122
  return choices
123
-
124
  AVAILABLE_VOICES = get_voice_choices()
125
  DEFAULT_VOICE_ID = "es-ES-JuanNeural"
126
  DEFAULT_VOICE_NAME = DEFAULT_VOICE_ID
@@ -128,20 +101,15 @@ for text, voice_id in AVAILABLE_VOICES:
128
  if voice_id == DEFAULT_VOICE_ID:
129
  DEFAULT_VOICE_NAME = text
130
  break
131
-
132
  if DEFAULT_VOICE_ID not in [v[1] for v in AVAILABLE_VOICES]:
133
  DEFAULT_VOICE_ID = AVAILABLE_VOICES[0][1] if AVAILABLE_VOICES else "en-US-AriaNeural"
134
  DEFAULT_VOICE_NAME = AVAILABLE_VOICES[0][0] if AVAILABLE_VOICES else "Aria (United States) - Female"
135
-
136
  logger.info(f"Voz por defecto seleccionada (ID): {DEFAULT_VOICE_ID}")
137
-
138
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
139
  if not PEXELS_API_KEY:
140
  logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
141
-
142
  MODEL_NAME = "datificate/gpt2-small-spanish"
143
  logger.info(f"Inicializando modelo GPT-2: {MODEL_NAME}")
144
-
145
  tokenizer = None
146
  model = None
147
  try:
@@ -153,7 +121,6 @@ try:
153
  except Exception as e:
154
  logger.error(f"FALLA CRÍTICA al cargar GPT-2: {str(e)}", exc_info=True)
155
  tokenizer = model = None
156
-
157
  logger.info("Cargando modelo KeyBERT...")
158
  kw_model = None
159
  try:
@@ -162,7 +129,6 @@ try:
162
  except Exception as e:
163
  logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
164
  kw_model = None
165
-
166
  def buscar_videos_pexels(query, api_key, per_page=5):
167
  if not api_key:
168
  logger.warning("No se puede buscar en Pexels: API Key no configurada.")
@@ -194,22 +160,18 @@ def buscar_videos_pexels(query, api_key, per_page=5):
194
  except Exception as e:
195
  logger.error(f"Error inesperado Pexels para '{query}': {str(e)}", exc_info=True)
196
  return []
197
-
198
  def generate_script(prompt, max_length=150):
199
  logger.info(f"Generando guión | Prompt: '{prompt[:50]}...' | Longitud máxima: {max_length}")
200
  if not tokenizer or not model:
201
  logger.warning("Modelos GPT-2 no disponibles - Usando prompt original como guion.")
202
  return prompt.strip()
203
-
204
  instruction_phrase_start = "Escribe un guion corto, interesante y coherente sobre:"
205
  ai_prompt = f"{instruction_phrase_start} {prompt}"
206
-
207
  try:
208
  inputs = tokenizer(ai_prompt, return_tensors="pt", truncation=True, max_length=512)
209
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
210
  model.to(device)
211
  inputs = {k: v.to(device) for k, v in inputs.items()}
212
-
213
  outputs = model.generate(
214
  **inputs,
215
  max_length=max_length + inputs[list(inputs.keys())[0]].size(1),
@@ -222,10 +184,8 @@ def generate_script(prompt, max_length=150):
222
  eos_token_id=tokenizer.eos_token_id,
223
  no_repeat_ngram_size=3
224
  )
225
-
226
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
227
  cleaned_text = text.strip()
228
-
229
  try:
230
  prompt_in_output_idx = text.lower().find(prompt.lower())
231
  if prompt_in_output_idx != -1:
@@ -242,15 +202,12 @@ def generate_script(prompt, max_length=150):
242
  except Exception as e:
243
  logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
244
  cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
245
-
246
  if not cleaned_text or len(cleaned_text) < 10:
247
  logger.warning("El guión generado parece muy corto o vacío después de la limpieza heurística. Usando el texto generado original (sin limpieza adicional).")
248
  cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
249
-
250
  cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
251
  cleaned_text = cleaned_text.lstrip(':').strip()
252
  cleaned_text = cleaned_text.lstrip('.').strip()
253
-
254
  sentences = cleaned_text.split('.')
255
  if sentences and sentences[0].strip():
256
  final_text = sentences[0].strip() + '.'
@@ -259,14 +216,12 @@ def generate_script(prompt, max_length=150):
259
  final_text = final_text.replace("..", ".")
260
  logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
261
  return final_text.strip()
262
-
263
  logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
264
  return cleaned_text.strip()
265
  except Exception as e:
266
  logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
267
  logger.warning("Usando prompt original como guion debido al error de generación.")
268
  return prompt.strip()
269
-
270
  async def text_to_speech(text, output_path, voice):
271
  logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
272
  if not text or not text.strip():
@@ -284,7 +239,6 @@ async def text_to_speech(text, output_path, voice):
284
  except Exception as e:
285
  logger.error(f"Error en TTS con voz '{voice}': {str(e)}", exc_info=True)
286
  return False
287
-
288
  def download_video_file(url, temp_dir):
289
  if not url:
290
  logger.warning("URL de video no proporcionada para descargar")
@@ -294,13 +248,11 @@ def download_video_file(url, temp_dir):
294
  os.makedirs(temp_dir, exist_ok=True)
295
  file_name = f"video_dl_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.mp4"
296
  output_path = os.path.join(temp_dir, file_name)
297
-
298
  with requests.get(url, stream=True, timeout=60) as r:
299
  r.raise_for_status()
300
  with open(output_path, 'wb') as f:
301
  for chunk in r.iter_content(chunk_size=8192):
302
  f.write(chunk)
303
-
304
  if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
305
  logger.info(f"Video descargado exitosamente: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
306
  return output_path
@@ -314,7 +266,6 @@ def download_video_file(url, temp_dir):
314
  except Exception as e:
315
  logger.error(f"Error inesperado descargando {url[:80]}... : {str(e)}", exc_info=True)
316
  return None
317
-
318
  def loop_audio_to_length(audio_clip, target_duration):
319
  logger.debug(f"Ajustando audio | Duración actual: {audio_clip.duration:.2f}s | Objetivo: {target_duration:.2f}s")
320
  if audio_clip is None or audio_clip.duration is None or audio_clip.duration <= 0:
@@ -325,7 +276,6 @@ def loop_audio_to_length(audio_clip, target_duration):
325
  except Exception as e:
326
  logger.error(f"Could not create silence clip: {e}", exc_info=True)
327
  return AudioFileClip(filename="")
328
-
329
  if audio_clip.duration >= target_duration:
330
  logger.debug("Audio clip already longer or equal to target. Trimming.")
331
  trimmed_clip = audio_clip.subclip(0, target_duration)
@@ -337,24 +287,20 @@ def loop_audio_to_length(audio_clip, target_duration):
337
  pass
338
  return AudioFileClip(filename="")
339
  return trimmed_clip
340
-
341
  loops = math.ceil(target_duration / audio_clip.duration)
342
  logger.debug(f"Creando {loops} loops de audio")
343
  audio_segments = [audio_clip] * loops
344
  looped_audio = None
345
  final_looped_audio = None
346
-
347
  try:
348
  looped_audio = concatenate_audioclips(audio_segments)
349
  if looped_audio.duration is None or looped_audio.duration <= 0:
350
  logger.error("Concatenated audio clip is invalid (None or zero duration).")
351
  raise ValueError("Invalid concatenated audio.")
352
-
353
  final_looped_audio = looped_audio.subclip(0, target_duration)
354
  if final_looped_audio.duration is None or final_looped_audio.duration <= 0:
355
  logger.error("Final subclipped audio clip is invalid (None or zero duration).")
356
  raise ValueError("Invalid final subclipped audio.")
357
-
358
  return final_looped_audio
359
  except Exception as e:
360
  logger.error(f"Error concatenating/subclipping audio clips for looping: {str(e)}", exc_info=True)
@@ -372,16 +318,13 @@ def loop_audio_to_length(audio_clip, target_duration):
372
  looped_audio.close()
373
  except:
374
  pass
375
-
376
  def extract_visual_keywords_from_script(script_text):
377
  logger.info("Extrayendo palabras clave del guion")
378
  if not script_text or not script_text.strip():
379
  logger.warning("Guion vacío, no se pueden extraer palabras clave.")
380
  return ["naturaleza", "ciudad", "paisaje"]
381
-
382
  clean_text = re.sub(r'[^\w\sáéíóúñÁÉÍÓÚÑ]', '', script_text)
383
  keywords_list = []
384
-
385
  if kw_model:
386
  try:
387
  logger.debug("Intentando extracción con KeyBERT...")
@@ -389,7 +332,6 @@ def extract_visual_keywords_from_script(script_text):
389
  keywords2 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(2, 2), stop_words='spanish', top_n=3)
390
  all_keywords = keywords1 + keywords2
391
  all_keywords.sort(key=lambda item: item[1], reverse=True)
392
-
393
  seen_keywords = set()
394
  for keyword, score in all_keywords:
395
  formatted_keyword = keyword.lower().replace(" ", "+")
@@ -398,40 +340,33 @@ def extract_visual_keywords_from_script(script_text):
398
  seen_keywords.add(formatted_keyword)
399
  if len(keywords_list) >= 5:
400
  break
401
-
402
  if keywords_list:
403
  logger.debug(f"Palabras clave extraídas por KeyBERT: {keywords_list}")
404
  return keywords_list
405
  except Exception as e:
406
  logger.warning(f"KeyBERT falló: {str(e)}. Intentando método simple.")
407
-
408
  logger.debug("Extrayendo palabras clave con método simple...")
409
  words = clean_text.lower().split()
410
  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",
411
  "nuestro", "vuestro", "este", "ese", "aquel", "esta", "esa", "aquella", "esto", "eso", "aquello", "mis", "tus",
412
  "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á"}
413
-
414
  valid_words = [word for word in words if len(word) > 3 and word not in stop_words]
415
  if not valid_words:
416
  logger.warning("No se encontraron palabras clave válidas con método simple. Usando palabras clave predeterminadas.")
417
  return ["espiritual", "terror", "matrix", "arcontes", "galaxia", "creepy", "magia", "gangstalking","conspiracy",]
418
-
419
  word_counts = Counter(valid_words)
420
  top_keywords = [word.replace(" ", "+") for word, _ in word_counts.most_common(5)]
421
  if not top_keywords:
422
  logger.warning("El método simple no produjo keywords. Usando palabras clave predeterminadas.")
423
  return ["espiritual", "terror", "matrix", "arcontes", "galaxia", "creepy", "magia", "gangstalking","conspiracy",]
424
-
425
  logger.info(f"Palabras clave finales: {top_keywords}")
426
  return top_keywords
427
-
428
  def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
429
  logger.info("="*80)
430
  logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
431
  logger.debug(f"Input: '{input_text[:100]}...'")
432
  logger.info(f"Voz seleccionada: {selected_voice}")
433
  start_time = datetime.now()
434
-
435
  temp_dir_intermediate = None
436
  output_filename = None
437
  permanent_path = None
@@ -444,24 +379,19 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
444
  video_final = None
445
  source_clips = []
446
  clips_to_concatenate = []
447
-
448
  try:
449
  if prompt_type == "Generar Guion con IA":
450
  guion = generate_script(input_text)
451
  else:
452
  guion = input_text.strip()
453
-
454
  logger.info(f"Guion final ({len(guion)} chars): '{guion[:100]}...'")
455
  if not guion.strip():
456
  logger.error("El guion resultante está vacío o solo contiene espacios.")
457
  raise ValueError("El guion está vacío.")
458
-
459
  temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
460
  logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
461
-
462
  logger.info("Generando audio de voz...")
463
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
464
-
465
  tts_voices_to_try = [selected_voice]
466
  fallback_juan = "es-ES-JuanNeural"
467
  fallback_elvira = "es-ES-ElviraNeural"
@@ -469,7 +399,6 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
469
  tts_voices_to_try.append(fallback_juan)
470
  if fallback_elvira and fallback_elvira != selected_voice and fallback_elvira not in tts_voices_to_try:
471
  tts_voices_to_try.append(fallback_elvira)
472
-
473
  tts_success = False
474
  tried_voices = set()
475
  for current_voice in tts_voices_to_try:
@@ -485,11 +414,9 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
485
  except Exception as e:
486
  logger.warning(f"Fallo al generar TTS con voz '{current_voice}': {str(e)}", exc_info=True)
487
  pass
488
-
489
  if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
490
  logger.error("Fallo en la generación de voz después de todos los intentos. Archivo de audio no creado o es muy pequeño.")
491
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
492
-
493
  temp_intermediate_files.append(voz_path)
494
  audio_tts_original = AudioFileClip(voz_path)
495
  if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
@@ -507,14 +434,12 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
507
  if voz_path in temp_intermediate_files:
508
  temp_intermediate_files.remove(voz_path)
509
  raise ValueError("Audio de voz generado es inválido después de procesamiento inicial.")
510
-
511
  audio_tts = audio_tts_original
512
  audio_duration = audio_tts_original.duration
513
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
514
  if audio_duration < 1.0:
515
  logger.error(f"Duración audio voz ({audio_duration:.2f}s) es muy corta.")
516
  raise ValueError("Generated voice audio is too short (min 1 second required).")
517
-
518
  logger.info("Extrayendo palabras clave...")
519
  try:
520
  keywords = extract_visual_keywords_from_script(guion)
@@ -522,15 +447,12 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
522
  except Exception as e:
523
  logger.error(f"Error extrayendo keywords: {str(e)}", exc_info=True)
524
  keywords = ["naturaleza", "paisaje"]
525
-
526
  if not keywords:
527
  keywords = ["video", "background"]
528
-
529
  logger.info("Buscando videos en Pexels...")
530
  videos_data = []
531
  total_desired_videos = 10
532
  per_page_per_keyword = max(1, total_desired_videos // len(keywords))
533
-
534
  for keyword in keywords:
535
  if len(videos_data) >= total_desired_videos:
536
  break
@@ -541,11 +463,9 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
541
  logger.info(f"Encontrados {len(videos)} videos para '{keyword}'. Total data: {len(videos_data)}")
542
  except Exception as e:
543
  logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
544
-
545
  if len(videos_data) < total_desired_videos / 2:
546
  logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
547
  generic_keywords = ["mystery", "alien", "ufo", "conspiracy", "paranormal", "supernatural", "horror", "fear", "suspense", "secret", "government", "cover_up", "simulation", "matrix", "apocalypse", "dystopian", "shadow", "occult", "unexplained", "creepy", "extraterrestrial", "abduction", "experiment", "secret_society", "illuminati", "new_world_order", "ancient_aliens", "ufo_sighting", "cryptid", "bigfoot", "loch_ness", "ghost", "haunting", "spirit", "demon", "possession", "exorcism", "witchcraft", "ritual", "cursed", "urban_legend", "myth", "legend", "folklore", "scary", "terror", "panic", "anxiety", "dread", "nightmare", "dark", "gloomy", "fog", "haunted", "cemetery", "asylum", "abandoned", "ruins", "underground", "tunnel", "bunker", "lab", "experiment", "government_secret", "mind_control", "brainwash", "propaganda", "surveillance", "spy", "whistleblower", "leak", "anonymous", "hack", "cyber", "virtual_reality", "ai", "artificial_intelligence", "robot", "cyborg", "apocalyptic", "post_apocalyptic", "zombie", "outbreak", "pandemic", "contagion", "biohazard", "radiation", "nuclear", "doomsday", "armageddon", "revelation", "prophecy", "symbolism", "hidden_meaning", "enigma", "puzzle", "code", "cipher", "mysterious", "unidentified", "anomaly", "glitch", "time_travel", "parallel_universe", "dimension", "portal"]
548
-
549
  for keyword in generic_keywords:
550
  if len(videos_data) >= total_desired_videos:
551
  break
@@ -556,26 +476,21 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
556
  logger.info(f"Encontrados {len(videos)} videos para '{keyword}' (genérico). Total data: {len(videos_data)}")
557
  except Exception as e:
558
  logger.warning(f"Error buscando videos genéricos para '{keyword}': {str(e)}")
559
-
560
  if not videos_data:
561
  logger.error("No se encontraron videos en Pexels para ninguna palabra clave.")
562
  raise ValueError("No se encontraron videos adecuados en Pexels.")
563
-
564
  video_paths = []
565
  logger.info(f"Intentando descargar {len(videos_data)} videos encontrados...")
566
-
567
  for video in videos_data:
568
  if 'video_files' not in video or not video['video_files']:
569
  logger.debug(f"Saltando video sin archivos de video: {video.get('id')}")
570
  continue
571
-
572
  try:
573
  best_quality = None
574
  for vf in sorted(video['video_files'], key=lambda x: x.get('width', 0) * x.get('height', 0), reverse=True):
575
  if 'link' in vf:
576
  best_quality = vf
577
  break
578
-
579
  if best_quality and 'link' in best_quality:
580
  path = download_video_file(best_quality['link'], temp_dir_intermediate)
581
  if path:
@@ -588,40 +503,32 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
588
  logger.warning(f"No se encontró enlace de descarga válido para video {video.get('id')}.")
589
  except Exception as e:
590
  logger.warning(f"Error procesando/descargando video {video.get('id')}: {str(e)}")
591
-
592
  logger.info(f"Descargados {len(video_paths)} archivos de video utilizables.")
593
  if not video_paths:
594
  logger.error("No se pudo descargar ningún archivo de video utilizable.")
595
  raise ValueError("No se pudo descargar ningún video utilizable de Pexels.")
596
-
597
  logger.info("Procesando y concatenando videos descargados...")
598
  current_duration = 0
599
  min_clip_duration = 0.5
600
  max_clip_segment = 10.0
601
-
602
  for i, path in enumerate(video_paths):
603
  if current_duration >= audio_duration + max_clip_segment:
604
  logger.debug(f"Video base suficiente ({current_duration:.1f}s >= {audio_duration:.1f}s + {max_clip_segment:.1f}s buffer). Dejando de procesar clips fuente restantes.")
605
  break
606
-
607
  clip = None
608
  try:
609
  logger.debug(f"[{i+1}/{len(video_paths)}] Abriendo clip: {path}")
610
  clip = VideoFileClip(path)
611
  source_clips.append(clip)
612
-
613
  if clip.reader is None or clip.duration is None or clip.duration <= 0:
614
  logger.warning(f"[{i+1}/{len(video_paths)}] Clip fuente {path} parece inválido (reader is None o duración <= 0). Saltando.")
615
  continue
616
-
617
  remaining_needed = audio_duration - current_duration
618
  potential_use_duration = min(clip.duration, max_clip_segment)
619
-
620
  if remaining_needed > 0:
621
  segment_duration = min(potential_use_duration, remaining_needed + min_clip_duration)
622
  segment_duration = max(min_clip_duration, segment_duration)
623
  segment_duration = min(segment_duration, clip.duration)
624
-
625
  if segment_duration >= min_clip_duration:
626
  try:
627
  sub = clip.subclip(0, segment_duration)
@@ -632,7 +539,6 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
632
  except:
633
  pass
634
  continue
635
-
636
  clips_to_concatenate.append(sub)
637
  current_duration += sub.duration
638
  logger.debug(f"[{i+1}/{len(video_paths)}] Segmento añadido: {sub.duration:.1f}s (total video: {current_duration:.1f}/{audio_duration:.1f}s)")
@@ -646,15 +552,12 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
646
  except Exception as e:
647
  logger.warning(f"[{i+1}/{len(video_paths)}] Error procesando video {path}: {str(e)}", exc_info=True)
648
  continue
649
-
650
  logger.info(f"Procesamiento de clips fuente finalizado. Se obtuvieron {len(clips_to_concatenate)} segmentos válidos.")
651
  if not clips_to_concatenate:
652
  logger.error("No hay segmentos de video válidos disponibles para crear la secuencia.")
653
  raise ValueError("No hay segmentos de video válidos disponibles para crear el video.")
654
-
655
  logger.info(f"Concatenando {len(clips_to_concatenate)} segmentos de video.")
656
  concatenated_base = None
657
-
658
  try:
659
  concatenated_base = concatenate_videoclips(clips_to_concatenate, method="chain")
660
  logger.info(f"Duración video base después de concatenación inicial: {concatenated_base.duration:.2f}s")
@@ -671,15 +574,12 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
671
  except:
672
  pass
673
  clips_to_concatenate = []
674
-
675
  video_base = concatenated_base
676
  final_video_base = video_base
677
-
678
  if final_video_base.duration < audio_duration:
679
  logger.info(f"Video base ({final_video_base.duration:.2f}s) es más corto que el audio ({audio_duration:.2f}s). Repitiendo...")
680
  num_full_repeats = int(audio_duration // final_video_base.duration)
681
  remaining_duration = audio_duration % final_video_base.duration
682
-
683
  repeated_clips_list = [final_video_base] * num_full_repeats
684
  if remaining_duration > 0:
685
  try:
@@ -695,7 +595,6 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
695
  logger.debug(f"Añadiendo segmento restante: {remaining_duration:.2f}s")
696
  except Exception as e:
697
  logger.warning(f"Error creando subclip para duración restante {remaining_duration:.2f}s: {str(e)}")
698
-
699
  if repeated_clips_list:
700
  logger.info(f"Concatenando {len(repeated_clips_list)} partes para repetición.")
701
  video_base_repeated = None
@@ -705,7 +604,6 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
705
  if video_base_repeated is None or video_base_repeated.duration is None or video_base_repeated.duration <= 0:
706
  logger.critical("Video base repetido concatenado es inválido.")
707
  raise ValueError("Fallo al crear video base repetido válido.")
708
-
709
  if final_video_base is not video_base_repeated:
710
  try:
711
  final_video_base.close()
@@ -723,7 +621,6 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
723
  clip.close()
724
  except:
725
  pass
726
-
727
  if final_video_base.duration > audio_duration:
728
  logger.info(f"Recortando video base ({final_video_base.duration:.2f}s) para que coincida con la duración del audio ({audio_duration:.2f}s).")
729
  trimmed_video_base = None
@@ -732,7 +629,6 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
732
  if trimmed_video_base is None or trimmed_video_base.duration is None or trimmed_video_base.duration <= 0:
733
  logger.critical("Video base recortado es inválido.")
734
  raise ValueError("Fallo al crear video base recortado válido.")
735
-
736
  if final_video_base is not trimmed_video_base:
737
  try:
738
  final_video_base.close()
@@ -742,21 +638,16 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
742
  except Exception as e:
743
  logger.critical(f"Error durante el recorte: {str(e)}", exc_info=True)
744
  raise ValueError("Fallo durante el recorte de video.")
745
-
746
  if final_video_base is None or final_video_base.duration is None or final_video_base.duration <= 0:
747
  logger.critical("Video base final es inválido antes de audio/escritura (None o duración cero).")
748
  raise ValueError("Video base final es inválido.")
749
-
750
  if final_video_base.size is None or final_video_base.size[0] <= 0 or final_video_base.size[1] <= 0:
751
  logger.critical(f"Video base final tiene tamaño inválido: {final_video_base.size}. No se puede escribir video.")
752
  raise ValueError("Video base final tiene tamaño inválido antes de escribir.")
753
-
754
  video_base = final_video_base
755
-
756
  logger.info("Procesando audio...")
757
  final_audio = audio_tts_original
758
  musica_audio_looped = None
759
-
760
  if musica_file:
761
  musica_audio_original = None
762
  try:
@@ -764,7 +655,6 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
764
  shutil.copyfile(musica_file, music_path)
765
  temp_intermediate_files.append(music_path)
766
  logger.info(f"Música de fondo copiada a: {music_path}")
767
-
768
  musica_audio_original = AudioFileClip(music_path)
769
  if musica_audio_original.reader is None or musica_audio_original.duration is None or musica_audio_original.duration <= 0:
770
  logger.warning("Clip de música de fondo parece inválido o tiene duración cero. Saltando música.")
@@ -776,7 +666,6 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
776
  else:
777
  musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
778
  logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
779
-
780
  if musica_audio_looped is None or musica_audio_looped.duration is None or musica_audio_looped.duration <= 0:
781
  logger.warning("Clip de música de fondo loopeado es inválido. Saltando música.")
782
  try:
@@ -784,13 +673,11 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
784
  except:
785
  pass
786
  musica_audio_looped = None
787
-
788
  if musica_audio_looped:
789
  composite_audio = CompositeAudioClip([
790
  musica_audio_looped.volumex(0.2),
791
  audio_tts_original.volumex(1.0)
792
  ])
793
-
794
  if composite_audio.duration is None or composite_audio.duration <= 0:
795
  logger.warning("Clip de audio compuesto es inválido (None o duración cero). Usando solo audio de voz.")
796
  try:
@@ -807,7 +694,6 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
807
  final_audio = audio_tts_original
808
  musica_audio = None
809
  logger.warning("Usando solo audio de voz debido a un error con la música.")
810
-
811
  if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
812
  logger.warning(f"Duración del audio final ({final_audio.duration:.2f}s) difiere significativamente del video base ({video_base.duration:.2f}s). Intentando recorte.")
813
  try:
@@ -829,13 +715,10 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
829
  logger.warning("Audio final recortado para que coincida con la duración del video.")
830
  except Exception as e:
831
  logger.warning(f"Error ajustando duración del audio final: {str(e)}")
832
-
833
  video_final = video_base.set_audio(final_audio)
834
-
835
  output_filename = f"video_{int(time.time())}.mp4"
836
  output_path = os.path.join(temp_dir_intermediate, output_filename)
837
  permanent_path = f"/tmp/{output_filename}"
838
-
839
  video_final.write_videofile(
840
  output_path,
841
  fps=24,
@@ -849,25 +732,21 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
849
  ],
850
  logger='bar'
851
  )
852
-
853
  try:
854
  shutil.copy(output_path, permanent_path)
855
  logger.info(f"Video guardado permanentemente en: {permanent_path}")
856
  except Exception as move_error:
857
  logger.error(f"Error moviendo archivo: {str(move_error)}. Usando path original.")
858
  permanent_path = output_path
859
-
860
  try:
861
  video_final.close()
862
  if 'video_base' in locals() and video_base is not None and video_base is not video_final:
863
  video_base.close()
864
  except Exception as close_error:
865
  logger.error(f"Error cerrando clips: {str(close_error)}")
866
-
867
  total_time = (datetime.now() - start_time).total_seconds()
868
  logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {permanent_path} | Tiempo total: {total_time:.2f}s")
869
  return permanent_path
870
-
871
  except ValueError as ve:
872
  logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
873
  raise ve
@@ -876,43 +755,36 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
876
  raise e
877
  finally:
878
  logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
879
-
880
  for clip in source_clips:
881
  try:
882
  clip.close()
883
  except Exception as e:
884
  logger.warning(f"Error cerrando clip de video fuente en finally: {str(e)}")
885
-
886
  for clip_segment in clips_to_concatenate:
887
  try:
888
  clip_segment.close()
889
  except Exception as e:
890
  logger.warning(f"Error cerrando segmento de video en finally: {str(e)}")
891
-
892
  if musica_audio is not None:
893
  try:
894
  musica_audio.close()
895
  except Exception as e:
896
  logger.warning(f"Error cerrando musica_audio (procesada) en finally: {str(e)}")
897
-
898
  if musica_audio_original is not None and musica_audio_original is not musica_audio:
899
  try:
900
  musica_audio_original.close()
901
  except Exception as e:
902
  logger.warning(f"Error cerrando musica_audio_original en finally: {str(e)}")
903
-
904
  if audio_tts is not None and audio_tts is not audio_tts_original:
905
  try:
906
  audio_tts.close()
907
  except Exception as e:
908
  logger.warning(f"Error cerrando audio_tts (procesada) en finally: {str(e)}")
909
-
910
  if audio_tts_original is not None:
911
  try:
912
  audio_tts_original.close()
913
  except Exception as e:
914
  logger.warning(f"Error cerrando audio_tts_original en finalmente: {str(e)}")
915
-
916
  if video_final is not None:
917
  try:
918
  video_final.close()
@@ -923,12 +795,10 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
923
  video_base.close()
924
  except Exception as e:
925
  logger.warning(f"Error cerrando video_base en finally: {str(e)}")
926
-
927
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
928
  final_output_in_temp = None
929
  if output_filename:
930
  final_output_in_temp = os.path.join(temp_dir_intermediate, output_filename)
931
-
932
  for path in temp_intermediate_files:
933
  try:
934
  if os.path.isfile(path) and path != final_output_in_temp and path != permanent_path:
@@ -938,43 +808,33 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
938
  logger.debug(f"Saltando eliminación del archivo de video final: {path}")
939
  except Exception as e:
940
  logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
941
-
942
  logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
943
-
944
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
945
  logger.info("="*80)
946
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
947
-
948
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
949
  output_video = None
950
  output_file = None
951
  status_msg = gr.update(value="⏳ Procesando...", interactive=False)
952
-
953
  if not input_text or not input_text.strip():
954
  logger.warning("Texto de entrada vacío.")
955
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
956
-
957
  voice_ids_disponibles = [v[1] for v in AVAILABLE_VOICES]
958
  if selected_voice not in voice_ids_disponibles:
959
  logger.warning(f"Voz seleccionada inválida o no encontrada en la lista: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE_ID}.")
960
  selected_voice = DEFAULT_VOICE_ID
961
  else:
962
  logger.info(f"Voz seleccionada validada: {selected_voice}")
963
-
964
  logger.info(f"Tipo de entrada: {prompt_type}")
965
  logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
966
-
967
  if musica_file:
968
  logger.info(f"Archivo de música recibido: {musica_file}")
969
  else:
970
  logger.info("No se proporcionó archivo de música.")
971
-
972
  logger.info(f"Voz final a usar (ID): {selected_voice}")
973
-
974
  try:
975
  logger.info("Llamando a crear_video...")
976
  video_path = crear_video(prompt_type, input_text, selected_voice, musica_file)
977
-
978
  if video_path and os.path.exists(video_path):
979
  logger.info(f"crear_video retornó path: {video_path}")
980
  logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
@@ -984,27 +844,21 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
984
  else:
985
  logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
986
  status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
987
-
988
  except ValueError as ve:
989
  logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
990
  status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
991
-
992
  except Exception as e:
993
  logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
994
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
995
-
996
  finally:
997
  logger.info("Fin del handler run_app.")
998
  return output_video, output_file, status_msg
999
-
1000
  with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
1001
  .gradio-container {max-width: 800px; margin: auto;}
1002
  h1 {text-align: center;}
1003
  """) as app:
1004
-
1005
  gr.Markdown("# 🎬 Generador Automático de Videos con IA")
1006
  gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
1007
-
1008
  with gr.Row():
1009
  with gr.Column():
1010
  prompt_type = gr.Radio(
@@ -1012,7 +866,6 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1012
  label="Método de Entrada",
1013
  value="Generar Guion con IA"
1014
  )
1015
-
1016
  with gr.Column(visible=True) as ia_guion_column:
1017
  prompt_ia = gr.Textbox(
1018
  label="Tema para IA",
@@ -1021,7 +874,6 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1021
  max_lines=4,
1022
  value=""
1023
  )
1024
-
1025
  with gr.Column(visible=False) as manual_guion_column:
1026
  prompt_manual = gr.Textbox(
1027
  label="Tu Guion Completo",
@@ -1030,36 +882,30 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1030
  max_lines=10,
1031
  value=""
1032
  )
1033
-
1034
  musica_input = gr.Audio(
1035
  label="Música de fondo (opcional)",
1036
  type="filepath",
1037
  interactive=True,
1038
  value=None
1039
  )
1040
-
1041
  voice_dropdown = gr.Dropdown(
1042
  label="Seleccionar Voz para Guion",
1043
  choices=AVAILABLE_VOICES,
1044
  value=DEFAULT_VOICE_ID,
1045
  interactive=True
1046
  )
1047
-
1048
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
1049
-
1050
  with gr.Column():
1051
  video_output = gr.Video(
1052
  label="Previsualización del Video Generado",
1053
  interactive=False,
1054
  height=400
1055
  )
1056
-
1057
  file_output = gr.File(
1058
  label="Descargar Archivo de Video",
1059
  interactive=False,
1060
  visible=False
1061
  )
1062
-
1063
  status_output = gr.Textbox(
1064
  label="Estado",
1065
  interactive=False,
@@ -1067,14 +913,12 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1067
  placeholder="Esperando acción...",
1068
  value="Esperando entrada..."
1069
  )
1070
-
1071
  prompt_type.change(
1072
  lambda x: (gr.update(visible=x == "Generar Guion con IA"),
1073
  gr.update(visible=x == "Usar Mi Guion")),
1074
  inputs=prompt_type,
1075
  outputs=[ia_guion_column, manual_guion_column]
1076
  )
1077
-
1078
  generate_btn.click(
1079
  lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
1080
  outputs=[video_output, file_output, status_output],
@@ -1088,7 +932,6 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1088
  inputs=[video_output, file_output, status_output],
1089
  outputs=[file_output]
1090
  )
1091
-
1092
  gr.Markdown("### Instrucciones:")
1093
  gr.Markdown("""
1094
  1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
@@ -1100,10 +943,8 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1100
  7. La previsualización aparecerá si es posible, y siempre un enlace **Descargar Archivo de Video** se mostrará si la generación fue exitosa.
1101
  8. Revisa `video_generator_full.log` para detalles si hay errores.
1102
  """)
1103
-
1104
  gr.Markdown("---")
1105
  gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
1106
-
1107
  if __name__ == "__main__":
1108
  logger.info("Verificando dependencias críticas...")
1109
  try:
@@ -1116,11 +957,33 @@ if __name__ == "__main__":
1116
  logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
1117
  except Exception as e:
1118
  logger.critical(f"Fallo al importar MoviePy. Asegúrate de que está instalado. Error: {e}", exc_info=True)
1119
-
1120
  os.environ['GRADIO_SERVER_TIMEOUT'] = '1200'
1121
  logger.info("Iniciando aplicación Gradio...")
1122
  try:
1123
  app.launch(server_name="0.0.0.0", server_port=7860, share=False)
1124
  except Exception as e:
1125
  logger.critical(f"No se pudo iniciar la app: {str(e)}", exc_info=True)
1126
- raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import asyncio
2
  import logging
3
  import tempfile
 
15
  import json
16
  from collections import Counter
17
  import time
 
18
  logging.basicConfig(
19
  level=logging.DEBUG,
20
  format='%(asctime)s - %(levelname)s - %(message)s',
 
27
  logger.info("="*80)
28
  logger.info("INICIO DE EJECUCIÓN - GENERADOR DE VIDEOS")
29
  logger.info("="*80)
 
30
  VOCES_DISPONIBLES = {
31
  "Español (España)": {
32
  "es-ES-JuanNeural": "Juan (España) - Masculino",
 
88
  "es-US-PalomaNeural": "Paloma (Estados Unidos) - Femenino"
89
  }
90
  }
 
91
  def get_voice_choices():
92
  choices = []
93
  for region, voices in VOCES_DISPONIBLES.items():
94
  for voice_id, voice_name in voices.items():
95
  choices.append((f"{voice_name} ({region})", voice_id))
96
  return choices
 
97
  AVAILABLE_VOICES = get_voice_choices()
98
  DEFAULT_VOICE_ID = "es-ES-JuanNeural"
99
  DEFAULT_VOICE_NAME = DEFAULT_VOICE_ID
 
101
  if voice_id == DEFAULT_VOICE_ID:
102
  DEFAULT_VOICE_NAME = text
103
  break
 
104
  if DEFAULT_VOICE_ID not in [v[1] for v in AVAILABLE_VOICES]:
105
  DEFAULT_VOICE_ID = AVAILABLE_VOICES[0][1] if AVAILABLE_VOICES else "en-US-AriaNeural"
106
  DEFAULT_VOICE_NAME = AVAILABLE_VOICES[0][0] if AVAILABLE_VOICES else "Aria (United States) - Female"
 
107
  logger.info(f"Voz por defecto seleccionada (ID): {DEFAULT_VOICE_ID}")
 
108
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
109
  if not PEXELS_API_KEY:
110
  logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
 
111
  MODEL_NAME = "datificate/gpt2-small-spanish"
112
  logger.info(f"Inicializando modelo GPT-2: {MODEL_NAME}")
 
113
  tokenizer = None
114
  model = None
115
  try:
 
121
  except Exception as e:
122
  logger.error(f"FALLA CRÍTICA al cargar GPT-2: {str(e)}", exc_info=True)
123
  tokenizer = model = None
 
124
  logger.info("Cargando modelo KeyBERT...")
125
  kw_model = None
126
  try:
 
129
  except Exception as e:
130
  logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
131
  kw_model = None
 
132
  def buscar_videos_pexels(query, api_key, per_page=5):
133
  if not api_key:
134
  logger.warning("No se puede buscar en Pexels: API Key no configurada.")
 
160
  except Exception as e:
161
  logger.error(f"Error inesperado Pexels para '{query}': {str(e)}", exc_info=True)
162
  return []
 
163
  def generate_script(prompt, max_length=150):
164
  logger.info(f"Generando guión | Prompt: '{prompt[:50]}...' | Longitud máxima: {max_length}")
165
  if not tokenizer or not model:
166
  logger.warning("Modelos GPT-2 no disponibles - Usando prompt original como guion.")
167
  return prompt.strip()
 
168
  instruction_phrase_start = "Escribe un guion corto, interesante y coherente sobre:"
169
  ai_prompt = f"{instruction_phrase_start} {prompt}"
 
170
  try:
171
  inputs = tokenizer(ai_prompt, return_tensors="pt", truncation=True, max_length=512)
172
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
173
  model.to(device)
174
  inputs = {k: v.to(device) for k, v in inputs.items()}
 
175
  outputs = model.generate(
176
  **inputs,
177
  max_length=max_length + inputs[list(inputs.keys())[0]].size(1),
 
184
  eos_token_id=tokenizer.eos_token_id,
185
  no_repeat_ngram_size=3
186
  )
 
187
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
188
  cleaned_text = text.strip()
 
189
  try:
190
  prompt_in_output_idx = text.lower().find(prompt.lower())
191
  if prompt_in_output_idx != -1:
 
202
  except Exception as e:
203
  logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
204
  cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
 
205
  if not cleaned_text or len(cleaned_text) < 10:
206
  logger.warning("El guión generado parece muy corto o vacío después de la limpieza heurística. Usando el texto generado original (sin limpieza adicional).")
207
  cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
 
208
  cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
209
  cleaned_text = cleaned_text.lstrip(':').strip()
210
  cleaned_text = cleaned_text.lstrip('.').strip()
 
211
  sentences = cleaned_text.split('.')
212
  if sentences and sentences[0].strip():
213
  final_text = sentences[0].strip() + '.'
 
216
  final_text = final_text.replace("..", ".")
217
  logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
218
  return final_text.strip()
 
219
  logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
220
  return cleaned_text.strip()
221
  except Exception as e:
222
  logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
223
  logger.warning("Usando prompt original como guion debido al error de generación.")
224
  return prompt.strip()
 
225
  async def text_to_speech(text, output_path, voice):
226
  logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
227
  if not text or not text.strip():
 
239
  except Exception as e:
240
  logger.error(f"Error en TTS con voz '{voice}': {str(e)}", exc_info=True)
241
  return False
 
242
  def download_video_file(url, temp_dir):
243
  if not url:
244
  logger.warning("URL de video no proporcionada para descargar")
 
248
  os.makedirs(temp_dir, exist_ok=True)
249
  file_name = f"video_dl_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.mp4"
250
  output_path = os.path.join(temp_dir, file_name)
 
251
  with requests.get(url, stream=True, timeout=60) as r:
252
  r.raise_for_status()
253
  with open(output_path, 'wb') as f:
254
  for chunk in r.iter_content(chunk_size=8192):
255
  f.write(chunk)
 
256
  if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
257
  logger.info(f"Video descargado exitosamente: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
258
  return output_path
 
266
  except Exception as e:
267
  logger.error(f"Error inesperado descargando {url[:80]}... : {str(e)}", exc_info=True)
268
  return None
 
269
  def loop_audio_to_length(audio_clip, target_duration):
270
  logger.debug(f"Ajustando audio | Duración actual: {audio_clip.duration:.2f}s | Objetivo: {target_duration:.2f}s")
271
  if audio_clip is None or audio_clip.duration is None or audio_clip.duration <= 0:
 
276
  except Exception as e:
277
  logger.error(f"Could not create silence clip: {e}", exc_info=True)
278
  return AudioFileClip(filename="")
 
279
  if audio_clip.duration >= target_duration:
280
  logger.debug("Audio clip already longer or equal to target. Trimming.")
281
  trimmed_clip = audio_clip.subclip(0, target_duration)
 
287
  pass
288
  return AudioFileClip(filename="")
289
  return trimmed_clip
 
290
  loops = math.ceil(target_duration / audio_clip.duration)
291
  logger.debug(f"Creando {loops} loops de audio")
292
  audio_segments = [audio_clip] * loops
293
  looped_audio = None
294
  final_looped_audio = None
 
295
  try:
296
  looped_audio = concatenate_audioclips(audio_segments)
297
  if looped_audio.duration is None or looped_audio.duration <= 0:
298
  logger.error("Concatenated audio clip is invalid (None or zero duration).")
299
  raise ValueError("Invalid concatenated audio.")
 
300
  final_looped_audio = looped_audio.subclip(0, target_duration)
301
  if final_looped_audio.duration is None or final_looped_audio.duration <= 0:
302
  logger.error("Final subclipped audio clip is invalid (None or zero duration).")
303
  raise ValueError("Invalid final subclipped audio.")
 
304
  return final_looped_audio
305
  except Exception as e:
306
  logger.error(f"Error concatenating/subclipping audio clips for looping: {str(e)}", exc_info=True)
 
318
  looped_audio.close()
319
  except:
320
  pass
 
321
  def extract_visual_keywords_from_script(script_text):
322
  logger.info("Extrayendo palabras clave del guion")
323
  if not script_text or not script_text.strip():
324
  logger.warning("Guion vacío, no se pueden extraer palabras clave.")
325
  return ["naturaleza", "ciudad", "paisaje"]
 
326
  clean_text = re.sub(r'[^\w\sáéíóúñÁÉÍÓÚÑ]', '', script_text)
327
  keywords_list = []
 
328
  if kw_model:
329
  try:
330
  logger.debug("Intentando extracción con KeyBERT...")
 
332
  keywords2 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(2, 2), stop_words='spanish', top_n=3)
333
  all_keywords = keywords1 + keywords2
334
  all_keywords.sort(key=lambda item: item[1], reverse=True)
 
335
  seen_keywords = set()
336
  for keyword, score in all_keywords:
337
  formatted_keyword = keyword.lower().replace(" ", "+")
 
340
  seen_keywords.add(formatted_keyword)
341
  if len(keywords_list) >= 5:
342
  break
 
343
  if keywords_list:
344
  logger.debug(f"Palabras clave extraídas por KeyBERT: {keywords_list}")
345
  return keywords_list
346
  except Exception as e:
347
  logger.warning(f"KeyBERT falló: {str(e)}. Intentando método simple.")
 
348
  logger.debug("Extrayendo palabras clave con método simple...")
349
  words = clean_text.lower().split()
350
  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",
351
  "nuestro", "vuestro", "este", "ese", "aquel", "esta", "esa", "aquella", "esto", "eso", "aquello", "mis", "tus",
352
  "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á"}
 
353
  valid_words = [word for word in words if len(word) > 3 and word not in stop_words]
354
  if not valid_words:
355
  logger.warning("No se encontraron palabras clave válidas con método simple. Usando palabras clave predeterminadas.")
356
  return ["espiritual", "terror", "matrix", "arcontes", "galaxia", "creepy", "magia", "gangstalking","conspiracy",]
 
357
  word_counts = Counter(valid_words)
358
  top_keywords = [word.replace(" ", "+") for word, _ in word_counts.most_common(5)]
359
  if not top_keywords:
360
  logger.warning("El método simple no produjo keywords. Usando palabras clave predeterminadas.")
361
  return ["espiritual", "terror", "matrix", "arcontes", "galaxia", "creepy", "magia", "gangstalking","conspiracy",]
 
362
  logger.info(f"Palabras clave finales: {top_keywords}")
363
  return top_keywords
 
364
  def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
365
  logger.info("="*80)
366
  logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
367
  logger.debug(f"Input: '{input_text[:100]}...'")
368
  logger.info(f"Voz seleccionada: {selected_voice}")
369
  start_time = datetime.now()
 
370
  temp_dir_intermediate = None
371
  output_filename = None
372
  permanent_path = None
 
379
  video_final = None
380
  source_clips = []
381
  clips_to_concatenate = []
 
382
  try:
383
  if prompt_type == "Generar Guion con IA":
384
  guion = generate_script(input_text)
385
  else:
386
  guion = input_text.strip()
 
387
  logger.info(f"Guion final ({len(guion)} chars): '{guion[:100]}...'")
388
  if not guion.strip():
389
  logger.error("El guion resultante está vacío o solo contiene espacios.")
390
  raise ValueError("El guion está vacío.")
 
391
  temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
392
  logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
 
393
  logger.info("Generando audio de voz...")
394
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
 
395
  tts_voices_to_try = [selected_voice]
396
  fallback_juan = "es-ES-JuanNeural"
397
  fallback_elvira = "es-ES-ElviraNeural"
 
399
  tts_voices_to_try.append(fallback_juan)
400
  if fallback_elvira and fallback_elvira != selected_voice and fallback_elvira not in tts_voices_to_try:
401
  tts_voices_to_try.append(fallback_elvira)
 
402
  tts_success = False
403
  tried_voices = set()
404
  for current_voice in tts_voices_to_try:
 
414
  except Exception as e:
415
  logger.warning(f"Fallo al generar TTS con voz '{current_voice}': {str(e)}", exc_info=True)
416
  pass
 
417
  if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
418
  logger.error("Fallo en la generación de voz después de todos los intentos. Archivo de audio no creado o es muy pequeño.")
419
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
 
420
  temp_intermediate_files.append(voz_path)
421
  audio_tts_original = AudioFileClip(voz_path)
422
  if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
 
434
  if voz_path in temp_intermediate_files:
435
  temp_intermediate_files.remove(voz_path)
436
  raise ValueError("Audio de voz generado es inválido después de procesamiento inicial.")
 
437
  audio_tts = audio_tts_original
438
  audio_duration = audio_tts_original.duration
439
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
440
  if audio_duration < 1.0:
441
  logger.error(f"Duración audio voz ({audio_duration:.2f}s) es muy corta.")
442
  raise ValueError("Generated voice audio is too short (min 1 second required).")
 
443
  logger.info("Extrayendo palabras clave...")
444
  try:
445
  keywords = extract_visual_keywords_from_script(guion)
 
447
  except Exception as e:
448
  logger.error(f"Error extrayendo keywords: {str(e)}", exc_info=True)
449
  keywords = ["naturaleza", "paisaje"]
 
450
  if not keywords:
451
  keywords = ["video", "background"]
 
452
  logger.info("Buscando videos en Pexels...")
453
  videos_data = []
454
  total_desired_videos = 10
455
  per_page_per_keyword = max(1, total_desired_videos // len(keywords))
 
456
  for keyword in keywords:
457
  if len(videos_data) >= total_desired_videos:
458
  break
 
463
  logger.info(f"Encontrados {len(videos)} videos para '{keyword}'. Total data: {len(videos_data)}")
464
  except Exception as e:
465
  logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
 
466
  if len(videos_data) < total_desired_videos / 2:
467
  logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
468
  generic_keywords = ["mystery", "alien", "ufo", "conspiracy", "paranormal", "supernatural", "horror", "fear", "suspense", "secret", "government", "cover_up", "simulation", "matrix", "apocalypse", "dystopian", "shadow", "occult", "unexplained", "creepy", "extraterrestrial", "abduction", "experiment", "secret_society", "illuminati", "new_world_order", "ancient_aliens", "ufo_sighting", "cryptid", "bigfoot", "loch_ness", "ghost", "haunting", "spirit", "demon", "possession", "exorcism", "witchcraft", "ritual", "cursed", "urban_legend", "myth", "legend", "folklore", "scary", "terror", "panic", "anxiety", "dread", "nightmare", "dark", "gloomy", "fog", "haunted", "cemetery", "asylum", "abandoned", "ruins", "underground", "tunnel", "bunker", "lab", "experiment", "government_secret", "mind_control", "brainwash", "propaganda", "surveillance", "spy", "whistleblower", "leak", "anonymous", "hack", "cyber", "virtual_reality", "ai", "artificial_intelligence", "robot", "cyborg", "apocalyptic", "post_apocalyptic", "zombie", "outbreak", "pandemic", "contagion", "biohazard", "radiation", "nuclear", "doomsday", "armageddon", "revelation", "prophecy", "symbolism", "hidden_meaning", "enigma", "puzzle", "code", "cipher", "mysterious", "unidentified", "anomaly", "glitch", "time_travel", "parallel_universe", "dimension", "portal"]
 
469
  for keyword in generic_keywords:
470
  if len(videos_data) >= total_desired_videos:
471
  break
 
476
  logger.info(f"Encontrados {len(videos)} videos para '{keyword}' (genérico). Total data: {len(videos_data)}")
477
  except Exception as e:
478
  logger.warning(f"Error buscando videos genéricos para '{keyword}': {str(e)}")
 
479
  if not videos_data:
480
  logger.error("No se encontraron videos en Pexels para ninguna palabra clave.")
481
  raise ValueError("No se encontraron videos adecuados en Pexels.")
 
482
  video_paths = []
483
  logger.info(f"Intentando descargar {len(videos_data)} videos encontrados...")
 
484
  for video in videos_data:
485
  if 'video_files' not in video or not video['video_files']:
486
  logger.debug(f"Saltando video sin archivos de video: {video.get('id')}")
487
  continue
 
488
  try:
489
  best_quality = None
490
  for vf in sorted(video['video_files'], key=lambda x: x.get('width', 0) * x.get('height', 0), reverse=True):
491
  if 'link' in vf:
492
  best_quality = vf
493
  break
 
494
  if best_quality and 'link' in best_quality:
495
  path = download_video_file(best_quality['link'], temp_dir_intermediate)
496
  if path:
 
503
  logger.warning(f"No se encontró enlace de descarga válido para video {video.get('id')}.")
504
  except Exception as e:
505
  logger.warning(f"Error procesando/descargando video {video.get('id')}: {str(e)}")
 
506
  logger.info(f"Descargados {len(video_paths)} archivos de video utilizables.")
507
  if not video_paths:
508
  logger.error("No se pudo descargar ningún archivo de video utilizable.")
509
  raise ValueError("No se pudo descargar ningún video utilizable de Pexels.")
 
510
  logger.info("Procesando y concatenando videos descargados...")
511
  current_duration = 0
512
  min_clip_duration = 0.5
513
  max_clip_segment = 10.0
 
514
  for i, path in enumerate(video_paths):
515
  if current_duration >= audio_duration + max_clip_segment:
516
  logger.debug(f"Video base suficiente ({current_duration:.1f}s >= {audio_duration:.1f}s + {max_clip_segment:.1f}s buffer). Dejando de procesar clips fuente restantes.")
517
  break
 
518
  clip = None
519
  try:
520
  logger.debug(f"[{i+1}/{len(video_paths)}] Abriendo clip: {path}")
521
  clip = VideoFileClip(path)
522
  source_clips.append(clip)
 
523
  if clip.reader is None or clip.duration is None or clip.duration <= 0:
524
  logger.warning(f"[{i+1}/{len(video_paths)}] Clip fuente {path} parece inválido (reader is None o duración <= 0). Saltando.")
525
  continue
 
526
  remaining_needed = audio_duration - current_duration
527
  potential_use_duration = min(clip.duration, max_clip_segment)
 
528
  if remaining_needed > 0:
529
  segment_duration = min(potential_use_duration, remaining_needed + min_clip_duration)
530
  segment_duration = max(min_clip_duration, segment_duration)
531
  segment_duration = min(segment_duration, clip.duration)
 
532
  if segment_duration >= min_clip_duration:
533
  try:
534
  sub = clip.subclip(0, segment_duration)
 
539
  except:
540
  pass
541
  continue
 
542
  clips_to_concatenate.append(sub)
543
  current_duration += sub.duration
544
  logger.debug(f"[{i+1}/{len(video_paths)}] Segmento añadido: {sub.duration:.1f}s (total video: {current_duration:.1f}/{audio_duration:.1f}s)")
 
552
  except Exception as e:
553
  logger.warning(f"[{i+1}/{len(video_paths)}] Error procesando video {path}: {str(e)}", exc_info=True)
554
  continue
 
555
  logger.info(f"Procesamiento de clips fuente finalizado. Se obtuvieron {len(clips_to_concatenate)} segmentos válidos.")
556
  if not clips_to_concatenate:
557
  logger.error("No hay segmentos de video válidos disponibles para crear la secuencia.")
558
  raise ValueError("No hay segmentos de video válidos disponibles para crear el video.")
 
559
  logger.info(f"Concatenando {len(clips_to_concatenate)} segmentos de video.")
560
  concatenated_base = None
 
561
  try:
562
  concatenated_base = concatenate_videoclips(clips_to_concatenate, method="chain")
563
  logger.info(f"Duración video base después de concatenación inicial: {concatenated_base.duration:.2f}s")
 
574
  except:
575
  pass
576
  clips_to_concatenate = []
 
577
  video_base = concatenated_base
578
  final_video_base = video_base
 
579
  if final_video_base.duration < audio_duration:
580
  logger.info(f"Video base ({final_video_base.duration:.2f}s) es más corto que el audio ({audio_duration:.2f}s). Repitiendo...")
581
  num_full_repeats = int(audio_duration // final_video_base.duration)
582
  remaining_duration = audio_duration % final_video_base.duration
 
583
  repeated_clips_list = [final_video_base] * num_full_repeats
584
  if remaining_duration > 0:
585
  try:
 
595
  logger.debug(f"Añadiendo segmento restante: {remaining_duration:.2f}s")
596
  except Exception as e:
597
  logger.warning(f"Error creando subclip para duración restante {remaining_duration:.2f}s: {str(e)}")
 
598
  if repeated_clips_list:
599
  logger.info(f"Concatenando {len(repeated_clips_list)} partes para repetición.")
600
  video_base_repeated = None
 
604
  if video_base_repeated is None or video_base_repeated.duration is None or video_base_repeated.duration <= 0:
605
  logger.critical("Video base repetido concatenado es inválido.")
606
  raise ValueError("Fallo al crear video base repetido válido.")
 
607
  if final_video_base is not video_base_repeated:
608
  try:
609
  final_video_base.close()
 
621
  clip.close()
622
  except:
623
  pass
 
624
  if final_video_base.duration > audio_duration:
625
  logger.info(f"Recortando video base ({final_video_base.duration:.2f}s) para que coincida con la duración del audio ({audio_duration:.2f}s).")
626
  trimmed_video_base = None
 
629
  if trimmed_video_base is None or trimmed_video_base.duration is None or trimmed_video_base.duration <= 0:
630
  logger.critical("Video base recortado es inválido.")
631
  raise ValueError("Fallo al crear video base recortado válido.")
 
632
  if final_video_base is not trimmed_video_base:
633
  try:
634
  final_video_base.close()
 
638
  except Exception as e:
639
  logger.critical(f"Error durante el recorte: {str(e)}", exc_info=True)
640
  raise ValueError("Fallo durante el recorte de video.")
 
641
  if final_video_base is None or final_video_base.duration is None or final_video_base.duration <= 0:
642
  logger.critical("Video base final es inválido antes de audio/escritura (None o duración cero).")
643
  raise ValueError("Video base final es inválido.")
 
644
  if final_video_base.size is None or final_video_base.size[0] <= 0 or final_video_base.size[1] <= 0:
645
  logger.critical(f"Video base final tiene tamaño inválido: {final_video_base.size}. No se puede escribir video.")
646
  raise ValueError("Video base final tiene tamaño inválido antes de escribir.")
 
647
  video_base = final_video_base
 
648
  logger.info("Procesando audio...")
649
  final_audio = audio_tts_original
650
  musica_audio_looped = None
 
651
  if musica_file:
652
  musica_audio_original = None
653
  try:
 
655
  shutil.copyfile(musica_file, music_path)
656
  temp_intermediate_files.append(music_path)
657
  logger.info(f"Música de fondo copiada a: {music_path}")
 
658
  musica_audio_original = AudioFileClip(music_path)
659
  if musica_audio_original.reader is None or musica_audio_original.duration is None or musica_audio_original.duration <= 0:
660
  logger.warning("Clip de música de fondo parece inválido o tiene duración cero. Saltando música.")
 
666
  else:
667
  musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
668
  logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
 
669
  if musica_audio_looped is None or musica_audio_looped.duration is None or musica_audio_looped.duration <= 0:
670
  logger.warning("Clip de música de fondo loopeado es inválido. Saltando música.")
671
  try:
 
673
  except:
674
  pass
675
  musica_audio_looped = None
 
676
  if musica_audio_looped:
677
  composite_audio = CompositeAudioClip([
678
  musica_audio_looped.volumex(0.2),
679
  audio_tts_original.volumex(1.0)
680
  ])
 
681
  if composite_audio.duration is None or composite_audio.duration <= 0:
682
  logger.warning("Clip de audio compuesto es inválido (None o duración cero). Usando solo audio de voz.")
683
  try:
 
694
  final_audio = audio_tts_original
695
  musica_audio = None
696
  logger.warning("Usando solo audio de voz debido a un error con la música.")
 
697
  if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
698
  logger.warning(f"Duración del audio final ({final_audio.duration:.2f}s) difiere significativamente del video base ({video_base.duration:.2f}s). Intentando recorte.")
699
  try:
 
715
  logger.warning("Audio final recortado para que coincida con la duración del video.")
716
  except Exception as e:
717
  logger.warning(f"Error ajustando duración del audio final: {str(e)}")
 
718
  video_final = video_base.set_audio(final_audio)
 
719
  output_filename = f"video_{int(time.time())}.mp4"
720
  output_path = os.path.join(temp_dir_intermediate, output_filename)
721
  permanent_path = f"/tmp/{output_filename}"
 
722
  video_final.write_videofile(
723
  output_path,
724
  fps=24,
 
732
  ],
733
  logger='bar'
734
  )
 
735
  try:
736
  shutil.copy(output_path, permanent_path)
737
  logger.info(f"Video guardado permanentemente en: {permanent_path}")
738
  except Exception as move_error:
739
  logger.error(f"Error moviendo archivo: {str(move_error)}. Usando path original.")
740
  permanent_path = output_path
 
741
  try:
742
  video_final.close()
743
  if 'video_base' in locals() and video_base is not None and video_base is not video_final:
744
  video_base.close()
745
  except Exception as close_error:
746
  logger.error(f"Error cerrando clips: {str(close_error)}")
 
747
  total_time = (datetime.now() - start_time).total_seconds()
748
  logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {permanent_path} | Tiempo total: {total_time:.2f}s")
749
  return permanent_path
 
750
  except ValueError as ve:
751
  logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
752
  raise ve
 
755
  raise e
756
  finally:
757
  logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
 
758
  for clip in source_clips:
759
  try:
760
  clip.close()
761
  except Exception as e:
762
  logger.warning(f"Error cerrando clip de video fuente en finally: {str(e)}")
 
763
  for clip_segment in clips_to_concatenate:
764
  try:
765
  clip_segment.close()
766
  except Exception as e:
767
  logger.warning(f"Error cerrando segmento de video en finally: {str(e)}")
 
768
  if musica_audio is not None:
769
  try:
770
  musica_audio.close()
771
  except Exception as e:
772
  logger.warning(f"Error cerrando musica_audio (procesada) en finally: {str(e)}")
 
773
  if musica_audio_original is not None and musica_audio_original is not musica_audio:
774
  try:
775
  musica_audio_original.close()
776
  except Exception as e:
777
  logger.warning(f"Error cerrando musica_audio_original en finally: {str(e)}")
 
778
  if audio_tts is not None and audio_tts is not audio_tts_original:
779
  try:
780
  audio_tts.close()
781
  except Exception as e:
782
  logger.warning(f"Error cerrando audio_tts (procesada) en finally: {str(e)}")
 
783
  if audio_tts_original is not None:
784
  try:
785
  audio_tts_original.close()
786
  except Exception as e:
787
  logger.warning(f"Error cerrando audio_tts_original en finalmente: {str(e)}")
 
788
  if video_final is not None:
789
  try:
790
  video_final.close()
 
795
  video_base.close()
796
  except Exception as e:
797
  logger.warning(f"Error cerrando video_base en finally: {str(e)}")
 
798
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
799
  final_output_in_temp = None
800
  if output_filename:
801
  final_output_in_temp = os.path.join(temp_dir_intermediate, output_filename)
 
802
  for path in temp_intermediate_files:
803
  try:
804
  if os.path.isfile(path) and path != final_output_in_temp and path != permanent_path:
 
808
  logger.debug(f"Saltando eliminación del archivo de video final: {path}")
809
  except Exception as e:
810
  logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
 
811
  logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
 
812
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
813
  logger.info("="*80)
814
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
 
815
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
816
  output_video = None
817
  output_file = None
818
  status_msg = gr.update(value="⏳ Procesando...", interactive=False)
 
819
  if not input_text or not input_text.strip():
820
  logger.warning("Texto de entrada vacío.")
821
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
 
822
  voice_ids_disponibles = [v[1] for v in AVAILABLE_VOICES]
823
  if selected_voice not in voice_ids_disponibles:
824
  logger.warning(f"Voz seleccionada inválida o no encontrada en la lista: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE_ID}.")
825
  selected_voice = DEFAULT_VOICE_ID
826
  else:
827
  logger.info(f"Voz seleccionada validada: {selected_voice}")
 
828
  logger.info(f"Tipo de entrada: {prompt_type}")
829
  logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
 
830
  if musica_file:
831
  logger.info(f"Archivo de música recibido: {musica_file}")
832
  else:
833
  logger.info("No se proporcionó archivo de música.")
 
834
  logger.info(f"Voz final a usar (ID): {selected_voice}")
 
835
  try:
836
  logger.info("Llamando a crear_video...")
837
  video_path = crear_video(prompt_type, input_text, selected_voice, musica_file)
 
838
  if video_path and os.path.exists(video_path):
839
  logger.info(f"crear_video retornó path: {video_path}")
840
  logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
 
844
  else:
845
  logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
846
  status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
 
847
  except ValueError as ve:
848
  logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
849
  status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
 
850
  except Exception as e:
851
  logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
852
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
 
853
  finally:
854
  logger.info("Fin del handler run_app.")
855
  return output_video, output_file, status_msg
 
856
  with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
857
  .gradio-container {max-width: 800px; margin: auto;}
858
  h1 {text-align: center;}
859
  """) as app:
 
860
  gr.Markdown("# 🎬 Generador Automático de Videos con IA")
861
  gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
 
862
  with gr.Row():
863
  with gr.Column():
864
  prompt_type = gr.Radio(
 
866
  label="Método de Entrada",
867
  value="Generar Guion con IA"
868
  )
 
869
  with gr.Column(visible=True) as ia_guion_column:
870
  prompt_ia = gr.Textbox(
871
  label="Tema para IA",
 
874
  max_lines=4,
875
  value=""
876
  )
 
877
  with gr.Column(visible=False) as manual_guion_column:
878
  prompt_manual = gr.Textbox(
879
  label="Tu Guion Completo",
 
882
  max_lines=10,
883
  value=""
884
  )
 
885
  musica_input = gr.Audio(
886
  label="Música de fondo (opcional)",
887
  type="filepath",
888
  interactive=True,
889
  value=None
890
  )
 
891
  voice_dropdown = gr.Dropdown(
892
  label="Seleccionar Voz para Guion",
893
  choices=AVAILABLE_VOICES,
894
  value=DEFAULT_VOICE_ID,
895
  interactive=True
896
  )
 
897
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
 
898
  with gr.Column():
899
  video_output = gr.Video(
900
  label="Previsualización del Video Generado",
901
  interactive=False,
902
  height=400
903
  )
 
904
  file_output = gr.File(
905
  label="Descargar Archivo de Video",
906
  interactive=False,
907
  visible=False
908
  )
 
909
  status_output = gr.Textbox(
910
  label="Estado",
911
  interactive=False,
 
913
  placeholder="Esperando acción...",
914
  value="Esperando entrada..."
915
  )
 
916
  prompt_type.change(
917
  lambda x: (gr.update(visible=x == "Generar Guion con IA"),
918
  gr.update(visible=x == "Usar Mi Guion")),
919
  inputs=prompt_type,
920
  outputs=[ia_guion_column, manual_guion_column]
921
  )
 
922
  generate_btn.click(
923
  lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
924
  outputs=[video_output, file_output, status_output],
 
932
  inputs=[video_output, file_output, status_output],
933
  outputs=[file_output]
934
  )
 
935
  gr.Markdown("### Instrucciones:")
936
  gr.Markdown("""
937
  1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
 
943
  7. La previsualización aparecerá si es posible, y siempre un enlace **Descargar Archivo de Video** se mostrará si la generación fue exitosa.
944
  8. Revisa `video_generator_full.log` para detalles si hay errores.
945
  """)
 
946
  gr.Markdown("---")
947
  gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
 
948
  if __name__ == "__main__":
949
  logger.info("Verificando dependencias críticas...")
950
  try:
 
957
  logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
958
  except Exception as e:
959
  logger.critical(f"Fallo al importar MoviePy. Asegúrate de que está instalado. Error: {e}", exc_info=True)
 
960
  os.environ['GRADIO_SERVER_TIMEOUT'] = '1200'
961
  logger.info("Iniciando aplicación Gradio...")
962
  try:
963
  app.launch(server_name="0.0.0.0", server_port=7860, share=False)
964
  except Exception as e:
965
  logger.critical(f"No se pudo iniciar la app: {str(e)}", exc_info=True)
966
+ raise
967
+
968
+
969
+ profile
970
+ GLM-4.5
971
+
972
+
973
+ El error ModuleNotFoundError: No module named 'moviepy.editor' indica que el módulo moviepy no está instalado en el entorno de Python donde se está ejecutando el código. Para solucionar este problema, necesito modificar el código para asegurarme de que las dependencias necesarias estén instaladas antes de intentar importarlas.
974
+
975
+
976
+
977
+
978
+ El error ModuleNotFoundError: No module named 'moviepy.editor' indica que el módulo moviepy no está instalado en el entorno de Python donde se está ejecutando el código. Para solucionar este problema, necesito modificar el código para asegurarme de que las dependencias necesarias estén instaladas antes de intentar importarlas.
979
+
980
+ La solución es añadir una sección al principio del código que instale las dependencias necesarias si no están presentes. Esto se puede hacer usando el módulo subprocess para ejecutar comandos de instalación de paquetes de Python.
981
+
982
+ Aquí está el código corregido:
983
+
984
+
985
+
986
+
987
+ python
988
+
989
+