Shreevathsam commited on
Commit
78f583a
·
verified ·
1 Parent(s): 3e31f54

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +4 -76
app.py CHANGED
@@ -17,13 +17,11 @@ import urllib.request
17
  from google import genai
18
  from google.genai import types
19
 
20
- # Create necessary directories
21
  os.makedirs('video_clips', exist_ok=True)
22
  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
27
  GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY', '')
28
  if GOOGLE_API_KEY:
29
  os.environ['GOOGLE_API_KEY'] = GOOGLE_API_KEY
@@ -369,30 +367,21 @@ 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
-
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,20 +389,16 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
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,17 +417,14 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
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,50 +434,39 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
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,24 +476,19 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
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,20 +498,16 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
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,30 +516,22 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
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,9 +546,7 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
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,21 +555,15 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
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,7 +571,6 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
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,22 +587,17 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
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,10 +605,8 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
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,9 +623,7 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
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,7 +632,6 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
696
  pass
697
  cleanup_resources()
698
  return None, "Generation cancelled"
699
-
700
  try:
701
  final_video.close()
702
  if voice_over_audio:
@@ -704,7 +639,6 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
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,15 +646,12 @@ def merge_videos_with_subtitles(text_input, voice_selection, audio_input, title_
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.")
723
-
724
  with gr.Row():
725
  with gr.Column():
726
  text_input = gr.Textbox(label="Text for TTS", lines=4, placeholder="Enter text to convert to speech...")
@@ -741,11 +672,9 @@ with gr.Blocks(title="Video Generator", theme=gr.themes.Soft()) as interface:
741
  with gr.Row():
742
  submit_btn = gr.Button("🎥 Generate Video", variant="primary", size="lg")
743
  stop_btn = gr.Button("⏹️ Stop", variant="stop", size="lg")
744
-
745
  with gr.Column():
746
  video_output = gr.Video(label="Generated Video")
747
  summary_output = gr.Textbox(label="Status", lines=8)
748
-
749
  submit_btn.click(
750
  fn=merge_videos_with_subtitles,
751
  inputs=[text_input, voice_dropdown, audio_input, title_input, duration_slider, quality_radio, transition_radio],
@@ -758,5 +687,4 @@ if __name__ == "__main__":
758
  server_name="0.0.0.0",
759
  server_port=7860,
760
  show_error=True
761
- )
762
- final_video_only
 
17
  from google import genai
18
  from google.genai import types
19
 
 
20
  os.makedirs('video_clips', exist_ok=True)
21
  os.makedirs('background_music', exist_ok=True)
22
  os.makedirs('voice_over', exist_ok=True)
23
  os.makedirs('exports', exist_ok=True)
24
 
 
25
  GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY', '')
26
  if GOOGLE_API_KEY:
27
  os.environ['GOOGLE_API_KEY'] = GOOGLE_API_KEY
 
367
  generation_cancelled = False
368
  current_video_clip = None
369
  progress(0, desc="Starting...")
 
370
  if generation_cancelled:
371
  return None, "Generation cancelled"
 
372
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
 
373
  source_path = 'video_clips'
374
  if not os.path.isdir(source_path):
375
+ return None, "Video clips folder not found"
 
376
  output_path = 'exports'
377
  os.makedirs(output_path, exist_ok=True)
 
378
  video_extensions = ('.mp4', '.avi', '.mkv', '.mov')
379
  all_files = [f for f in os.listdir(source_path) if f.lower().endswith(video_extensions)]
 
380
  if not all_files:
381
+ return None, "No video files found"
 
382
  random.shuffle(all_files)
 
383
  if generation_cancelled:
384
  return None, "Generation cancelled"
 
385
  bg_music_path = None
386
  bg_music_folder_path = 'background_music'
387
  if os.path.isdir(bg_music_folder_path):
 
389
  possible_files = [f for f in os.listdir(bg_music_folder_path) if f.lower().endswith(audio_extensions) and not f.startswith('voiceover_')]
390
  if len(possible_files) >= 1:
391
  bg_music_path = os.path.join(bg_music_folder_path, possible_files[0])
 
392
  target_duration_seconds = 0
393
  voice_over_audio = None
394
  linelevel_subtitles = None
395
  voice_over_path = None
 
396
  if text_input and text_input.strip():
397
  progress(0.1, desc="Generating TTS...")
398
  voice_name = AVAILABLE_VOICES[voice_selection]["name"] if voice_selection in AVAILABLE_VOICES else "Puck"
399
  tts_path, tts_message = generate_tts_audio(text_input, voice_name)
 
400
  if generation_cancelled:
401
  return None, "Generation cancelled"
 
402
  if tts_path:
403
  voice_over_folder_path = 'voice_over'
404
  os.makedirs(voice_over_folder_path, exist_ok=True)
 
417
  saved_voice_path = os.path.join(voice_over_folder_path, voice_filename)
418
  shutil.copy2(audio_input, saved_voice_path)
419
  voice_over_path = saved_voice_path
 
420
  if voice_over_path:
421
  try:
422
  progress(0.2, desc="Processing audio...")
423
  if generation_cancelled:
424
  return None, "Generation cancelled"
 
425
  voice_over_audio = AudioFileClip(voice_over_path)
426
  target_duration_seconds = voice_over_audio.duration
427
  linelevel_subtitles, _ = process_voiceover_to_subtitles(voice_over_path)
 
428
  if generation_cancelled:
429
  voice_over_audio.close()
430
  return None, "Generation cancelled"
 
434
  if not bg_music_path:
435
  return None, "Need text/audio or background music"
436
  target_duration_seconds = duration_minutes * 60
 
437
  progress(0.3, desc="Preparing audio...")
 
438
  if generation_cancelled:
439
  if voice_over_audio:
440
  voice_over_audio.close()
441
  return None, "Generation cancelled"
 
442
  audio_tracks = []
443
  if voice_over_audio:
444
  audio_tracks.append(voice_over_audio)
 
445
  if bg_music_path:
446
  try:
447
  background_audio = AudioFileClip(bg_music_path)
448
+ background_audio = background_audio.fx(afx.volumex, 0.10)
449
  background_audio = background_audio.fx(afx.audio_loop, duration=target_duration_seconds)
450
  audio_tracks.append(background_audio)
451
  except Exception as e:
452
  print(f"Background music error: {e}")
 
453
  final_audio = CompositeAudioClip(audio_tracks) if len(audio_tracks) > 1 else (audio_tracks[0] if audio_tracks else None)
 
454
  progress(0.4, desc="Setting up video...")
 
455
  if generation_cancelled:
456
  cleanup_resources()
457
  return None, "Generation cancelled"
 
458
  if video_quality == "High":
459
  target_height, bitrate, preset, crf = 1080, "8000k", "veryfast", "20"
460
  elif video_quality == "Standard":
461
  target_height, bitrate, preset, crf = 720, "4000k", "veryfast", "24"
462
  else:
463
  target_height, bitrate, preset, crf = 480, "1000k", "ultrafast", "28"
 
464
  progress(0.5, desc="Processing clips...")
 
465
  video_clips = []
466
  current_duration = 0
467
  file_index = 0
468
  safety_counter = 0
469
  max_iterations = len(all_files) * 3
 
470
  while current_duration < target_duration_seconds and safety_counter < max_iterations:
471
  if generation_cancelled:
472
  for clip in video_clips:
 
476
  pass
477
  cleanup_resources()
478
  return None, "Generation cancelled"
 
479
  if file_index >= len(all_files):
480
  file_index = 0
481
  random.shuffle(all_files)
 
482
  video_file = all_files[file_index]
483
  file_index += 1
484
  safety_counter += 1
 
485
  try:
486
  full_clip = VideoFileClip(os.path.join(source_path, video_file))
487
  current_video_clip = full_clip
 
488
  if generation_cancelled:
489
  full_clip.close()
490
  cleanup_resources()
491
  return None, "Generation cancelled"
 
492
  if full_clip.h != target_height:
493
  aspect_ratio = full_clip.w / full_clip.h
494
  new_width = int(target_height * aspect_ratio)
 
498
  full_clip = full_clip.resize((new_width, adjusted_height))
499
  else:
500
  full_clip = ensure_even_dimensions(full_clip)
 
501
  subclip = get_random_subclip_and_slow(full_clip)
502
  remaining_duration = target_duration_seconds - current_duration
 
503
  if subclip.duration > remaining_duration:
504
  subclip = subclip.subclip(0, remaining_duration)
 
505
  video_clips.append(ensure_even_dimensions(subclip))
506
  current_duration += subclip.duration
507
  progress(0.5 + (safety_counter * 0.1 / max_iterations), desc=f"Clip {len(video_clips)}")
508
  except Exception as e:
509
  print(f"Error: {e}")
510
  continue
 
511
  if generation_cancelled:
512
  for clip in video_clips:
513
  try:
 
516
  pass
517
  cleanup_resources()
518
  return None, "Generation cancelled"
 
519
  if not video_clips:
520
  return None, "No clips processed"
 
 
521
  total_video_duration = sum(clip.duration for clip in video_clips)
522
  duration_diff = total_video_duration - target_duration_seconds
 
523
  if abs(duration_diff) > 0.1:
524
  if duration_diff > 0:
 
525
  trim_amount = duration_diff
526
  if video_clips[-1].duration > trim_amount:
527
  new_last_clip = video_clips[-1].subclip(0, video_clips[-1].duration - trim_amount)
528
  video_clips[-1] = new_last_clip
529
  else:
 
530
  extend_amount = abs(duration_diff)
531
  new_last_clip = video_clips[-1].fx(vfx.loop, duration=video_clips[-1].duration + extend_amount)
532
  video_clips[-1] = new_last_clip
 
533
  progress(0.6, desc="Applying transitions...")
534
  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)
 
535
  processed_clips = []
536
  for i in range(len(video_clips)):
537
  if i == 0:
 
546
  else:
547
  _, clip_with_transition = apply_transition_effect(video_clips[i-1], video_clips[i], transition_type, transition_duration)
548
  processed_clips.append(clip_with_transition)
 
549
  progress(0.7, desc="Concatenating...")
 
550
  if generation_cancelled:
551
  for c in processed_clips:
552
  try:
 
555
  pass
556
  cleanup_resources()
557
  return None, "Generation cancelled"
 
558
  if transition_type == "Snap Cut":
559
  final_video_only = concatenate_videoclips(processed_clips, method="compose")
560
  else:
561
  final_video_only = concatenate_videoclips(processed_clips, method="compose", padding=-transition_duration)
 
562
  final_video_only = ensure_even_dimensions(final_video_only)
563
  current_video_clip = final_video_only
 
 
564
  if final_audio and final_video_only.duration < final_audio.duration:
565
  final_video_only = final_video_only.fx(vfx.loop, duration=final_audio.duration)
 
566
  progress(0.8, desc="Adding overlays...")
 
567
  if generation_cancelled:
568
  try:
569
  final_video_only.close()
 
571
  pass
572
  cleanup_resources()
573
  return None, "Generation cancelled"
 
