gnosticdev commited on
Commit
52c5b8e
·
verified ·
1 Parent(s): db50614

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +101 -153
app.py CHANGED
@@ -34,7 +34,6 @@ logger.info("="*80)
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
@@ -55,7 +54,7 @@ except Exception as e:
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)
@@ -205,22 +204,18 @@ def download_video_file(url, temp_dir):
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
- # Handle cases where the input audio clip is invalid
209
  if audio_clip is None or audio_clip.duration is None or audio_clip.duration <= 0:
210
  logger.warning("Input audio clip is invalid (None or zero duration), cannot loop.")
211
- # Return a silent clip of target duration as fallback
212
  try:
213
- # Ensure sampling rate is set, default is 44100
214
- sr = getattr(audio_clip, 'fps', 44100) if audio_clip else 44100 # Use fps for audio clips
215
  return AudioClip(lambda t: 0, duration=target_duration, sr=sr)
216
  except Exception as e:
217
  logger.error(f"Could not create silence clip: {e}", exc_info=True)
218
- return AudioFileClip(filename="") # Return empty clip on failure
219
 
220
  if audio_clip.duration >= target_duration:
221
  logger.debug("Audio clip already longer or equal to target. Trimming.")
222
  trimmed_clip = audio_clip.subclip(0, target_duration)
223
- # Check trimmed clip validity (should be ok, but good practice)
224
  if trimmed_clip.duration is None or trimmed_clip.duration <= 0:
225
  logger.error("Trimmed audio clip is invalid.")
226
  try: trimmed_clip.close()
@@ -232,19 +227,17 @@ def loop_audio_to_length(audio_clip, target_duration):
232
  logger.debug(f"Creating {loops} audio loops")
233
 
234
  audio_segments = [audio_clip] * loops
235
- looped_audio = None # Initialize for finally block
236
- final_looped_audio = None # Initialize for return value
237
  try:
238
  looped_audio = concatenate_audioclips(audio_segments)
239
 
240
- # Verify the concatenated audio clip is valid
241
  if looped_audio.duration is None or looped_audio.duration <= 0:
242
  logger.error("Concatenated audio clip is invalid (None or zero duration).")
243
  raise ValueError("Invalid concatenated audio.")
244
 
245
  final_looped_audio = looped_audio.subclip(0, target_duration)
246
 
247
- # Verify the final subclipped audio clip is valid
248
  if final_looped_audio.duration is None or final_looped_audio.duration <= 0:
249
  logger.error("Final subclipped audio clip is invalid (None or zero duration).")
250
  raise ValueError("Invalid final subclipped audio.")
@@ -253,18 +246,16 @@ def loop_audio_to_length(audio_clip, target_duration):
253
 
254
  except Exception as e:
255
  logger.error(f"Error concatenating/subclipping audio clips for looping: {str(e)}", exc_info=True)
256
- # Fallback: try returning the original clip trimmed if possible
257
  try:
258
  if audio_clip.duration is not None and audio_clip.duration > 0:
259
  logger.warning("Returning original audio clip (may be too short).")
260
  return audio_clip.subclip(0, min(audio_clip.duration, target_duration))
261
  except:
262
- pass # Ignore errors during fallback
263
  logger.error("Fallback to original audio clip failed.")
264
- return AudioFileClip(filename="") # Return empty clip if fallback fails
265
 
266
  finally:
267
- # Clean up the temporary concatenated clip if it was created but not returned
268
  if looped_audio is not None and looped_audio is not final_looped_audio:
269
  try: looped_audio.close()
270
  except: pass
@@ -332,15 +323,14 @@ def crear_video(prompt_type, input_text, musica_file=None):
332
  start_time = datetime.now()
333
  temp_dir_intermediate = None
334
 
335
- # Initialize clips and audio objects to None for cleanup in finally
336
- audio_tts_original = None # Keep original TTS clip for cleanup
337
- musica_audio_original = None # Keep original music clip for cleanup
338
- audio_tts = None # This will be the potentially modified/validated TTS clip
339
- musica_audio = None # This will be the potentially modified/validated music clip
340
- video_base = None # This will hold the final video base clip before adding audio
341
- video_final = None # This will hold the final clip with video and audio
342
- source_clips = [] # Clips loaded from downloaded files for proper closing
343
- clips_to_concatenate = [] # Segments extracted from source_clips
344
 
345
  try:
346
  # 1. Generate or use script
@@ -369,16 +359,14 @@ def crear_video(prompt_type, input_text, musica_file=None):
369
 
370
  audio_tts_original = AudioFileClip(voz_path)
371
 
372
- # Verify initial TTS audio clip
373
  if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
374
  logger.critical("Initial TTS audio clip is invalid (reader is None or duration <= 0).")
375
- # Try to close the invalid clip before raising
376
  try: audio_tts_original.close()
377
  except: pass
378
- audio_tts_original = None # Ensure it's None
379
  raise ValueError("Generated voice audio is invalid.")
380
 
381
- audio_tts = audio_tts_original # Use the original valid TTS clip
382
  audio_duration = audio_tts.duration
383
  logger.info(f"Voice audio duration: {audio_duration:.2f} seconds")
384
 
@@ -386,7 +374,6 @@ def crear_video(prompt_type, input_text, musica_file=None):
386
  logger.error(f"Voice audio duration ({audio_duration:.2f}s) is too short.")
387
  raise ValueError("Generated voice audio is too short (min 1 second required).")
388
 
389
-
390
  # 3. Extract keywords
391
  logger.info("Extracting keywords...")
392
  try:
@@ -428,7 +415,6 @@ def crear_video(prompt_type, input_text, musica_file=None):
428
  except Exception as e:
429
  logger.warning(f"Error searching generic videos for '{keyword}': {str(e)}")
430
 
431
-
432
  if not videos_data:
433
  logger.error("No videos found on Pexels for any keyword.")
434
  raise ValueError("No suitable videos found on Pexels.")
@@ -470,26 +456,23 @@ def crear_video(prompt_type, input_text, musica_file=None):
470
  logger.info("Processing and concatenating downloaded videos...")
471
  current_duration = 0
472
  min_clip_duration = 0.5
473
- max_clip_segment = 10.0 # Max segment length from one source clip
474
 
475
  for i, path in enumerate(video_paths):
476
- # Stop if we have enough duration plus a buffer
477
  if current_duration >= audio_duration + max_clip_segment:
478
  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.")
479
  break
480
 
481
- clip = None # Initialize for finally block
482
  try:
483
  logger.debug(f"[{i+1}/{len(video_paths)}] Opening clip: {path}")
484
  clip = VideoFileClip(path)
485
- source_clips.append(clip) # Add to list for later cleanup
486
 
487
- # Verify the source clip is valid
488
  if clip.reader is None or clip.duration is None or clip.duration <= 0:
489
  logger.warning(f"[{i+1}/{len(video_paths)}] Source clip {path} seems invalid (reader is None or duration <= 0). Skipping.")
490
  continue
491
 
492
- # Calculate how much to take from this clip
493
  remaining_needed = audio_duration - current_duration
494
  potential_use_duration = min(clip.duration, max_clip_segment)
495
 
@@ -500,12 +483,10 @@ def crear_video(prompt_type, input_text, musica_file=None):
500
 
501
  if segment_duration >= min_clip_duration:
502
  try:
503
- # Create a subclip. This creates a *new* clip object.
504
  sub = clip.subclip(0, segment_duration)
505
- # Verify the subclip is valid (it should be a VideoFileClip still)
506
  if sub.reader is None or sub.duration is None or sub.duration <= 0:
507
  logger.warning(f"[{i+1}/{len(video_paths)}] Generated subclip from {path} is invalid. Skipping.")
508
- try: sub.close() # Close the invalid subclip
509
  except: pass
510
  continue
511
 
@@ -524,7 +505,6 @@ def crear_video(prompt_type, input_text, musica_file=None):
524
  except Exception as e:
525
  logger.warning(f"[{i+1}/{len(video_paths)}] Error processing video {path}: {str(e)}", exc_info=True)
526
  continue
527
- # Source clips are closed in the main finally block
528
 
529
  logger.info(f"Source clip processing finished. Obtained {len(clips_to_concatenate)} valid segments.")
530
 
@@ -533,13 +513,11 @@ def crear_video(prompt_type, input_text, musica_file=None):
533
  raise ValueError("No valid video segments available to create the video.")
534
 
535
  logger.info(f"Concatenating {len(clips_to_concatenate)} video segments.")
536
- concatenated_base = None # Hold the result temporarily
537
  try:
538
- # Concatenate the collected valid segments
539
  concatenated_base = concatenate_videoclips(clips_to_concatenate, method="chain")
540
  logger.info(f"Base video duration after initial concatenation: {concatenated_base.duration:.2f}s")
541
 
542
- # Verify the resulting concatenated clip is valid (CompositeVideoClip)
543
  if concatenated_base is None or concatenated_base.duration is None or concatenated_base.duration <= 0:
544
  logger.critical("Concatenated video base clip is invalid after first concatenation (None or zero duration).")
545
  raise ValueError("Failed to create valid video base from segments.")
@@ -548,16 +526,14 @@ def crear_video(prompt_type, input_text, musica_file=None):
548
  logger.critical(f"Error during initial concatenation: {str(e)}", exc_info=True)
549
  raise ValueError("Failed during initial video concatenation.")
550
  finally:
551
- # IMPORTANT: Close all the individual segments that were concatenated *regardless of success*
552
  for clip_segment in clips_to_concatenate:
553
  try: clip_segment.close()
554
  except: pass
555
- clips_to_concatenate = [] # Clear the list
556
 
557
- video_base = concatenated_base # Assign the valid concatenated clip
558
 
559
- # --- REVISED REPETITION AND TRIMMING LOGIC ---
560
- final_video_base = video_base # Start with the concatenated base
561
 
562
  if final_video_base.duration < audio_duration:
563
  logger.info(f"Base video ({final_video_base.duration:.2f}s) is shorter than audio ({audio_duration:.2f}s). Repeating...")
