gnosticdev commited on
Commit
ef98f47
·
verified ·
1 Parent(s): 2b26186

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +397 -327
app.py CHANGED
@@ -9,14 +9,14 @@ 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, AudioClip
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,19 +27,18 @@ logging.basicConfig(
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
- # Uncomment to force fail if not set:
38
- # raise ValueError("Pexels API key not configured")
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:
@@ -47,26 +46,26 @@ try:
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 = {
@@ -75,7 +74,7 @@ def buscar_videos_pexels(query, api_key, per_page=5):
75
  "orientation": "landscape",
76
  "size": "medium"
77
  }
78
-
79
  response = requests.get(
80
  "https://api.pexels.com/videos/search",
81
  headers=headers,
@@ -83,38 +82,43 @@ def buscar_videos_pexels(query, api_key, per_page=5):
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"Generando guión | 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,
@@ -124,87 +128,127 @@ def generate_script(prompt, max_length=150):
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"Guion generado (Truncado): '{final_text[:100]}...'")
141
  return final_text.strip()
142
-
143
- logger.info(f"Guion generado (sin oraciones completas): '{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
  with open(output_path, 'wb') as f:
186
  for chunk in r.iter_content(chunk_size=8192):
187
  f.write(chunk)
188
 
189
  if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
190
- logger.info(f"Video downloaded successfully: {output_path} | Size: {os.path.getsize(output_path)} bytes")
191
  return output_path
192
  else:
193
- 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")
194
  if os.path.exists(output_path):
195
  os.remove(output_path)
196
  return None
197
 
198
  except requests.exceptions.RequestException as e:
199
- logger.error(f"Download error for {url[:80]}... : {str(e)}")
200
  except Exception as e:
201
- logger.error(f"Unexpected error downloading {url[:80]}... : {str(e)}", exc_info=True)
202
-
203
  return None
204
 
205
  def loop_audio_to_length(audio_clip, target_duration):
206
- logger.debug(f"Adjusting audio | Current duration: {audio_clip.duration:.2f}s | Target: {target_duration:.2f}s")
207
-
208
  if audio_clip is None or audio_clip.duration is None or audio_clip.duration <= 0:
209
  logger.warning("Input audio clip is invalid (None or zero duration), cannot loop.")
210
  try:
@@ -223,16 +267,16 @@ def loop_audio_to_length(audio_clip, target_duration):
223
  except: pass
224
  return AudioFileClip(filename="")
225
  return trimmed_clip
226
-
227
  loops = math.ceil(target_duration / audio_clip.duration)
228
- logger.debug(f"Creating {loops} audio loops")
229
-
230
  audio_segments = [audio_clip] * loops
231
  looped_audio = None
232
  final_looped_audio = None
233
  try:
234
  looped_audio = concatenate_audioclips(audio_segments)
235
-
236
  if looped_audio.duration is None or looped_audio.duration <= 0:
237
  logger.error("Concatenated audio clip is invalid (None or zero duration).")
238
  raise ValueError("Invalid concatenated audio.")
@@ -263,9 +307,9 @@ def loop_audio_to_length(audio_clip, target_duration):
263
 
264
 
265
  def extract_visual_keywords_from_script(script_text):
266
- logger.info("Extracting keywords from script")
267
  if not script_text or not script_text.strip():
268
- logger.warning("Empty script, cannot extract keywords.")
269
  return ["naturaleza", "ciudad", "paisaje"]
270
 
271
  clean_text = re.sub(r'[^\w\sáéíóúñÁÉÍÓÚÑ]', '', script_text)
@@ -273,13 +317,13 @@ def extract_visual_keywords_from_script(script_text):
273
 
274
  if kw_model:
275
  try:
276
- logger.debug("Attempting KeyBERT extraction...")
277
  keywords1 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(1, 1), stop_words='spanish', top_n=5)
278
  keywords2 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(2, 2), stop_words='spanish', top_n=3)
279
-
280
  all_keywords = keywords1 + keywords2
281
  all_keywords.sort(key=lambda item: item[1], reverse=True)
282
-
283
  seen_keywords = set()
284
  for keyword, score in all_keywords:
285
  formatted_keyword = keyword.lower().replace(" ", "+")
@@ -290,76 +334,78 @@ def extract_visual_keywords_from_script(script_text):
290
  break
291
 
292
  if keywords_list:
293
- logger.debug(f"KeyBERT extracted keywords: {keywords_list}")
294
  return keywords_list
295
 
296
  except Exception as e:
297
- logger.warning(f"KeyBERT failed: {str(e)}. Trying simple method.")
298
-
299
- logger.debug("Extracting keywords with simple method...")
300
  words = clean_text.lower().split()
301
- 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á"}
 
302
 
303
  valid_words = [word for word in words if len(word) > 3 and word not in stop_words]
304
-
305
  if not valid_words:
306
- logger.warning("No valid keywords found with simple method. Using default keywords.")
307
  return ["naturaleza", "ciudad", "paisaje"]
308
-
309
  word_counts = Counter(valid_words)
310
  top_keywords = [word.replace(" ", "+") for word, _ in word_counts.most_common(5)]
311
-
312
  if not top_keywords:
313
- logger.warning("Simple method produced no keywords. Using default keywords.")
314
  return ["naturaleza", "ciudad", "paisaje"]
315
 
316
- logger.info(f"Final keywords: {top_keywords}")
317
  return top_keywords
318
 
319
  def crear_video(prompt_type, input_text, musica_file=None):
320
  logger.info("="*80)
321
- logger.info(f"STARTING VIDEO CREATION | Type: {prompt_type}")
322
  logger.debug(f"Input: '{input_text[:100]}...'")
323
-
324
  start_time = datetime.now()
325
  temp_dir_intermediate = None
326
-
327
  audio_tts_original = None
328
  musica_audio_original = None
329
- audio_tts = None
330
- musica_audio = None
331
  video_base = None
332
  video_final = None
333
  source_clips = []
334
  clips_to_concatenate = []
335
 
336
  try:
337
- # 1. Generate or use script
338
  if prompt_type == "Generar Guion con IA":
339
  guion = generate_script(input_text)
340
  else:
341
  guion = input_text.strip()
342
 
343
- logger.info(f"Final script ({len(guion)} chars): '{guion[:100]}...'")
344
-
345
  if not guion.strip():
346
- logger.error("Resulting script is empty.")
347
- raise ValueError("The script is empty.")
348
-
349
  temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
350
- logger.info(f"Intermediate temporary directory created: {temp_dir_intermediate}")
351
  temp_intermediate_files = []
352
 
353
- # 2. Generate voice audio
354
- logger.info("Generating voice audio...")
355
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
356
- if not asyncio.run(text_to_speech(guion, voz_path)):
357
- logger.error("Failed to generate voice audio.")
358
- raise ValueError("Error generating voice audio.")
 
359
  temp_intermediate_files.append(voz_path)
360
 
361
  audio_tts_original = AudioFileClip(voz_path)
362
-
363
  if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
364
  logger.critical("Initial TTS audio clip is invalid (reader is None or duration <= 0).")
365
  try: audio_tts_original.close()
@@ -368,27 +414,27 @@ def crear_video(prompt_type, input_text, musica_file=None):
368
  raise ValueError("Generated voice audio is invalid.")
369
 
370
  audio_tts = audio_tts_original
371
- audio_duration = audio_tts.duration
372
- logger.info(f"Voice audio duration: {audio_duration:.2f} seconds")
373
 
374
  if audio_duration < 1.0:
375
- logger.error(f"Voice audio duration ({audio_duration:.2f}s) is too short.")
376
  raise ValueError("Generated voice audio is too short (min 1 second required).")
377
 
378
- # 3. Extract keywords
379
- logger.info("Extracting keywords...")
380
  try:
381
  keywords = extract_visual_keywords_from_script(guion)
382
- logger.info(f"Identified keywords: {keywords}")
383
  except Exception as e:
384
- logger.error(f"Error extracting keywords: {str(e)}", exc_info=True)
385
  keywords = ["naturaleza", "paisaje"]
386
 
387
  if not keywords:
388
  keywords = ["video", "background"]
389
 
390
- # 4. Search and download videos
391
- logger.info("Searching videos on Pexels...")
392
  videos_data = []
393
  total_desired_videos = 10
394
  per_page_per_keyword = max(1, total_desired_videos // len(keywords))
@@ -399,12 +445,12 @@ def crear_video(prompt_type, input_text, musica_file=None):
399
  videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=per_page_per_keyword)
400
  if videos:
401
  videos_data.extend(videos)
402
- logger.info(f"Found {len(videos)} videos for '{keyword}'. Total data: {len(videos_data)}")
403
  except Exception as e:
404
- logger.warning(f"Error searching videos for '{keyword}': {str(e)}")
405
 
406
  if len(videos_data) < total_desired_videos / 2:
407
- logger.warning(f"Few videos found ({len(videos_data)}). Trying generic keywords.")
408
  generic_keywords = ["nature", "city", "background", "abstract"]
409
  for keyword in generic_keywords:
410
  if len(videos_data) >= total_desired_videos: break
