skylinkd commited on
Commit
ddde70c
Β·
verified Β·
1 Parent(s): 337c45e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +213 -252
app.py CHANGED
@@ -58,7 +58,7 @@ os.makedirs(TEMP_DIR, exist_ok=True)
58
  model_load_lock = Lock()
59
  loaded_detectors = {}
60
  whisper_model = None
61
- whisper_model_name = "" # <-- ADD THIS LINE
62
 
63
  # --- Default Presets for Transfer Tab (Flat Dictionary) ---
64
  DEFAULT_LINK_PRESETS = {
@@ -238,14 +238,13 @@ def get_detector(name):
238
  return loaded_detectors[name]
239
 
240
  def load_whisper_model(model_name="base"):
241
- global whisper_model, whisper_model_name # <-- Add whisper_model_name here
242
  if whisper:
243
  with model_load_lock:
244
- # The check now uses our own variable instead of the model's property
245
  if whisper_model is None or whisper_model_name != model_name:
246
  print(f"Loading Whisper model '{model_name}'... (This may download files on first run)")
247
  whisper_model = whisper.load_model(model_name)
248
- whisper_model_name = model_name # <-- Set our variable after loading
249
  print("Whisper model loaded.")
250
  return whisper_model
251
  return None
@@ -267,10 +266,8 @@ def parse_color(color_str):
267
  if color_str.startswith('rgba'):
268
  parts = re.findall(r"[\d\.]+", color_str)
269
  if len(parts) >= 3:
270
- # Convert float parts to integers
271
  return (int(float(parts[0])), int(float(parts[1])), int(float(parts[2])))
272
 
273
- # If it's a hex string or a named color, PIL can handle it directly.
274
  return color_str
275
 
276
 
@@ -600,7 +597,7 @@ def create_zoom_videos(files, duration, zoom_ratio, zoom_direction, combine_vide
600
  if audio_for_clip:
601
  cmd.extend(["-i", audio_for_clip, "-c:a", "aac", "-shortest"])
602
 
603
- cmd.extend(["-vf", zoom_filter, "-c:v", "libx264", "-t", str(duration), "-pix_fmt", "yuv420p", "-b:v", "5M", "-y", output_path])
604
  run_ffmpeg_command(cmd, f"Creating zoom video for {os.path.basename(input_path)}")
605
 
606
  batch_kwargs = {}
@@ -794,11 +791,9 @@ def remove_video_background(video_path):
794
  return output_video_path
795
 
796
  def transcribe_media(media_path, model_name):
797
- if media_path is None:
798
- raise gr.Error("Please upload a video or audio file first.")
799
  model = load_whisper_model(model_name)
800
- if model is None:
801
- raise gr.Error("Whisper model is not available.")
802
  audio_path = media_path.name
803
  base_name = os.path.splitext(os.path.basename(media_path.name))[0]
804
 
@@ -808,16 +803,13 @@ def transcribe_media(media_path, model_name):
808
  run_ffmpeg_command(["ffmpeg", "-i", audio_path, "-q:a", "0", "-map", "a", "-y", audio_path_temp])
809
  audio_path = audio_path_temp
810
  except gr.Error as e:
811
- if "does not contain any stream" in str(e):
812
- raise gr.Error("The uploaded video has no audio track.")
813
- else:
814
- raise e
815
 
816
  result = model.transcribe(audio_path, verbose=False)
817
 
818
  def format_ts(s):
819
- h, r = divmod(s, 3600)
820
- m, s = divmod(r, 60)
821
  return f"{int(h):02}:{int(m):02}:{int(s):02},{int((s-int(s))*1000):03}"
822
 
823
  srt_path = os.path.join(TEMP_DIR, f"{base_name}.srt")
@@ -830,127 +822,95 @@ def transcribe_media(media_path, model_name):
830
  srt_f.write(f"{i + 1}\n{format_ts(start)} --> {format_ts(end)}\n{text}\n\n")
831
  vtt_f.write(f"{format_ts(start).replace(',', '.')} --> {format_ts(end).replace(',', '.')}\n{text}\n\n")
832
 
833
- return result["text"], [srt_path, vtt_path], result.get("segments", [])
834
 
835
  def transcribe_and_prep_burn(media_file, model_name):
836
- if not media_file:
837
- raise gr.Error("Please upload a file first.")
838
-
839
  is_video = media_file.name.lower().endswith(('.mp4', '.mov', '.mkv', '.avi', '.webm'))
840
-
841
- full_text, subtitle_files, segments = transcribe_media(media_file, model_name)
842
-
843
- df_data = []
844
- if segments:
845
- for i, seg in enumerate(segments):
846
- df_data.append([i+1, f"{seg['start']:.2f}", f"{seg['end']:.2f}", seg['text']])
847
 
848
- if is_video:
849
- return full_text, subtitle_files, media_file.name, segments, df_data, gr.update(visible=True)
850
- else:
851
- return full_text, subtitle_files, None, segments, df_data, gr.update(visible=False)
 
 
 
852
 
853
- def burn_subtitles(video_path, srt_file_obj, font_size_scale, font_color):
854
- if not video_path: raise gr.Error("Original video path not found. Please re-transcribe.")
855
- if not srt_file_obj or not srt_file_obj[0].name: raise gr.Error("SRT file not found. Please re-transcribe.")
856
- srt_path = srt_file_obj[0].name
857
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
858
- output_video_path = os.path.join(TEMP_DIR, f"subtitled_video_{timestamp}.mp4")
859
- _, video_h = get_video_dimensions(video_path)
860
- if video_h == 0: video_h = 720
861
- divisor = 32 - (font_size_scale * 2)
862
- calculated_font_size = int(video_h / divisor)
863
- color_bgr = font_color[5:7] + font_color[3:5] + font_color[1:3]
864
- ffmpeg_color = f"&H00{color_bgr.upper()}"
865
- escaped_srt_path = srt_path.replace('\\', '/').replace(':', r'\:')
866
- vf_filter = f"subtitles='{escaped_srt_path}':force_style='Fontsize={calculated_font_size},PrimaryColour={ffmpeg_color},BorderStyle=1,Outline=1,Shadow=0.5,MarginV=15'"
867
- cmd = ["ffmpeg", "-i", video_path, "-vf", vf_filter, "-c:a", "copy", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-y", output_video_path]
868
- run_ffmpeg_command(cmd, "Burning subtitles into video...")
869
- return output_video_path
870
-
871
- def generate_ass_from_df(df_data, font_size_scale, font_color, outline_color, video_h):
872
- """Generates an .ass subtitle file content from a DataFrame and style settings."""
873
- if video_h == 0: video_h = 720
874
-
875
- # Calculate font sizes and margins based on video height
876
- divisor = 32 - (font_size_scale * 2)
877
- font_size = int(video_h / divisor)
878
- outline_width = max(1.0, font_size / 10.0)
879
- margin_v = int(video_h * 0.05)
880
-
881
- # Convert hex colors to ASS format (#RRGGBB -> &HBBGGRR)
882
- primary_bgr = font_color[5:7] + font_color[3:5] + font_color[1:3]
883
- primary_ass_color = f"&H00{primary_bgr.upper()}"
884
-
885
- outline_bgr = outline_color[5:7] + outline_color[3:5] + outline_color[1:3]
886
- outline_ass_color = f"&H00{outline_bgr.upper()}"
887
-
888
- # .ass file header
889
- header = f"""[Script Info]
890
- Title: Generated by Skriptz
891
- ScriptType: v4.00+
892
- WrapStyle: 0
893
- ScaledBorderAndShadow: yes
894
- YCbCr Matrix: None
895
-
896
- [V4+ Styles]
897
- Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
898
- Style: Default,Arial,{font_size},{primary_ass_color},&H000000FF,{outline_ass_color},&H00000000,0,0,0,0,100,100,0,0,1,{outline_width},1,2,10,10,{margin_v},1
899
-
900
- [Events]
901
- Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
902
- """
903
 
904
- # Format each line from the dataframe
905
- dialogue_lines = []
906
- for _, row in enumerate(df_data):
907
- # row format is [index, start_time, end_time, text]
908
- start_time_str = str(row[1])
909
- end_time_str = str(row[2])
910
- text = str(row[3]).replace('\n', '\\N') # Replace newlines with ASS newline character
911
-
912
- # Convert float seconds to H:MM:SS.ss format
913
- def format_ass_time(s):
914
- s = float(s)
915
- h, r = divmod(s, 3600)
916
- m, s = divmod(r, 60)
917
- return f"{int(h):d}:{int(m):02d}:{s:05.2f}"
918
-
919
- start_formatted = format_ass_time(start_time_str)
920
- end_formatted = format_ass_time(end_time_str)
921
 
922
- dialogue_lines.append(f"Dialogue: 0,{start_formatted},{end_formatted},Default,,0,0,0,,{text}")
 
 
 
 
 
 
 
923
 
924
- return header + "\n".join(dialogue_lines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
925
 
926
- def burn_subtitles_from_df(video_path, df_data, font_size_scale, font_color, outline_color):
927
- """Burns subtitles directly from DataFrame data onto the video."""
928
- if not video_path: raise gr.Error("Original video path not found. Please re-transcribe.")
929
-
930
- # Convert DataFrame object to list of lists if it's not already
931
- if hasattr(df_data, 'values'):
932
- df_data = df_data.values.tolist()
933
-
934
- if not df_data: raise gr.Error("No subtitle data to burn.")
935
 
936
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
937
- output_video_path = os.path.join(TEMP_DIR, f"subtitled_video_{timestamp}.mp4")
938
- ass_path = os.path.join(TEMP_DIR, f"temp_subs_{timestamp}.ass")
939
 
940
- _, video_h = get_video_dimensions(video_path)
941
- ass_content = generate_ass_from_df(df_data, font_size_scale, font_color, outline_color, video_h)
942
-
943
- with open(ass_path, 'w', encoding='utf-8') as f:
944
- f.write(ass_content)
945
 
946
- # Use ffmpeg to burn the .ass file
947
- escaped_ass_path = ass_path.replace('\\', '/').replace(':', r'\:')
948
- vf_filter = f"subtitles='{escaped_ass_path}'"
949
- cmd = ["ffmpeg", "-i", video_path, "-vf", vf_filter, "-c:a", "copy", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-y", output_video_path]
950
- run_ffmpeg_command(cmd, "Burning subtitles into video...")
951
-
952
- os.remove(ass_path)
953
- return output_video_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
954
 
955
  def remove_background_single(input_path, output_path, **kwargs):
956
  with Image.open(input_path) as img:
@@ -1096,18 +1056,16 @@ def preview_color_grading_ffmpeg(image_np, brightness, contrast, saturation, sha
1096
 
1097
  subprocess.run(cmd, capture_output=True, text=True, check=False)
1098
 
1099
- # FIX: Use a 'with' statement to properly open and close the file handle, preventing file lock errors on Windows.
1100
  if os.path.exists(output_path):
1101
  with Image.open(output_path) as img:
1102
- return img.copy() # Load into memory and return a copy
1103
  else:
1104
- return Image.fromarray(image_np) # Fallback
1105
 
1106
  except Exception as e:
1107
  print(f"Error in FFMPEG preview: {e}")
1108
  return Image.fromarray(image_np)
1109
  finally:
1110
- # This will now execute after the file handle from Image.open() is closed.
1111
  if os.path.exists(input_path): os.remove(input_path)
1112
  if os.path.exists(output_path): os.remove(output_path)
1113
 
@@ -1396,6 +1354,7 @@ def auto_jump_cut(video_path, silence_threshold, min_silence_duration, resolutio
1396
 
1397
  return output_video_path
1398
 
 
1399
  # --- STORYBOARD / ANIMATIC CREATOR FUNCTIONS ---
1400
 
1401
  def get_file_type(file_path):
@@ -1625,7 +1584,7 @@ def set_resolution_from_first_asset(timeline_state):
1625
  gr.Warning(f"Could not get dimensions for the first asset: '{first_item['name']}'.")
1626
  return gr.update(), gr.update()
1627
 
1628
- def create_animatic(timeline_data, audio_path, out_w, out_h):
1629
  if not timeline_data:
1630
  raise gr.Error("Timeline is empty. Please add assets to the timeline.")
1631
 
@@ -1645,25 +1604,48 @@ def create_animatic(timeline_data, audio_path, out_w, out_h):
1645
  for i, item in enumerate(timeline_data):
1646
  item_path, item_type, item_duration = item['path'], item['type'], item['duration']
1647
  output_clip_path = os.path.join(job_temp_dir, f"clip_{i:04d}.mp4")
1648
- vf_filter = f"scale={output_res_str}:force_original_aspect_ratio=decrease,pad={output_res_str}:(ow-iw)/2:(oh-ih)/2"
1649
 
1650
  cmd = ["ffmpeg"]
1651
- if item_type == "image":
1652
- cmd.extend(["-loop", "1"])
1653
- elif item_type == "video":
 
 
 
 
 
1654
  start_time = item.get('start_time', 0)
1655
  if start_time > 0:
1656
  cmd.extend(["-ss", str(start_time)])
1657
 
1658
  cmd.extend(["-i", item_path])
1659
 
1660
- cmd.extend([
1661
- "-t", str(item_duration), "-c:v", "libx264",
1662
- "-pix_fmt", "yuv420p", "-vf", vf_filter,
1663
- "-an", "-y", output_clip_path
1664
- ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1665
 
1666
- run_ffmpeg_command(cmd, f"Processing clip {i+1}")
1667
  clip_paths.append(output_clip_path)
1668
 
1669
  file_list_path = os.path.join(job_temp_dir, "files.txt")
@@ -1671,18 +1653,22 @@ def create_animatic(timeline_data, audio_path, out_w, out_h):
1671
  for path in clip_paths:
1672
  f.write(f"file '{os.path.abspath(path)}'\n")
1673
 
1674
- silent_animatic_path = os.path.join(job_temp_dir, "silent_final.mp4")
1675
- run_ffmpeg_command(["ffmpeg", "-f", "concat", "-safe", "0", "-i", file_list_path, "-c", "copy", "-y", silent_animatic_path], "Joining video clips")
 
1676
 
1677
  final_output_path = os.path.join(TEMP_DIR, f"animatic_final_{timestamp}.mp4")
1678
- if audio_path:
1679
- run_ffmpeg_command(["ffmpeg", "-i", silent_animatic_path, "-i", audio_path, "-c:v", "copy", "-c:a", "aac", "-shortest", "-y", final_output_path], "Muxing audio")
 
 
 
1680
  else:
1681
- shutil.move(silent_animatic_path, final_output_path)
 
1682
 
1683
  shutil.rmtree(job_temp_dir)
1684
  return final_output_path
1685
-
1686
  def detect_bpm(audio_path):
1687
  if not audio_path:
1688
  return "Please upload an audio track first."
@@ -1740,7 +1726,8 @@ def create_rhythmic_animatic(timeline_data, audio_path, measure_choice, out_w, o
1740
 
1741
  gr.Info(f"Re-timed {len(rhythmic_timeline)} clips to ~{clip_duration:.2f}s each based on {tempo:.2f} BPM.")
1742
 
1743
- return create_animatic(rhythmic_timeline, audio_path, out_w, out_h)
 
1744
 
1745
  # --- NEW CREATIVE FUNCTIONS ---
1746
 
@@ -2632,6 +2619,7 @@ with gr.Blocks(
2632
  clear_timeline_btn = gr.Button("πŸ’₯ Clear Timeline")
2633
 
2634
  gr.Markdown("### 3. Output Settings")
 
2635
  animatic_audio = gr.Audio(label="Project Audio Track (Narration/Music)", type="filepath")
2636
  with gr.Row():
2637
  animatic_out_w = gr.Number(label="Output Width", value=1920)
@@ -2650,13 +2638,13 @@ with gr.Blocks(
2650
  )
2651
  generate_rhythmic_btn = gr.Button("🎢 Generate Rhythmic Animatic", variant="primary")
2652
 
2653
- animatic_output_video = gr.Video(label="Final Video Output", interactive=True, show_download_button=True)
2654
 
2655
  with gr.Column(scale=2, min_width=300):
2656
  with gr.Group():
2657
  gr.Markdown("### 4. Clip Properties")
2658
  gr.Info("Select a clip in the timeline table to edit it.")
2659
- clip_preview = gr.Video(label="Selected Clip Preview", interactive=True, elem_id="storyboard_clip_preview")
2660
  clip_duration_input = gr.Number(label="Set Duration (seconds)", interactive=True, precision=2)
2661
  update_clip_btn = gr.Button("πŸ”„ Update Clip Duration")
2662
 
@@ -2700,7 +2688,9 @@ with gr.Blocks(
2700
  clear_assets_btn.click(lambda: ([], gr.update(value=None), None), outputs=[assets_state, asset_gallery, asset_preview_gallery])
2701
  clear_timeline_btn.click(lambda: [], outputs=[timeline_state])
2702
 
2703
- generate_animatic_btn.click(fn=create_animatic, inputs=[timeline_state, animatic_audio, animatic_out_w, animatic_out_h], outputs=animatic_output_video).then(fn=None, js=fire_confetti_and_sound_js)
 
 
2704
 
2705
  analyze_bpm_btn.click(detect_bpm, animatic_audio, bpm_display)
2706
 
@@ -2729,7 +2719,7 @@ with gr.Blocks(
2729
 
2730
  slideshow_generate_btn = gr.Button("πŸš€ Generate Slideshow", variant="primary")
2731
  with gr.Column(scale=3):
2732
- slideshow_output_video = gr.Video(label="Generated Slideshow Video", show_download_button=True)
2733
 
2734
  slideshow_generate_btn.click(
2735
  fn=slideshow_wrapper,
@@ -2742,7 +2732,7 @@ with gr.Blocks(
2742
  gr.Info("Automatically cuts a source video to the beat of a music track, creating a dynamic music video.")
2743
  with gr.Row():
2744
  with gr.Column(scale=2):
2745
- remix_input_video = gr.Video(label="Upload Source Video")
2746
  remix_audio = gr.Audio(label="Upload Music Track", type="filepath")
2747
 
2748
  with gr.Accordion("βš™οΈ Remix Settings", open=True):
@@ -2768,7 +2758,7 @@ with gr.Blocks(
2768
 
2769
  remix_generate_btn = gr.Button("🎀 Generate Music Video", variant="primary")
2770
  with gr.Column(scale=3):
2771
- remix_output_video = gr.Video(label="Generated Music Video", show_download_button=True)
2772
 
2773
  remix_resolution.change(
2774
  fn=lambda choice: gr.update(visible=(choice == "Custom")),
@@ -2787,7 +2777,7 @@ with gr.Blocks(
2787
  gr.Info("Upload a long video, and this tool will find the most action-packed moments to create a short, dynamic trailer.")
2788
  with gr.Row():
2789
  with gr.Column(scale=2):
2790
- trailer_input_video = gr.Video(label="Upload Source Video")
2791
  trailer_music = gr.Audio(label="Add Background Music (Optional)", type="filepath")
2792
 
2793
  with gr.Accordion("βš™οΈ Trailer Settings", open=True):
@@ -2802,7 +2792,7 @@ with gr.Blocks(
2802
 
2803
  trailer_generate_btn = gr.Button("πŸš€ Generate Trailer", variant="primary")
2804
  with gr.Column(scale=3):
2805
- trailer_output_video = gr.Video(label="Generated Trailer Video", show_download_button=True)
2806
 
2807
  trailer_generate_btn.click(
2808
  fn=auto_trailer_wrapper,
@@ -2819,7 +2809,7 @@ with gr.Blocks(
2819
  gr.Info("Upload a video with an audio track to generate a dynamic waveform visualization.")
2820
  with gr.Row():
2821
  with gr.Column():
2822
- waveform_input_video = gr.Video(label="Input Video")
2823
  with gr.Row():
2824
  waveform_style = gr.Dropdown(["line", "p2p", "point"], value="line", label="Waveform Style")
2825
  waveform_size = gr.Textbox(value="800x200", label="Size (WxH)")
@@ -2828,7 +2818,7 @@ with gr.Blocks(
2828
  waveform_color = gr.ColorPicker(value="#38bdf8", label="Color")
2829
  waveform_btn = gr.Button("🎢 Generate Waveform Video", variant="primary")
2830
  with gr.Column():
2831
- waveform_output_video = gr.Video(label="Output Video with Waveform", show_download_button=True)
2832
  waveform_btn.click(
2833
  fn=generate_waveform_video,
2834
  inputs=[waveform_input_video, waveform_style, waveform_size, waveform_position, waveform_color],
@@ -2840,7 +2830,7 @@ with gr.Blocks(
2840
  gr.Info("Upload a main video and an overlay (video or image). The overlay will be placed on top of the main video.")
2841
  with gr.Row():
2842
  with gr.Column():
2843
- pip_main_video = gr.Video(label="Main Video (Background)")
2844
  pip_overlay_media = gr.File(label="Overlay Media (Video or Image)", file_types=["video", "image"])
2845
  with gr.Row():
2846
  pip_position = gr.Dropdown(
@@ -2850,7 +2840,7 @@ with gr.Blocks(
2850
  pip_scale = gr.Slider(0.01, 0.75, 0.25, step=0.01, label="Overlay Size")
2851
  pip_btn = gr.Button("✨ Create PiP Video", variant="primary")
2852
  with gr.Column():
2853
- pip_output_video = gr.Video(label="Output PiP Video", show_download_button=True)
2854
  pip_btn.click(
2855
  fn=create_pip_video,
2856
  inputs=[pip_main_video, pip_overlay_media, pip_position, pip_scale],
@@ -2909,7 +2899,7 @@ with gr.Blocks(
2909
  input_audio_i2v = gr.Audio(label="Add Music (Optional)", type="filepath")
2910
  compile_i2v_btn = gr.Button("🎬 Create Looping Video", variant="primary")
2911
  with gr.Column():
2912
- output_video_i2v = gr.Video(label="Output Looping Video", interactive=True, show_download_button=True)
2913
  compile_i2v_btn.click(image_to_looping_video, [input_image_i2v, duration_slider_i2v, input_audio_i2v], output_video_i2v).then(fn=None, js=fire_confetti_and_sound_js)
2914
 
2915
  with gr.TabItem("πŸ”Ž Zoom Video"):
@@ -2929,7 +2919,7 @@ with gr.Blocks(
2929
  i2zv_btn = gr.Button("✨ Create Zoom Video(s)", variant="primary")
2930
  with gr.Column():
2931
  i2zv_output_gallery = gr.Gallery(label="Output Video Previews", columns=2, object_fit="contain", visible=True)
2932
- i2zv_output_video = gr.Video(label="Combined Output Video", interactive=True, visible=False, show_download_button=True)
2933
  i2zv_output_zip = gr.File(label="Download All as .zip", interactive=False)
2934
 
2935
  i2zv_combine.change(
@@ -3019,7 +3009,7 @@ with gr.Blocks(
3019
  gr.Markdown("### Extract the very first and very last frames of a video.")
3020
  with gr.Row():
3021
  with gr.Column():
3022
- input_video_fl = gr.Video(label="Input Video")
3023
  extract_fl_btn = gr.Button("🎬 Extract Frames", variant="primary")
3024
  with gr.Column():
3025
  output_gallery_fl = gr.Gallery(label="Output Frames (First, Last)", columns=2, object_fit="contain", height="auto")
@@ -3029,7 +3019,7 @@ with gr.Blocks(
3029
  gr.Markdown("### Extract all individual frames from a video file.")
3030
  with gr.Row():
3031
  with gr.Column():
3032
- input_video_v2f = gr.Video(label="Input Video")
3033
  v2f_fps_display = gr.Textbox(label="Detected FPS", interactive=False, value="N/A")
3034
  with gr.Accordion("βš™οΈ Advanced Options", open=False):
3035
  v2f_skip_rate = gr.Slider(1, 30, 1, step=1, label="Extract Every Nth Frame")
@@ -3063,7 +3053,7 @@ with gr.Blocks(
3063
  f2v_height = gr.Number(label="Output Height", value=576, interactive=False)
3064
  compile_f2v_btn = gr.Button("πŸ“½οΈ Create Video", variant="primary")
3065
  with gr.Column():
3066
- output_video_f2v = gr.Video(label="Compiled Video", interactive=True, show_download_button=True)
3067
  f2v_resize.change(lambda x: [gr.update(interactive=x), gr.update(interactive=x)], f2v_resize, [f2v_width, f2v_height])
3068
  compile_f2v_btn.click(create_video_from_frames, [input_frames_f2v, fps_slider_f2v, f2v_rotation, f2v_resize, f2v_width, f2v_height], output_video_f2v).then(fn=None, js=fire_confetti_and_sound_js)
3069
 
@@ -3074,7 +3064,7 @@ with gr.Blocks(
3074
  gr.Info("1. Upload a video. A waveform will appear. 2. Adjust the 'Silence Threshold' slider so it's above the thin 'noise floor' line but below your voice peaks. 3. Process the video!")
3075
  with gr.Row():
3076
  with gr.Column(scale=2):
3077
- jumpcut_input_video = gr.Video(label="Input Video (with audio)")
3078
  jc_waveform_preview = gr.Image(label="Audio Waveform Preview", interactive=False)
3079
  with gr.Accordion("βš™οΈ Cut Settings", open=True):
3080
  jumpcut_threshold = gr.Slider(minimum=-60, maximum=-20, value=-30, step=1, label="Silence Threshold (dB)", info="Anything quieter than this is 'silence'. Lower values are more strict.")
@@ -3089,7 +3079,7 @@ with gr.Blocks(
3089
  jc_custom_h = gr.Number(label="Custom Height", value=1080)
3090
  jumpcut_btn = gr.Button("πŸ”ͺ Perform Jump-Cut", variant="primary")
3091
  with gr.Column(scale=3):
3092
- jumpcut_output_video = gr.Video(label="Edited Video Output", show_download_button=True)
3093
 
3094
  jumpcut_input_video.upload(fn=generate_waveform_preview, inputs=jumpcut_input_video, outputs=jc_waveform_preview)
3095
 
@@ -3110,14 +3100,14 @@ with gr.Blocks(
3110
  gr.Info("Apply a single transformation like inverting colors, flipping, or rotating to every frame of a video.")
3111
  with gr.Row():
3112
  with gr.Column():
3113
- vmanip_input_video = gr.Video(label="Input Video")
3114
  vmanip_operation_radio = gr.Radio(
3115
  ["Invert Colors", "Flip Horizontal", "Flip Vertical", "Rotate 90Β° Right", "Rotate 90Β° Left"],
3116
  label="Select Operation", value="Invert Colors"
3117
  )
3118
  vmanip_apply_btn = gr.Button("✨ Apply Manipulation", variant="primary")
3119
  with gr.Column():
3120
- vmanip_output_video = gr.Video(label="Output Video", interactive=True, show_download_button=True)
3121
 
3122
  vmanip_apply_btn.click(fn=manipulate_video, inputs=[vmanip_input_video, vmanip_operation_radio], outputs=vmanip_output_video).then(fn=None, js=fire_confetti_and_sound_js)
3123
 
@@ -3125,22 +3115,22 @@ with gr.Blocks(
3125
  gr.Markdown("### Create a forward-then-reverse video loop (Boomerang).")
3126
  with gr.Row():
3127
  with gr.Column():
3128
- input_video_pingpong = gr.Video(label="Input Video")
3129
  audio_option_pingpong = gr.Radio(["Remove Audio", "Original Audio Only", "Reverse Audio"], value="Remove Audio", label="Audio Handling")
3130
  pingpong_btn = gr.Button("πŸ“ Create Ping-Pong Video", variant="primary")
3131
  with gr.Column():
3132
- output_video_pingpong = gr.Video(label="Ping-Pong Video", interactive=True, show_download_button=True)
3133
  pingpong_btn.click(fn=ping_pong_video, inputs=[input_video_pingpong, audio_option_pingpong], outputs=output_video_pingpong).then(fn=None, js=fire_confetti_and_sound_js)
3134
 
3135
  with gr.TabItem("Reverse"):
3136
  gr.Markdown("### Reverse a video clip.")
3137
  with gr.Row():
3138
  with gr.Column():
3139
- input_video_reverse = gr.Video(label="Input Video")
3140
  audio_option_reverse = gr.Radio(["Remove Audio", "Reverse Audio"], value="Remove Audio", label="Audio Handling")
3141
  reverse_btn = gr.Button("πŸ”„ Reverse Video", variant="primary")
3142
  with gr.Column():
3143
- output_video_reverse = gr.Video(label="Reversed Video", interactive=True, show_download_button=True)
3144
  reverse_btn.click(fn=reverse_video, inputs=[input_video_reverse, audio_option_reverse], outputs=output_video_reverse).then(fn=None, js=fire_confetti_and_sound_js)
3145
 
3146
  with gr.TabItem("Merger"):
@@ -3151,7 +3141,7 @@ with gr.Blocks(
3151
  merger_input_videos = gr.File(label="Upload Videos (2 or more)", file_count="multiple", file_types=["video"])
3152
  merger_btn = gr.Button("πŸ”— Merge Videos", variant="primary")
3153
  with gr.Column():
3154
- merger_output_video = gr.Video(label="Merged Video", show_download_button=True)
3155
  merger_btn.click(
3156
  fn=merge_videos,
3157
  inputs=merger_input_videos,
@@ -3163,12 +3153,12 @@ with gr.Blocks(
3163
  gr.Info("This tool uses a two-pass process to analyze and stabilize shaky videos. Higher 'Shakiness' values are for very unstable footage. Higher 'Smoothing' creates a more fluid, gliding look but can introduce slight cropping/warping.")
3164
  with gr.Row():
3165
  with gr.Column():
3166
- stab_input_video = gr.Video(label="Input Shaky Video")
3167
  stab_shakiness = gr.Slider(minimum=1, maximum=10, value=5, step=1, label="Shakiness Level", info="How shaky is the source video? (1=low, 10=high)")
3168
  stab_smoothing = gr.Slider(minimum=1, maximum=50, value=10, step=1, label="Smoothing Amount", info="How much smoothing to apply. (More = smoother but might feel 'floaty')")
3169
  stab_btn = gr.Button("✨ Stabilize Video", variant="primary")
3170
  with gr.Column():
3171
- stab_output_video = gr.Video(label="Stabilized Output Video", show_download_button=True)
3172
  stab_btn.click(
3173
  fn=stabilize_video,
3174
  inputs=[stab_input_video, stab_shakiness, stab_smoothing],
@@ -3180,7 +3170,7 @@ with gr.Blocks(
3180
  gr.Info("Keyboard hotkeys enabled: K = Play/Pause, J = Back 1 Frame, L = Forward 1 Frame (hover mouse over video)")
3181
  with gr.Row():
3182
  with gr.Column(scale=2):
3183
- input_video_trim = gr.Video(label="Input Video", elem_id="video-trim-input")
3184
  with gr.Row():
3185
  set_start_btn = gr.Button("Set Current Frame as START")
3186
  set_end_btn = gr.Button("Set Current Frame as END")
@@ -3194,7 +3184,7 @@ with gr.Blocks(
3194
  trim_start_time = gr.Number(value=0, visible=False)
3195
  trim_end_time = gr.Number(value=0, visible=False)
3196
  with gr.Row():
3197
- output_video_trim = gr.Video(label="Trimmed Video", interactive=True, show_download_button=True)
3198
  get_current_time_js = """()=>{const e=document.querySelector("#video-trim-input video");return e?e.currentTime:0}"""
3199
  def get_frame_from_time_wrapper(v,t): return get_frame_at_time(v,t), f"{t:.3f}"
3200
  set_start_btn.click(fn=None, js=get_current_time_js, outputs=[trim_start_time])
@@ -3208,7 +3198,7 @@ with gr.Blocks(
3208
  gr.Markdown("### Visually crop a video.")
3209
  with gr.Row():
3210
  with gr.Column(scale=2):
3211
- crop_input_video = gr.Video(label="Input Video")
3212
  crop_preview_image = gr.Image(label="Frame Preview", interactive=False)
3213
  with gr.Column(scale=1):
3214
  gr.Markdown("#### Crop Settings")
@@ -3224,7 +3214,7 @@ with gr.Blocks(
3224
  crop_do_resize = gr.Checkbox(label="Enable Resizing", value=False)
3225
  crop_resize_w = gr.Number(label="Output Width", value=1024, interactive=False)
3226
  crop_resize_h = gr.Number(label="Output Height", value=576, interactive=False)
3227
- output_video_crop = gr.Video(label="Cropped Video", interactive=True, show_download_button=True)
3228
  crop_input_video.upload(fn=get_frame_at_time, inputs=crop_input_video, outputs=crop_preview_image)
3229
  crop_do_resize.change(lambda x: [gr.update(interactive=x), gr.update(interactive=x)], inputs=crop_do_resize, outputs=[crop_resize_w, crop_resize_h])
3230
  crop_btn.click(fn=crop_video, inputs=[crop_input_video, crop_x, crop_y, crop_w, crop_h, crop_do_resize, crop_resize_w, crop_resize_h], outputs=output_video_crop).then(fn=None, js=fire_confetti_and_sound_js)
@@ -3233,11 +3223,11 @@ with gr.Blocks(
3233
  gr.Markdown("### Create slow-motion or fast-forward videos.")
3234
  with gr.Row():
3235
  with gr.Column():
3236
- input_video_speed = gr.Video(label="Input Video")
3237
  speed_multiplier = gr.Slider(0.1, 10.0, 1.0, step=0.1, label="Speed Multiplier")
3238
  speed_btn = gr.Button("πŸƒ Change Speed", variant="primary")
3239
  with gr.Column():
3240
- output_video_speed = gr.Video(label="Modified Video", interactive=True, show_download_button=True)
3241
  speed_btn.click(fn=change_video_speed, inputs=[input_video_speed, speed_multiplier], outputs=output_video_speed).then(fn=None, js=fire_confetti_and_sound_js)
3242
 
3243
  with gr.TabItem("🎨 Effects & Overlays"):
@@ -3246,13 +3236,13 @@ with gr.Blocks(
3246
  gr.Markdown("### Apply Fade-In and/or Fade-Out to a Video")
3247
  with gr.Row():
3248
  with gr.Column():
3249
- fade_input_video = gr.Video(label="Input Video")
3250
  with gr.Row():
3251
  fade_in_slider = gr.Slider(0.0, 10.0, 1.0, step=0.1, label="Fade-In Duration (s)")
3252
  fade_out_slider = gr.Slider(0.0, 10.0, 1.0, step=0.1, label="Fade-Out Duration (s)")
3253
  fade_video_btn = gr.Button("✨ Apply Fade", variant="primary")
3254
  with gr.Column():
3255
- fade_output_video = gr.Video(label="Faded Video", interactive=True, show_download_button=True)
3256
  fade_video_btn.click(apply_video_fade, [fade_input_video, fade_in_slider, fade_out_slider], fade_output_video).then(fn=None, js=fire_confetti_and_sound_js)
3257
 
3258
  with gr.TabItem("🎨 Color Grading"):
@@ -3260,7 +3250,7 @@ with gr.Blocks(
3260
  gr.Info("Upload a video, load a preview frame, adjust the sliders for a live preview, and then apply the changes to the full video. The preview is 100% accurate to the final render.")
3261
  with gr.Row(equal_height=False):
3262
  with gr.Column(scale=1):
3263
- color_grade_input_video = gr.Video(label="Input Video")
3264
  load_preview_btn = gr.Button("πŸ–ΌοΈ Load/Reset Preview Frame")
3265
 
3266
  gr.Markdown("#### Adjustments")
@@ -3275,7 +3265,7 @@ with gr.Blocks(
3275
  with gr.Row():
3276
  cg_before_preview = gr.Image(label="Before", type="numpy", interactive=False)
3277
  cg_after_preview = gr.Image(label="After (Accurate Preview)", type="pil", interactive=False)
3278
- color_grade_output_video = gr.Video(label="Graded Video Output", interactive=True, show_download_button=True)
3279
 
3280
  grading_inputs = [cg_before_preview, cg_brightness, cg_contrast, cg_saturation, cg_sharpness]
3281
 
@@ -3314,17 +3304,17 @@ with gr.Blocks(
3314
  gr.Warning("This is a very slow process. A short video can take several minutes. Output is a .webm file.")
3315
  with gr.Row():
3316
  with gr.Column():
3317
- vbg_input_video = gr.Video(label="Input Video")
3318
  vbg_btn = gr.Button("βœ‚οΈ Remove Video Background", variant="primary")
3319
  with gr.Column():
3320
- vbg_output_video = gr.Video(label="Output Video with Transparency (.webm)", interactive=True, show_download_button=True)
3321
  vbg_btn.click(remove_video_background, vbg_input_video, vbg_output_video).then(fn=None, js=fire_confetti_and_sound_js)
3322
 
3323
  with gr.TabItem("Watermark"):
3324
  gr.Markdown("### Apply a text watermark to a video.")
3325
  with gr.Row():
3326
  with gr.Column():
3327
- wm_input_video = gr.Video(label="Input Video")
3328
  wm_text = gr.Textbox(label="Watermark Text", placeholder="(c) My Video 2025")
3329
  wm_pos = gr.Radio(["Top-Left", "Top-Right", "Bottom-Left", "Bottom-Right", "Center"], value="Bottom-Right", label="Position")
3330
  wm_opacity = gr.Slider(0, 100, 70, step=1, label="Opacity (%)")
@@ -3333,14 +3323,14 @@ with gr.Blocks(
3333
  wm_color = gr.ColorPicker(value="#FFFFFF", label="Font Color")
3334
  wm_btn = gr.Button("πŸ–‹οΈ Apply Watermark", variant="primary")
3335
  with gr.Column():
3336
- wm_output_video = gr.Video(label="Watermarked Video", interactive=True, show_download_button=True)
3337
  wm_btn.click(apply_video_watermark, [wm_input_video, wm_text, wm_pos, wm_opacity, wm_size, wm_color], wm_output_video).then(fn=None, js=fire_confetti_and_sound_js)
3338
 
3339
  with gr.TabItem("Create GIF"):
3340
  gr.Markdown("### Convert a video clip into a high-quality animated GIF.")
3341
  with gr.Row():
3342
  with gr.Column():
3343
- input_video_gif = gr.Video(label="Input Video")
3344
  with gr.Row():
3345
  gif_start_time = gr.Number(value=0, label="Start Time (s)")
3346
  gif_end_time = gr.Number(value=0, label="End Time (s)", info="Set to 0 for full duration")
@@ -3350,23 +3340,23 @@ with gr.Blocks(
3350
  gif_btn.click(create_gif_from_video, [input_video_gif, gif_start_time, gif_end_time], output_gif).then(fn=None, js=fire_confetti_and_sound_js)
3351
 
3352
  with gr.TabItem("πŸ”Š Audio & Transcription"):
3353
- with gr.Tabs():
3354
  with gr.TabItem("Add Audio"):
3355
  gr.Markdown("### Combine a silent video with an audio file.")
3356
  with gr.Row():
3357
  with gr.Column():
3358
- input_video_audio = gr.Video(label="Input Video")
3359
  input_audio = gr.Audio(type="filepath", label="Input Audio")
3360
  add_audio_btn = gr.Button("🎢 Add Audio", variant="primary")
3361
  with gr.Column():
3362
- output_video_audio = gr.Video(label="Final Video with Audio", interactive=True, show_download_button=True)
3363
  add_audio_btn.click(add_audio_to_video, [input_video_audio, input_audio], output_video_audio).then(fn=None, js=fire_confetti_and_sound_js)
3364
 
3365
  with gr.TabItem("Extract Audio"):
3366
  gr.Markdown("### Strip the audio track from a video file.")
3367
  with gr.Row():
3368
  with gr.Column():
3369
- extract_audio_input_video = gr.Video(label="Input Video")
3370
  extract_audio_format = gr.Dropdown(["mp3", "wav", "aac"], value="mp3", label="Output Audio Format")
3371
  extract_audio_btn = gr.Button("🎡 Extract Audio", variant="primary")
3372
  with gr.Column():
@@ -3390,69 +3380,40 @@ with gr.Blocks(
3390
  audio_trim_output = gr.Audio(label="Processed Audio", type="filepath")
3391
  audio_trim_fade_btn.click(trim_and_fade_audio, [audio_trim_input, audio_start_time, audio_end_time, audio_fade_in, audio_fade_out], audio_trim_output).then(fn=None, js=fire_confetti_and_sound_js)
3392
 
3393
- with gr.TabItem("Transcription & Subtitles", visible=(whisper is not None)):
3394
- gr.Markdown("## Transcribe Speech and Edit Subtitles")
3395
- gr.Info("1. Transcribe your media. 2. Edit timings or text in the table. 3. Click 'Update Preview' to see your changes. 4. Click 'Burn Final Subtitles' for the final video.")
3396
-
3397
  transcribed_video_path_state = gr.State(None)
3398
- transcribed_segments_state = gr.State(None)
3399
-
3400
  with gr.Row():
3401
- with gr.Column(scale=1):
3402
  transcribe_input = gr.File(label="Upload Video or Audio File", file_types=["video", "audio"])
3403
  transcribe_model = gr.Dropdown(["tiny", "base", "small", "medium", "large"], value="base", label="Whisper Model Size")
3404
- transcribe_btn = gr.Button("πŸŽ™οΈ 1. Transcribe Media", variant="primary")
3405
-
3406
- with gr.Accordion("πŸ“œ Full Transcript & Downloads", open=False):
3407
- with gr.Row():
3408
- transcribe_text = gr.Textbox(label="Full Transcription", lines=10, interactive=True, elem_id="transcription_textbox")
3409
- copy_transcription_btn = gr.Button("πŸ“‹ Copy")
3410
- transcribe_files = gr.File(label="Download Subtitle Files (.srt, .vtt)", file_count="multiple", interactive=False)
3411
-
3412
- with gr.Column(scale=2, visible=False) as editor_column:
3413
- gr.Markdown("### Subtitle Editor & Preview")
3414
- subtitle_df = gr.DataFrame(
3415
- headers=["#", "Start (s)", "End (s)", "Text"],
3416
- datatype=["number", "str", "str", "str"],
3417
- label="Edit Subtitles",
3418
- interactive=True,
3419
- row_count=(10, "fixed")
3420
- )
3421
- with gr.Accordion("🎨 Styling & Rendering", open=True):
3422
- with gr.Row():
3423
- burn_font_size = gr.Slider(1, 10, 5, step=1, label="Font Size")
3424
- burn_font_color = gr.ColorPicker(value="#FFFFFF", label="Font Color")
3425
- burn_outline_color = gr.ColorPicker(value="#000000", label="Outline Color")
3426
- update_preview_btn = gr.Button("πŸ”„ 2. Update Preview", variant="secondary")
3427
- burn_final_btn = gr.Button("πŸ”₯ 3. Burn Final Subtitles", variant="primary")
3428
-
3429
- subtitle_preview_video = gr.Video(label="Video Preview with Subtitles", show_download_button=True)
3430
 
3431
- copy_transcription_btn.click(fn=None, js=copy_transcription_js)
 
 
 
 
 
 
 
3432
 
 
3433
  transcribe_btn.click(
3434
  fn=transcribe_and_prep_burn,
3435
  inputs=[transcribe_input, transcribe_model],
3436
- outputs=[
3437
- transcribe_text,
3438
- transcribe_files,
3439
- transcribed_video_path_state,
3440
- transcribed_segments_state,
3441
- subtitle_df,
3442
- editor_column
3443
- ]
3444
  ).then(fn=None, js=fire_confetti_and_sound_js)
3445
-
3446
- update_preview_btn.click(
3447
- fn=burn_subtitles_from_df,
3448
- inputs=[transcribed_video_path_state, subtitle_df, burn_font_size, burn_font_color, burn_outline_color],
3449
- outputs=subtitle_preview_video
3450
- )
3451
-
3452
- burn_final_btn.click(
3453
- fn=burn_subtitles_from_df,
3454
- inputs=[transcribed_video_path_state, subtitle_df, burn_font_size, burn_font_color, burn_outline_color],
3455
- outputs=subtitle_preview_video
3456
  ).then(fn=None, js=fire_confetti_and_sound_js)
3457
 
3458
  with gr.TabItem("🧠 ControlNet", elem_id="controlnet_tab"):
@@ -3462,11 +3423,11 @@ with gr.Blocks(
3462
  gr.Markdown("### Convert a Video into a ControlNet-Ready Map")
3463
  with gr.Row():
3464
  with gr.Column():
3465
- input_video_cn = gr.Video(label="Input Video")
3466
  detector_dropdown_cn = gr.Dropdown(choices=list(DETECTOR_CONFIG.keys()), value="Canny", label="Choose Detector")
3467
  process_btn_cn = gr.Button("✨ Process Video", variant="primary")
3468
  with gr.Column():
3469
- output_video_cn = gr.Video(label="Output ControlNet Video", interactive=True, show_download_button=True)
3470
  process_btn_cn.click(fn=process_video_with_detector, inputs=[input_video_cn, detector_dropdown_cn], outputs=output_video_cn).then(fn=None, js=fire_confetti_and_sound_js)
3471
  with gr.TabItem("Process Batch Images"):
3472
  gr.Markdown("### Generate ControlNet Maps from one or more images.")
@@ -3485,9 +3446,9 @@ with gr.Blocks(
3485
  gr.Info("Convert your video to a different format, change the codec, reduce the quality to save space, or downscale the resolution.")
3486
  with gr.Row():
3487
  with gr.Column():
3488
- conv_input_video = gr.Video(label="Input Video")
3489
  conv_btn = gr.Button("βš™οΈ Convert & Compress", variant="primary")
3490
- conv_output_video = gr.Video(label="Converted Video", interactive=True, show_download_button=True)
3491
  with gr.Column():
3492
  gr.Markdown("#### Output Settings")
3493
  with gr.Row():
@@ -3548,4 +3509,4 @@ if __name__ == "__main__":
3548
  try: shutil.rmtree(TEMP_DIR)
3549
  except OSError as e: print(f"Error removing temp directory {TEMP_DIR}: {e}")
3550
  os.makedirs(TEMP_DIR, exist_ok=True)
3551
- demo.launch(inbrowser=True)
 
58
  model_load_lock = Lock()
59
  loaded_detectors = {}
60
  whisper_model = None
61
+ whisper_model_name = ""
62
 
63
  # --- Default Presets for Transfer Tab (Flat Dictionary) ---
64
  DEFAULT_LINK_PRESETS = {
 
238
  return loaded_detectors[name]
239
 
240
  def load_whisper_model(model_name="base"):
241
+ global whisper_model, whisper_model_name
242
  if whisper:
243
  with model_load_lock:
 
244
  if whisper_model is None or whisper_model_name != model_name:
245
  print(f"Loading Whisper model '{model_name}'... (This may download files on first run)")
246
  whisper_model = whisper.load_model(model_name)
247
+ whisper_model_name = model_name
248
  print("Whisper model loaded.")
249
  return whisper_model
250
  return None
 
266
  if color_str.startswith('rgba'):
267
  parts = re.findall(r"[\d\.]+", color_str)
268
  if len(parts) >= 3:
 
269
  return (int(float(parts[0])), int(float(parts[1])), int(float(parts[2])))
270
 
 
271
  return color_str
272
 
273
 
 
597
  if audio_for_clip:
598
  cmd.extend(["-i", audio_for_clip, "-c:a", "aac", "-shortest"])
599
 
600
+ cmd.extend(["-vf", zoom_filter, "-c:v", "libx264", "-t", str(duration), "-pix_fmt", "yuv420p", "-b:v", "5M", "-y", output_video_path])
601
  run_ffmpeg_command(cmd, f"Creating zoom video for {os.path.basename(input_path)}")
602
 
603
  batch_kwargs = {}
 
791
  return output_video_path
792
 
793
  def transcribe_media(media_path, model_name):
794
+ if media_path is None: raise gr.Error("Please upload a video or audio file first.")
 
795
  model = load_whisper_model(model_name)
796
+ if model is None: raise gr.Error("Whisper model is not available.")
 
797
  audio_path = media_path.name
798
  base_name = os.path.splitext(os.path.basename(media_path.name))[0]
799
 
 
803
  run_ffmpeg_command(["ffmpeg", "-i", audio_path, "-q:a", "0", "-map", "a", "-y", audio_path_temp])
804
  audio_path = audio_path_temp
805
  except gr.Error as e:
806
+ if "does not contain any stream" in str(e): raise gr.Error("The uploaded video has no audio track.")
807
+ else: raise e
 
 
808
 
809
  result = model.transcribe(audio_path, verbose=False)
810
 
811
  def format_ts(s):
812
+ h, r = divmod(s, 3600); m, s = divmod(r, 60)
 
813
  return f"{int(h):02}:{int(m):02}:{int(s):02},{int((s-int(s))*1000):03}"
814
 
815
  srt_path = os.path.join(TEMP_DIR, f"{base_name}.srt")
 
822
  srt_f.write(f"{i + 1}\n{format_ts(start)} --> {format_ts(end)}\n{text}\n\n")
823
  vtt_f.write(f"{format_ts(start).replace(',', '.')} --> {format_ts(end).replace(',', '.')}\n{text}\n\n")
824
 
825
+ return result["text"], [srt_path, vtt_path]
826
 
827
  def transcribe_and_prep_burn(media_file, model_name):
828
+ if not media_file: raise gr.Error("Please upload a file first.")
 
 
829
  is_video = media_file.name.lower().endswith(('.mp4', '.mov', '.mkv', '.avi', '.webm'))
830
+ text, files = transcribe_media(media_file, model_name)
831
+ if is_video: return text, files, media_file.name, gr.update(visible=True)
832
+ else: return text, files, None, gr.update(visible=False)
 
 
 
 
833
 
834
+ def reformat_srt_for_word_wrap(original_srt_path, words_per_line):
835
+ """
836
+ Reads an SRT file and adds line breaks to the text blocks without changing timestamps,
837
+ then writes the result to a new temporary SRT file.
838
+ """
839
+ if not original_srt_path or not os.path.exists(original_srt_path):
840
+ return None
841
 
 
 
 
 
842
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
843
+ reformatted_path = os.path.join(TEMP_DIR, f"reformatted_{timestamp}.srt")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
844
 
845
+ with open(original_srt_path, 'r', encoding='utf-8') as f_in, \
846
+ open(reformatted_path, 'w', encoding='utf-8') as f_out:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
847
 
848
+ content = f_in.read()
849
+ blocks = content.strip().split('\n\n')
850
+
851
+ for block in blocks:
852
+ lines = block.split('\n')
853
+ if len(lines) < 3: # Not a valid block (e.g., just a number and timestamp)
854
+ f_out.write(block + '\n\n')
855
+ continue
856
 
857
+ text_lines = lines[2:]
858
+ full_text = ' '.join(text_lines)
859
+ words = full_text.split()
860
+
861
+ new_text_lines = []
862
+ current_line = []
863
+ for word in words:
864
+ current_line.append(word)
865
+ if len(current_line) >= words_per_line:
866
+ new_text_lines.append(' '.join(current_line))
867
+ current_line = []
868
+
869
+ if current_line: # Add any remaining words
870
+ new_text_lines.append(' '.join(current_line))
871
+
872
+ # Reconstruct the block with the new text formatting
873
+ f_out.write(lines[0] + '\n') # Index
874
+ f_out.write(lines[1] + '\n') # Timestamp
875
+ f_out.write('\n'.join(new_text_lines) + '\n\n')
876
 
877
+ return reformatted_path
 
 
 
 
 
 
 
 
878
 
879
+ def burn_subtitles(video_path, srt_file_obj, font_size_scale, font_color, words_per_line):
880
+ if not video_path: raise gr.Error("Original video path not found. Please re-transcribe.")
881
+ if not srt_file_obj or not srt_file_obj[0].name: raise gr.Error("SRT file not found. Please re-transcribe.")
882
 
883
+ original_srt_path = srt_file_obj[0].name
884
+ reformatted_srt_path = None
 
 
 
885
 
886
+ try:
887
+ # Reformat the SRT file based on the slider value
888
+ reformatted_srt_path = reformat_srt_for_word_wrap(original_srt_path, words_per_line)
889
+ if not reformatted_srt_path:
890
+ raise gr.Error("Failed to reformat subtitle file.")
891
+
892
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
893
+ output_video_path = os.path.join(TEMP_DIR, f"subtitled_video_{timestamp}.mp4")
894
+ _, video_h = get_video_dimensions(video_path)
895
+ if video_h == 0: video_h = 720
896
+ divisor = 32 - (font_size_scale * 2)
897
+ calculated_font_size = int(video_h / divisor)
898
+ color_bgr = font_color[5:7] + font_color[3:5] + font_color[1:3]
899
+ ffmpeg_color = f"&H00{color_bgr.upper()}"
900
+
901
+ # Use the new, reformatted path and escape it for FFMPEG
902
+ escaped_srt_path = reformatted_srt_path.replace('\\', '/').replace(':', r'\:')
903
+
904
+ vf_filter = f"subtitles='{escaped_srt_path}':force_style='Fontsize={calculated_font_size},PrimaryColour={ffmpeg_color},BorderStyle=1,Outline=1,Shadow=0.5,MarginV=15'"
905
+ cmd = ["ffmpeg", "-i", video_path, "-vf", vf_filter, "-c:a", "copy", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-y", output_video_path]
906
+ run_ffmpeg_command(cmd, "Burning subtitles into video...")
907
+
908
+ return output_video_path
909
+
910
+ finally:
911
+ # Clean up the temporary reformatted file
912
+ if reformatted_srt_path and os.path.exists(reformatted_srt_path):
913
+ os.remove(reformatted_srt_path)
914
 
915
  def remove_background_single(input_path, output_path, **kwargs):
916
  with Image.open(input_path) as img:
 
1056
 
1057
  subprocess.run(cmd, capture_output=True, text=True, check=False)
1058
 
 
1059
  if os.path.exists(output_path):
1060
  with Image.open(output_path) as img:
1061
+ return img.copy()
1062
  else:
1063
+ return Image.fromarray(image_np)
1064
 
1065
  except Exception as e:
1066
  print(f"Error in FFMPEG preview: {e}")
1067
  return Image.fromarray(image_np)
1068
  finally:
 
1069
  if os.path.exists(input_path): os.remove(input_path)
1070
  if os.path.exists(output_path): os.remove(output_path)
1071
 
 
1354
 
1355
  return output_video_path
1356
 
1357
+
1358
  # --- STORYBOARD / ANIMATIC CREATOR FUNCTIONS ---
1359
 
1360
  def get_file_type(file_path):
 
1584
  gr.Warning(f"Could not get dimensions for the first asset: '{first_item['name']}'.")
1585
  return gr.update(), gr.update()
1586
 
1587
+ def create_animatic(timeline_data, audio_path, out_w, out_h, keep_original_audio):
1588
  if not timeline_data:
1589
  raise gr.Error("Timeline is empty. Please add assets to the timeline.")
1590
 
 
1604
  for i, item in enumerate(timeline_data):
1605
  item_path, item_type, item_duration = item['path'], item['type'], item['duration']
1606
  output_clip_path = os.path.join(job_temp_dir, f"clip_{i:04d}.mp4")
1607
+ vf_filter = f"scale={output_res_str}:force_original_aspect_ratio=decrease,pad={output_res_str}:(ow-iw)/2:(oh-ih)/2,setsar=1"
1608
 
1609
  cmd = ["ffmpeg"]
1610
+
1611
+ # --- Input Handling ---
1612
+ # For images, we might need a silent audio source for concat to work
1613
+ if item_type == "image" and keep_original_audio:
1614
+ cmd.extend(["-f", "lavfi", "-i", "anullsrc=channel_layout=stereo:sample_rate=44100"])
1615
+
1616
+ # For videos, handle the start time trim
1617
+ if item_type == "video":
1618
  start_time = item.get('start_time', 0)
1619
  if start_time > 0:
1620
  cmd.extend(["-ss", str(start_time)])
1621
 
1622
  cmd.extend(["-i", item_path])
1623
 
1624
+ # --- Output Options ---
1625
+ output_options = [
1626
+ "-t", str(item_duration),
1627
+ "-c:v", "libx264",
1628
+ "-pix_fmt", "yuv420p",
1629
+ "-vf", vf_filter
1630
+ ]
1631
+
1632
+ # --- CORRECTED AUDIO LOGIC ---
1633
+ if keep_original_audio:
1634
+ # If it's an image, map the silent audio source we added.
1635
+ if item_type == "image":
1636
+ cmd.extend(["-c:v", "libx264", "-t", str(item_duration), "-pix_fmt", "yuv420p", "-vf", vf_filter])
1637
+ output_options = ["-shortest", "-c:a", "aac"]
1638
+ # For videos, just re-encode the existing audio to a standard format.
1639
+ else:
1640
+ output_options.extend(["-c:a", "aac"])
1641
+ else:
1642
+ # If we don't keep audio, make the clip silent.
1643
+ output_options.append("-an")
1644
+
1645
+ cmd.extend(output_options)
1646
+ cmd.extend(["-y", output_clip_path])
1647
 
1648
+ run_ffmpeg_command(cmd, f"Processing clip {i+1}/{len(timeline_data)}")
1649
  clip_paths.append(output_clip_path)
1650
 
1651
  file_list_path = os.path.join(job_temp_dir, "files.txt")
 
1653
  for path in clip_paths:
1654
  f.write(f"file '{os.path.abspath(path)}'\n")
1655
 
1656
+ # This path now contains the final video, either silent or with original audio
1657
+ combined_video_path = os.path.join(job_temp_dir, "combined_video.mp4")
1658
+ run_ffmpeg_command(["ffmpeg", "-f", "concat", "-safe", "0", "-i", file_list_path, "-c", "copy", "-y", combined_video_path], "Joining clips...")
1659
 
1660
  final_output_path = os.path.join(TEMP_DIR, f"animatic_final_{timestamp}.mp4")
1661
+
1662
+ # Decide what to do with the audio
1663
+ if not keep_original_audio and audio_path:
1664
+ # Mux the project audio track onto the silent video
1665
+ run_ffmpeg_command(["ffmpeg", "-i", combined_video_path, "-i", audio_path, "-c:v", "copy", "-c:a", "aac", "-shortest", "-y", final_output_path], "Muxing audio")
1666
  else:
1667
+ # Either we kept original audio, or there's no project audio, so just use the combined video as-is
1668
+ shutil.move(combined_video_path, final_output_path)
1669
 
1670
  shutil.rmtree(job_temp_dir)
1671
  return final_output_path
 
1672
  def detect_bpm(audio_path):
1673
  if not audio_path:
1674
  return "Please upload an audio track first."
 
1726
 
1727
  gr.Info(f"Re-timed {len(rhythmic_timeline)} clips to ~{clip_duration:.2f}s each based on {tempo:.2f} BPM.")
1728
 
1729
+ # The rhythmic animatic will always use the project audio, so keep_original_audio is False
1730
+ return create_animatic(rhythmic_timeline, audio_path, out_w, out_h, keep_original_audio=False)
1731
 
1732
  # --- NEW CREATIVE FUNCTIONS ---
1733
 
 
2619
  clear_timeline_btn = gr.Button("πŸ’₯ Clear Timeline")
2620
 
2621
  gr.Markdown("### 3. Output Settings")
2622
+ keep_audio_checkbox = gr.Checkbox(label="Keep Original Audio from Video Clips", value=False, info="If checked, the Project Audio Track below will be ignored.")
2623
  animatic_audio = gr.Audio(label="Project Audio Track (Narration/Music)", type="filepath")
2624
  with gr.Row():
2625
  animatic_out_w = gr.Number(label="Output Width", value=1920)
 
2638
  )
2639
  generate_rhythmic_btn = gr.Button("🎢 Generate Rhythmic Animatic", variant="primary")
2640
 
2641
+ animatic_output_video = gr.Video(label="Final Video Output", interactive=True, show_download_button=True, height=480)
2642
 
2643
  with gr.Column(scale=2, min_width=300):
2644
  with gr.Group():
2645
  gr.Markdown("### 4. Clip Properties")
2646
  gr.Info("Select a clip in the timeline table to edit it.")
2647
+ clip_preview = gr.Video(label="Selected Clip Preview", interactive=True, elem_id="storyboard_clip_preview", height=480)
2648
  clip_duration_input = gr.Number(label="Set Duration (seconds)", interactive=True, precision=2)
2649
  update_clip_btn = gr.Button("πŸ”„ Update Clip Duration")
2650
 
 
2688
  clear_assets_btn.click(lambda: ([], gr.update(value=None), None), outputs=[assets_state, asset_gallery, asset_preview_gallery])
2689
  clear_timeline_btn.click(lambda: [], outputs=[timeline_state])
2690
 
2691
+ keep_audio_checkbox.change(fn=lambda x: gr.update(interactive=not x), inputs=keep_audio_checkbox, outputs=animatic_audio)
2692
+
2693
+ generate_animatic_btn.click(fn=create_animatic, inputs=[timeline_state, animatic_audio, animatic_out_w, animatic_out_h, keep_audio_checkbox], outputs=animatic_output_video).then(fn=None, js=fire_confetti_and_sound_js)
2694
 
2695
  analyze_bpm_btn.click(detect_bpm, animatic_audio, bpm_display)
2696
 
 
2719
 
2720
  slideshow_generate_btn = gr.Button("πŸš€ Generate Slideshow", variant="primary")
2721
  with gr.Column(scale=3):
2722
+ slideshow_output_video = gr.Video(label="Generated Slideshow Video", show_download_button=True, height=480)
2723
 
2724
  slideshow_generate_btn.click(
2725
  fn=slideshow_wrapper,
 
2732
  gr.Info("Automatically cuts a source video to the beat of a music track, creating a dynamic music video.")
2733
  with gr.Row():
2734
  with gr.Column(scale=2):
2735
+ remix_input_video = gr.Video(label="Upload Source Video", height=480)
2736
  remix_audio = gr.Audio(label="Upload Music Track", type="filepath")
2737
 
2738
  with gr.Accordion("βš™οΈ Remix Settings", open=True):
 
2758
 
2759
  remix_generate_btn = gr.Button("🎀 Generate Music Video", variant="primary")
2760
  with gr.Column(scale=3):
2761
+ remix_output_video = gr.Video(label="Generated Music Video", show_download_button=True, height=480)
2762
 
2763
  remix_resolution.change(
2764
  fn=lambda choice: gr.update(visible=(choice == "Custom")),
 
2777
  gr.Info("Upload a long video, and this tool will find the most action-packed moments to create a short, dynamic trailer.")
2778
  with gr.Row():
2779
  with gr.Column(scale=2):
2780
+ trailer_input_video = gr.Video(label="Upload Source Video", height=480)
2781
  trailer_music = gr.Audio(label="Add Background Music (Optional)", type="filepath")
2782
 
2783
  with gr.Accordion("βš™οΈ Trailer Settings", open=True):
 
2792
 
2793
  trailer_generate_btn = gr.Button("πŸš€ Generate Trailer", variant="primary")
2794
  with gr.Column(scale=3):
2795
+ trailer_output_video = gr.Video(label="Generated Trailer Video", show_download_button=True, height=480)
2796
 
2797
  trailer_generate_btn.click(
2798
  fn=auto_trailer_wrapper,
 
2809
  gr.Info("Upload a video with an audio track to generate a dynamic waveform visualization.")
2810
  with gr.Row():
2811
  with gr.Column():
2812
+ waveform_input_video = gr.Video(label="Input Video", height=480)
2813
  with gr.Row():
2814
  waveform_style = gr.Dropdown(["line", "p2p", "point"], value="line", label="Waveform Style")
2815
  waveform_size = gr.Textbox(value="800x200", label="Size (WxH)")
 
2818
  waveform_color = gr.ColorPicker(value="#38bdf8", label="Color")
2819
  waveform_btn = gr.Button("🎢 Generate Waveform Video", variant="primary")
2820
  with gr.Column():
2821
+ waveform_output_video = gr.Video(label="Output Video with Waveform", show_download_button=True, height=480)
2822
  waveform_btn.click(
2823
  fn=generate_waveform_video,
2824
  inputs=[waveform_input_video, waveform_style, waveform_size, waveform_position, waveform_color],
 
2830
  gr.Info("Upload a main video and an overlay (video or image). The overlay will be placed on top of the main video.")
2831
  with gr.Row():
2832
  with gr.Column():
2833
+ pip_main_video = gr.Video(label="Main Video (Background)", height=480)
2834
  pip_overlay_media = gr.File(label="Overlay Media (Video or Image)", file_types=["video", "image"])
2835
  with gr.Row():
2836
  pip_position = gr.Dropdown(
 
2840
  pip_scale = gr.Slider(0.01, 0.75, 0.25, step=0.01, label="Overlay Size")
2841
  pip_btn = gr.Button("✨ Create PiP Video", variant="primary")
2842
  with gr.Column():
2843
+ pip_output_video = gr.Video(label="Output PiP Video", show_download_button=True, height=480)
2844
  pip_btn.click(
2845
  fn=create_pip_video,
2846
  inputs=[pip_main_video, pip_overlay_media, pip_position, pip_scale],
 
2899
  input_audio_i2v = gr.Audio(label="Add Music (Optional)", type="filepath")
2900
  compile_i2v_btn = gr.Button("🎬 Create Looping Video", variant="primary")
2901
  with gr.Column():
2902
+ output_video_i2v = gr.Video(label="Output Looping Video", interactive=True, show_download_button=True, height=480)
2903
  compile_i2v_btn.click(image_to_looping_video, [input_image_i2v, duration_slider_i2v, input_audio_i2v], output_video_i2v).then(fn=None, js=fire_confetti_and_sound_js)
2904
 
2905
  with gr.TabItem("πŸ”Ž Zoom Video"):
 
2919
  i2zv_btn = gr.Button("✨ Create Zoom Video(s)", variant="primary")
2920
  with gr.Column():
2921
  i2zv_output_gallery = gr.Gallery(label="Output Video Previews", columns=2, object_fit="contain", visible=True)
2922
+ i2zv_output_video = gr.Video(label="Combined Output Video", interactive=True, visible=False, show_download_button=True, height=480)
2923
  i2zv_output_zip = gr.File(label="Download All as .zip", interactive=False)
2924
 
2925
  i2zv_combine.change(
 
3009
  gr.Markdown("### Extract the very first and very last frames of a video.")
3010
  with gr.Row():
3011
  with gr.Column():
3012
+ input_video_fl = gr.Video(label="Input Video", height=480)
3013
  extract_fl_btn = gr.Button("🎬 Extract Frames", variant="primary")
3014
  with gr.Column():
3015
  output_gallery_fl = gr.Gallery(label="Output Frames (First, Last)", columns=2, object_fit="contain", height="auto")
 
3019
  gr.Markdown("### Extract all individual frames from a video file.")
3020
  with gr.Row():
3021
  with gr.Column():
3022
+ input_video_v2f = gr.Video(label="Input Video", height=480)
3023
  v2f_fps_display = gr.Textbox(label="Detected FPS", interactive=False, value="N/A")
3024
  with gr.Accordion("βš™οΈ Advanced Options", open=False):
3025
  v2f_skip_rate = gr.Slider(1, 30, 1, step=1, label="Extract Every Nth Frame")
 
3053
  f2v_height = gr.Number(label="Output Height", value=576, interactive=False)
3054
  compile_f2v_btn = gr.Button("πŸ“½οΈ Create Video", variant="primary")
3055
  with gr.Column():
3056
+ output_video_f2v = gr.Video(label="Compiled Video", interactive=True, show_download_button=True, height=480)
3057
  f2v_resize.change(lambda x: [gr.update(interactive=x), gr.update(interactive=x)], f2v_resize, [f2v_width, f2v_height])
3058
  compile_f2v_btn.click(create_video_from_frames, [input_frames_f2v, fps_slider_f2v, f2v_rotation, f2v_resize, f2v_width, f2v_height], output_video_f2v).then(fn=None, js=fire_confetti_and_sound_js)
3059
 
 
3064
  gr.Info("1. Upload a video. A waveform will appear. 2. Adjust the 'Silence Threshold' slider so it's above the thin 'noise floor' line but below your voice peaks. 3. Process the video!")
3065
  with gr.Row():
3066
  with gr.Column(scale=2):
3067
+ jumpcut_input_video = gr.Video(label="Input Video (with audio)", height=480)
3068
  jc_waveform_preview = gr.Image(label="Audio Waveform Preview", interactive=False)
3069
  with gr.Accordion("βš™οΈ Cut Settings", open=True):
3070
  jumpcut_threshold = gr.Slider(minimum=-60, maximum=-20, value=-30, step=1, label="Silence Threshold (dB)", info="Anything quieter than this is 'silence'. Lower values are more strict.")
 
3079
  jc_custom_h = gr.Number(label="Custom Height", value=1080)
3080
  jumpcut_btn = gr.Button("πŸ”ͺ Perform Jump-Cut", variant="primary")
3081
  with gr.Column(scale=3):
3082
+ jumpcut_output_video = gr.Video(label="Edited Video Output", show_download_button=True, height=480)
3083
 
3084
  jumpcut_input_video.upload(fn=generate_waveform_preview, inputs=jumpcut_input_video, outputs=jc_waveform_preview)
3085
 
 
3100
  gr.Info("Apply a single transformation like inverting colors, flipping, or rotating to every frame of a video.")
3101
  with gr.Row():
3102
  with gr.Column():
3103
+ vmanip_input_video = gr.Video(label="Input Video", height=480)
3104
  vmanip_operation_radio = gr.Radio(
3105
  ["Invert Colors", "Flip Horizontal", "Flip Vertical", "Rotate 90Β° Right", "Rotate 90Β° Left"],
3106
  label="Select Operation", value="Invert Colors"
3107
  )
3108
  vmanip_apply_btn = gr.Button("✨ Apply Manipulation", variant="primary")
3109
  with gr.Column():
3110
+ vmanip_output_video = gr.Video(label="Output Video", interactive=True, show_download_button=True, height=480)
3111
 
3112
  vmanip_apply_btn.click(fn=manipulate_video, inputs=[vmanip_input_video, vmanip_operation_radio], outputs=vmanip_output_video).then(fn=None, js=fire_confetti_and_sound_js)
3113
 
 
3115
  gr.Markdown("### Create a forward-then-reverse video loop (Boomerang).")
3116
  with gr.Row():
3117
  with gr.Column():
3118
+ input_video_pingpong = gr.Video(label="Input Video", height=480)
3119
  audio_option_pingpong = gr.Radio(["Remove Audio", "Original Audio Only", "Reverse Audio"], value="Remove Audio", label="Audio Handling")
3120
  pingpong_btn = gr.Button("πŸ“ Create Ping-Pong Video", variant="primary")
3121
  with gr.Column():
3122
+ output_video_pingpong = gr.Video(label="Ping-Pong Video", interactive=True, show_download_button=True, height=480)
3123
  pingpong_btn.click(fn=ping_pong_video, inputs=[input_video_pingpong, audio_option_pingpong], outputs=output_video_pingpong).then(fn=None, js=fire_confetti_and_sound_js)
3124
 
3125
  with gr.TabItem("Reverse"):
3126
  gr.Markdown("### Reverse a video clip.")
3127
  with gr.Row():
3128
  with gr.Column():
3129
+ input_video_reverse = gr.Video(label="Input Video", height=480)
3130
  audio_option_reverse = gr.Radio(["Remove Audio", "Reverse Audio"], value="Remove Audio", label="Audio Handling")
3131
  reverse_btn = gr.Button("πŸ”„ Reverse Video", variant="primary")
3132
  with gr.Column():
3133
+ output_video_reverse = gr.Video(label="Reversed Video", interactive=True, show_download_button=True, height=480)
3134
  reverse_btn.click(fn=reverse_video, inputs=[input_video_reverse, audio_option_reverse], outputs=output_video_reverse).then(fn=None, js=fire_confetti_and_sound_js)
3135
 
3136
  with gr.TabItem("Merger"):
 
3141
  merger_input_videos = gr.File(label="Upload Videos (2 or more)", file_count="multiple", file_types=["video"])
3142
  merger_btn = gr.Button("πŸ”— Merge Videos", variant="primary")
3143
  with gr.Column():
3144
+ merger_output_video = gr.Video(label="Merged Video", show_download_button=True, height=480)
3145
  merger_btn.click(
3146
  fn=merge_videos,
3147
  inputs=merger_input_videos,
 
3153
  gr.Info("This tool uses a two-pass process to analyze and stabilize shaky videos. Higher 'Shakiness' values are for very unstable footage. Higher 'Smoothing' creates a more fluid, gliding look but can introduce slight cropping/warping.")
3154
  with gr.Row():
3155
  with gr.Column():
3156
+ stab_input_video = gr.Video(label="Input Shaky Video", height=480)
3157
  stab_shakiness = gr.Slider(minimum=1, maximum=10, value=5, step=1, label="Shakiness Level", info="How shaky is the source video? (1=low, 10=high)")
3158
  stab_smoothing = gr.Slider(minimum=1, maximum=50, value=10, step=1, label="Smoothing Amount", info="How much smoothing to apply. (More = smoother but might feel 'floaty')")
3159
  stab_btn = gr.Button("✨ Stabilize Video", variant="primary")
3160
  with gr.Column():
3161
+ stab_output_video = gr.Video(label="Stabilized Output Video", show_download_button=True, height=480)
3162
  stab_btn.click(
3163
  fn=stabilize_video,
3164
  inputs=[stab_input_video, stab_shakiness, stab_smoothing],
 
3170
  gr.Info("Keyboard hotkeys enabled: K = Play/Pause, J = Back 1 Frame, L = Forward 1 Frame (hover mouse over video)")
3171
  with gr.Row():
3172
  with gr.Column(scale=2):
3173
+ input_video_trim = gr.Video(label="Input Video", elem_id="video-trim-input", height=480)
3174
  with gr.Row():
3175
  set_start_btn = gr.Button("Set Current Frame as START")
3176
  set_end_btn = gr.Button("Set Current Frame as END")
 
3184
  trim_start_time = gr.Number(value=0, visible=False)
3185
  trim_end_time = gr.Number(value=0, visible=False)
3186
  with gr.Row():
3187
+ output_video_trim = gr.Video(label="Trimmed Video", interactive=True, show_download_button=True, height=480)
3188
  get_current_time_js = """()=>{const e=document.querySelector("#video-trim-input video");return e?e.currentTime:0}"""
3189
  def get_frame_from_time_wrapper(v,t): return get_frame_at_time(v,t), f"{t:.3f}"
3190
  set_start_btn.click(fn=None, js=get_current_time_js, outputs=[trim_start_time])
 
3198
  gr.Markdown("### Visually crop a video.")
3199
  with gr.Row():
3200
  with gr.Column(scale=2):
3201
+ crop_input_video = gr.Video(label="Input Video", height=480)
3202
  crop_preview_image = gr.Image(label="Frame Preview", interactive=False)
3203
  with gr.Column(scale=1):
3204
  gr.Markdown("#### Crop Settings")
 
3214
  crop_do_resize = gr.Checkbox(label="Enable Resizing", value=False)
3215
  crop_resize_w = gr.Number(label="Output Width", value=1024, interactive=False)
3216
  crop_resize_h = gr.Number(label="Output Height", value=576, interactive=False)
3217
+ output_video_crop = gr.Video(label="Cropped Video", interactive=True, show_download_button=True, height=480)
3218
  crop_input_video.upload(fn=get_frame_at_time, inputs=crop_input_video, outputs=crop_preview_image)
3219
  crop_do_resize.change(lambda x: [gr.update(interactive=x), gr.update(interactive=x)], inputs=crop_do_resize, outputs=[crop_resize_w, crop_resize_h])
3220
  crop_btn.click(fn=crop_video, inputs=[crop_input_video, crop_x, crop_y, crop_w, crop_h, crop_do_resize, crop_resize_w, crop_resize_h], outputs=output_video_crop).then(fn=None, js=fire_confetti_and_sound_js)
 
3223
  gr.Markdown("### Create slow-motion or fast-forward videos.")
3224
  with gr.Row():
3225
  with gr.Column():
3226
+ input_video_speed = gr.Video(label="Input Video", height=480)
3227
  speed_multiplier = gr.Slider(0.1, 10.0, 1.0, step=0.1, label="Speed Multiplier")
3228
  speed_btn = gr.Button("πŸƒ Change Speed", variant="primary")
3229
  with gr.Column():
3230
+ output_video_speed = gr.Video(label="Modified Video", interactive=True, show_download_button=True, height=480)
3231
  speed_btn.click(fn=change_video_speed, inputs=[input_video_speed, speed_multiplier], outputs=output_video_speed).then(fn=None, js=fire_confetti_and_sound_js)
3232
 
3233
  with gr.TabItem("🎨 Effects & Overlays"):
 
3236
  gr.Markdown("### Apply Fade-In and/or Fade-Out to a Video")
3237
  with gr.Row():
3238
  with gr.Column():
3239
+ fade_input_video = gr.Video(label="Input Video", height=480)
3240
  with gr.Row():
3241
  fade_in_slider = gr.Slider(0.0, 10.0, 1.0, step=0.1, label="Fade-In Duration (s)")
3242
  fade_out_slider = gr.Slider(0.0, 10.0, 1.0, step=0.1, label="Fade-Out Duration (s)")
3243
  fade_video_btn = gr.Button("✨ Apply Fade", variant="primary")
3244
  with gr.Column():
3245
+ fade_output_video = gr.Video(label="Faded Video", interactive=True, show_download_button=True, height=480)
3246
  fade_video_btn.click(apply_video_fade, [fade_input_video, fade_in_slider, fade_out_slider], fade_output_video).then(fn=None, js=fire_confetti_and_sound_js)
3247
 
3248
  with gr.TabItem("🎨 Color Grading"):
 
3250
  gr.Info("Upload a video, load a preview frame, adjust the sliders for a live preview, and then apply the changes to the full video. The preview is 100% accurate to the final render.")
3251
  with gr.Row(equal_height=False):
3252
  with gr.Column(scale=1):
3253
+ color_grade_input_video = gr.Video(label="Input Video", height=480)
3254
  load_preview_btn = gr.Button("πŸ–ΌοΈ Load/Reset Preview Frame")
3255
 
3256
  gr.Markdown("#### Adjustments")
 
3265
  with gr.Row():
3266
  cg_before_preview = gr.Image(label="Before", type="numpy", interactive=False)
3267
  cg_after_preview = gr.Image(label="After (Accurate Preview)", type="pil", interactive=False)
3268
+ color_grade_output_video = gr.Video(label="Graded Video Output", interactive=True, show_download_button=True, height=480)
3269
 
3270
  grading_inputs = [cg_before_preview, cg_brightness, cg_contrast, cg_saturation, cg_sharpness]
3271
 
 
3304
  gr.Warning("This is a very slow process. A short video can take several minutes. Output is a .webm file.")
3305
  with gr.Row():
3306
  with gr.Column():
3307
+ vbg_input_video = gr.Video(label="Input Video", height=480)
3308
  vbg_btn = gr.Button("βœ‚οΈ Remove Video Background", variant="primary")
3309
  with gr.Column():
3310
+ vbg_output_video = gr.Video(label="Output Video with Transparency (.webm)", interactive=True, show_download_button=True, height=480)
3311
  vbg_btn.click(remove_video_background, vbg_input_video, vbg_output_video).then(fn=None, js=fire_confetti_and_sound_js)
3312
 
3313
  with gr.TabItem("Watermark"):
3314
  gr.Markdown("### Apply a text watermark to a video.")
3315
  with gr.Row():
3316
  with gr.Column():
3317
+ wm_input_video = gr.Video(label="Input Video", height=480)
3318
  wm_text = gr.Textbox(label="Watermark Text", placeholder="(c) My Video 2025")
3319
  wm_pos = gr.Radio(["Top-Left", "Top-Right", "Bottom-Left", "Bottom-Right", "Center"], value="Bottom-Right", label="Position")
3320
  wm_opacity = gr.Slider(0, 100, 70, step=1, label="Opacity (%)")
 
3323
  wm_color = gr.ColorPicker(value="#FFFFFF", label="Font Color")
3324
  wm_btn = gr.Button("πŸ–‹οΈ Apply Watermark", variant="primary")
3325
  with gr.Column():
3326
+ wm_output_video = gr.Video(label="Watermarked Video", interactive=True, show_download_button=True, height=480)
3327
  wm_btn.click(apply_video_watermark, [wm_input_video, wm_text, wm_pos, wm_opacity, wm_size, wm_color], wm_output_video).then(fn=None, js=fire_confetti_and_sound_js)
3328
 
3329
  with gr.TabItem("Create GIF"):
3330
  gr.Markdown("### Convert a video clip into a high-quality animated GIF.")
3331
  with gr.Row():
3332
  with gr.Column():
3333
+ input_video_gif = gr.Video(label="Input Video", height=480)
3334
  with gr.Row():
3335
  gif_start_time = gr.Number(value=0, label="Start Time (s)")
3336
  gif_end_time = gr.Number(value=0, label="End Time (s)", info="Set to 0 for full duration")
 
3340
  gif_btn.click(create_gif_from_video, [input_video_gif, gif_start_time, gif_end_time], output_gif).then(fn=None, js=fire_confetti_and_sound_js)
3341
 
3342
  with gr.TabItem("πŸ”Š Audio & Transcription"):
3343
+ with gr.Tabs():
3344
  with gr.TabItem("Add Audio"):
3345
  gr.Markdown("### Combine a silent video with an audio file.")
3346
  with gr.Row():
3347
  with gr.Column():
3348
+ input_video_audio = gr.Video(label="Input Video", height=480)
3349
  input_audio = gr.Audio(type="filepath", label="Input Audio")
3350
  add_audio_btn = gr.Button("🎢 Add Audio", variant="primary")
3351
  with gr.Column():
3352
+ output_video_audio = gr.Video(label="Final Video with Audio", interactive=True, show_download_button=True, height=480)
3353
  add_audio_btn.click(add_audio_to_video, [input_video_audio, input_audio], output_video_audio).then(fn=None, js=fire_confetti_and_sound_js)
3354
 
3355
  with gr.TabItem("Extract Audio"):
3356
  gr.Markdown("### Strip the audio track from a video file.")
3357
  with gr.Row():
3358
  with gr.Column():
3359
+ extract_audio_input_video = gr.Video(label="Input Video", height=480)
3360
  extract_audio_format = gr.Dropdown(["mp3", "wav", "aac"], value="mp3", label="Output Audio Format")
3361
  extract_audio_btn = gr.Button("🎡 Extract Audio", variant="primary")
3362
  with gr.Column():
 
3380
  audio_trim_output = gr.Audio(label="Processed Audio", type="filepath")
3381
  audio_trim_fade_btn.click(trim_and_fade_audio, [audio_trim_input, audio_start_time, audio_end_time, audio_fade_in, audio_fade_out], audio_trim_output).then(fn=None, js=fire_confetti_and_sound_js)
3382
 
3383
+ with gr.TabItem("Transcription", visible=(whisper is not None)):
3384
+ gr.Markdown("## Transcribe Speech and Burn Subtitles")
3385
+ gr.Info("Uses OpenAI's Whisper model. First run will download model files. After transcribing a video, options to burn subtitles will appear.")
 
3386
  transcribed_video_path_state = gr.State(None)
 
 
3387
  with gr.Row():
3388
+ with gr.Column():
3389
  transcribe_input = gr.File(label="Upload Video or Audio File", file_types=["video", "audio"])
3390
  transcribe_model = gr.Dropdown(["tiny", "base", "small", "medium", "large"], value="base", label="Whisper Model Size")
3391
+ transcribe_btn = gr.Button("πŸŽ™οΈ Transcribe", variant="primary")
3392
+ with gr.Column():
3393
+ with gr.Row():
3394
+ transcribe_text = gr.Textbox(label="Transcription Result", lines=10, interactive=True, elem_id="transcription_textbox")
3395
+ copy_transcription_btn = gr.Button("πŸ“‹ Copy")
3396
+ transcribe_files = gr.File(label="Download Subtitle Files (.srt, .vtt)", file_count="multiple", interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3397
 
3398
+ with gr.Accordion("πŸ”₯ Burn Subtitles onto Video", open=True, visible=False) as burn_accordion:
3399
+ gr.Markdown("Set styling and burn the generated subtitles into the video.")
3400
+ with gr.Row():
3401
+ burn_font_size = gr.Slider(1, 10, 5, step=1, label="Relative Font Size")
3402
+ burn_words_per_line = gr.Slider(1, 20, 7, step=1, label="Max Words Per Line", info="Splits long subtitle lines for better readability.")
3403
+ burn_font_color = gr.ColorPicker(value="#FFFFFF", label="Font Color")
3404
+ burn_btn = gr.Button("πŸ”₯ Burn Subtitles", variant="primary")
3405
+ burn_output_video = gr.Video(label="Video with Burned-in Subtitles", interactive=True, show_download_button=True, height=480)
3406
 
3407
+ copy_transcription_btn.click(fn=None, js=copy_transcription_js)
3408
  transcribe_btn.click(
3409
  fn=transcribe_and_prep_burn,
3410
  inputs=[transcribe_input, transcribe_model],
3411
+ outputs=[transcribe_text, transcribe_files, transcribed_video_path_state, burn_accordion]
 
 
 
 
 
 
 
3412
  ).then(fn=None, js=fire_confetti_and_sound_js)
3413
+ burn_btn.click(
3414
+ fn=burn_subtitles,
3415
+ inputs=[transcribed_video_path_state, transcribe_files, burn_font_size, burn_font_color, burn_words_per_line],
3416
+ outputs=burn_output_video
 
 
 
 
 
 
 
3417
  ).then(fn=None, js=fire_confetti_and_sound_js)
3418
 
3419
  with gr.TabItem("🧠 ControlNet", elem_id="controlnet_tab"):
 
3423
  gr.Markdown("### Convert a Video into a ControlNet-Ready Map")
3424
  with gr.Row():
3425
  with gr.Column():
3426
+ input_video_cn = gr.Video(label="Input Video", height=480)
3427
  detector_dropdown_cn = gr.Dropdown(choices=list(DETECTOR_CONFIG.keys()), value="Canny", label="Choose Detector")
3428
  process_btn_cn = gr.Button("✨ Process Video", variant="primary")
3429
  with gr.Column():
3430
+ output_video_cn = gr.Video(label="Output ControlNet Video", interactive=True, show_download_button=True, height=480)
3431
  process_btn_cn.click(fn=process_video_with_detector, inputs=[input_video_cn, detector_dropdown_cn], outputs=output_video_cn).then(fn=None, js=fire_confetti_and_sound_js)
3432
  with gr.TabItem("Process Batch Images"):
3433
  gr.Markdown("### Generate ControlNet Maps from one or more images.")
 
3446
  gr.Info("Convert your video to a different format, change the codec, reduce the quality to save space, or downscale the resolution.")
3447
  with gr.Row():
3448
  with gr.Column():
3449
+ conv_input_video = gr.Video(label="Input Video", height=480)
3450
  conv_btn = gr.Button("βš™οΈ Convert & Compress", variant="primary")
3451
+ conv_output_video = gr.Video(label="Converted Video", interactive=True, show_download_button=True, height=480)
3452
  with gr.Column():
3453
  gr.Markdown("#### Output Settings")
3454
  with gr.Row():
 
3509
  try: shutil.rmtree(TEMP_DIR)
3510
  except OSError as e: print(f"Error removing temp directory {TEMP_DIR}: {e}")
3511
  os.makedirs(TEMP_DIR, exist_ok=True)
3512
+ demo.launch(inbrowser=True)