@@ -565,12 +541,11 @@ def crear_video(prompt_type, input_text, musica_file=None):
565
  num_full_repeats = int(audio_duration // final_video_base.duration)
566
  remaining_duration = audio_duration % final_video_base.duration
567
 
568
- repeated_clips_list = [final_video_base] * num_full_repeats # List contains the *same* clip object repeated
569
 
570
  if remaining_duration > 0:
571
  try:
572
  remaining_clip = final_video_base.subclip(0, remaining_duration)
573
- # Verify remaining clip is valid (should be a CompositeVideoClip from subclip)
574
  if remaining_clip is None or remaining_clip.duration is None or remaining_clip.duration <= 0:
575
  logger.warning(f"Generated subclip for remaining duration {remaining_duration:.2f}s is invalid. Skipping.")
576
  try: remaining_clip.close()
@@ -584,58 +559,45 @@ def crear_video(prompt_type, input_text, musica_file=None):
584
 
585
  if repeated_clips_list:
586
  logger.info(f"Concatenating {len(repeated_clips_list)} parts for repetition.")
587
- video_base_repeated = None # Hold result temporarily
588
  try:
589
- # Concatenate the repeated parts
590
- # If repeated_clips_list contains duplicates of the same object, this is fine for concatenate_videoclips
591
  video_base_repeated = concatenate_videoclips(repeated_clips_list, method="chain")
592
  logger.info(f"Duration of repeated video base: {video_base_repeated.duration:.2f}s")
593
 
594
- # Verify the repeated clip is valid
595
  if video_base_repeated is None or video_base_repeated.duration is None or video_base_repeated.duration <= 0:
596
  logger.critical("Concatenated repeated video base clip is invalid.")
597
  raise ValueError("Failed to create valid repeated video base.")
598
 
599
- # Close the old base clip *only if it's different from the new one*
600
  if final_video_base is not video_base_repeated:
601
  try: final_video_base.close()
602
  except: pass
603
 
604
- # Assign the new valid repeated clip
605
  final_video_base = video_base_repeated
606
 
607
  except Exception as e:
608
  logger.critical(f"Error during repetition concatenation: {str(e)}", exc_info=True)
609
- # If repetition fails, the error is raised. The original final_video_base will be closed in main finally.
610
  raise ValueError("Failed during video repetition.")
611
  finally:
612
- # Close the clips in the repeated list, EXCEPT the one assigned to final_video_base
613
- # This needs care as list items might be the same object
614
  if 'repeated_clips_list' in locals():
615
  for clip in repeated_clips_list:
616
- # Only close if it's not the final clip and not already closed (MoviePy tracks this)
617
  if clip is not final_video_base:
618
  try: clip.close()
619
  except: pass
620
 
621
 
622
- # After repetition (or if no repetition happened), ensure duration matches audio exactly
623
  if final_video_base.duration > audio_duration:
624
  logger.info(f"Trimming video base ({final_video_base.duration:.2f}s) to match audio duration ({audio_duration:.2f}s).")
625
- trimmed_video_base = None # Hold result temporarily
626
  try:
627
  trimmed_video_base = final_video_base.subclip(0, audio_duration)
628
- # Verify the trimmed clip is valid
629
  if trimmed_video_base is None or trimmed_video_base.duration is None or trimmed_video_base.duration <= 0:
630
  logger.critical("Trimmed video base clip is invalid.")
631
  raise ValueError("Failed to create valid trimmed video base.")
632
 
633
- # Close the old clip
634
  if final_video_base is not trimmed_video_base:
635
  try: final_video_base.close()
636
  except: pass
637
 
638
- # Assign the new valid trimmed clip
639
  final_video_base = trimmed_video_base
640
 
641
  except Exception as e:
@@ -643,27 +605,25 @@ def crear_video(prompt_type, input_text, musica_file=None):
643
  raise ValueError("Failed during video trimming.")
644
 
645
 
646
- # Final check on video_base before setting audio/writing
647
  if final_video_base is None or final_video_base.duration is None or final_video_base.duration <= 0:
648
  logger.critical("Final video base clip is invalid before audio/writing (None or zero duration).")
649
  raise ValueError("Final video base clip is invalid.")
650
 
651
- # Also check size, as MoviePy needs it for writing
652
  if final_video_base.size is None or final_video_base.size[0] <= 0 or final_video_base.size[1] <= 0:
653
  logger.critical(f"Final video base has invalid size: {final_video_base.size}. Cannot write video.")
654
  raise ValueError("Final video base has invalid size before writing.")
655
 
656
- video_base = final_video_base # Use the final adjusted video_base for subsequent steps
657
 
658
  # 6. Handle background music
659
  logger.info("Processing audio...")
660
 
661
- final_audio = audio_tts # Start with TTS audio
662
 
663
- musica_audio_looped = None # Initialize for cleanup
664
 
665
  if musica_file:
666
- musica_audio_original = None # Initialize for cleanup
667
  try:
668
  music_path = os.path.join(temp_dir_intermediate, "musica_bg.mp3")
669
  shutil.copyfile(musica_file, music_path)
@@ -672,101 +632,77 @@ def crear_video(prompt_type, input_text, musica_file=None):
672
 
673
  musica_audio_original = AudioFileClip(music_path)
674
 
675
- # Verify initial music audio clip
676
  if musica_audio_original.reader is None or musica_audio_original.duration is None or musica_audio_original.duration <= 0:
677
  logger.warning("Background music clip seems invalid or has zero duration. Skipping music.")
678
- # Close the invalid clip before skipping
679
  try: musica_audio_original.close()
680
  except: pass
681
- musica_audio_original = None # Ensure it's None
682
  else:
683
  musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
684
  logger.debug(f"Music adjusted to video duration: {musica_audio_looped.duration:.2f}s")
685
 
686
- # Verify the looped music clip is valid
687
  if musica_audio_looped is None or musica_audio_looped.duration is None or musica_audio_looped.duration <= 0:
688
  logger.warning("Looped background music clip is invalid. Skipping music.")
689
- # Close the invalid looped clip
690
  try: musica_audio_looped.close()
691
  except: pass
692
- musica_audio_looped = None # Ensure it's None
693
 
694
 
695
- if musica_audio_looped: # Only proceed if looped music is valid
696
- # CompositeAudioClip uses the *current* audio_tts and musica_audio_looped clips
697
- final_audio = CompositeAudioClip([
698
- musica_audio_looped.volumex(0.2), # Apply effect to the looped clip
699
- audio_tts.volumex(1.0) # Apply effect to the TTS clip (safer than assuming no effects needed)
700
  ])
701
- # Verify the resulting composite audio
702
- if final_audio.duration is None or final_audio.duration <= 0:
703
  logger.warning("Composite audio clip is invalid (None or zero duration). Using voice audio only.")
704
- # If CompositeAudioClip fails, need to close its components if they are not the originals
705
- try:
706
- if musica_audio_looped is not musica_audio_original: musica_audio_looped.close()
707
- if audio_tts is not audio_tts_original: audio_tts.close()
708
- # Close the invalid composite audio
709
- try: final_audio.close()
710
- except: pass
711
- except: pass # Ignore errors during cleanup
712
- final_audio = audio_tts_original # Fallback to the original valid TTS
713
- musica_audio = None # Ensure musica_audio variable is None
714
- audio_tts = audio_tts_original # Ensure audio_tts variable points to the original valid TTS
715
-
716
  else:
717
  logger.info("Audio mix completed (voice + music).")
718
- # composite audio is valid, set musica_audio variable for later cleanup
719
- musica_audio = musica_audio_looped
720
- # audio_tts variable already points to original which is handled in main finally
721
-
722
 
723
  except Exception as e:
724
  logger.warning(f"Error processing background music: {str(e)}", exc_info=True)
725
- # Fallback to just TTS audio
726
  final_audio = audio_tts_original
727
- musica_audio = None # Ensure variable is None
728
- audio_tts = audio_tts_original # Ensure variable is original
729
  logger.warning("Using voice audio only due to music processing error.")
730
 
731
 
732
- # Ensure final_audio duration matches video_base duration if possible
733
- # Check for significant duration mismatch allowing small floating point differences
734
  if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
735
- logger.warning(f"Final audio duration ({final_audio.duration:.2f}s) differs significantly from video base ({video_base.duration:.2f}s). Attempting trim/extend.")
736
  try:
737
- # Need to create a *new* clip if trimming, and handle closing the old one
738
  if final_audio.duration > video_base.duration:
739
  trimmed_final_audio = final_audio.subclip(0, video_base.duration)
740
  if trimmed_final_audio.duration is None or trimmed_final_audio.duration <= 0:
741
  logger.warning("Trimmed final audio is invalid. Using original final_audio.")
742
  try: trimmed_final_audio.close()
743
- except: pass # Close the invalid trimmed clip
744
  else:
745
- # Safely close the old final_audio if it's different
746
  if final_audio is not trimmed_final_audio:
747
  try: final_audio.close()
748
  except: pass
749
- final_audio = trimmed_final_audio # Use the valid trimmed clip
750
  logger.warning("Trimmed final audio to match video duration.")
751
- # MoviePy often extends audio automatically if it's too short, so we don't explicitly extend here.
752
  except Exception as e:
753
  logger.warning(f"Error adjusting final audio duration: {str(e)}")
754
 
755
 
756
  # Final check on video_final before writing
757
- # video_final is a composite of video_base and final_audio
758
  video_final = video_base.set_audio(final_audio)
759
 
760
  if video_final is None or video_final.duration is None or video_final.duration <= 0:
761
  logger.critical("Final video clip (with audio) is invalid before writing (None or zero duration).")
762
  raise ValueError("Final video clip is invalid before writing.")
763
 
764
-
765
  output_filename = "final_video.mp4"
766
  output_path = os.path.join(temp_dir_intermediate, output_filename)
767
  logger.info(f"Writing final video to: {output_path}")
768
 
769
-
770
  video_final.write_videofile(
771
  output_path,
772
  fps=24,
@@ -791,51 +727,34 @@ def crear_video(prompt_type, input_text, musica_file=None):
791
  finally:
792
  logger.info("Starting cleanup of clips and intermediate temporary files...")
793
 
794
- # Close all initially opened source *video* clips
795
  for clip in source_clips:
796
  try: clip.close()
797
  except Exception as e: logger.warning(f"Error closing source video clip in finally: {str(e)}")
798
 
799
- # Close any video segments left in the list (should be empty if successful)
800
  for clip_segment in clips_to_concatenate:
801
  try: clip_segment.close()
802
  except Exception as e: logger.warning(f"Error closing video segment clip in finally: {str(e)}")
803
 
804
- # Close the main MoviePy objects if they were successfully created
805
  try:
806
- # Close the audio clips
807
- # Start with the potentially looped/trimmed music clip if it exists
808
- if musica_audio is not None:
809
- try: musica_audio.close()
810
- except Exception as e: logger.warning(f"Error closing musica_audio in finally: {str(e)}")
811
- # Then close the original music audio clip if it exists and is different
812
- if musica_audio_original is not None and musica_audio_original is not musica_audio:
813
- try: musica_audio_original.close()
814
- except Exception as e: logger.warning(f"Error closing musica_audio_original in finally: {str(e)}")
815
-
816
- # Close the potentially modified/trimmed TTS clip if it exists and is different from original
817
- if audio_tts is not None and audio_tts is not audio_tts_original:
818
- try: audio_tts.close()
819
- except Exception as e: logger.warning(f"Error closing audio_tts (modified) in finally: {str(e)}")
820
- # Close the original TTS clip if it exists (it's the base)
821
- if audio_tts_original is not None:
822
- try: audio_tts_original.close()
823
- except Exception as e: logger.warning(f"Error closing audio_tts_original in finally: {str(e)}")
824
-
825
- # Close video_final first, which should cascade to video_base and final_audio (and their components)
826
  if video_final is not None:
827
  try: video_final.close()
828
  except Exception as e: logger.warning(f"Error closing video_final in finally: {str(e)}")
829
- # If video_final wasn't created but video_base was (due to error before set_audio), close video_base
830
  elif video_base is not None:
831
  try: video_base.close()
832
  except Exception as e: logger.warning(f"Error closing video_base in finally: {str(e)}")
833
 
834
-
835
  except Exception as e:
836
  logger.warning(f"Error during final clip closing in finally: {str(e)}")
837
 
838
- # Clean up intermediate files, but NOT the final video file
839
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
840
  final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4")
841
 
@@ -856,9 +775,15 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
856
 
857
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
858
 
 
 
 
 
 
859
  if not input_text or not input_text.strip():
860
  logger.warning("Empty input text.")
861
- return None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.")
 
862
 
863
  logger.info(f"Input Type: {prompt_type}")
864
  logger.debug(f"Input Text: '{input_text[:100]}...'")
@@ -874,19 +799,26 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
874
  if video_path and os.path.exists(video_path):
875
  logger.info(f"crear_video returned path: {video_path}")
876
  logger.info(f"Size of returned video file: {os.path.getsize(video_path)} bytes")
877
- return video_path, gr.update(value="✅ Video generado exitosamente.", interactive=False)
 
 
878
  else:
879
  logger.error(f"crear_video did not return a valid path or file does not exist: {video_path}")
880
- return None, gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
 
881
 
882
  except ValueError as ve:
883
  logger.warning(f"Validation error during video creation: {str(ve)}")
884
- return None, gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
 
885
  except Exception as e:
886
  logger.critical(f"Critical error during video creation: {str(e)}", exc_info=True)
887
- return None, gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
 
888
  finally:
889
  logger.info("End of run_app handler.")
 
 
890
 
891
 
892
  # Gradio Interface
@@ -935,10 +867,16 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
935
 
936
  with gr.Column():
937
  video_output = gr.Video(
938
- label="Generated Video",
939
  interactive=False,
940
  height=400
941
  )
 
 
 
 
 
 
942
  status_output = gr.Textbox(
943
  label="Status",
944
  interactive=False,
@@ -954,16 +892,25 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
954
  outputs=[ia_guion_column, manual_guion_column]
955
  )
956
 
 
957
  generate_btn.click(
958
- lambda: (None, gr.update(value="⏳ Processing... This can take 2-5 minutes depending on length and resources.", interactive=False)),
959
- outputs=[video_output, status_output],
 
960
  queue=True,
961
  ).then(
 
962
  run_app,
963
  inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
964
- outputs=[video_output, status_output]
 
 
 
 
 
965
  )
966
 
 
967
  gr.Markdown("### Instructions:")
968
  gr.Markdown("""
969
  1. **Pexels API Key:** Ensure you have set the `PEXELS_API_KEY` environment variable.
@@ -973,17 +920,18 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
973
  3. **Upload Music** (optional): Select an audio file (MP3, WAV, etc.) for background music.
974
  4. **Click "✨ Generate Video"**.
975
  5. Wait for the video to process. Processing time may vary. Check the status box.
976
- 6. If there are errors, check the `video_generator_full.log` file for details.
 
977
  """)
978
  gr.Markdown("---")
979
- gr.Markdown("Developed by [Your Name/Company/Alias - Optional]")
980
 
981
  if __name__ == "__main__":
982
  logger.info("Verifying critical dependencies...")
983
  try:
984
  from moviepy.editor import ColorClip
985
  try:
986
- temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1) # Use a very short duration
987
  temp_clip.close()
988
  logger.info("MoviePy base clips (like ColorClip) created and closed successfully. FFmpeg seems accessible.")
989
  except Exception as e:
 
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
  # raise ValueError("Pexels API key not configured")
38
 
39
  # Model Initialization
 
54
  logger.info("Loading KeyBERT model...")
55
  kw_model = None
56
  try:
57
+ kw_model = KeyBERT('distilbert-base-multilingual-cased')
58
  logger.info("KeyBERT initialized successfully")
59
  except Exception as e:
60
  logger.error(f"FAILURE loading KeyBERT: {str(e)}", exc_info=True)
 
204
  def loop_audio_to_length(audio_clip, target_duration):
205
  logger.debug(f"Adjusting audio | Current duration: {audio_clip.duration:.2f}s | Target: {target_duration:.2f}s")
206
 
 
207
  if audio_clip is None or audio_clip.duration is None or audio_clip.duration <= 0:
208
  logger.warning("Input audio clip is invalid (None or zero duration), cannot loop.")
 
209
  try:
210
+ sr = getattr(audio_clip, 'fps', 44100) if audio_clip else 44100
 
211
  return AudioClip(lambda t: 0, duration=target_duration, sr=sr)
212
  except Exception as e:
213
  logger.error(f"Could not create silence clip: {e}", exc_info=True)
214
+ return AudioFileClip(filename="")
215
 
216
  if audio_clip.duration >= target_duration:
217
  logger.debug("Audio clip already longer or equal to target. Trimming.")
218
  trimmed_clip = audio_clip.subclip(0, target_duration)
 
219
  if trimmed_clip.duration is None or trimmed_clip.duration <= 0:
220
  logger.error("Trimmed audio clip is invalid.")
221
  try: trimmed_clip.close()
 
227
  logger.debug(f"Creating {loops} audio loops")
228
 
229
  audio_segments = [audio_clip] * loops
230
+ looped_audio = None
231
+ final_looped_audio = None
232
  try:
233
  looped_audio = concatenate_audioclips(audio_segments)
234
 
 
235
  if looped_audio.duration is None or looped_audio.duration <= 0:
236
  logger.error("Concatenated audio clip is invalid (None or zero duration).")
237
  raise ValueError("Invalid concatenated audio.")
238
 
239
  final_looped_audio = looped_audio.subclip(0, target_duration)
240
 
 
241
  if final_looped_audio.duration is None or final_looped_audio.duration <= 0:
242
  logger.error("Final subclipped audio clip is invalid (None or zero duration).")
243
  raise ValueError("Invalid final subclipped audio.")
 
246
 
247
  except Exception as e:
248
  logger.error(f"Error concatenating/subclipping audio clips for looping: {str(e)}", exc_info=True)
 
249
  try:
250
  if audio_clip.duration is not None and audio_clip.duration > 0:
251
  logger.warning("Returning original audio clip (may be too short).")
252
  return audio_clip.subclip(0, min(audio_clip.duration, target_duration))
253
  except:
254
+ pass
255
  logger.error("Fallback to original audio clip failed.")
256
+ return AudioFileClip(filename="")
257
 
258
  finally:
 
259
  if looped_audio is not None and looped_audio is not final_looped_audio:
260
  try: looped_audio.close()
261
  except: pass
 
323
  start_time = datetime.now()
324
  temp_dir_intermediate = None
325
 
326
+ audio_tts_original = None
327
+ musica_audio_original = None
328
+ audio_tts = None
329
+ musica_audio = None
330
+ video_base = None
331
+ video_final = None
332
+ source_clips = []
333
+ clips_to_concatenate = []
 
334
 
335
  try:
336
  # 1. Generate or use script
 
359
 
360
  audio_tts_original = AudioFileClip(voz_path)
361
 
 
362
  if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
363
  logger.critical("Initial TTS audio clip is invalid (reader is None or duration <= 0).")
 
364
  try: audio_tts_original.close()
365
  except: pass
366
+ audio_tts_original = None
367
  raise ValueError("Generated voice audio is invalid.")
368
 
369
+ audio_tts = audio_tts_original
370
  audio_duration = audio_tts.duration
371
  logger.info(f"Voice audio duration: {audio_duration:.2f} seconds")
372
 
 
374
  logger.error(f"Voice audio duration ({audio_duration:.2f}s) is too short.")
375
  raise ValueError("Generated voice audio is too short (min 1 second required).")
376
 
 
377
  # 3. Extract keywords
378
  logger.info("Extracting keywords...")
379
  try:
 
415
  except Exception as e:
416
  logger.warning(f"Error searching generic videos for '{keyword}': {str(e)}")
417
 
 
418
  if not videos_data:
419
  logger.error("No videos found on Pexels for any keyword.")
420
  raise ValueError("No suitable videos found on Pexels.")
 
456
  logger.info("Processing and concatenating downloaded videos...")
457
  current_duration = 0
458
  min_clip_duration = 0.5
459
+ max_clip_segment = 10.0
460
 
461
  for i, path in enumerate(video_paths):
 
462
  if current_duration >= audio_duration + max_clip_segment:
463
  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.")
464
  break
465
 
466
+ clip = None
467
  try:
468
  logger.debug(f"[{i+1}/{len(video_paths)}] Opening clip: {path}")
469
  clip = VideoFileClip(path)
470
+ source_clips.append(clip)
471
 
 
472
  if clip.reader is None or clip.duration is None or clip.duration <= 0:
473
  logger.warning(f"[{i+1}/{len(video_paths)}] Source clip {path} seems invalid (reader is None or duration <= 0). Skipping.")
474
  continue
475
 
 
476
  remaining_needed = audio_duration - current_duration
477
  potential_use_duration = min(clip.duration, max_clip_segment)
478
 
 
483
 
484
  if segment_duration >= min_clip_duration:
485
  try:
 
486
  sub = clip.subclip(0, segment_duration)
 
487
  if sub.reader is None or sub.duration is None or sub.duration <= 0:
488
  logger.warning(f"[{i+1}/{len(video_paths)}] Generated subclip from {path} is invalid. Skipping.")
489
+ try: sub.close()
490
  except: pass
491
  continue
492
 
 
505
  except Exception as e:
506
  logger.warning(f"[{i+1}/{len(video_paths)}] Error processing video {path}: {str(e)}", exc_info=True)
507
  continue
 
508
 
509
  logger.info(f"Source clip processing finished. Obtained {len(clips_to_concatenate)} valid segments.")
510
 
 
513
  raise ValueError("No valid video segments available to create the video.")
514
 
515
  logger.info(f"Concatenating {len(clips_to_concatenate)} video segments.")
516
+ concatenated_base = None
517
  try:
 
518
  concatenated_base = concatenate_videoclips(clips_to_concatenate, method="chain")
519
  logger.info(f"Base video duration after initial concatenation: {concatenated_base.duration:.2f}s")
520
 
 
521
  if concatenated_base is None or concatenated_base.duration is None or concatenated_base.duration <= 0:
522
  logger.critical("Concatenated video base clip is invalid after first concatenation (None or zero duration).")
523
  raise ValueError("Failed to create valid video base from segments.")
 
526
  logger.critical(f"Error during initial concatenation: {str(e)}", exc_info=True)
527
  raise ValueError("Failed during initial video concatenation.")
528
  finally:
 
529
  for clip_segment in clips_to_concatenate:
530
  try: clip_segment.close()
531
  except: pass
532
+ clips_to_concatenate = []
533
 
534
+ video_base = concatenated_base
535
 
536
+ final_video_base = video_base
 
537
 
538
  if final_video_base.duration < audio_duration:
539
  logger.info(f"Base video ({final_video_base.duration:.2f}s) is shorter than audio ({audio_duration:.2f}s). Repeating...")
 
541
  num_full_repeats = int(audio_duration // final_video_base.duration)
542
  remaining_duration = audio_duration % final_video_base.duration
543
 
544
+ repeated_clips_list = [final_video_base] * num_full_repeats
545
 
546
  if remaining_duration > 0:
547
  try:
548
  remaining_clip = final_video_base.subclip(0, remaining_duration)
 
549
  if remaining_clip is None or remaining_clip.duration is None or remaining_clip.duration <= 0:
550
  logger.warning(f"Generated subclip for remaining duration {remaining_duration:.2f}s is invalid. Skipping.")
551
  try: remaining_clip.close()
 
559
 
560
  if repeated_clips_list:
561
  logger.info(f"Concatenating {len(repeated_clips_list)} parts for repetition.")
562
+ video_base_repeated = None
563
  try:
 
 
564
  video_base_repeated = concatenate_videoclips(repeated_clips_list, method="chain")
565
  logger.info(f"Duration of repeated video base: {video_base_repeated.duration:.2f}s")
566
 
 
567
  if video_base_repeated is None or video_base_repeated.duration is None or video_base_repeated.duration <= 0:
568
  logger.critical("Concatenated repeated video base clip is invalid.")
569
  raise ValueError("Failed to create valid repeated video base.")
570
 
 
571
  if final_video_base is not video_base_repeated:
572
  try: final_video_base.close()
573
  except: pass
574
 
 
575
  final_video_base = video_base_repeated
576
 
577
  except Exception as e:
578
  logger.critical(f"Error during repetition concatenation: {str(e)}", exc_info=True)
 
579
  raise ValueError("Failed during video repetition.")
580
  finally:
 
 
581
  if 'repeated_clips_list' in locals():
582
  for clip in repeated_clips_list:
 
583
  if clip is not final_video_base:
584
  try: clip.close()
585
  except: pass
586
 
587
 
 
588
  if final_video_base.duration > audio_duration:
589
  logger.info(f"Trimming video base ({final_video_base.duration:.2f}s) to match audio duration ({audio_duration:.2f}s).")
590
+ trimmed_video_base = None
591
  try:
592
  trimmed_video_base = final_video_base.subclip(0, audio_duration)
 
593
  if trimmed_video_base is None or trimmed_video_base.duration is None or trimmed_video_base.duration <= 0:
594
  logger.critical("Trimmed video base clip is invalid.")
595
  raise ValueError("Failed to create valid trimmed video base.")
596
 
 
597
  if final_video_base is not trimmed_video_base:
598
  try: final_video_base.close()
599
  except: pass
600
 
 
601
  final_video_base = trimmed_video_base
602
 
603
  except Exception as e:
 
605
  raise ValueError("Failed during video trimming.")
606
 
607
 
 
608
  if final_video_base is None or final_video_base.duration is None or final_video_base.duration <= 0:
609
  logger.critical("Final video base clip is invalid before audio/writing (None or zero duration).")
610
  raise ValueError("Final video base clip is invalid.")
611
 
 
612
  if final_video_base.size is None or final_video_base.size[0] <= 0 or final_video_base.size[1] <= 0:
613
  logger.critical(f"Final video base has invalid size: {final_video_base.size}. Cannot write video.")
614
  raise ValueError("Final video base has invalid size before writing.")
615
 
616
+ video_base = final_video_base
617
 
618
  # 6. Handle background music
619
  logger.info("Processing audio...")
620
 
621
+ final_audio = audio_tts_original # Start with the original valid TTS audio
622
 
623
+ musica_audio_looped = None
624
 
625
  if musica_file:
626
+ musica_audio_original = None
627
  try:
628
  music_path = os.path.join(temp_dir_intermediate, "musica_bg.mp3")
629
  shutil.copyfile(musica_file, music_path)
 
632
 
633
  musica_audio_original = AudioFileClip(music_path)
634
 
 
635
  if musica_audio_original.reader is None or musica_audio_original.duration is None or musica_audio_original.duration <= 0:
636
  logger.warning("Background music clip seems invalid or has zero duration. Skipping music.")
 
637
  try: musica_audio_original.close()
638
  except: pass
639
+ musica_audio_original = None
640
  else:
641
  musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
642
  logger.debug(f"Music adjusted to video duration: {musica_audio_looped.duration:.2f}s")
643
 
 
644
  if musica_audio_looped is None or musica_audio_looped.duration is None or musica_audio_looped.duration <= 0:
645
  logger.warning("Looped background music clip is invalid. Skipping music.")
 
646
  try: musica_audio_looped.close()
647
  except: pass
648
+ musica_audio_looped = None
649
 
650
 
651
+ if musica_audio_looped:
652
+ # Use the looped music and the current audio_tts (which is the original)
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
+ # Components were likely closed by composite_audio.close() or will be in main finally
663
+ final_audio = audio_tts_original # Fallback
 
 
 
 
 
 
 
 
664
  else:
665
  logger.info("Audio mix completed (voice + music).")
666
+ final_audio = composite_audio # Use the valid composite audio
667
+ musica_audio = musica_audio_looped # Assign for cleanup
 
 
668
 
669
  except Exception as e:
670
  logger.warning(f"Error processing background music: {str(e)}", exc_info=True)
 
671
  final_audio = audio_tts_original
672
+ musica_audio = None
 
673
  logger.warning("Using voice audio only due to music processing error.")
674
 
675
 
 
 
676
  if final_audio.duration is not None and abs(final_audio.duration - video_base.duration) > 0.2:
677
+ logger.warning(f"Final audio duration ({final_audio.duration:.2f}s) differs significantly from video base ({video_base.duration:.2f}s). Attempting trim.")
678
  try:
 
679
  if final_audio.duration > video_base.duration:
680
  trimmed_final_audio = final_audio.subclip(0, video_base.duration)
681
  if trimmed_final_audio.duration is None or trimmed_final_audio.duration <= 0:
682
  logger.warning("Trimmed final audio is invalid. Using original final_audio.")
683
  try: trimmed_final_audio.close()
684
+ except: pass
685
  else:
 
686
  if final_audio is not trimmed_final_audio:
687
  try: final_audio.close()
688
  except: pass
689
+ final_audio = trimmed_final_audio
690
  logger.warning("Trimmed final audio to match video duration.")
 
691
  except Exception as e:
692
  logger.warning(f"Error adjusting final audio duration: {str(e)}")
693
 
694
 
695
  # Final check on video_final before writing
 
696
  video_final = video_base.set_audio(final_audio)
697
 
698
  if video_final is None or video_final.duration is None or video_final.duration <= 0:
699
  logger.critical("Final video clip (with audio) is invalid before writing (None or zero duration).")
700
  raise ValueError("Final video clip is invalid before writing.")
701
 
 
702
  output_filename = "final_video.mp4"
703
  output_path = os.path.join(temp_dir_intermediate, output_filename)
704
  logger.info(f"Writing final video to: {output_path}")
705
 
 
706
  video_final.write_videofile(
707
  output_path,
708
  fps=24,
 
727
  finally:
728
  logger.info("Starting cleanup of clips and intermediate temporary files...")
729
 
 
730
  for clip in source_clips:
731
  try: clip.close()
732
  except Exception as e: logger.warning(f"Error closing source video clip in finally: {str(e)}")
733
 
 
734
  for clip_segment in clips_to_concatenate:
735
  try: clip_segment.close()
736
  except Exception as e: logger.warning(f"Error closing video segment clip in finally: {str(e)}")
737
 
 
738
  try:
739
+ # Close audio clips: looped music, original music, then final audio (which might close its components)
740
+ if musica_audio is not None: try: musica_audio.close() except Exception as e: logger.warning(f"Error closing musica_audio in finally: {str(e)}")
741
+ if musica_audio_original is not None and musica_audio_original is not musica_audio: try: musica_audio_original.close() except Exception as e: logger.warning(f"Error closing musica_audio_original in finally: {str(e)}")
742
+
743
+ # Close TTS clips: potentially modified/trimmed TTS, then original TTS
744
+ if audio_tts is not None and audio_tts is not audio_tts_original: try: audio_tts.close() except Exception as e: logger.warning(f"Error closing audio_tts (modified) in finally: {str(e)}")
745
+ if audio_tts_original is not None: try: audio_tts_original.close() except Exception as e: logger.warning(f"Error closing audio_tts_original in finally: {str(e)}")
746
+
747
+ # Close video clips: final video (should cascade), then video base if it wasn't the final
 
 
 
 
 
 
 
 
 
 
 
748
  if video_final is not None:
749
  try: video_final.close()
750
  except Exception as e: logger.warning(f"Error closing video_final in finally: {str(e)}")
 
751
  elif video_base is not None:
752
  try: video_base.close()
753
  except Exception as e: logger.warning(f"Error closing video_base in finally: {str(e)}")
754
 
 
755
  except Exception as e:
756
  logger.warning(f"Error during final clip closing in finally: {str(e)}")
757
 
 
758
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
759
  final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4")
760
 
 
775
 
776
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
777
 
778
+ # Initialize outputs to None and default status
779
+ output_video = None
780
+ output_file = None
781
+ status_msg = gr.update(value="⏳ Procesando...", interactive=False)
782
+
783
  if not input_text or not input_text.strip():
784
  logger.warning("Empty input text.")
785
+ # Return None for video and file, update status
786
+ return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
787
 
788
  logger.info(f"Input Type: {prompt_type}")
789
  logger.debug(f"Input Text: '{input_text[:100]}...'")
 
799
  if video_path and os.path.exists(video_path):
800
  logger.info(f"crear_video returned path: {video_path}")
801
  logger.info(f"Size of returned video file: {os.path.getsize(video_path)} bytes")
802
+ output_video = video_path # Set video component value
803
+ output_file = video_path # Set file component value for download
804
+ status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
805
  else:
806
  logger.error(f"crear_video did not return a valid path or file does not exist: {video_path}")
807
+ # Leave video and file outputs as None
808
+ status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
809
 
810
  except ValueError as ve:
811
  logger.warning(f"Validation error during video creation: {str(ve)}")
812
+ # Leave video and file outputs as None
813
+ status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
814
  except Exception as e:
815
  logger.critical(f"Critical error during video creation: {str(e)}", exc_info=True)
816
+ # Leave video and file outputs as None
817
+ status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
818
  finally:
819
  logger.info("End of run_app handler.")
820
+ # Return all three outputs
821
+ return output_video, output_file, status_msg
822
 
823
 
824
  # Gradio Interface
 
867
 
868
  with gr.Column():
869
  video_output = gr.Video(
870
+ label="Generated Video Preview", # Changed label
871
  interactive=False,
872
  height=400
873
  )
874
+ # Add the File component for download
875
+ file_output = gr.File(
876
+ label="Download Video",
877
+ interactive=False, # Not interactive for user upload
878
+ visible=False # Hide initially
879
+ )
880
  status_output = gr.Textbox(
881
  label="Status",
882
  interactive=False,
 
892
  outputs=[ia_guion_column, manual_guion_column]
893
  )
894
 
895
+ # Modify the click event to return 3 outputs
896
  generate_btn.click(
897
+ # Action 1: Reset outputs and set status to processing
898
+ lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar 2-5 minutos.", interactive=False)),
899
+ outputs=[video_output, file_output, status_output],
900
  queue=True,
901
  ).then(
902
+ # Action 2: Call the main processing function
903
  run_app,
904
  inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
905
+ outputs=[video_output, file_output, status_output] # Match the 3 outputs
906
+ ).then(
907
+ # Action 3: Make the download link visible if a file was returned
908
+ lambda video_path: gr.update(visible=video_path is not None), # Check if video_output has a value
909
+ inputs=[video_output], # Use video_output as input to check if generation was successful
910
+ outputs=[file_output] # Update visibility of file_output
911
  )
912
 
913
+
914
  gr.Markdown("### Instructions:")
915
  gr.Markdown("""
916
  1. **Pexels API Key:** Ensure you have set the `PEXELS_API_KEY` environment variable.
 
920
  3. **Upload Music** (optional): Select an audio file (MP3, WAV, etc.) for background music.
921
  4. **Click "✨ Generate Video"**.
922
  5. Wait for the video to process. Processing time may vary. Check the status box.
923
+ 6. The generated video will appear above, and a download link will show if successful.
924
+ 7. If there are errors, check the `video_generator_full.log` file for details.
925
  """)
926
  gr.Markdown("---")
927
+ gr.Markdown("Developed by [Your Name/Company/Alias - Opcional]")
928
 
929
  if __name__ == "__main__":
930
  logger.info("Verifying critical dependencies...")
931
  try:
932
  from moviepy.editor import ColorClip
933
  try:
934
+ temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
935
  temp_clip.close()
936
  logger.info("MoviePy base clips (like ColorClip) created and closed successfully. FFmpeg seems accessible.")
937
  except Exception as e: