Shreevathsam commited on
Commit
3e31f54
·
verified ·
1 Parent(s): 9b6da36

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +72 -8
app.py CHANGED
@@ -23,7 +23,7 @@ os.makedirs('background_music', exist_ok=True)
23
  os.makedirs('voice_over', exist_ok=True)
24
  os.makedirs('exports', exist_ok=True)
25
 
26
- # Get API key from environment variable (will be set in Hugging Face Space settings)
27
  GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY', '')
28
  if GOOGLE_API_KEY:
29
  os.environ['GOOGLE_API_KEY'] = GOOGLE_API_KEY
@@ -369,24 +369,30 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
369
  generation_cancelled = False
370
  current_video_clip = None
371
  progress(0, desc="Starting...")
 
372
  if generation_cancelled:
373
  return None, "Generation cancelled"
 
374
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
375
 
376
- # Updated paths for Hugging Face
377
  source_path = 'video_clips'
378
  if not os.path.isdir(source_path):
379
  return None, "Video clips folder not found. Please upload video clips to the 'video_clips' folder."
 
380
  output_path = 'exports'
381
  os.makedirs(output_path, exist_ok=True)
382
 
383
  video_extensions = ('.mp4', '.avi', '.mkv', '.mov')
384
  all_files = [f for f in os.listdir(source_path) if f.lower().endswith(video_extensions)]
 
385
  if not all_files:
386
  return None, "No video files found in 'video_clips' folder"
 
387
  random.shuffle(all_files)
 
388
  if generation_cancelled:
389
  return None, "Generation cancelled"
 
390
  bg_music_path = None
391
  bg_music_folder_path = 'background_music'
392
  if os.path.isdir(bg_music_folder_path):
@@ -394,16 +400,20 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
394
  possible_files = [f for f in os.listdir(bg_music_folder_path) if f.lower().endswith(audio_extensions) and not f.startswith('voiceover_')]
395
  if len(possible_files) >= 1:
396
  bg_music_path = os.path.join(bg_music_folder_path, possible_files[0])
 
397
  target_duration_seconds = 0
398
  voice_over_audio = None
399
  linelevel_subtitles = None
400
  voice_over_path = None
 
401
  if text_input and text_input.strip():
402
  progress(0.1, desc="Generating TTS...")
403
  voice_name = AVAILABLE_VOICES[voice_selection]["name"] if voice_selection in AVAILABLE_VOICES else "Puck"
404
  tts_path, tts_message = generate_tts_audio(text_input, voice_name)
 
405
  if generation_cancelled:
406
  return None, "Generation cancelled"
 
407
  if tts_path:
408
  voice_over_folder_path = 'voice_over'
409
  os.makedirs(voice_over_folder_path, exist_ok=True)
@@ -422,14 +432,17 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
422
  saved_voice_path = os.path.join(voice_over_folder_path, voice_filename)
423
  shutil.copy2(audio_input, saved_voice_path)
424
  voice_over_path = saved_voice_path
 
425
  if voice_over_path:
426
  try:
427
  progress(0.2, desc="Processing audio...")
428
  if generation_cancelled:
429
  return None, "Generation cancelled"
 
430
  voice_over_audio = AudioFileClip(voice_over_path)
431
  target_duration_seconds = voice_over_audio.duration
432
  linelevel_subtitles, _ = process_voiceover_to_subtitles(voice_over_path)
 
433
  if generation_cancelled:
434
  voice_over_audio.close()
435
  return None, "Generation cancelled"
@@ -439,39 +452,50 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
439
  if not bg_music_path:
440
  return None, "Need text/audio or background music"
441
  target_duration_seconds = duration_minutes * 60
 
442
  progress(0.3, desc="Preparing audio...")
 
443
  if generation_cancelled:
444
  if voice_over_audio:
445
  voice_over_audio.close()
446
  return None, "Generation cancelled"
 
447
  audio_tracks = []
448
  if voice_over_audio:
449
  audio_tracks.append(voice_over_audio)
 
450
  if bg_music_path:
451
  try:
452
  background_audio = AudioFileClip(bg_music_path)
