Update app.py
Browse files
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 = ""
|
| 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
|
| 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
|
| 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",
|
| 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 |
-
|
| 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]
|
| 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 |
-
|
| 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 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 905 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 923 |
|
| 924 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 925 |
|
| 926 |
-
|
| 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 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
with open(ass_path, 'w', encoding='utf-8') as f:
|
| 944 |
-
f.write(ass_content)
|
| 945 |
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|
| 1103 |
else:
|
| 1104 |
-
return Image.fromarray(image_np)
|
| 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 |
-
|
| 1652 |
-
|
| 1653 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1661 |
-
|
| 1662 |
-
"-
|
| 1663 |
-
"-
|
| 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 |
-
|
| 1675 |
-
|
|
|
|
| 1676 |
|
| 1677 |
final_output_path = os.path.join(TEMP_DIR, f"animatic_final_{timestamp}.mp4")
|
| 1678 |
-
|
| 1679 |
-
|
|
|
|
|
|
|
|
|
|
| 1680 |
else:
|
| 1681 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
| 3394 |
-
gr.Markdown("## Transcribe Speech and
|
| 3395 |
-
gr.Info("
|
| 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(
|
| 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("ποΈ
|
| 3405 |
-
|
| 3406 |
-
with gr.
|
| 3407 |
-
|
| 3408 |
-
|
| 3409 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 3447 |
-
|
| 3448 |
-
|
| 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)
|