Spaces:
Running
Running
Update app.py
Browse files
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 =
|
| 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 |
-
|
| 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="")
|
| 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
|
| 236 |
-
final_looped_audio = None
|
| 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
|
| 263 |
logger.error("Fallback to original audio clip failed.")
|
| 264 |
-
return AudioFileClip(filename="")
|
| 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 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 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
|
| 379 |
raise ValueError("Generated voice audio is invalid.")
|
| 380 |
|
| 381 |
-
audio_tts = audio_tts_original
|
| 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
|
| 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
|
| 482 |
try:
|
| 483 |
logger.debug(f"[{i+1}/{len(video_paths)}] Opening clip: {path}")
|
| 484 |
clip = VideoFileClip(path)
|
| 485 |
-
source_clips.append(clip)
|
| 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()
|
| 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
|
| 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 = []
|
| 556 |
|
| 557 |
-
video_base = concatenated_base
|
| 558 |
|
| 559 |
-
|
| 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
|
| 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
|
| 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
|
| 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
|
| 657 |
|
| 658 |
# 6. Handle background music
|
| 659 |
logger.info("Processing audio...")
|
| 660 |
|
| 661 |
-
final_audio =
|
| 662 |
|
| 663 |
-
musica_audio_looped = None
|
| 664 |
|
| 665 |
if musica_file:
|
| 666 |
-
musica_audio_original = None
|
| 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
|
| 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
|
| 693 |
|
| 694 |
|
| 695 |
-
if musica_audio_looped:
|
| 696 |
-
#
|
| 697 |
-
|
| 698 |
-
musica_audio_looped.volumex(0.2),
|
| 699 |
-
|
| 700 |
])
|
| 701 |
-
|
| 702 |
-
if
|
| 703 |
logger.warning("Composite audio clip is invalid (None or zero duration). Using voice audio only.")
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 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 |
-
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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
|
| 807 |
-
|
| 808 |
-
if
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
if
|
| 813 |
-
|
| 814 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 878 |
else:
|
| 879 |
logger.error(f"crear_video did not return a valid path or file does not exist: {video_path}")
|
| 880 |
-
|
|
|
|
| 881 |
|
| 882 |
except ValueError as ve:
|
| 883 |
logger.warning(f"Validation error during video creation: {str(ve)}")
|
| 884 |
-
|
|
|
|
| 885 |
except Exception as e:
|
| 886 |
logger.critical(f"Critical error during video creation: {str(e)}", exc_info=True)
|
| 887 |
-
|
|
|
|
| 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 |
-
|
| 959 |
-
|
|
|
|
| 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.
|
|
|
|
| 977 |
""")
|
| 978 |
gr.Markdown("---")
|
| 979 |
-
gr.Markdown("Developed by [Your Name/Company/Alias -
|
| 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)
|
| 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:
|