453
- background_audio = background_audio.fx(afx.volumex, 0.10)
454
  background_audio = background_audio.fx(afx.audio_loop, duration=target_duration_seconds)
455
  audio_tracks.append(background_audio)
456
  except Exception as e:
457
  print(f"Background music error: {e}")
 
458
  final_audio = CompositeAudioClip(audio_tracks) if len(audio_tracks) > 1 else (audio_tracks[0] if audio_tracks else None)
 
459
  progress(0.4, desc="Setting up video...")
 
460
  if generation_cancelled:
461
  cleanup_resources()
462
  return None, "Generation cancelled"
 
463
  if video_quality == "High":
464
  target_height, bitrate, preset, crf = 1080, "8000k", "veryfast", "20"
465
  elif video_quality == "Standard":
466
  target_height, bitrate, preset, crf = 720, "4000k", "veryfast", "24"
467
  else:
468
  target_height, bitrate, preset, crf = 480, "1000k", "ultrafast", "28"
 
469
  progress(0.5, desc="Processing clips...")
 
470
  video_clips = []
471
  current_duration = 0
472
  file_index = 0
473
  safety_counter = 0
474
  max_iterations = len(all_files) * 3
 
475
  while current_duration < target_duration_seconds and safety_counter < max_iterations:
476
  if generation_cancelled:
477
  for clip in video_clips:
@@ -481,19 +505,24 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
481
  pass
482
  cleanup_resources()
483
  return None, "Generation cancelled"
 
484
  if file_index >= len(all_files):
485
  file_index = 0
486
  random.shuffle(all_files)
 
487
  video_file = all_files[file_index]
488
  file_index += 1
489
  safety_counter += 1
 
490
  try:
491
  full_clip = VideoFileClip(os.path.join(source_path, video_file))
492
  current_video_clip = full_clip
 
493
  if generation_cancelled:
494
  full_clip.close()
495
  cleanup_resources()
496
  return None, "Generation cancelled"
 
497
  if full_clip.h != target_height:
498
  aspect_ratio = full_clip.w / full_clip.h
499
  new_width = int(target_height * aspect_ratio)
@@ -503,16 +532,20 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
503
  full_clip = full_clip.resize((new_width, adjusted_height))
504
  else:
505
  full_clip = ensure_even_dimensions(full_clip)
 
506
  subclip = get_random_subclip_and_slow(full_clip)
507
  remaining_duration = target_duration_seconds - current_duration
 
508
  if subclip.duration > remaining_duration:
509
  subclip = subclip.subclip(0, remaining_duration)
 
510
  video_clips.append(ensure_even_dimensions(subclip))
511
  current_duration += subclip.duration
512
  progress(0.5 + (safety_counter * 0.1 / max_iterations), desc=f"Clip {len(video_clips)}")
513
  except Exception as e:
514
  print(f"Error: {e}")
515
  continue
 
516
  if generation_cancelled:
517
  for clip in video_clips:
518
  try:
@@ -521,22 +554,30 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
521
  pass
522
  cleanup_resources()
523
  return None, "Generation cancelled"
 
524
  if not video_clips:
525
  return None, "No clips processed"
526
 
 
527
  total_video_duration = sum(clip.duration for clip in video_clips)
528
  duration_diff = total_video_duration - target_duration_seconds
 
529
  if abs(duration_diff) > 0.1:
530
  if duration_diff > 0:
 
531
  trim_amount = duration_diff
532
- new_last_clip = video_clips[-1].subclip(0, video_clips[-1].duration - trim_amount)
533
- video_clips[-1] = new_last_clip
 
534
  else:
 
535
  extend_amount = abs(duration_diff)
536
  new_last_clip = video_clips[-1].fx(vfx.loop, duration=video_clips[-1].duration + extend_amount)
537
  video_clips[-1] = new_last_clip
 
538
  progress(0.6, desc="Applying transitions...")
539
  transition_duration = {"Snap Cut": 0.1, "Whip Pan": 0.3, "Dreamy Fade": 0.8, "Smooth Blend": 0.5, "Ken Burns Zoom": 0.5}.get(transition_type, 0.5)
 
540
  processed_clips = []
541
  for i in range(len(video_clips)):
542
  if i == 0:
@@ -551,7 +592,9 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
551
  else:
552
  _, clip_with_transition = apply_transition_effect(video_clips[i-1], video_clips[i], transition_type, transition_duration)
553
  processed_clips.append(clip_with_transition)
 
554
  progress(0.7, desc="Concatenating...")
 
555
  if generation_cancelled:
556
  for c in processed_clips:
557
  try:
@@ -560,15 +603,21 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
560
  pass
561
  cleanup_resources()
562
  return None, "Generation cancelled"
 
563
  if transition_type == "Snap Cut":
564
  final_video_only = concatenate_videoclips(processed_clips, method="compose")
565
  else:
566
  final_video_only = concatenate_videoclips(processed_clips, method="compose", padding=-transition_duration)
 
567
  final_video_only = ensure_even_dimensions(final_video_only)
568
  current_video_clip = final_video_only
569
- if final_audio:
570
- final_video_only = final_video_only.set_duration(final_audio.duration)
 
 
 
571
  progress(0.8, desc="Adding overlays...")
 
572
  if generation_cancelled:
573
  try:
574
  final_video_only.close()
@@ -576,6 +625,7 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
576
  pass
577
  cleanup_resources()
578
  return None, "Generation cancelled"
 
579
  all_subtitle_clips = []
580
  if linelevel_subtitles:
581
  for line in linelevel_subtitles:
@@ -592,17 +642,22 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
592
  except Exception as e:
593
  print(f"Subtitle error: {e}")
594
  continue
 
595
  all_clips = [final_video_only.set_opacity(0.65)]
596
  if all_subtitle_clips:
597
  all_clips.extend(all_subtitle_clips)
598
  if title_text and title_text.strip():
599
  title_clips = create_title_overlay(title_text, final_video_only.size, duration=4)
600
  all_clips.extend(title_clips)
 
601
  final_video = CompositeVideoClip(all_clips)
602
  current_video_clip = final_video
 
603
  if final_audio:
604
  final_video = final_video.set_audio(final_audio)
 
605
  progress(0.9, desc="Exporting...")
 
606
  if generation_cancelled:
607
  try:
608
  final_video.close()
@@ -610,8 +665,10 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
610
  pass
611
  cleanup_resources()
612
  return None, "Generation cancelled"
 
613
  output_filename = f'video_{timestamp}.mp4'
614
  final_output_path = os.path.join(output_path, output_filename)
 
615
  try:
616
  final_video.write_videofile(
617
  final_output_path,
@@ -628,7 +685,9 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
628
  if generation_cancelled:
629
  return None, "Generation cancelled"
630
  return None, f"Export error: {str(e)}"
 
631
  progress(1.0, desc="Done")
 
632
  if generation_cancelled:
633
  try:
634
  if os.path.exists(final_output_path):
@@ -637,6 +696,7 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
637
  pass
638
  cleanup_resources()
639
  return None, "Generation cancelled"
 
640
  try:
641
  final_video.close()
642
  if voice_over_audio:
@@ -644,6 +704,7 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
644
  current_video_clip = None
645
  except:
646
  pass
 
647
  audio_source = ""
648
  if text_input and text_input.strip():
649
  audio_source = f"TTS ({AVAILABLE_VOICES[voice_selection]['name'] if voice_selection in AVAILABLE_VOICES else 'Puck'})"
@@ -651,9 +712,11 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
651
  audio_source = "Uploaded Audio"
652
  else:
653
  audio_source = "Background Music"
 
654
  summary = f"Complete\n{output_filename}\n{audio_source}\n{transition_type}\n{target_duration_seconds:.1f}s\n{len(linelevel_subtitles) if linelevel_subtitles else 0} subtitles"
655
  return final_output_path, summary
656
 
 
657
  with gr.Blocks(title="Video Generator", theme=gr.themes.Soft()) as interface:
658
  gr.Markdown("# 🎬 AI Video Generator")
659
  gr.Markdown("Upload video clips to `video_clips` folder and optionally background music to `background_music` folder.")
@@ -695,4 +758,5 @@ if __name__ == "__main__":
695
  server_name="0.0.0.0",
696
  server_port=7860,
697
  show_error=True
698
- )
 
 
23
  os.makedirs('voice_over', exist_ok=True)
24
  os.makedirs('exports', exist_ok=True)
25
 
26
+ # Get API key from environment variable
27
  GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY', '')
28
  if GOOGLE_API_KEY:
29
  os.environ['GOOGLE_API_KEY'] = GOOGLE_API_KEY
 
369
  generation_cancelled = False
370
  current_video_clip = None
371
  progress(0, desc="Starting...")
372
+
373
  if generation_cancelled:
374
  return None, "Generation cancelled"
375
+
376
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
377
 
 
378
  source_path = 'video_clips'
379
  if not os.path.isdir(source_path):
380
  return None, "Video clips folder not found. Please upload video clips to the 'video_clips' folder."
381
+
382
  output_path = 'exports'
383
  os.makedirs(output_path, exist_ok=True)
384
 
385
  video_extensions = ('.mp4', '.avi', '.mkv', '.mov')
386
  all_files = [f for f in os.listdir(source_path) if f.lower().endswith(video_extensions)]
387
+
388
  if not all_files:
389
  return None, "No video files found in 'video_clips' folder"
390
+
391
  random.shuffle(all_files)
392
+
393
  if generation_cancelled:
394
  return None, "Generation cancelled"
395
+
396
  bg_music_path = None
397
  bg_music_folder_path = 'background_music'
398
  if os.path.isdir(bg_music_folder_path):
 
400
  possible_files = [f for f in os.listdir(bg_music_folder_path) if f.lower().endswith(audio_extensions) and not f.startswith('voiceover_')]
401
  if len(possible_files) >= 1:
402
  bg_music_path = os.path.join(bg_music_folder_path, possible_files[0])
403
+
404
  target_duration_seconds = 0
405
  voice_over_audio = None
406
  linelevel_subtitles = None
407
  voice_over_path = None
408
+
409
  if text_input and text_input.strip():
410
  progress(0.1, desc="Generating TTS...")
411
  voice_name = AVAILABLE_VOICES[voice_selection]["name"] if voice_selection in AVAILABLE_VOICES else "Puck"
412
  tts_path, tts_message = generate_tts_audio(text_input, voice_name)
413
+
414
  if generation_cancelled:
415
  return None, "Generation cancelled"
416
+
417
  if tts_path:
418
  voice_over_folder_path = 'voice_over'
419
  os.makedirs(voice_over_folder_path, exist_ok=True)
 
432
  saved_voice_path = os.path.join(voice_over_folder_path, voice_filename)
433
  shutil.copy2(audio_input, saved_voice_path)
434
  voice_over_path = saved_voice_path
435
+
436
  if voice_over_path:
437
  try:
438
  progress(0.2, desc="Processing audio...")
439
  if generation_cancelled:
440
  return None, "Generation cancelled"
441
+
442
  voice_over_audio = AudioFileClip(voice_over_path)
443
  target_duration_seconds = voice_over_audio.duration
444
  linelevel_subtitles, _ = process_voiceover_to_subtitles(voice_over_path)
445
+
446
  if generation_cancelled:
447
  voice_over_audio.close()
448
  return None, "Generation cancelled"
 
452
  if not bg_music_path:
453
  return None, "Need text/audio or background music"
454
  target_duration_seconds = duration_minutes * 60
455
+
456
  progress(0.3, desc="Preparing audio...")
457
+
458
  if generation_cancelled:
459
  if voice_over_audio:
460
  voice_over_audio.close()
461
  return None, "Generation cancelled"
462
+
463
  audio_tracks = []
464
  if voice_over_audio:
465
  audio_tracks.append(voice_over_audio)
466
+
467
  if bg_music_path:
468
  try:
469
  background_audio = AudioFileClip(bg_music_path)
470
+ background_audio = background_audio.fx(afx.volumex, 0.10) # Increased from 0.015 to 0.10
471
  background_audio = background_audio.fx(afx.audio_loop, duration=target_duration_seconds)
472
  audio_tracks.append(background_audio)
473
  except Exception as e:
474
  print(f"Background music error: {e}")