@@ -412,21 +458,21 @@ def crear_video(prompt_type, input_text, musica_file=None):
412
  videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=2)
413
  if videos:
414
  videos_data.extend(videos)
415
- logger.info(f"Found {len(videos)} videos for '{keyword}' (generic). Total data: {len(videos_data)}")
416
  except Exception as e:
417
- logger.warning(f"Error searching generic videos for '{keyword}': {str(e)}")
418
 
419
  if not videos_data:
420
- logger.error("No videos found on Pexels for any keyword.")
421
- raise ValueError("No suitable videos found on Pexels.")
422
-
423
  video_paths = []
424
- logger.info(f"Attempting to download {len(videos_data)} found videos...")
425
  for video in videos_data:
426
  if 'video_files' not in video or not video['video_files']:
427
- logger.debug(f"Skipping video without video files: {video.get('id')}")
428
  continue
429
-
430
  try:
431
  best_quality = None
432
  for vf in sorted(video['video_files'], key=lambda x: x.get('width', 0) * x.get('height', 0), reverse=True):
@@ -439,39 +485,39 @@ def crear_video(prompt_type, input_text, musica_file=None):
439
  if path:
440
  video_paths.append(path)
441
  temp_intermediate_files.append(path)
442
- logger.info(f"Video downloaded OK from {best_quality['link'][:50]}...")
443
  else:
444
- logger.warning(f"Could not download video from {best_quality['link'][:50]}...")
445
  else:
446
- logger.warning(f"No valid download link found for video {video.get('id')}.")
447
 
448
  except Exception as e:
449
- logger.warning(f"Error processing/downloading video {video.get('id')}: {str(e)}")
450
 
451
- logger.info(f"Downloaded {len(video_paths)} usable video files.")
452
  if not video_paths:
453
- logger.error("Could not download any usable video file.")
454
- raise ValueError("Could not download any usable video from Pexels.")
455
-
456
- # 5. Process and concatenate video clips
457
- logger.info("Processing and concatenating downloaded videos...")
458
  current_duration = 0
459
  min_clip_duration = 0.5
460
  max_clip_segment = 10.0
461
 
462
  for i, path in enumerate(video_paths):
463
  if current_duration >= audio_duration + max_clip_segment:
464
- 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.")
465
  break
466
 
467
  clip = None
468
  try:
469
- logger.debug(f"[{i+1}/{len(video_paths)}] Opening clip: {path}")
470
  clip = VideoFileClip(path)
471
  source_clips.append(clip)
472
 
473
  if clip.reader is None or clip.duration is None or clip.duration <= 0:
474
- logger.warning(f"[{i+1}/{len(video_paths)}] Source clip {path} seems invalid (reader is None or duration <= 0). Skipping.")
475
  continue
476
 
477
  remaining_needed = audio_duration - current_duration
@@ -486,46 +532,46 @@ def crear_video(prompt_type, input_text, musica_file=None):
486
  try:
487
  sub = clip.subclip(0, segment_duration)
488
  if sub.reader is None or sub.duration is None or sub.duration <= 0:
489
- logger.warning(f"[{i+1}/{len(video_paths)}] Generated subclip from {path} is invalid. Skipping.")
490
  try: sub.close()
491
  except: pass
492
  continue
493
-
494
  clips_to_concatenate.append(sub)
495
- current_duration += sub.duration
496
- logger.debug(f"[{i+1}/{len(video_paths)}] Segment added: {sub.duration:.1f}s (total video: {current_duration:.1f}/{audio_duration:.1f}s)")
497
-
498
  except Exception as sub_e:
499
- logger.warning(f"[{i+1}/{len(video_paths)}] Error creating subclip from {path} ({segment_duration:.1f}s): {str(sub_e)}")
500
  continue
501
  else:
502
- 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.")
503
  else:
504
- logger.debug(f"[{i+1}/{len(video_paths)}] Video base duration already reached. Skipping clip.")
505
 
506
  except Exception as e:
507
- logger.warning(f"[{i+1}/{len(video_paths)}] Error processing video {path}: {str(e)}", exc_info=True)
508
  continue
509
 
510
- logger.info(f"Source clip processing finished. Obtained {len(clips_to_concatenate)} valid segments.")
511
 
512
  if not clips_to_concatenate:
513
- logger.error("No valid video segments available to create the sequence.")
514
- raise ValueError("No valid video segments available to create the video.")
515
-
516
- logger.info(f"Concatenating {len(clips_to_concatenate)} video segments.")
517
  concatenated_base = None
518
  try:
519
  concatenated_base = concatenate_videoclips(clips_to_concatenate, method="chain")
520
- logger.info(f"Base video duration after initial concatenation: {concatenated_base.duration:.2f}s")
521
 
522
  if concatenated_base is None or concatenated_base.duration is None or concatenated_base.duration <= 0:
523
- logger.critical("Concatenated video base clip is invalid after first concatenation (None or zero duration).")
524
- raise ValueError("Failed to create valid video base from segments.")
525
 
526
  except Exception as e:
527
- logger.critical(f"Error during initial concatenation: {str(e)}", exc_info=True)
528
- raise ValueError("Failed during initial video concatenation.")
529
  finally:
530
  for clip_segment in clips_to_concatenate:
531
  try: clip_segment.close()
@@ -537,47 +583,47 @@ def crear_video(prompt_type, input_text, musica_file=None):
537
  final_video_base = video_base
538
 
539
  if final_video_base.duration < audio_duration:
540
- logger.info(f"Base video ({final_video_base.duration:.2f}s) is shorter than audio ({audio_duration:.2f}s). Repeating...")
541
-
542
  num_full_repeats = int(audio_duration // final_video_base.duration)
543
  remaining_duration = audio_duration % final_video_base.duration
544
-
545
  repeated_clips_list = [final_video_base] * num_full_repeats
546
-
547
  if remaining_duration > 0:
548
  try:
549
  remaining_clip = final_video_base.subclip(0, remaining_duration)
550
  if remaining_clip is None or remaining_clip.duration is None or remaining_clip.duration <= 0:
551
- logger.warning(f"Generated subclip for remaining duration {remaining_duration:.2f}s is invalid. Skipping.")
552
  try: remaining_clip.close()
553
  except: pass
554
  else:
555
  repeated_clips_list.append(remaining_clip)
556
- logger.debug(f"Adding remaining segment: {remaining_duration:.2f}s")
557
 
558
  except Exception as e:
559
- logger.warning(f"Error creating subclip for remaining duration {remaining_duration:.2f}s: {str(e)}")
560
 
561
  if repeated_clips_list:
562
- logger.info(f"Concatenating {len(repeated_clips_list)} parts for repetition.")
563
  video_base_repeated = None
564
  try:
565
  video_base_repeated = concatenate_videoclips(repeated_clips_list, method="chain")
566
- logger.info(f"Duration of repeated video base: {video_base_repeated.duration:.2f}s")
567
-
568
  if video_base_repeated is None or video_base_repeated.duration is None or video_base_repeated.duration <= 0:
569
- logger.critical("Concatenated repeated video base clip is invalid.")
570
- raise ValueError("Failed to create valid repeated video base.")
571
 
572
  if final_video_base is not video_base_repeated:
573
  try: final_video_base.close()
574
  except: pass
575
-
576
  final_video_base = video_base_repeated
577
 
578
  except Exception as e:
579
- logger.critical(f"Error during repetition concatenation: {str(e)}", exc_info=True)
580
- raise ValueError("Failed during video repetition.")
581
  finally:
582
  if 'repeated_clips_list' in locals():
583
  for clip in repeated_clips_list:
@@ -587,13 +633,13 @@ def crear_video(prompt_type, input_text, musica_file=None):
587
 
588
 
589
  if final_video_base.duration > audio_duration:
590
- logger.info(f"Trimming video base ({final_video_base.duration:.2f}s) to match audio duration ({audio_duration:.2f}s).")
591
  trimmed_video_base = None
592
  try:
593
  trimmed_video_base = final_video_base.subclip(0, audio_duration)
594
  if trimmed_video_base is None or trimmed_video_base.duration is None or trimmed_video_base.duration <= 0:
595
- logger.critical("Trimmed video base clip is invalid.")
596
- raise ValueError("Failed to create valid trimmed video base.")
597
 
598
  if final_video_base is not trimmed_video_base:
599
  try: final_video_base.close()
@@ -602,23 +648,23 @@ def crear_video(prompt_type, input_text, musica_file=None):
602
  final_video_base = trimmed_video_base
603
 
604
  except Exception as e:
605
- logger.critical(f"Error during trimming: {str(e)}", exc_info=True)
606
- raise ValueError("Failed during video trimming.")
607
 
608
 
609
  if final_video_base is None or final_video_base.duration is None or final_video_base.duration <= 0:
610
- logger.critical("Final video base clip is invalid before audio/writing (None or zero duration).")
611
- raise ValueError("Final video base clip is invalid.")
612
 
613
  if final_video_base.size is None or final_video_base.size[0] <= 0 or final_video_base.size[1] <= 0:
614
- logger.critical(f"Final video base has invalid size: {final_video_base.size}. Cannot write video.")
615
- raise ValueError("Final video base has invalid size before writing.")
616
 
617
  video_base = final_video_base
618
 
619
- # 6. Handle background music
620
- logger.info("Processing audio...")
621
-
622
  final_audio = audio_tts_original
623
 
624
  musica_audio_looped = None
@@ -629,56 +675,59 @@ def crear_video(prompt_type, input_text, musica_file=None):
629
  music_path = os.path.join(temp_dir_intermediate, "musica_bg.mp3")
630
  shutil.copyfile(musica_file, music_path)
631
  temp_intermediate_files.append(music_path)
632
- logger.info(f"Background music copied to: {music_path}")
633
-
634
  musica_audio_original = AudioFileClip(music_path)
635
-
636
  if musica_audio_original.reader is None or musica_audio_original.duration is None or musica_audio_original.duration <= 0:
637
- logger.warning("Background music clip seems invalid or has zero duration. Skipping music.")
638
  try: musica_audio_original.close()
639
  except: pass
640
  musica_audio_original = None
641
  else:
 
642
  musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
643
- logger.debug(f"Music adjusted to video duration: {musica_audio_looped.duration:.2f}s")
644
 
645
  if musica_audio_looped is None or musica_audio_looped.duration is None or musica_audio_looped.duration <= 0:
646
- logger.warning("Looped background music clip is invalid. Skipping music.")
647
  try: musica_audio_looped.close()
648
  except: pass
649
  musica_audio_looped = None
650
 
651
 
652
  if musica_audio_looped:
 
 
653
  composite_audio = CompositeAudioClip([
654
  musica_audio_looped.volumex(0.2),
655
  audio_tts_original.volumex(1.0)
656
  ])
657
-
658
  if composite_audio.duration is None or composite_audio.duration <= 0:
659
- logger.warning("Composite audio clip is invalid (None or zero duration). Using voice audio only.")
660
  try: composite_audio.close()
661
  except: pass
662
  final_audio = audio_tts_original
663
  else:
664
- logger.info("Audio mix completed (voice + music).")
665
  final_audio = composite_audio
666
  musica_audio = musica_audio_looped
667
 
668
  except Exception as e:
669
- logger.warning(f"Error processing background music: {str(e)}", exc_info=True)
670
  final_audio = audio_tts_original
671
  musica_audio = None
672
- logger.warning("Using voice audio only due to music processing error.")
673
 
674
 
675
  if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
676
- logger.warning(f"Final audio duration ({final_audio.duration:.2f}s) differs significantly from video base ({video_base.duration:.2f}s). Attempting trim.")
677
  try:
678
  if final_audio.duration > video_base.duration:
679
  trimmed_final_audio = final_audio.subclip(0, video_base.duration)
680
  if trimmed_final_audio.duration is None or trimmed_final_audio.duration <= 0:
681
- logger.warning("Trimmed final audio is invalid. Using original final_audio.")
682
  try: trimmed_final_audio.close()
683
  except: pass
684
  else:
@@ -686,21 +735,21 @@ def crear_video(prompt_type, input_text, musica_file=None):
686
  try: final_audio.close()
687
  except: pass
688
  final_audio = trimmed_final_audio
689
- logger.warning("Trimmed final audio to match video duration.")
690
  except Exception as e:
691
- logger.warning(f"Error adjusting final audio duration: {str(e)}")
692
-
693
 
 
 
694
  video_final = video_base.set_audio(final_audio)
695
 
696
  if video_final is None or video_final.duration is None or video_final.duration <= 0:
697
- logger.critical("Final video clip (with audio) is invalid before writing (None or zero duration).")
698
- raise ValueError("Final video clip is invalid before writing.")
699
-
700
 
701
  output_filename = "final_video.mp4"
702
  output_path = os.path.join(temp_dir_intermediate, output_filename)
703
- logger.info(f"Writing final video to: {output_path}")
704
 
705
  video_final.write_videofile(
706
  output_path,
@@ -711,253 +760,274 @@ def crear_video(prompt_type, input_text, musica_file=None):
711
  preset="medium",
712
  logger='bar'
713
  )
714
-
715
  total_time = (datetime.now() - start_time).total_seconds()
716
- logger.info(f"VIDEO PROCESS FINISHED | Output: {output_path} | Total time: {total_time:.2f}s")
717
-
718
  return output_path
719
-
720
  except ValueError as ve:
721
- logger.error(f"CONTROLLED ERROR in crear_video: {str(ve)}")
722
  raise ve
723
  except Exception as e:
724
- logger.critical(f"CRITICAL UNHANDLED ERROR in crear_video: {str(e)}", exc_info=True)
725
  raise e
726
  finally:
727
- logger.info("Starting cleanup of clips and intermediate temporary files...")
728
-
 
729
  for clip in source_clips:
730
  try:
731
  clip.close()
732
  except Exception as e:
733
- logger.warning(f"Error closing source video clip in finally: {str(e)}")
734
-
735
- for clip_segment in clips_to_concatenate:
736
- try:
737
- clip_segment.close()
738
- except Exception as e:
739
- logger.warning(f"Error closing video segment clip in finally: {str(e)}")
740
 
741
- # Close audio clips: looped music, original music, then final audio (which might close its components)
742
- if musica_audio is not None:
743
- try:
744
- musica_audio.close()
745
- except Exception as e:
746
- logger.warning(f"Error closing musica_audio (processed) in finally: {str(e)}")
747
-
748
- if musica_audio_original is not None and musica_audio_original is not musica_audio:
749
  try:
750
- musica_audio_original.close()
751
  except Exception as e:
752
- logger.warning(f"Error closing musica_audio_original in finally: {str(e)}")
753
 
754
- # Close TTS clips: potentially modified/trimmed TTS, then original TTS
755
- # Note: audio_tts variable is only assigned audio_tts_original in this code, but keep structure for safety
756
- if audio_tts is not None and audio_tts is not audio_tts_original:
757
- try:
758
- audio_tts.close()
759
- except Exception as e:
760
- logger.warning(f"Error closing audio_tts (processed) in finally: {str(e)}")
 
761
 
762
- if audio_tts_original is not None:
763
- try:
764
- audio_tts_original.close()
765
- except Exception as e:
766
- logger.warning(f"Error closing audio_tts_original in finally: {str(e)}")
 
 
 
 
 
 
 
767
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
768
 
769
- # Close video clips: final video (should cascade), then video base if it wasn't the final
770
- if video_final is not None:
771
- try:
772
- video_final.close()
773
- except Exception as e:
774
- logger.warning(f"Error closing video_final in finally: {str(e)}")
775
- # Note: video_base variable is reassigned to final_video_base. Close only if it exists and is different from video_final
776
- elif video_base is not None and video_base is not video_final: # Check if video_base holds a different clip
777
- try:
778
- video_base.close()
779
- except Exception as e:
780
- logger.warning(f"Error closing video_base in finally: {str(e)}")
781
 
 
 
782
 
783
- # Clean up intermediate files, but NOT the final video file
784
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
785
  final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4")
786
-
787
  for path in temp_intermediate_files:
788
  try:
 
789
  if os.path.isfile(path) and path != final_output_in_temp:
790
- logger.debug(f"Deleting temporary file: {path}")
791
  os.remove(path)
 
 
792
  except Exception as e:
793
- logger.warning(f"Could not delete temporary file {path}: {str(e)}")
794
-
795
- logger.info(f"Intermediate temporary directory {temp_dir_intermediate} will persist for Gradio to read the final video.")
796
 
 
 
797
 
798
- def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
 
799
  logger.info("="*80)
800
- logger.info("REQUEST RECEIVED IN INTERFACE")
801
-
802
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
803
-
804
  output_video = None
805
- output_file = None
806
  status_msg = gr.update(value="⏳ Procesando...", interactive=False)
807
 
808
  if not input_text or not input_text.strip():
809
- logger.warning("Empty input text.")
810
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
811
 
812
- logger.info(f"Input Type: {prompt_type}")
813
- logger.debug(f"Input Text: '{input_text[:100]}...'")
814
  if musica_file:
815
- logger.info(f"Music file received: {musica_file}")
816
  else:
817
- logger.info("No music file provided.")
818
 
819
  try:
820
- logger.info("Calling crear_video...")
821
  video_path = crear_video(prompt_type, input_text, musica_file)
822
-
823
  if video_path and os.path.exists(video_path):
824
- logger.info(f"crear_video returned path: {video_path}")
825
- logger.info(f"Size of returned video file: {os.path.getsize(video_path)} bytes")
826
- output_video = video_path
827
- output_file = video_path
828
  status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
829
  else:
830
- logger.error(f"crear_video did not return a valid path or file does not exist: {video_path}")
 
831
  status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
832
 
833
  except ValueError as ve:
834
- logger.warning(f"Validation error during video creation: {str(ve)}")
 
835
  status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
836
  except Exception as e:
837
- logger.critical(f"Critical error during video creation: {str(e)}", exc_info=True)
 
838
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
839
  finally:
840
- logger.info("End of run_app handler.")
 
841
  return output_video, output_file, status_msg
842
 
843
 
844
- # Gradio Interface
845
  with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
846
  .gradio-container {max-width: 800px; margin: auto;}
847
  h1 {text-align: center;}
848
  """) as app:
849
-
850
- gr.Markdown("# 🎬 Automatic AI Video Generator")
851
- gr.Markdown("Generate short videos from a topic or script, using stock footage from Pexels and generated voice.")
852
-
853
  with gr.Row():
854
  with gr.Column():
855
  prompt_type = gr.Radio(
856
- ["Generar Guion con IA", "Usar Mi Guion"],
857
- label="Input Method",
858
  value="Generar Guion con IA"
859
  )
860
-
861
  with gr.Column(visible=True) as ia_guion_column:
862
  prompt_ia = gr.Textbox(
863
- label="Topic for AI",
864
  lines=2,
865
- placeholder="Ex: A natural landscape with mountains and rivers at sunrise, showing the beauty of nature...",
866
  max_lines=4,
867
  value=""
868
  )
869
-
870
  with gr.Column(visible=False) as manual_guion_column:
871
  prompt_manual = gr.Textbox(
872
- label="Your Full Script",
873
  lines=5,
874
- 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!",
875
  max_lines=10,
876
  value=""
877
  )
878
-
879
  musica_input = gr.Audio(
880
- label="Background Music (optional)",
881
  type="filepath",
882
  interactive=True,
883
  value=None
884
  )
885
-
886
- generate_btn = gr.Button("✨ Generate Video", variant="primary")
887
-
888
  with gr.Column():
889
  video_output = gr.Video(
890
- label="Generated Video Preview",
891
  interactive=False,
892
  height=400
893
  )
 
894
  file_output = gr.File(
895
- label="Download Video",
896
  interactive=False,
897
- visible=False
898
  )
899
  status_output = gr.Textbox(
900
- label="Status",
901
  interactive=False,
902
  show_label=False,
903
- placeholder="Waiting for action...",
904
- value="Waiting for input..."
905
  )
906
 
907
  prompt_type.change(
908
- lambda x: (gr.update(visible=x == "Generar Guion con IA"),
909
  gr.update(visible=x == "Usar Mi Guion")),
910
  inputs=prompt_type,
911
  outputs=[ia_guion_column, manual_guion_column]
912
  )
913
 
 
914
  generate_btn.click(
915
- lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar 2-5 minutos.", interactive=False)),
 
916
  outputs=[video_output, file_output, status_output],
917
- queue=True,
918
  ).then(
 
919
  run_app,
920
  inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
921
- outputs=[video_output, file_output, status_output]
922
  ).then(
923
- lambda video_path, file_path: gr.update(visible=video_path is not None or file_path is not None), # Show download if either video or file path is returned
924
- inputs=[video_output, file_output],
925
- outputs=[file_output]
 
 
926
  )
927
 
928
 
929
- gr.Markdown("### Instructions:")
930
  gr.Markdown("""
931
- 1. **Pexels API Key:** Ensure you have set the `PEXELS_API_KEY` environment variable.
932
- 2. **Select Input Method**:
933
- - "Generate Script with AI": Describe a topic (e.g., "The beauty of mountains"). AI will generate a short script.
934
- - "Use My Script": Write the full script for your video.
935
- 3. **Upload Music** (optional): Select an audio file (MP3, WAV, etc.) for background music.
936
- 4. **Click "✨ Generate Video"**.
937
- 5. Wait for the video to process. Processing time may vary. Check the status box.
938
- 6. The generated video will appear above, and a download link will show if successful.
939
- 7. If there are errors, check the `video_generator_full.log` file for details.
940
  """)
941
  gr.Markdown("---")
942
- gr.Markdown("Developed by [Your Name/Company/Alias - Opcional]")
943
 
944
  if __name__ == "__main__":
945
- logger.info("Verifying critical dependencies...")
946
  try:
947
  from moviepy.editor import ColorClip
948
  try:
949
  temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
950
  temp_clip.close()
951
- logger.info("MoviePy base clips (like ColorClip) created and closed successfully. FFmpeg seems accessible.")
952
  except Exception as e:
953
- logger.critical(f"Failed to create basic MoviePy clip. Often indicates FFmpeg/ImageMagick issues. Error: {e}", exc_info=True)
954
 
955
  except Exception as e:
956
- logger.critical(f"Failed to import MoviePy. Ensure it is installed. Error: {e}", exc_info=True)
957
 
958
- logger.info("Starting Gradio app...")
959
  try:
 
 
960
  app.launch(server_name="0.0.0.0", server_port=7860, share=False)
961
  except Exception as e:
962
- logger.critical(f"Could not launch app: {str(e)}", exc_info=True)
963
  raise
 
9
  import torch
10
  from transformers import GPT2Tokenizer, GPT2LMHeadModel
11
  from keybert import KeyBERT
12
+ from moviepy.editor import VideoFileClip, concatenate_videoclip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
13
  import re
14
  import math
15
  import shutil
16
  import json
17
  from collections import Counter
18
 
19
+ # Configuración de logging
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("INICIO DE EJECUCIÓN - GENERADOR DE VIDEOS")
31
  logger.info("="*80)
32
 
33
+ # Clave API de Pexels
34
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
35
  if not PEXELS_API_KEY:
36
+ logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
37
+ # raise ValueError("API key de Pexels no configurada") # Descomentar para forzar fallo si no está
 
38
 
39
+ # Inicialización de modelos
40
  MODEL_NAME = "datificate/gpt2-small-spanish"
41
+ logger.info(f"Inicializando modelo GPT-2: {MODEL_NAME}")
42
  tokenizer = None
43
  model = None
44
  try:
 
46
  model = GPT2LMHeadModel.from_pretrained(MODEL_NAME).eval()
47
  if tokenizer.pad_token is None:
48
  tokenizer.pad_token = tokenizer.eos_token
49
+ logger.info(f"Modelo GPT-2 cargado | Vocabulario: {len(tokenizer)} tokens")
50
  except Exception as e:
51
+ logger.error(f"FALLA CRÍTICA al cargar GPT-2: {str(e)}", exc_info=True)
52
  tokenizer = model = None
53
 
54
+ logger.info("Cargando modelo KeyBERT...")
55
  kw_model = None
56
  try:
57
  kw_model = KeyBERT('distilbert-base-multilingual-cased')
58
+ logger.info("KeyBERT inicializado correctamente")
59
  except Exception as e:
60
+ logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
61
  kw_model = None
62
 
63
  def buscar_videos_pexels(query, api_key, per_page=5):
64
  if not api_key:
65
+ logger.warning("No se puede buscar en Pexels: API Key no configurada.")
66
  return []
67
 
68
+ logger.debug(f"Buscando en Pexels: '{query}' | Resultados: {per_page}")
69
  headers = {"Authorization": api_key}
70
  try:
71
  params = {
 
74
  "orientation": "landscape",
75
  "size": "medium"
76
  }
77
+
78
  response = requests.get(
79
  "https://api.pexels.com/videos/search",
80
  headers=headers,
 
82
  timeout=20
83
  )
84
  response.raise_for_status()
85
+
86
  data = response.json()
87
  videos = data.get('videos', [])
88
+ logger.info(f"Pexels: {len(videos)} videos encontrados para '{query}'")
89
  return videos
90
+
91
  except requests.exceptions.RequestException as e:
92
+ logger.error(f"Error de conexión Pexels para '{query}': {str(e)}")
93
  except json.JSONDecodeError:
94
+ logger.error(f"Pexels: JSON inválido recibido | Status: {response.status_code} | Respuesta: {response.text[:200]}...")
95
  except Exception as e:
96
+ logger.error(f"Error inesperado Pexels para '{query}': {str(e)}", exc_info=True)
97
+
98
  return []
99
 
100
  def generate_script(prompt, max_length=150):
101
+ logger.info(f"Generando guión | Prompt: '{prompt[:50]}...' | Longitud máxima: {max_length}")
102
  if not tokenizer or not model:
103
+ logger.warning("Modelos GPT-2 no disponibles - Usando prompt original como guion.")
104
+ return prompt.strip()
105
+
106
+ # Frase de instrucción que se le da a la IA
107
+ instruction_phrase_start = "Escribe un guion corto, interesante y coherente sobre:"
108
+ # Construir el prompt exacto que se le pasará a la IA
109
+ ai_prompt = f"{instruction_phrase_start} {prompt}"
110
+
111
  try:
112
+ # Generar texto usando el prompt completo
113
+ inputs = tokenizer(ai_prompt, return_tensors="pt", truncation=True, max_length=512)
114
+
115
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
116
  model.to(device)
117
  inputs = {k: v.to(device) for k, v in inputs.items()}
118
 
119
  outputs = model.generate(
120
  **inputs,
121
+ max_length=max_length + inputs[list(inputs.keys())[0]].size(1), # Longitud máxima incluyendo los tokens de entrada
122
  do_sample=True,
123
  top_p=0.9,
124
  top_k=40,
 
128
  eos_token_id=tokenizer.eos_token_id,
129
  no_repeat_ngram_size=3
130
  )
131
+
132
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
133
+
134
+ # --- Limpiar la frase de instrucción inicial de la salida de la IA ---
135
+ cleaned_text = text.strip()
136
+ # Intentar encontrar el inicio de la respuesta real después de la instrucción
137
+ # A veces la IA repite el prompt o la instrucción
138
+ try:
139
+ # Buscar el final de la frase de instrucción literal en la salida
140
+ instruction_end_idx = text.find(instruction_phrase)
141
+ if instruction_end_idx != -1:
142
+ # Tomar el texto que viene *después* de la instrucción exacta
143
+ cleaned_text = text[instruction_end_idx + len(instruction_phrase):].strip()
144
+ logger.debug("Instrucción inicial encontrada y eliminada del guión generado.")
145
+ else:
146
+ # Si no se encuentra la frase exacta, buscar solo el inicio de la instrucción base
147
+ instruction_start_idx = text.find(instruction_phrase_start)
148
+ if instruction_start_idx != -1:
149
+ # Tomar texto después de la frase base + prompt (heurística)
150
+ prompt_in_output_idx = text.find(prompt, instruction_start_idx)
151
+ if prompt_in_output_idx != -1:
152
+ cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
153
+ logger.debug("Instrucción base y prompt encontrados y eliminados del guión generado.")
154
+ else:
155
+ # Fallback: si la instrucción base está pero no el prompt después, tomar después de la instrucción base
156
+ cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
157
+ logger.debug("Instrucción base encontrada, eliminada del guión generado (sin prompt detectado).")
158
+
159
+ except Exception as e:
160
+ logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
161
+ cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Limpieza básica como fallback
162
+
163
+ # Asegurarse de que el texto resultante no sea solo la instrucción o vacío
164
+ if not cleaned_text or len(cleaned_text) < 10: # Umbral de longitud mínima
165
+ logger.warning("El guión generado parece muy corto o vacío después de la limpieza. Usando el texto generado original (sin limpieza heurística).")
166
+ cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Fallback al texto original limpio
167
+
168
+ # Limpieza final de caracteres especiales y espacios
169
+ cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
170
+ cleaned_text = cleaned_text.lstrip(':').strip() # Quitar posibles ':' al inicio
171
+ cleaned_text = cleaned_text.lstrip('.').strip() # Quitar posibles '.' al inicio
172
+
173
+
174
+ # Intentar obtener al menos una oración completa si es posible
175
+ sentences = cleaned_text.split('.')
176
  if sentences and sentences[0].strip():
177
  final_text = sentences[0].strip() + '.'
178
+ # Añadir la segunda oración si existe y es razonable
179
+ if len(sentences) > 1 and sentences[1].strip() and len(final_text.split()) < max_length * 0.7: # Usar un 70% de max_length como umbral
180
  final_text += " " + sentences[1].strip() + "."
181
+ final_text = final_text.replace("..", ".") # Limpiar doble punto
182
 
183
+ logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
184
  return final_text.strip()
185
+
186
+ logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
187
+ return cleaned_text.strip() # Si no se puede formar una oración, devolver el texto limpio tal cual
188
 
189
  except Exception as e:
190
+ logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
191
+ logger.warning("Usando prompt original como guion debido al error de generación.")
192
  return prompt.strip()
193
 
194
+ # CAMBIO: Voz por defecto a "es-ES-JuanNeural"
195
+ async def text_to_speech(text, output_path, voice="es-ES-JuanNeural"):
196
+ logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
197
  if not text or not text.strip():
198
+ logger.warning("Texto vacío para TTS")
199
  return False
200
+
201
  try:
202
  communicate = edge_tts.Communicate(text, voice)
203
  await communicate.save(output_path)
204
+
205
  if os.path.exists(output_path) and os.path.getsize(output_path) > 100:
206
+ logger.info(f"Audio guardado exitosamente en: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
207
  return True
208
  else:
209
+ logger.error(f"TTS guardó un archivo pequeño o vacío en: {output_path}")
210
  return False
211
+
212
  except Exception as e:
213
+ logger.error(f"Error en TTS: {str(e)}", exc_info=True)
214
  return False
215
 
216
  def download_video_file(url, temp_dir):
217
  if not url:
218
+ logger.warning("URL de video no proporcionada para descargar")
219
  return None
220
+
221
  try:
222
+ logger.info(f"Descargando video desde: {url[:80]}...")
223
  os.makedirs(temp_dir, exist_ok=True)
224
  file_name = f"video_dl_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.mp4"
225
  output_path = os.path.join(temp_dir, file_name)
226
+
227
+ with requests.get(url, stream=True, timeout=60) as r:
228
  r.raise_for_status()
229
  with open(output_path, 'wb') as f:
230
  for chunk in r.iter_content(chunk_size=8192):
231
  f.write(chunk)
232
 
233
  if os.path.exists(output_path) and os.path.getsize(output_path) > 1000:
234
+ logger.info(f"Video descargado exitosamente: {output_path} | Tamaño: {os.path.getsize(output_path)} bytes")
235
  return output_path
236
  else:
237
+ 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")
238
  if os.path.exists(output_path):
239
  os.remove(output_path)
240
  return None
241
 
242
  except requests.exceptions.RequestException as e:
243
+ logger.error(f"Error de descarga para {url[:80]}... : {str(e)}")
244
  except Exception as e:
245
+ logger.error(f"Error inesperado descargando {url[:80]}... : {str(e)}", exc_info=True)
246
+
247
  return None
248
 
249
  def loop_audio_to_length(audio_clip, target_duration):
250
+ logger.debug(f"Ajustando audio | Duración actual: {audio_clip.duration:.2f}s | Objetivo: {target_duration:.2f}s")
251
+
252
  if audio_clip is None or audio_clip.duration is None or audio_clip.duration <= 0:
253
  logger.warning("Input audio clip is invalid (None or zero duration), cannot loop.")
254
  try:
 
267
  except: pass
268
  return AudioFileClip(filename="")
269
  return trimmed_clip
270
+
271
  loops = math.ceil(target_duration / audio_clip.duration)
272
+ logger.debug(f"Creando {loops} loops de audio")
273
+
274
  audio_segments = [audio_clip] * loops
275
  looped_audio = None
276
  final_looped_audio = None
277
  try:
278
  looped_audio = concatenate_audioclips(audio_segments)
279
+
280
  if looped_audio.duration is None or looped_audio.duration <= 0:
281
  logger.error("Concatenated audio clip is invalid (None or zero duration).")
282
  raise ValueError("Invalid concatenated audio.")
 
307
 
308
 
309
  def extract_visual_keywords_from_script(script_text):
310
+ logger.info("Extrayendo palabras clave del guion")
311
  if not script_text or not script_text.strip():
312
+ logger.warning("Guion vacío, no se pueden extraer palabras clave.")
313
  return ["naturaleza", "ciudad", "paisaje"]
314
 
315
  clean_text = re.sub(r'[^\w\sáéíóúñÁÉÍÓÚÑ]', '', script_text)
 
317
 
318
  if kw_model:
319
  try:
320
+ logger.debug("Intentando extracción con KeyBERT...")
321
  keywords1 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(1, 1), stop_words='spanish', top_n=5)
322
  keywords2 = kw_model.extract_keywords(clean_text, keyphrase_ngram_range=(2, 2), stop_words='spanish', top_n=3)
323
+
324
  all_keywords = keywords1 + keywords2
325
  all_keywords.sort(key=lambda item: item[1], reverse=True)
326
+
327
  seen_keywords = set()
328
  for keyword, score in all_keywords:
329
  formatted_keyword = keyword.lower().replace(" ", "+")
 
334
  break
335
 
336
  if keywords_list:
337
+ logger.debug(f"Palabras clave extraídas por KeyBERT: {keywords_list}")
338
  return keywords_list
339
 
340
  except Exception as e:
341
+ logger.warning(f"KeyBERT falló: {str(e)}. Intentando método simple.")
342
+
343
+ logger.debug("Extrayendo palabras clave con método simple...")
344
  words = clean_text.lower().split()
345
+ 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",
346
+ "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á"}
347
 
348
  valid_words = [word for word in words if len(word) > 3 and word not in stop_words]
349
+
350
  if not valid_words:
351
+ logger.warning("No se encontraron palabras clave válidas con método simple. Usando palabras clave predeterminadas.")
352
  return ["naturaleza", "ciudad", "paisaje"]
353
+
354
  word_counts = Counter(valid_words)
355
  top_keywords = [word.replace(" ", "+") for word, _ in word_counts.most_common(5)]
356
+
357
  if not top_keywords:
358
+ logger.warning("El método simple no produjo keywords. Usando palabras clave predeterminadas.")
359
  return ["naturaleza", "ciudad", "paisaje"]
360
 
361
+ logger.info(f"Palabras clave finales: {top_keywords}")
362
  return top_keywords
363
 
364
  def crear_video(prompt_type, input_text, 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
+
369
  start_time = datetime.now()
370
  temp_dir_intermediate = None
371
+
372
  audio_tts_original = None
373
  musica_audio_original = None
374
+ audio_tts = None # Variable to hold potentially modified TTS clip (currently not used, but kept for structure)
375
+ musica_audio = None # Variable to hold potentially modified music clip (looped/trimmed)
376
  video_base = None
377
  video_final = None
378
  source_clips = []
379
  clips_to_concatenate = []
380
 
381
  try:
382
+ # 1. Generar o usar guion
383
  if prompt_type == "Generar Guion con IA":
384
  guion = generate_script(input_text)
385
  else:
386
  guion = input_text.strip()
387
 
388
+ logger.info(f"Guion final ({len(guion)} chars): '{guion[:100]}...'")
389
+
390
  if not guion.strip():
391
+ logger.error("El guion resultante está vacío o solo contiene espacios.")
392
+ raise ValueError("El guion está vacío.")
393
+
394
  temp_dir_intermediate = tempfile.mkdtemp(prefix="video_gen_intermediate_")
395
+ logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
396
  temp_intermediate_files = []
397
 
398
+ # 2. Generar audio de voz
399
+ logger.info("Generando audio de voz...")
400
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
401
+ # CAMBIO: Usar voz de Juan por defecto en la llamada
402
+ if not asyncio.run(text_to_speech(guion, voz_path, voice="es-ES-JuanNeural")):
403
+ logger.error("Fallo en generación de voz")
404
+ raise ValueError("Error generando voz a partir del guion.")
405
  temp_intermediate_files.append(voz_path)
406
 
407
  audio_tts_original = AudioFileClip(voz_path)
408
+
409
  if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
410
  logger.critical("Initial TTS audio clip is invalid (reader is None or duration <= 0).")
411
  try: audio_tts_original.close()
 
414
  raise ValueError("Generated voice audio is invalid.")
415
 
416
  audio_tts = audio_tts_original
417
+ audio_duration = audio_tts_original.duration
418
+ logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
419
 
420
  if audio_duration < 1.0:
421
+ logger.error(f"Duración audio voz ({audio_duration:.2f}s) es muy corta.")
422
  raise ValueError("Generated voice audio is too short (min 1 second required).")
423
 
424
+ # 3. Extraer palabras clave
425
+ logger.info("Extrayendo palabras clave...")
426
  try:
427
  keywords = extract_visual_keywords_from_script(guion)
428
+ logger.info(f"Palabras clave identificadas: {keywords}")
429
  except Exception as e:
430
+ logger.error(f"Error extrayendo keywords: {str(e)}", exc_info=True)
431
  keywords = ["naturaleza", "paisaje"]
432
 
433
  if not keywords:
434
  keywords = ["video", "background"]
435
 
436
+ # 4. Buscar y descargar videos
437
+ logger.info("Buscando videos en Pexels...")
438
  videos_data = []
439
  total_desired_videos = 10
440
  per_page_per_keyword = max(1, total_desired_videos // len(keywords))
 
445
  videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=per_page_per_keyword)
446
  if videos:
447
  videos_data.extend(videos)
448
+ logger.info(f"Encontrados {len(videos)} videos para '{keyword}'. Total data: {len(videos_data)}")
449
  except Exception as e:
450
+ logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
451
 
452
  if len(videos_data) < total_desired_videos / 2:
453
+ logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
454
  generic_keywords = ["nature", "city", "background", "abstract"]
455
  for keyword in generic_keywords:
456
  if len(videos_data) >= total_desired_videos: break
 
458
  videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=2)
459
  if videos:
460
  videos_data.extend(videos)
461
+ logger.info(f"Encontrados {len(videos)} videos para '{keyword}' (genérico). Total data: {len(videos_data)}")
462
  except Exception as e:
463
+ logger.warning(f"Error buscando videos genéricos para '{keyword}': {str(e)}")
464
 
465
  if not videos_data:
466
+ logger.error("No se encontraron videos en Pexels para ninguna palabra clave.")
467
+ raise ValueError("No se encontraron videos adecuados en Pexels.")
468
+
469
  video_paths = []
470
+ logger.info(f"Intentando descargar {len(videos_data)} videos encontrados...")
471
  for video in videos_data:
472
  if 'video_files' not in video or not video['video_files']:
473
+ logger.debug(f"Saltando video sin archivos de video: {video.get('id')}")
474
  continue
475
+
476
  try:
477
  best_quality = None
478
  for vf in sorted(video['video_files'], key=lambda x: x.get('width', 0) * x.get('height', 0), reverse=True):
 
485
  if path:
486
  video_paths.append(path)
487
  temp_intermediate_files.append(path)
488
+ logger.info(f"Video descargado OK desde {best_quality['link'][:50]}...")
489
  else:
490
+ logger.warning(f"No se pudo descargar video desde {best_quality['link'][:50]}...")
491
  else:
492
+ logger.warning(f"No se encontró enlace de descarga válido para video {video.get('id')}.")
493
 
494
  except Exception as e:
495
+ logger.warning(f"Error procesando/descargando video {video.get('id')}: {str(e)}")
496
 
497
+ logger.info(f"Descargados {len(video_paths)} archivos de video utilizables.")
498
  if not video_paths:
499
+ logger.error("No se pudo descargar ningún archivo de video utilizable.")
500
+ raise ValueError("No se pudo descargar ningún video utilizable de Pexels.")
501
+
502
+ # 5. Procesar y concatenar clips de video
503
+ logger.info("Procesando y concatenando videos descargados...")
504
  current_duration = 0
505
  min_clip_duration = 0.5
506
  max_clip_segment = 10.0
507
 
508
  for i, path in enumerate(video_paths):
509
  if current_duration >= audio_duration + max_clip_segment:
510
+ 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.")
511
  break
512
 
513
  clip = None
514
  try:
515
+ logger.debug(f"[{i+1}/{len(video_paths)}] Abriendo clip: {path}")
516
  clip = VideoFileClip(path)
517
  source_clips.append(clip)
518
 
519
  if clip.reader is None or clip.duration is None or clip.duration <= 0:
520
+ logger.warning(f"[{i+1}/{len(video_paths)}] Clip fuente {path} parece inválido (reader is None o duration <= 0). Saltando.")
521
  continue
522
 
523
  remaining_needed = audio_duration - current_duration
 
532
  try:
533
  sub = clip.subclip(0, segment_duration)
534
  if sub.reader is None or sub.duration is None or sub.duration <= 0:
535
+ logger.warning(f"[{i+1}/{len(video_paths)}] Subclip generado de {path} es inválido. Saltando.")
536
  try: sub.close()
537
  except: pass
538
  continue
539
+
540
  clips_to_concatenate.append(sub)
541
+ current_duration += sub.duration
542
+ logger.debug(f"[{i+1}/{len(video_paths)}] Segmento añadido: {sub.duration:.1f}s (total video: {current_duration:.1f}/{audio_duration:.1f}s)")
543
+
544
  except Exception as sub_e:
545
+ logger.warning(f"[{i+1}/{len(video_paths)}] Error creando subclip de {path} ({segment_duration:.1f}s): {str(sub_e)}")
546
  continue
547
  else:
548
+ logger.debug(f"[{i+1}/{len(video_paths)}] Clip {path} ({clip.duration:.1f}s) no contribuye un segmento suficiente ({segment_duration:.1f}s necesario). Saltando.")
549
  else:
550
+ logger.debug(f"[{i+1}/{len(video_paths)}] Duración de video base ya alcanzada. Saltando clip.")
551
 
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
 
556
+ logger.info(f"Procesamiento de clips fuente finalizado. Se obtuvieron {len(clips_to_concatenate)} segmentos válidos.")
557
 
558
  if not clips_to_concatenate:
559
+ logger.error("No hay segmentos de video válidos disponibles para crear la secuencia.")
560
+ raise ValueError("No hay segmentos de video válidos disponibles para crear el video.")
561
+
562
+ logger.info(f"Concatenando {len(clips_to_concatenate)} segmentos de video.")
563
  concatenated_base = None
564
  try:
565
  concatenated_base = concatenate_videoclips(clips_to_concatenate, method="chain")
566
+ logger.info(f"Duración video base después de concatenación inicial: {concatenated_base.duration:.2f}s")
567
 
568
  if concatenated_base is None or concatenated_base.duration is None or concatenated_base.duration <= 0:
569
+ logger.critical("Video base concatenado es inválido después de la primera concatenación (None o duración cero).")
570
+ raise ValueError("Fallo al crear video base válido a partir de segmentos.")
571
 
572
  except Exception as e:
573
+ logger.critical(f"Error durante la concatenación inicial: {str(e)}", exc_info=True)
574
+ raise ValueError("Fallo durante la concatenación de video inicial.")
575
  finally:
576
  for clip_segment in clips_to_concatenate:
577
  try: clip_segment.close()
 
583
  final_video_base = video_base
584
 
585
  if final_video_base.duration < audio_duration:
586
+ logger.info(f"Video base ({final_video_base.duration:.2f}s) es más corto que el audio ({audio_duration:.2f}s). Repitiendo...")
587
+
588
  num_full_repeats = int(audio_duration // final_video_base.duration)
589
  remaining_duration = audio_duration % final_video_base.duration
590
+
591
  repeated_clips_list = [final_video_base] * num_full_repeats
592
+
593
  if remaining_duration > 0:
594
  try:
595
  remaining_clip = final_video_base.subclip(0, remaining_duration)
596
  if remaining_clip is None or remaining_clip.duration is None or remaining_clip.duration <= 0:
597
+ logger.warning(f"Subclip generado para duración restante {remaining_duration:.2f}s es inválido. Saltando.")
598
  try: remaining_clip.close()
599
  except: pass
600
  else:
601
  repeated_clips_list.append(remaining_clip)
602
+ logger.debug(f"Añadiendo segmento restante: {remaining_duration:.2f}s")
603
 
604
  except Exception as e:
605
+ logger.warning(f"Error creando subclip para duración restante {remaining_duration:.2f}s: {str(e)}")
606
 
607
  if repeated_clips_list:
608
+ logger.info(f"Concatenando {len(repeated_clips_list)} partes para repetición.")
609
  video_base_repeated = None
610
  try:
611
  video_base_repeated = concatenate_videoclips(repeated_clips_list, method="chain")
612
+ logger.info(f"Duración del video base repetido: {video_base_repeated.duration:.2f}s")
613
+
614
  if video_base_repeated is None or video_base_repeated.duration is None or video_base_repeated.duration <= 0:
615
+ logger.critical("Video base repetido concatenado es inválido.")
616
+ raise ValueError("Fallo al crear video base repetido válido.")
617
 
618
  if final_video_base is not video_base_repeated:
619
  try: final_video_base.close()
620
  except: pass
621
+
622
  final_video_base = video_base_repeated
623
 
624
  except Exception as e:
625
+ logger.critical(f"Error durante la concatenación de repetición: {str(e)}", exc_info=True)
626
+ raise ValueError("Fallo durante la repetición de video.")
627
  finally:
628
  if 'repeated_clips_list' in locals():
629
  for clip in repeated_clips_list:
 
633
 
634
 
635
  if final_video_base.duration > audio_duration:
636
+ 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).")
637
  trimmed_video_base = None
638
  try:
639
  trimmed_video_base = final_video_base.subclip(0, audio_duration)
640
  if trimmed_video_base is None or trimmed_video_base.duration is None or trimmed_video_base.duration <= 0:
641
+ logger.critical("Video base recortado es inválido.")
642
+ raise ValueError("Fallo al crear video base recortado válido.")
643
 
644
  if final_video_base is not trimmed_video_base:
645
  try: final_video_base.close()
 
648
  final_video_base = trimmed_video_base
649
 
650
  except Exception as e:
651
+ logger.critical(f"Error durante el recorte: {str(e)}", exc_info=True)
652
+ raise ValueError("Fallo durante el recorte de video.")
653
 
654
 
655
  if final_video_base is None or final_video_base.duration is None or final_video_base.duration <= 0:
656
+ logger.critical("Video base final es inválido antes de audio/escritura (None o duración cero).")
657
+ raise ValueError("Video base final es inválido.")
658
 
659
  if final_video_base.size is None or final_video_base.size[0] <= 0 or final_video_base.size[1] <= 0:
660
+ logger.critical(f"Video base final tiene tamaño inválido: {final_video_base.size}. No se puede escribir video.")
661
+ raise ValueError("Video base final tiene tamaño inválido antes de escribir.")
662
 
663
  video_base = final_video_base
664
 
665
+ # 6. Manejar música de fondo
666
+ logger.info("Procesando audio...")
667
+
668
  final_audio = audio_tts_original
669
 
670
  musica_audio_looped = None
 
675
  music_path = os.path.join(temp_dir_intermediate, "musica_bg.mp3")
676
  shutil.copyfile(musica_file, music_path)
677
  temp_intermediate_files.append(music_path)
678
+ logger.info(f"Música de fondo copiada a: {music_path}")
679
+
680
  musica_audio_original = AudioFileClip(music_path)
681
+
682
  if musica_audio_original.reader is None or musica_audio_original.duration is None or musica_audio_original.duration <= 0:
683
+ logger.warning("Clip de música de fondo parece inválido o tiene duración cero. Saltando música.")
684
  try: musica_audio_original.close()
685
  except: pass
686
  musica_audio_original = None
687
  else:
688
+ # CAMBIO: Asegurar que loop_audio_to_length recibe la duración correcta del video base
689
  musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
690
+ logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
691
 
692
  if musica_audio_looped is None or musica_audio_looped.duration is None or musica_audio_looped.duration <= 0:
693
+ logger.warning("Clip de música de fondo loopeado es inválido. Saltando música.")
694
  try: musica_audio_looped.close()
695
  except: pass
696
  musica_audio_looped = None
697
 
698
 
699
  if musica_audio_looped:
700
+ # CAMBIO: Usar volumex(0.2) para la música (ya estaba en 0.2, confirmamos)
701
+ # CAMBIO: Usar volumex(1.0) para la voz (ya estaba en 1.0, confirmamos)
702
  composite_audio = CompositeAudioClip([
703
  musica_audio_looped.volumex(0.2),
704
  audio_tts_original.volumex(1.0)
705
  ])
706
+
707
  if composite_audio.duration is None or composite_audio.duration <= 0:
708
+ logger.warning("Clip de audio compuesto es inválido (None o duración cero). Usando solo audio de voz.")
709
  try: composite_audio.close()
710
  except: pass
711
  final_audio = audio_tts_original
712
  else:
713
+ logger.info("Mezcla de audio completada (voz + música).")
714
  final_audio = composite_audio
715
  musica_audio = musica_audio_looped
716
 
717
  except Exception as e:
718
+ logger.warning(f"Error procesando música de fondo: {str(e)}", exc_info=True)
719
  final_audio = audio_tts_original
720
  musica_audio = None
721
+ logger.warning("Usando solo audio de voz debido a un error con la música.")
722
 
723
 
724
  if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
725
+ 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.")
726
  try:
727
  if final_audio.duration > video_base.duration:
728
  trimmed_final_audio = final_audio.subclip(0, video_base.duration)
729
  if trimmed_final_audio.duration is None or trimmed_final_audio.duration <= 0:
730
+ logger.warning("Audio final recortado es inválido. Usando audio final original.")
731
  try: trimmed_final_audio.close()
732
  except: pass
733
  else:
 
735
  try: final_audio.close()
736
  except: pass
737
  final_audio = trimmed_final_audio
738
+ logger.warning("Audio final recortado para que coincida con la duración del video.")
739
  except Exception as e:
740
+ logger.warning(f"Error ajustando duración del audio final: {str(e)}")
 
741
 
742
+ # 7. Crear video final
743
+ logger.info("Renderizando video final...")
744
  video_final = video_base.set_audio(final_audio)
745
 
746
  if video_final is None or video_final.duration is None or video_final.duration <= 0:
747
+ logger.critical("Clip de video final (con audio) es inválido antes de escribir (None o duración cero).")
748
+ raise ValueError("Clip de video final es inválido antes de escribir.")
 
749
 
750
  output_filename = "final_video.mp4"
751
  output_path = os.path.join(temp_dir_intermediate, output_filename)
752
+ logger.info(f"Escribiendo video final a: {output_path}")
753
 
754
  video_final.write_videofile(
755
  output_path,
 
760
  preset="medium",
761
  logger='bar'
762
  )
763
+
764
  total_time = (datetime.now() - start_time).total_seconds()
765
+ logger.info(f"PROCESO DE VIDEO FINALIZADO | Output: {output_path} | Tiempo total: {total_time:.2f}s")
766
+
767
  return output_path
768
+
769
  except ValueError as ve:
770
+ logger.error(f"ERROR CONTROLADO en crear_video: {str(ve)}")
771
  raise ve
772
  except Exception as e:
773
+ logger.critical(f"ERROR CRÍTICO NO CONTROLADO en crear_video: {str(e)}", exc_info=True)
774
  raise e
775
  finally:
776
+ logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
777
+
778
+ # Cerrar todos los clips de video fuente iniciales abiertos
779
  for clip in source_clips:
780
  try:
781
  clip.close()
782
  except Exception as e:
783
+ logger.warning(f"Error cerrando clip de video fuente en finally: {str(e)}")
 
 
 
 
 
 
784
 
785
+ # Cerrar cualquier segmento de video que quede en la lista (debería estar vacía si tuvo éxito)
786
+ for clip_segment in clips_to_concatenate:
 
 
 
 
 
 
787
  try:
788
+ clip_segment.close()
789
  except Exception as e:
790
+ logger.warning(f"Error cerrando segmento de video en finally: {str(e)}")
791
 
792
+ # Cerrar los objetos principales de MoviePy si se crearon correctamente
793
+ try:
794
+ # Cerrar clips de audio en orden: música loopeada, música original (si es diferente), TTS original
795
+ if musica_audio is not None: # musica_audio holds the potentially looped clip
796
+ try:
797
+ musica_audio.close()
798
+ except Exception as e:
799
+ logger.warning(f"Error cerrando musica_audio (procesada) en finally: {str(e)}")
800
 
801
+ if musica_audio_original is not None and musica_audio_original is not musica_audio:
802
+ try:
803
+ musica_audio_original.close()
804
+ except Exception as e:
805
+ logger.warning(f"Error cerrando musica_audio_original en finally: {str(e)}")
806
+
807
+ # audio_tts currently only holds audio_tts_original, but keep structure
808
+ if audio_tts is not None and audio_tts is not audio_tts_original:
809
+ try:
810
+ audio_tts.close()
811
+ except Exception as e:
812
+ logger.warning(f"Error cerrando audio_tts (procesada) en finally: {str(e)}")
813
 
814
+ if audio_tts_original is not None:
815
+ try:
816
+ audio_tts_original.close()
817
+ except Exception as e:
818
+ logger.warning(f"Error cerrando audio_tts_original en finally: {str(e)}")
819
+
820
+
821
+ # Cerrar clips de video en orden: video_final (debería cerrar sus componentes), luego video_base (si es diferente de video_final)
822
+ if video_final is not None:
823
+ try:
824
+ video_final.close()
825
+ except Exception as e:
826
+ logger.warning(f"Error cerrando video_final en finally: {str(e)}")
827
+ elif video_base is not None and video_base is not video_final: # Asegurarse de que video_base no es el mismo objeto que video_final
828
+ try:
829
+ video_base.close()
830
+ except Exception as e:
831
+ logger.warning(f"Error cerrando video_base en finally: {str(e)}")
832
 
 
 
 
 
 
 
 
 
 
 
 
 
833
 
834
+ except Exception as e:
835
+ logger.warning(f"Error durante el cierre de clips finales en finally: {str(e)}")
836
 
837
+ # Limpiar archivos intermedios, pero NO el archivo de video final
838
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
839
  final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4")
840
+
841
  for path in temp_intermediate_files:
842
  try:
843
+ # Verificar explícitamente que la ruta no sea la ruta de salida del video final antes de eliminar
844
  if os.path.isfile(path) and path != final_output_in_temp:
845
+ logger.debug(f"Eliminando archivo temporal intermedio: {path}")
846
  os.remove(path)
847
+ elif os.path.isfile(path) and path == final_output_in_temp:
848
+ logger.debug(f"Saltando eliminación del archivo de video final: {path}")
849
  except Exception as e:
850
+ logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
 
 
851
 
852
+ # El directorio temporal *persistirá* porque contiene el archivo final
853
+ logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
854
 
855
+
856
+ def run_app(prompt_type, input_text, musica_file):
857
  logger.info("="*80)
858
+ logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
859
+
860
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
861
+
862
  output_video = None
863
+ output_file = None # Inicializar a None
864
  status_msg = gr.update(value="⏳ Procesando...", interactive=False)
865
 
866
  if not input_text or not input_text.strip():
867
+ logger.warning("Texto de entrada vacío.")
868
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
869
 
870
+ logger.info(f"Tipo de entrada: {prompt_type}")
871
+ logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
872
  if musica_file:
873
+ logger.info(f"Archivo de música recibido: {musica_file}")
874
  else:
875
+ logger.info("No se proporcionó archivo de música.")
876
 
877
  try:
878
+ logger.info("Llamando a crear_video...")
879
  video_path = crear_video(prompt_type, input_text, musica_file)
880
+
881
  if video_path and os.path.exists(video_path):
882
+ logger.info(f"crear_video retornó path: {video_path}")
883
+ logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
884
+ output_video = video_path # Establecer valor del componente de video
885
+ output_file = video_path # Establecer valor del componente de archivo para descarga
886
  status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
887
  else:
888
+ logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
889
+ # Dejar las salidas de video y archivo como None
890
  status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
891
 
892
  except ValueError as ve:
893
+ logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
894
+ # Dejar las salidas de video y archivo como None
895
  status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
896
  except Exception as e:
897
+ logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
898
+ # Dejar las salidas de video y archivo como None
899
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
900
  finally:
901
+ logger.info("Fin del handler run_app.")
902
+ # Retornar las tres salidas
903
  return output_video, output_file, status_msg
904
 
905
 
906
+ # Interfaz de Gradio
907
  with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
908
  .gradio-container {max-width: 800px; margin: auto;}
909
  h1 {text-align: center;}
910
  """) as app:
911
+
912
+ gr.Markdown("# 🎬 Generador Automático de Videos con IA")
913
+ gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
914
+
915
  with gr.Row():
916
  with gr.Column():
917
  prompt_type = gr.Radio(
918
+ ["Generar Guion con IA", "Usar Mi Guion"],
919
+ label="Método de Entrada",
920
  value="Generar Guion con IA"
921
  )
922
+
923
  with gr.Column(visible=True) as ia_guion_column:
924
  prompt_ia = gr.Textbox(
925
+ label="Tema para IA",
926
  lines=2,
927
+ placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer, mostrando la belleza de la naturaleza...",
928
  max_lines=4,
929
  value=""
930
  )
931
+
932
  with gr.Column(visible=False) as manual_guion_column:
933
  prompt_manual = gr.Textbox(
934
+ label="Tu Guion Completo",
935
  lines=5,
936
+ 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!",
937
  max_lines=10,
938
  value=""
939
  )
940
+
941
  musica_input = gr.Audio(
942
+ label="Música de fondo (opcional)",
943
  type="filepath",
944
  interactive=True,
945
  value=None
946
  )
947
+
948
+ generate_btn = gr.Button("✨ Generar Video", variant="primary")
949
+
950
  with gr.Column():
951
  video_output = gr.Video(
952
+ label="Previsualización del Video Generado", # Etiqueta cambiada
953
  interactive=False,
954
  height=400
955
  )
956
+ # Añadir el componente File para la descarga
957
  file_output = gr.File(
958
+ label="Descargar Archivo de Video", # Etiqueta cambiada
959
  interactive=False,
960
+ visible=False # Ocultar inicialmente
961
  )
962
  status_output = gr.Textbox(
963
+ label="Estado",
964
  interactive=False,
965
  show_label=False,
966
+ placeholder="Esperando acción...",
967
+ value="Esperando entrada..."
968
  )
969
 
970
  prompt_type.change(
971
+ lambda x: (gr.update(visible=x == "Generar Guion con IA"),
972
  gr.update(visible=x == "Usar Mi Guion")),
973
  inputs=prompt_type,
974
  outputs=[ia_guion_column, manual_guion_column]
975
  )
976
 
977
+ # Modificar el evento click para retornar 3 salidas
978
  generate_btn.click(
979
+ # Acción 1: Resetear salidas y establecer estado a procesando
980
+ lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar 2-5 minutos o más para videos largos.", interactive=False)), # Mensaje de estado mejorado
981
  outputs=[video_output, file_output, status_output],
982
+ queue=True, # Mantener la cola habilitada
983
  ).then(
984
+ # Acción 2: Llamar a la función principal de procesamiento
985
  run_app,
986
  inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
987
+ outputs=[video_output, file_output, status_output] # Coincidir las 3 salidas
988
  ).then(
989
+ # Acción 3: Hacer visible el enlace de descarga si se retornó un archivo
990
+ # Verificar si file_output tiene un valor
991
+ lambda video_path, file_path: gr.update(visible=file_path is not None), # Usar file_path para visibilidad
992
+ inputs=[video_output, file_output], # Ambas salidas como entrada
993
+ outputs=[file_output] # Actualizar visibilidad de file_output
994
  )
995
 
996
 
997
+ gr.Markdown("### Instrucciones:")
998
  gr.Markdown("""
999
+ 1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
1000
+ 2. **Selecciona el tipo de entrada**:
1001
+ - "Generar Guion con IA": Describe brevemente un tema (ej. "La belleza de las montañas"). La IA generará un guion corto.
1002
+ - "Usar Mi Guion": Escribe el guion completo que quieres para el video.
1003
+ 3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.) para usar como música de fondo.
1004
+ 4. **Haz clic en "✨ Generar Video"**.
1005
+ 5. Espera a que se procese el video. El tiempo de espera puede variar. Verás el estado en el cuadro de texto.
1006
+ 6. La previsualización del video aparecerá arriba (puede fallar para archivos grandes), y un enlace **Descargar Archivo de Video** se mostrará si la generación fue exitosa.
1007
+ 7. Si hay errores, revisa el log `video_generator_full.log` para más detalles.
1008
  """)
1009
  gr.Markdown("---")
1010
+ gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
1011
 
1012
  if __name__ == "__main__":
1013
+ logger.info("Verificando dependencias críticas...")
1014
  try:
1015
  from moviepy.editor import ColorClip
1016
  try:
1017
  temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
1018
  temp_clip.close()
1019
+ logger.info("Clips base de MoviePy (como ColorClip) creados y cerrados exitosamente. FFmpeg parece accesible.")
1020
  except Exception as e:
1021
+ logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
1022
 
1023
  except Exception as e:
1024
+ logger.critical(f"Fallo al importar MoviePy. Asegúrate de que está instalado. Error: {e}", exc_info=True)
1025
 
1026
+ logger.info("Iniciando aplicación Gradio...")
1027
  try:
1028
+ # Gradio Queue maneja tareas largas, no es necesario un ajuste global de timeout aquí.
1029
+ # El timeout se gestiona por solicitud o por el límite del worker de la cola.
1030
  app.launch(server_name="0.0.0.0", server_port=7860, share=False)
1031
  except Exception as e:
1032
+ logger.critical(f"No se pudo iniciar la app: {str(e)}", exc_info=True)
1033
  raise