574
  all_subtitle_clips = []
575
  if linelevel_subtitles:
576
  for line in linelevel_subtitles:
 
587
  except Exception as e:
588
  print(f"Subtitle error: {e}")
589
  continue
 
590
  all_clips = [final_video_only.set_opacity(0.65)]
591
  if all_subtitle_clips:
592
  all_clips.extend(all_subtitle_clips)
593
  if title_text and title_text.strip():
594
  title_clips = create_title_overlay(title_text, final_video_only.size, duration=4)
595
  all_clips.extend(title_clips)
 
596
  final_video = CompositeVideoClip(all_clips)
597
  current_video_clip = final_video
 
598
  if final_audio:
599
  final_video = final_video.set_audio(final_audio)
 
600
  progress(0.9, desc="Exporting...")
 
601
  if generation_cancelled:
602
  try:
603
  final_video.close()
 
605
  pass
606
  cleanup_resources()
607
  return None, "Generation cancelled"
 
608
  output_filename = f'video_{timestamp}.mp4'
609
  final_output_path = os.path.join(output_path, output_filename)
 
610
  try:
611
  final_video.write_videofile(
612
  final_output_path,
 
623
  if generation_cancelled:
624
  return None, "Generation cancelled"
625
  return None, f"Export error: {str(e)}"
 
626
  progress(1.0, desc="Done")
 
627
  if generation_cancelled:
628
  try:
629
  if os.path.exists(final_output_path):
 
632
  pass
633
  cleanup_resources()
634
  return None, "Generation cancelled"
 
635
  try:
636
  final_video.close()
637
  if voice_over_audio:
 
639
  current_video_clip = None
640
  except:
641
  pass
 
642
  audio_source = ""
643
  if text_input and text_input.strip():
644
  audio_source = f"TTS ({AVAILABLE_VOICES[voice_selection]['name'] if voice_selection in AVAILABLE_VOICES else 'Puck'})"
 
646
  audio_source = "Uploaded Audio"
647
  else:
648
  audio_source = "Background Music"
 
649
  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"
650
  return final_output_path, summary
651
 
 
652
  with gr.Blocks(title="Video Generator", theme=gr.themes.Soft()) as interface:
653
  gr.Markdown("# 🎬 AI Video Generator")
654
  gr.Markdown("Upload video clips to `video_clips` folder and optionally background music to `background_music` folder.")
 
655
  with gr.Row():
656
  with gr.Column():
657
  text_input = gr.Textbox(label="Text for TTS", lines=4, placeholder="Enter text to convert to speech...")
 
672
  with gr.Row():
673
  submit_btn = gr.Button("🎥 Generate Video", variant="primary", size="lg")
674
  stop_btn = gr.Button("⏹️ Stop", variant="stop", size="lg")
 
675
  with gr.Column():
676
  video_output = gr.Video(label="Generated Video")
677
  summary_output = gr.Textbox(label="Status", lines=8)
 
678
  submit_btn.click(
679
  fn=merge_videos_with_subtitles,
680
  inputs=[text_input, voice_dropdown, audio_input, title_input, duration_slider, quality_radio, transition_radio],
 
687
  server_name="0.0.0.0",
688
  server_port=7860,
689
  show_error=True
690
+ )