475
+
476
  final_audio = CompositeAudioClip(audio_tracks) if len(audio_tracks) > 1 else (audio_tracks[0] if audio_tracks else None)
477
+
478
  progress(0.4, desc="Setting up video...")
479
+
480
  if generation_cancelled:
481
  cleanup_resources()
482
  return None, "Generation cancelled"
483
+
484
  if video_quality == "High":
485
  target_height, bitrate, preset, crf = 1080, "8000k", "veryfast", "20"
486
  elif video_quality == "Standard":
487
  target_height, bitrate, preset, crf = 720, "4000k", "veryfast", "24"
488
  else:
489
  target_height, bitrate, preset, crf = 480, "1000k", "ultrafast", "28"
490
+
491
  progress(0.5, desc="Processing clips...")
492
+
493
  video_clips = []
494
  current_duration = 0
495
  file_index = 0
496
  safety_counter = 0
497
  max_iterations = len(all_files) * 3
498
+
499
  while current_duration < target_duration_seconds and safety_counter < max_iterations:
500
  if generation_cancelled:
501
  for clip in video_clips:
 
505
  pass
506
  cleanup_resources()
507
  return None, "Generation cancelled"
508
+
509
  if file_index >= len(all_files):
510
  file_index = 0
511
  random.shuffle(all_files)
512
+
513
  video_file = all_files[file_index]
514
  file_index += 1
515
  safety_counter += 1
516
+
517
  try:
518
  full_clip = VideoFileClip(os.path.join(source_path, video_file))
519
  current_video_clip = full_clip
520
+
521
  if generation_cancelled:
522
  full_clip.close()
523
  cleanup_resources()
524
  return None, "Generation cancelled"
525
+
526
  if full_clip.h != target_height:
527
  aspect_ratio = full_clip.w / full_clip.h
528
  new_width = int(target_height * aspect_ratio)
 
532
  full_clip = full_clip.resize((new_width, adjusted_height))
533
  else:
534
  full_clip = ensure_even_dimensions(full_clip)
535
+
536
  subclip = get_random_subclip_and_slow(full_clip)
537
  remaining_duration = target_duration_seconds - current_duration
538
+
539
  if subclip.duration > remaining_duration:
540
  subclip = subclip.subclip(0, remaining_duration)
541
+
542
  video_clips.append(ensure_even_dimensions(subclip))
543
  current_duration += subclip.duration
544
  progress(0.5 + (safety_counter * 0.1 / max_iterations), desc=f"Clip {len(video_clips)}")
545
  except Exception as e:
546
  print(f"Error: {e}")
547
  continue
548
+
549
  if generation_cancelled:
550
  for clip in video_clips:
551
  try:
 
554
  pass
555
  cleanup_resources()
556
  return None, "Generation cancelled"
557
+
558
  if not video_clips:
559
  return None, "No clips processed"
560
 
561
+ # Fix: Ensure video clips match audio duration exactly
562
  total_video_duration = sum(clip.duration for clip in video_clips)
563
  duration_diff = total_video_duration - target_duration_seconds
564
+
565
  if abs(duration_diff) > 0.1:
566
  if duration_diff > 0:
567
+ # Video is longer than audio - trim the end
568
  trim_amount = duration_diff
569
+ if video_clips[-1].duration > trim_amount:
570
+ new_last_clip = video_clips[-1].subclip(0, video_clips[-1].duration - trim_amount)
571
+ video_clips[-1] = new_last_clip
572
  else:
573
+ # Video is shorter than audio - loop the last clip to extend
574
  extend_amount = abs(duration_diff)
575
  new_last_clip = video_clips[-1].fx(vfx.loop, duration=video_clips[-1].duration + extend_amount)
576
  video_clips[-1] = new_last_clip
577
+
578
  progress(0.6, desc="Applying transitions...")
579
  transition_duration = {"Snap Cut": 0.1, "Whip Pan": 0.3, "Dreamy Fade": 0.8, "Smooth Blend": 0.5, "Ken Burns Zoom": 0.5}.get(transition_type, 0.5)
580
+
581
  processed_clips = []
582
  for i in range(len(video_clips)):
583
  if i == 0:
 
592
  else:
593
  _, clip_with_transition = apply_transition_effect(video_clips[i-1], video_clips[i], transition_type, transition_duration)
594
  processed_clips.append(clip_with_transition)
595
+
596
  progress(0.7, desc="Concatenating...")
597
+
598
  if generation_cancelled:
599
  for c in processed_clips:
600
  try:
 
603
  pass
604
  cleanup_resources()
605
  return None, "Generation cancelled"
606
+
607
  if transition_type == "Snap Cut":
608
  final_video_only = concatenate_videoclips(processed_clips, method="compose")
609
  else:
610
  final_video_only = concatenate_videoclips(processed_clips, method="compose", padding=-transition_duration)
611
+
612
  final_video_only = ensure_even_dimensions(final_video_only)
613
  current_video_clip = final_video_only
614
+
615
+ # Fix: Loop video if shorter than audio to prevent black screen
616
+ if final_audio and final_video_only.duration < final_audio.duration:
617
+ final_video_only = final_video_only.fx(vfx.loop, duration=final_audio.duration)
618
+
619
  progress(0.8, desc="Adding overlays...")
620
+
621
  if generation_cancelled:
622
  try:
623
  final_video_only.close()
 
625
  pass
626
  cleanup_resources()
627
  return None, "Generation cancelled"
628
+
629
  all_subtitle_clips = []
630
  if linelevel_subtitles:
631
  for line in linelevel_subtitles:
 
642
  except Exception as e:
643
  print(f"Subtitle error: {e}")
644
  continue
645
+
646
  all_clips = [final_video_only.set_opacity(0.65)]
647
  if all_subtitle_clips:
648
  all_clips.extend(all_subtitle_clips)
649
  if title_text and title_text.strip():
650
  title_clips = create_title_overlay(title_text, final_video_only.size, duration=4)
651
  all_clips.extend(title_clips)
652
+
653
  final_video = CompositeVideoClip(all_clips)
654
  current_video_clip = final_video
655
+
656
  if final_audio:
657
  final_video = final_video.set_audio(final_audio)
658
+
659
  progress(0.9, desc="Exporting...")
660
+
661
  if generation_cancelled:
662
  try:
663
  final_video.close()
 
665
  pass
666
  cleanup_resources()
667
  return None, "Generation cancelled"
668
+
669
  output_filename = f'video_{timestamp}.mp4'
670
  final_output_path = os.path.join(output_path, output_filename)
671
+
672
  try:
673
  final_video.write_videofile(
674
  final_output_path,
 
685
  if generation_cancelled:
686
  return None, "Generation cancelled"
687
  return None, f"Export error: {str(e)}"
688
+
689
  progress(1.0, desc="Done")
690
+
691
  if generation_cancelled:
692
  try:
693
  if os.path.exists(final_output_path):
 
696
  pass
697
  cleanup_resources()
698
  return None, "Generation cancelled"
699
+
700
  try:
701
  final_video.close()
702
  if voice_over_audio:
 
704
  current_video_clip = None
705
  except:
706
  pass
707
+
708
  audio_source = ""
709
  if text_input and text_input.strip():
710
  audio_source = f"TTS ({AVAILABLE_VOICES[voice_selection]['name'] if voice_selection in AVAILABLE_VOICES else 'Puck'})"
 
712
  audio_source = "Uploaded Audio"
713
  else:
714
  audio_source = "Background Music"
715
+
716
  summary = f"Complete\n{output_filename}\n{audio_source}\n{transition_type}\n{target_duration_seconds:.1f}s\n{len(linelevel_subtitles) if linelevel_subtitles else 0} subtitles"
717
  return final_output_path, summary
718
 
719
+ # Gradio Interface
720
  with gr.Blocks(title="Video Generator", theme=gr.themes.Soft()) as interface:
721
  gr.Markdown("# 🎬 AI Video Generator")
722
  gr.Markdown("Upload video clips to `video_clips` folder and optionally background music to `background_music` folder.")
 
758
  server_name="0.0.0.0",
759
  server_port=7860,
760
  show_error=True
761
+ )
762
+ final_video_only