diff --git a/app.py b/app.py new file mode 100644 index 0000000..5fe5437 --- /dev/null +++ b/app.py @@ -0,0 +1,175 @@ +import tkinter as tk +from tkinter import filedialog +from moviepy import VideoFileClip, TextClip, CompositeVideoClip +import threading +import json + +# Global settings with defaults +settings = { + "subtitle_y_px": 1550, + "highlight_offset": -8, + "font_size_subtitle": 65, + "font_size_highlight": 68, + "highlight_x_offset": 0, + "video_path": None, + "selected_font": "Arial" # Default font +} + +# Compatible fonts that work across different systems +COMPATIBLE_FONTS = [ + "Arial", + "Times-Roman", + "Helvetica", + "Courier", + "Comic-Sans-MS", + "Impact", + "Verdana", + "Tahoma", + "Georgia", + "Trebuchet-MS" +] + +preset_file = "subtitle_gui_presets.json" + +def save_presets(): + with open(preset_file, "w") as f: + json.dump(settings, f) + print("💾 Presets saved!") + +def load_presets(): + global settings + try: + with open(preset_file, "r") as f: + loaded = json.load(f) + settings.update(loaded) + print("✅ Presets loaded!") + sync_gui() + except FileNotFoundError: + print("⚠️ No presets found.") + +def sync_gui(): + sub_y_slider.set(settings["subtitle_y_px"]) + highlight_slider.set(settings["highlight_offset"]) + highlight_x_slider.set(settings["highlight_x_offset"]) + sub_font_slider.set(settings["font_size_subtitle"]) + highlight_font_slider.set(settings["font_size_highlight"]) + font_var.set(settings["selected_font"]) + +def render_preview(): + if not settings["video_path"]: + print("⚠️ No video selected.") + return + + clip = VideoFileClip(settings["video_path"]).subclipped(0, 3) # Use first 3 seconds + vertical_clip = clip.resized(height=1920).cropped(width=1080, x_center=clip.w / 2) + + subtitle_text = "THIS IS A TEST SUBTITLE" + highlight_word = "SUBTITLE" + + base_subtitle = TextClip( + text=subtitle_text, + font_size=settings["font_size_subtitle"], + font=settings["selected_font"], + color='white', + stroke_color='black', + stroke_width=5 + ).with_duration(3).with_position(('center', settings["subtitle_y_px"])) + + # Compute highlight word position + full_text = subtitle_text.upper() + words = full_text.split() + highlight_index = words.index(highlight_word.upper()) + chars_before = sum(len(w) + 1 for w in words[:highlight_index]) + char_width = 35 + total_width = len(full_text) * char_width + x_offset = (chars_before * char_width) - (total_width // 2) + settings["highlight_x_offset"] + + highlighted_word = TextClip( + text=highlight_word, + font_size=settings["font_size_highlight"], + font=settings["selected_font"], + color='#FFD700', + stroke_color='#FF6B35', + stroke_width=5 + ).with_duration(1.5).with_start(0.75).with_position((540 + x_offset, settings["subtitle_y_px"] + settings["highlight_offset"])) + + final = CompositeVideoClip([vertical_clip, base_subtitle, highlighted_word], size=(1080, 1920)) + # Scale down the preview to fit 1080p monitor (max height ~900px to leave room for taskbar) + preview_scale = 900 / 1920 # Scale factor to fit height + preview_width = int(1080 * preview_scale) + preview_height = int(1920 * preview_scale) + preview_clip = final.resized((preview_width, preview_height)) + preview_clip.preview(fps=24, audio=False) + + clip.close() + final.close() + preview_clip.close() + +def update_setting(var_name, value): + settings[var_name] = int(value) + +def update_font(font_name): + settings["selected_font"] = font_name + +def open_video(): + file_path = filedialog.askopenfilename(filetypes=[("MP4 files", "*.mp4")]) + if file_path: + settings["video_path"] = file_path + print(f"📂 Loaded video: {file_path}") + +def start_preview_thread(): + threading.Thread(target=render_preview).start() + +# GUI Setup +root = tk.Tk() +root.title("Subtitle Positioning Tool") +root.geometry("400x600") + +load_btn = tk.Button(root, text="🎥 Load Video", command=open_video) +load_btn.pack(pady=10) + +tk.Label(root, text="Font Family").pack() +font_var = tk.StringVar(value=settings["selected_font"]) +font_dropdown = tk.OptionMenu(root, font_var, *COMPATIBLE_FONTS, command=update_font) +font_dropdown.pack(pady=5) + +tk.Label(root, text="Subtitle Y Position").pack() +sub_y_slider = tk.Scale(root, from_=1000, to=1800, orient="horizontal", + command=lambda v: update_setting("subtitle_y_px", v)) +sub_y_slider.set(settings["subtitle_y_px"]) +sub_y_slider.pack() + +tk.Label(root, text="Highlight Y Offset").pack() +highlight_slider = tk.Scale(root, from_=-100, to=100, orient="horizontal", + command=lambda v: update_setting("highlight_offset", v)) +highlight_slider.set(settings["highlight_offset"]) +highlight_slider.pack() + +tk.Label(root, text="Highlight X Offset").pack() +highlight_x_slider = tk.Scale(root, from_=-300, to=300, orient="horizontal", + command=lambda v: update_setting("highlight_x_offset", v)) +highlight_x_slider.set(settings["highlight_x_offset"]) +highlight_x_slider.pack() + +tk.Label(root, text="Subtitle Font Size").pack() +sub_font_slider = tk.Scale(root, from_=30, to=100, orient="horizontal", + command=lambda v: update_setting("font_size_subtitle", v)) +sub_font_slider.set(settings["font_size_subtitle"]) +sub_font_slider.pack() + +tk.Label(root, text="Highlight Font Size").pack() +highlight_font_slider = tk.Scale(root, from_=30, to=100, orient="horizontal", + command=lambda v: update_setting("font_size_highlight", v)) +highlight_font_slider.set(settings["font_size_highlight"]) +highlight_font_slider.pack() + +preview_btn = tk.Button(root, text="▶️ Preview Clip", command=start_preview_thread) +preview_btn.pack(pady=10) + +save_btn = tk.Button(root, text="💾 Save Preset", command=save_presets) +save_btn.pack(pady=5) + +load_preset_btn = tk.Button(root, text="📂 Load Preset", command=load_presets) +load_preset_btn.pack(pady=5) + +root.mainloop() diff --git a/app2.py b/app2.py new file mode 100644 index 0000000..939612d --- /dev/null +++ b/app2.py @@ -0,0 +1,322 @@ +import tkinter as tk +from tkinter import filedialog +from moviepy import VideoFileClip, TextClip, CompositeVideoClip +import threading +import json +import re +import os +import platform + +def get_system_fonts(): + """Get list of available system fonts""" + fonts = [] + + if platform.system() == "Windows": + # Common Windows font paths + font_paths = [ + "C:/Windows/Fonts/", + "C:/Windows/System32/Fonts/" + ] + + common_fonts = [] + for font_path in font_paths: + if os.path.exists(font_path): + for file in os.listdir(font_path): + if file.endswith(('.ttf', '.otf')): + # Extract font name without extension + font_name = os.path.splitext(file)[0] + # Clean up common variations + if 'arial' in font_name.lower() and 'bold' not in font_name.lower(): + common_fonts.append('arial.ttf') + elif 'times' in font_name.lower() and 'bold' not in font_name.lower(): + common_fonts.append('times.ttf') + elif 'courier' in font_name.lower() and 'bold' not in font_name.lower(): + common_fonts.append('cour.ttf') + elif 'comic' in font_name.lower(): + common_fonts.append('comic.ttf') + elif 'impact' in font_name.lower(): + common_fonts.append('impact.ttf') + elif 'verdana' in font_name.lower(): + common_fonts.append('verdana.ttf') + elif 'tahoma' in font_name.lower(): + common_fonts.append('tahoma.ttf') + + # Add found fonts, fallback to common Windows fonts + fonts = list(set(common_fonts)) if common_fonts else [ + 'arial.ttf', 'times.ttf', 'cour.ttf', 'comic.ttf', + 'impact.ttf', 'verdana.ttf', 'tahoma.ttf' + ] + + # Add option to use no font (system default) + fonts.insert(0, 'System Default') + return fonts + +AVAILABLE_FONTS = get_system_fonts() + +# Global settings with defaults +settings = { + "subtitle_y_px": 1550, + "highlight_offset": -8, + "font_size_subtitle": 65, + "font_size_highlight": 68, + "highlight_x_offset": 0, + "video_path": None, + "font": "System Default", + "subtitles": [], + "current_index": 0 +} + +# Compatible fonts that work across different systems +COMPATIBLE_FONTS = [ + "Arial", + "Times-Roman", + "Helvetica", + "Courier", + "Comic-Sans-MS", + "Impact", + "Verdana", + "Tahoma", + "Georgia", + "Trebuchet-MS" +] + +preset_file = "subtitle_gui_presets.json" + +# === SRT PARSER === +def parse_srt(file_path): + with open(file_path, 'r', encoding='utf-8') as f: + contents = f.read() + pattern = r"(\d+)\s+(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})\s+([\s\S]*?)(?=\n\d+|\Z)" + matches = re.findall(pattern, contents) + subtitles = [] + for _, start, end, text in matches: + subtitles.append({ + "start": srt_time_to_seconds(start), + "end": srt_time_to_seconds(end), + "text": text.replace('\n', ' ') + }) + return subtitles + +def srt_time_to_seconds(time_str): + h, m, s_ms = time_str.split(':') + s, ms = s_ms.split(',') + return int(h)*3600 + int(m)*60 + int(s) + int(ms)/1000 + +# === PRESETS === +def save_presets(): + with open(preset_file, "w") as f: + json.dump(settings, f) + print("📂 Presets saved!") + +def load_presets(): + global settings + try: + with open(preset_file, "r") as f: + loaded = json.load(f) + settings.update(loaded) + print("✅ Presets loaded!") + sync_gui() + except FileNotFoundError: + print("⚠️ No presets found.") + +# === SYNC === +def sync_gui(): + sub_y_slider.set(settings["subtitle_y_px"]) + highlight_slider.set(settings["highlight_offset"]) + highlight_x_slider.set(settings["highlight_x_offset"]) + sub_font_slider.set(settings["font_size_subtitle"]) + highlight_font_slider.set(settings["font_size_highlight"]) + font_dropdown_var.set(settings["font"]) + +def render_preview(): + if not settings["video_path"] or not settings["subtitles"]: + print("⚠️ Video or subtitles not loaded.") + return + + sub = settings["subtitles"][settings["current_index"]] + subtitle_text = sub["text"] + start_time = sub["start"] + end_time = sub["end"] + duration = end_time - start_time + + clip = VideoFileClip(settings["video_path"]).subclipped(start_time, end_time) + vertical_clip = clip.resized(height=1920).cropped(width=1080, x_center=clip.w / 2) + + highlight_word = subtitle_text.split()[-1] # Highlight last word for now + + # Create TextClip with font if specified, otherwise use system default + if settings["font"] == "System Default": + base_subtitle = TextClip( + text=subtitle_text, + font_size=settings["font_size_subtitle"], + color='white', + stroke_color='black', + stroke_width=5 + ).with_duration(duration).with_position(('center', settings["subtitle_y_px"])) + else: + try: + base_subtitle = TextClip( + text=subtitle_text, + font=settings["font"], + font_size=settings["font_size_subtitle"], + color='white', + stroke_color='black', + stroke_width=5 + ).with_duration(duration).with_position(('center', settings["subtitle_y_px"])) + except: + # Fallback to system default if font fails + print(f"⚠️ Font {settings['font']} failed, using system default") + base_subtitle = TextClip( + text=subtitle_text, + font_size=settings["font_size_subtitle"], + color='white', + stroke_color='black', + stroke_width=5 + ).with_duration(duration).with_position(('center', settings["subtitle_y_px"])) + + full_text = subtitle_text.upper() + words = full_text.split() + try: + highlight_index = words.index(highlight_word.upper()) + except ValueError: + highlight_index = len(words) - 1 + + chars_before = sum(len(w) + 1 for w in words[:highlight_index]) + char_width = 35 + total_width = len(full_text) * char_width + x_offset = (chars_before * char_width) - (total_width // 2) + settings["highlight_x_offset"] + + # Create highlighted word with same font logic + if settings["font"] == "System Default": + highlighted_word = TextClip( + text=highlight_word, + font_size=settings["font_size_highlight"], + color='#FFD700', + stroke_color='#FF6B35', + stroke_width=5 + ).with_duration(duration / 2).with_start(duration / 4).with_position((540 + x_offset, settings["subtitle_y_px"] + settings["highlight_offset"])) + else: + try: + highlighted_word = TextClip( + text=highlight_word, + font=settings["font"], + font_size=settings["font_size_highlight"], + color='#FFD700', + stroke_color='#FF6B35', + stroke_width=5 + ).with_duration(duration / 2).with_start(duration / 4).with_position((540 + x_offset, settings["subtitle_y_px"] + settings["highlight_offset"])) + except: + # Fallback to system default if font fails + highlighted_word = TextClip( + text=highlight_word, + font_size=settings["font_size_highlight"], + color='#FFD700', + stroke_color='#FF6B35', + stroke_width=5 + ).with_duration(duration / 2).with_start(duration / 4).with_position((540 + x_offset, settings["subtitle_y_px"] + settings["highlight_offset"])) + + final = CompositeVideoClip([vertical_clip, base_subtitle, highlighted_word], size=(1080, 1920)) + # Scale down the preview to fit 1080p monitor (max height ~900px to leave room for taskbar) + preview_scale = 900 / 1920 # Scale factor to fit height + preview_width = int(1080 * preview_scale) + preview_height = int(1920 * preview_scale) + preview_clip = final.resized((preview_width, preview_height)) + preview_clip.preview(fps=24, audio=False) + + clip.close() + final.close() + preview_clip.close() + +def update_setting(var_name, value): + settings[var_name] = int(value) if var_name.startswith("font_size") or "offset" in var_name or "y_px" in var_name else value + +def update_font(value): + settings["font"] = value + +def open_video(): + file_path = filedialog.askopenfilename(filetypes=[("MP4 files", "*.mp4")]) + if file_path: + settings["video_path"] = file_path + print(f"📂 Loaded video: {file_path}") + +def load_srt(): + file_path = filedialog.askopenfilename(filetypes=[("SRT Subtitle", "*.srt")]) + if file_path: + settings["subtitles"] = parse_srt(file_path) + settings["current_index"] = 0 + print(f"📝 Loaded {len(settings['subtitles'])} subtitles from {file_path}") + +def next_sub(): + if settings["current_index"] < len(settings["subtitles"]) - 1: + settings["current_index"] += 1 + start_preview_thread() + +def prev_sub(): + if settings["current_index"] > 0: + settings["current_index"] -= 1 + start_preview_thread() + +def start_preview_thread(): + threading.Thread(target=render_preview).start() + +# === GUI === +root = tk.Tk() +root.title("Subtitle Positioning Tool") +root.geometry("420x700") + +load_btn = tk.Button(root, text="🎥 Load Video", command=open_video) +load_btn.pack(pady=10) + +load_srt_btn = tk.Button(root, text="📑 Load SRT Subtitles", command=load_srt) +load_srt_btn.pack(pady=5) + +tk.Label(root, text="Subtitle Y Position").pack() +sub_y_slider = tk.Scale(root, from_=1000, to=1800, orient="horizontal", + command=lambda v: update_setting("subtitle_y_px", v)) +sub_y_slider.set(settings["subtitle_y_px"]) +sub_y_slider.pack() + +tk.Label(root, text="Highlight Y Offset").pack() +highlight_slider = tk.Scale(root, from_=-100, to=100, orient="horizontal", + command=lambda v: update_setting("highlight_offset", v)) +highlight_slider.set(settings["highlight_offset"]) +highlight_slider.pack() + +tk.Label(root, text="Highlight X Offset").pack() +highlight_x_slider = tk.Scale(root, from_=-300, to=300, orient="horizontal", + command=lambda v: update_setting("highlight_x_offset", v)) +highlight_x_slider.set(settings["highlight_x_offset"]) +highlight_x_slider.pack() + +tk.Label(root, text="Subtitle Font Size").pack() +sub_font_slider = tk.Scale(root, from_=30, to=100, orient="horizontal", + command=lambda v: update_setting("font_size_subtitle", v)) +sub_font_slider.set(settings["font_size_subtitle"]) +sub_font_slider.pack() + +tk.Label(root, text="Highlight Font Size").pack() +highlight_font_slider = tk.Scale(root, from_=30, to=100, orient="horizontal", + command=lambda v: update_setting("font_size_highlight", v)) +highlight_font_slider.set(settings["font_size_highlight"]) +highlight_font_slider.pack() + +tk.Label(root, text="Font").pack() +font_dropdown_var = tk.StringVar(value=settings["font"]) +font_dropdown = tk.OptionMenu(root, font_dropdown_var, *AVAILABLE_FONTS, command=update_font) +font_dropdown.pack(pady=5) + +preview_btn = tk.Button(root, text="▶️ Preview Clip", command=start_preview_thread) +preview_btn.pack(pady=10) + +nav_frame = tk.Frame(root) +tk.Button(nav_frame, text="⏮️ Prev", command=prev_sub).pack(side="left", padx=5) +tk.Button(nav_frame, text="⏭️ Next", command=next_sub).pack(side="right", padx=5) +nav_frame.pack(pady=5) + +save_btn = tk.Button(root, text="📂 Save Preset", command=save_presets) +save_btn.pack(pady=5) + +load_preset_btn = tk.Button(root, text="📂 Load Preset", command=load_presets) +load_preset_btn.pack(pady=5) + +root.mainloop() diff --git a/shorts_generator2.py b/shorts_generator2.py index b9f4b7c..ec223d6 100644 --- a/shorts_generator2.py +++ b/shorts_generator2.py @@ -2,6 +2,9 @@ import os import numpy as np from moviepy import VideoFileClip, TextClip, CompositeVideoClip from faster_whisper import WhisperModel +import tkinter as tk +from tkinter import filedialog, messagebox, ttk +import threading def detect_loud_moments(video_path, chunk_duration=5, threshold_db=10): print("🔍 Analyzing audio...") @@ -82,7 +85,6 @@ def create_short_clip(video_path, start, end, subtitles, output_path): # Base subtitle base_subtitle = TextClip( text=chunk_text.upper(), - font='C:/Windows/Fonts/LatoWeb-Bold.ttf', font_size=65, color='white', stroke_color='black', @@ -102,7 +104,6 @@ def create_short_clip(video_path, start, end, subtitles, output_path): highlighted_word = TextClip( text=word.upper(), - font='C:/Windows/Fonts/LatoWeb-Bold.ttf', font_size=68, color='#FFD700', stroke_color='#FF6B35', @@ -111,7 +112,7 @@ def create_short_clip(video_path, start, end, subtitles, output_path): word_width, _ = highlighted_word.size word_x = current_x + (word_width / 2) - highlighted_word = highlighted_word.with_start(word_start).with_end(word_end).with_position((word_x -8, subtitle_y_px)) + highlighted_word = highlighted_word.with_start(word_start).with_end(word_end).with_position((word_x -125, subtitle_y_px)) clips.append(highlighted_word) current_x += word_width + 20 # Add spacing between words @@ -129,19 +130,434 @@ def create_short_clip(video_path, start, end, subtitles, output_path): clip.audio.reader.close() final.close() -def generate_shorts(video_path, max_clips=3, output_folder="shorts"): +def validate_video(video_path, min_duration=30): + """Validate video file and return duration""" + try: + clip = VideoFileClip(video_path) + duration = clip.duration + clip.close() + + if duration < min_duration: + raise ValueError(f"Video is too short ({duration:.1f}s). Minimum {min_duration}s required.") + + return duration + except Exception as e: + if "No such file" in str(e): + raise FileNotFoundError(f"Video file not found: {video_path}") + elif "could not open" in str(e).lower(): + raise ValueError(f"Invalid or corrupted video file: {video_path}") + else: + raise ValueError(f"Error reading video: {str(e)}") + +def generate_shorts(video_path, max_clips=3, output_folder="shorts", progress_callback=None, threshold_db=-30, clip_duration=5): os.makedirs(output_folder, exist_ok=True) - best_moments = detect_loud_moments(video_path, threshold_db=-30) + + # Validate video first + try: + video_duration = validate_video(video_path, min_duration=clip_duration * 2) + if progress_callback: + progress_callback(f"✅ Video validated ({video_duration:.1f}s)", 5) + except Exception as e: + if progress_callback: + progress_callback(f"❌ Video validation failed", 0) + raise e + + if progress_callback: + progress_callback("🔍 Analyzing audio for loud moments...", 10) + + best_moments = detect_loud_moments(video_path, chunk_duration=clip_duration, threshold_db=threshold_db) selected = best_moments[:max_clips] + + if not selected: + raise ValueError(f"No loud moments found with threshold {threshold_db} dB. Try lowering the threshold or use a different video.") + + if progress_callback: + progress_callback(f"📊 Found {len(selected)} clips to generate", 20) + for i, (start, end) in enumerate(selected): + if progress_callback: + progress_callback(f"🗣️ Transcribing clip {i+1}/{len(selected)}", 30 + (i * 20)) + subtitles = transcribe_and_extract_subtitles(video_path, start, end) out_path = os.path.join(output_folder, f"short_{i+1}.mp4") + + if progress_callback: + progress_callback(f"🎬 Creating video {i+1}/{len(selected)}", 50 + (i * 20)) + create_short_clip(video_path, start, end, subtitles, out_path) + + if progress_callback: + progress_callback("✅ All shorts generated successfully!", 100) + +# GUI Components +class ShortsGeneratorGUI: + def __init__(self, root): + self.root = root + self.root.title("AI Shorts Generator") + self.root.geometry("500x400") + + self.video_path = None + self.output_folder = "shorts" + self.max_clips = 3 + self.threshold_db = -30 + self.clip_duration = 5 + + self.create_widgets() + + def create_widgets(self): + # Title + title_label = tk.Label(self.root, text="🎬 AI Shorts Generator", font=("Arial", 16, "bold")) + title_label.pack(pady=10) + + # Video selection + video_frame = tk.Frame(self.root) + video_frame.pack(pady=10, padx=20, fill="x") + + tk.Label(video_frame, text="Select Video File:").pack(anchor="w") + video_select_frame = tk.Frame(video_frame) + video_select_frame.pack(fill="x", pady=5) + + self.video_label = tk.Label(video_select_frame, text="No video selected", bg="white", relief="sunken") + self.video_label.pack(side="left", fill="x", expand=True, padx=(0, 5)) + + tk.Button(video_select_frame, text="Browse", command=self.select_video).pack(side="right") + + # Output folder selection + output_frame = tk.Frame(self.root) + output_frame.pack(pady=10, padx=20, fill="x") + + tk.Label(output_frame, text="Output Folder:").pack(anchor="w") + output_select_frame = tk.Frame(output_frame) + output_select_frame.pack(fill="x", pady=5) + + self.output_label = tk.Label(output_select_frame, text="shorts/", bg="white", relief="sunken") + self.output_label.pack(side="left", fill="x", expand=True, padx=(0, 5)) + + tk.Button(output_select_frame, text="Browse", command=self.select_output_folder).pack(side="right") + + # Settings frame + settings_frame = tk.LabelFrame(self.root, text="Settings", padx=10, pady=10) + settings_frame.pack(pady=10, padx=20, fill="x") + + # Max clips with on/off toggle + clips_frame = tk.Frame(settings_frame) + clips_frame.pack(fill="x", pady=5) + + clips_left_frame = tk.Frame(clips_frame) + clips_left_frame.pack(side="left") + + self.use_max_clips = tk.BooleanVar(value=True) + clips_checkbox = tk.Checkbutton(clips_left_frame, variable=self.use_max_clips, text="Max Clips to Generate:") + clips_checkbox.pack(side="left") + + self.clips_var = tk.IntVar(value=3) + self.clips_spinbox = tk.Spinbox(clips_frame, from_=1, to=10, width=5, textvariable=self.clips_var) + self.clips_spinbox.pack(side="right") + + # Bind checkbox to enable/disable spinbox + def toggle_clips_limit(): + if self.use_max_clips.get(): + self.clips_spinbox.config(state="normal") + else: + self.clips_spinbox.config(state="disabled") + + self.use_max_clips.trace("w", lambda *args: toggle_clips_limit()) + clips_checkbox.config(command=toggle_clips_limit) + + # Audio threshold + threshold_frame = tk.Frame(settings_frame) + threshold_frame.pack(fill="x", pady=5) + tk.Label(threshold_frame, text="Audio Threshold (dB):").pack(side="left") + self.threshold_var = tk.IntVar(value=-30) + threshold_spinbox = tk.Spinbox(threshold_frame, from_=-50, to=0, width=5, textvariable=self.threshold_var) + threshold_spinbox.pack(side="right") + + # Clip duration (increased to 120 seconds max) + duration_frame = tk.Frame(settings_frame) + duration_frame.pack(fill="x", pady=5) + tk.Label(duration_frame, text="Clip Duration (seconds):").pack(side="left") + self.duration_var = tk.IntVar(value=5) + duration_spinbox = tk.Spinbox(duration_frame, from_=3, to=120, width=5, textvariable=self.duration_var) + duration_spinbox.pack(side="right") + + # Preview button + self.preview_btn = tk.Button(self.root, text="🔍 Preview Clips", + command=self.preview_clips, bg="#2196F3", fg="white", + font=("Arial", 10, "bold"), pady=5) + self.preview_btn.pack(pady=10) + + # Generate button + self.generate_btn = tk.Button(self.root, text="🎬 Generate Shorts", + command=self.start_generation, bg="#4CAF50", fg="white", + font=("Arial", 12, "bold"), pady=10) + self.generate_btn.pack(pady=20) + + # Progress frame + progress_frame = tk.Frame(self.root) + progress_frame.pack(pady=10, padx=20, fill="x") + + self.progress_label = tk.Label(progress_frame, text="Ready to generate shorts") + self.progress_label.pack() + + self.progress_bar = ttk.Progressbar(progress_frame, length=400, mode="determinate") + self.progress_bar.pack(pady=5) + + def select_video(self): + file_path = filedialog.askopenfilename( + title="Select Video File", + filetypes=[("Video files", "*.mp4 *.mov *.avi *.mkv *.wmv")] + ) + if file_path: + self.video_path = file_path + self.video_label.config(text=os.path.basename(file_path)) + + def select_output_folder(self): + folder_path = filedialog.askdirectory(title="Select Output Folder") + if folder_path: + self.output_folder = folder_path + self.output_label.config(text=folder_path) + + def preview_clips(self): + if not self.video_path: + messagebox.showwarning("Warning", "Please select a video file first!") + return + + try: + # Validate video first + validate_video(self.video_path, min_duration=self.duration_var.get() * 2) + + # Analyze for loud moments + self.preview_btn.config(state="disabled", text="Analyzing...") + self.root.update() + + loud_moments = detect_loud_moments( + self.video_path, + chunk_duration=self.duration_var.get(), + threshold_db=self.threshold_var.get() + ) + + if not loud_moments: + messagebox.showinfo("Preview", f"No loud moments found with threshold {self.threshold_var.get()} dB.\nTry lowering the threshold.") + return + + # Show preview window + preview_window = tk.Toplevel(self.root) + preview_window.title("Preview and Select Clips") + preview_window.geometry("500x400") + + tk.Label(preview_window, text=f"Found {len(loud_moments)} loud moments:", font=("Arial", 12, "bold")).pack(pady=10) + + # Create scrollable frame for checkboxes + canvas = tk.Canvas(preview_window) + scrollbar = tk.Scrollbar(preview_window, orient="vertical", command=canvas.yview) + scrollable_frame = tk.Frame(canvas) + + scrollable_frame.bind( + "", + lambda e: canvas.configure(scrollregion=canvas.bbox("all")) + ) + + canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + + # Store checkbox variables and clip data + self.clip_vars = [] + # Use all clips if max clips is disabled, otherwise limit by setting + clips_to_show = loud_moments if not self.use_max_clips.get() else loud_moments[:self.clips_var.get()] + self.preview_clips_data = clips_to_show + + # Add selectable clips with checkboxes + for i, (start, end) in enumerate(self.preview_clips_data, 1): + duration = end - start + time_str = f"Clip {i}: {start//60:02.0f}:{start%60:05.2f} - {end//60:02.0f}:{end%60:05.2f} ({duration:.1f}s)" + + clip_var = tk.BooleanVar(value=True) # Default selected + self.clip_vars.append(clip_var) + + clip_frame = tk.Frame(scrollable_frame) + clip_frame.pack(fill="x", padx=10, pady=2) + + checkbox = tk.Checkbutton(clip_frame, variable=clip_var, text=time_str, + font=("Courier", 10), anchor="w") + checkbox.pack(fill="x") + + canvas.pack(side="left", fill="both", expand=True, padx=10, pady=5) + scrollbar.pack(side="right", fill="y") + + # Button frame + button_frame = tk.Frame(preview_window) + button_frame.pack(fill="x", padx=10, pady=10) + + # Select/Deselect all buttons + control_frame = tk.Frame(button_frame) + control_frame.pack(fill="x", pady=5) + + tk.Button(control_frame, text="Select All", + command=lambda: [var.set(True) for var in self.clip_vars]).pack(side="left", padx=5) + tk.Button(control_frame, text="Deselect All", + command=lambda: [var.set(False) for var in self.clip_vars]).pack(side="left", padx=5) + + # Generate selected clips button (fixed size for full text visibility) + generate_selected_btn = tk.Button(button_frame, text="🎬 Generate Selected Clips", + command=lambda: self.generate_selected_clips(preview_window), + bg="#4CAF50", fg="white", font=("Arial", 11, "bold"), + pady=8, width=25) + generate_selected_btn.pack(fill="x", pady=5) + + # Close button + tk.Button(button_frame, text="Close", command=preview_window.destroy).pack(pady=5) + + except Exception as e: + messagebox.showerror("Preview Error", f"Error analyzing video: {str(e)}") + finally: + self.preview_btn.config(state="normal", text="🔍 Preview Clips") + + def generate_selected_clips(self, preview_window): + """Generate only the selected clips from preview""" + try: + # Get selected clips + selected_clips = [] + for i, (clip_var, clip_data) in enumerate(zip(self.clip_vars, self.preview_clips_data)): + if clip_var.get(): + selected_clips.append((i+1, clip_data)) # (clip_number, (start, end)) + + if not selected_clips: + messagebox.showwarning("Warning", "Please select at least one clip to generate!") + return + + # Close preview window + preview_window.destroy() + + # Show confirmation + clip_count = len(selected_clips) + clip_numbers = [str(num) for num, _ in selected_clips] + confirm_msg = f"Generate {clip_count} selected clips (#{', #'.join(clip_numbers)})?" + + if not messagebox.askyesno("Confirm Generation", confirm_msg): + return + + # Start generation in background thread + self.selected_clips_data = [clip_data for _, clip_data in selected_clips] + self.generate_btn.config(state="disabled", text="Generating Selected...") + thread = threading.Thread(target=self.selected_generation_worker) + thread.daemon = True + thread.start() + + except Exception as e: + messagebox.showerror("Generation Error", f"Error starting generation: {str(e)}") + + def selected_generation_worker(self): + """Generate only selected clips""" + try: + # Check available disk space + import shutil + free_space_gb = shutil.disk_usage(self.output_folder)[2] / (1024**3) + if free_space_gb < 1: + raise RuntimeError(f"Insufficient disk space. Only {free_space_gb:.1f} GB available. Need at least 1 GB.") + + # Validate video first + try: + video_duration = validate_video(self.video_path, min_duration=self.duration_var.get() * 2) + self.update_progress(f"✅ Video validated ({video_duration:.1f}s)", 5) + except Exception as e: + self.update_progress(f"❌ Video validation failed", 0) + raise e + + os.makedirs(self.output_folder, exist_ok=True) + + selected_count = len(self.selected_clips_data) + self.update_progress(f"📊 Generating {selected_count} selected clips", 10) + + for i, (start, end) in enumerate(self.selected_clips_data): + self.update_progress(f"🗣️ Transcribing clip {i+1}/{selected_count}", 20 + (i * 30)) + + subtitles = transcribe_and_extract_subtitles(self.video_path, start, end) + out_path = os.path.join(self.output_folder, f"short_{i+1}.mp4") + + self.update_progress(f"🎬 Creating video {i+1}/{selected_count}", 40 + (i * 30)) + + create_short_clip(self.video_path, start, end, subtitles, out_path) + + self.update_progress("✅ Selected clips generated successfully!", 100) + messagebox.showinfo("Success", f"Successfully generated {selected_count} selected clips in '{self.output_folder}' folder!") + + except FileNotFoundError as e: + messagebox.showerror("File Error", str(e)) + except ValueError as e: + messagebox.showerror("Video Error", str(e)) + except RuntimeError as e: + messagebox.showerror("System Error", str(e)) + except Exception as e: + messagebox.showerror("Error", f"An unexpected error occurred: {str(e)}") + finally: + self.generate_btn.config(state="normal", text="🎬 Generate Shorts") + self.progress_bar["value"] = 0 + self.progress_label.config(text="Ready to generate shorts") + + def update_progress(self, message, percent): + self.progress_label.config(text=message) + self.progress_bar["value"] = percent + self.root.update() + + def generation_worker(self): + try: + # Check available disk space + import shutil + free_space_gb = shutil.disk_usage(self.output_folder)[2] / (1024**3) + if free_space_gb < 1: + raise RuntimeError(f"Insufficient disk space. Only {free_space_gb:.1f} GB available. Need at least 1 GB.") + + generate_shorts( + self.video_path, + max_clips=self.clips_var.get() if self.use_max_clips.get() else len(detect_loud_moments(self.video_path, chunk_duration=self.duration_var.get(), threshold_db=self.threshold_var.get())), + output_folder=self.output_folder, + progress_callback=self.update_progress, + threshold_db=self.threshold_var.get(), + clip_duration=self.duration_var.get() + ) + messagebox.showinfo("Success", f"Successfully generated shorts in '{self.output_folder}' folder!") + except FileNotFoundError as e: + messagebox.showerror("File Error", str(e)) + except ValueError as e: + messagebox.showerror("Video Error", str(e)) + except RuntimeError as e: + messagebox.showerror("System Error", str(e)) + except Exception as e: + messagebox.showerror("Error", f"An unexpected error occurred: {str(e)}") + finally: + self.generate_btn.config(state="normal", text="🎬 Generate Shorts") + self.progress_bar["value"] = 0 + self.progress_label.config(text="Ready to generate shorts") + + def start_generation(self): + if not self.video_path: + messagebox.showwarning("Warning", "Please select a video file first!") + return + + self.generate_btn.config(state="disabled", text="Generating...") + thread = threading.Thread(target=self.generation_worker) + thread.daemon = True + thread.start() + +def run_gui(): + root = tk.Tk() + app = ShortsGeneratorGUI(root) + root.mainloop() if __name__ == "__main__": import sys - if len(sys.argv) < 2: - print("Usage: python shorts_generator.py your_video.mp4") + if len(sys.argv) > 1 and sys.argv[1] == "--gui": + # Run GUI mode + run_gui() + elif len(sys.argv) < 2: + print("Usage: python shorts_generator2.py your_video.mp4") + print(" or: python shorts_generator2.py --gui") + run_gui() # Default to GUI if no args else: - generate_shorts(sys.argv[1]) + # Run command line mode + try: + generate_shorts(sys.argv[1]) + print("✅ Shorts generation completed successfully!") + except Exception as e: + print(f"❌ Error: {str(e)}") diff --git a/subtitle_generator.py b/subtitle_generator.py new file mode 100644 index 0000000..7412f0f --- /dev/null +++ b/subtitle_generator.py @@ -0,0 +1,155 @@ +import os +import math +import tempfile +import moviepy as mp +import speech_recognition as sr +import tkinter as tk +from tkinter import filedialog, messagebox, ttk + + +def format_time(seconds): + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + secs = int(seconds % 60) + millis = int((seconds - int(seconds)) * 1000) + return f"{hours:02}:{minutes:02}:{secs:02},{millis:03}" + + +def wrap_text(text, max_len=40): + """ + Wraps text to ~max_len characters per line without cutting words. + """ + words = text.split() + lines = [] + current_line = "" + + for word in words: + if len(current_line + " " + word) <= max_len: + current_line += (" " if current_line else "") + word + else: + lines.append(current_line) + current_line = word + + if current_line: + lines.append(current_line) + + return "\n".join(lines) + + +def write_srt(subtitles, output_path): + with open(output_path, 'w', encoding='utf-8') as f: + for i, sub in enumerate(subtitles, 1): + f.write(f"{i}\n") + f.write(f"{format_time(sub['start'])} --> {format_time(sub['end'])}\n") + f.write(f"{wrap_text(sub['text'])}\n\n") + + +def transcribe_video_to_srt(video_path, srt_output_path, progress_callback=None, chunk_duration=10): + try: + video = mp.VideoFileClip(video_path) + audio = video.audio + + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_audio_file: + temp_audio_path = temp_audio_file.name + audio.write_audiofile(temp_audio_path, logger=None) + + recognizer = sr.Recognizer() + subtitles = [] + + with sr.AudioFile(temp_audio_path) as source: + audio_duration = source.DURATION + num_chunks = math.ceil(audio_duration / chunk_duration) + + for i in range(num_chunks): + start_time = i * chunk_duration + end_time = min((i + 1) * chunk_duration, audio_duration) + + source_offset = start_time + duration = end_time - start_time + + audio_data = recognizer.record(source, offset=source_offset, duration=duration) + + try: + text = recognizer.recognize_google(audio_data) + subtitles.append({ + "start": start_time, + "end": end_time, + "text": text + }) + except sr.UnknownValueError: + pass + except sr.RequestError as e: + print(f"API error: {e}") + + # Update progress bar + if progress_callback: + progress_callback(i + 1, num_chunks) + + os.remove(temp_audio_path) + write_srt(subtitles, srt_output_path) + return True + except Exception as e: + print(f"Error: {e}") + return False + + +# -------------------- GUI -------------------- + +def select_file_and_generate(): + video_path = filedialog.askopenfilename( + title="Select a video file", + filetypes=[("Video files", "*.mp4 *.mov *.avi *.mkv")] + ) + + if not video_path: + return + + srt_output_path = filedialog.asksaveasfilename( + title="Save SRT subtitles as...", + defaultextension=".srt", + filetypes=[("Subtitle files", "*.srt")] + ) + + if not srt_output_path: + return + + progress_bar["value"] = 0 + progress_label.config(text="Starting...") + root.update() + + def update_progress(current, total): + percent = (current / total) * 100 + progress_bar["value"] = percent + progress_label.config(text=f"Progress: {current}/{total} chunks") + root.update() + + success = transcribe_video_to_srt(video_path, srt_output_path, progress_callback=update_progress) + + if success: + messagebox.showinfo("Success", f"Subtitles saved to:\n{srt_output_path}") + else: + messagebox.showerror("Error", "Something went wrong. See console for details.") + + progress_label.config(text="Done") + + +# GUI Setup +root = tk.Tk() +root.title("Auto Subtitle Generator (.srt) with Progress") + +frame = tk.Frame(root, padx=20, pady=20) +frame.pack() + +label = tk.Label(frame, text="Select a video file to auto-generate subtitles (SRT):") +label.pack(pady=(0, 10)) + +select_button = tk.Button(frame, text="Select Video and Generate Subtitles", command=select_file_and_generate) +select_button.pack(pady=5) + +progress_bar = ttk.Progressbar(frame, length=300, mode="determinate") +progress_bar.pack(pady=(15, 5)) + +progress_label = tk.Label(frame, text="Idle") +progress_label.pack() + +root.mainloop() diff --git a/subtitle_gui_presets.json b/subtitle_gui_presets.json new file mode 100644 index 0000000..8858791 --- /dev/null +++ b/subtitle_gui_presets.json @@ -0,0 +1 @@ +{"subtitle_y_px": 1550, "highlight_offset": 0, "font_size_subtitle": 65, "font_size_highlight": 65, "highlight_x_offset": -53, "video_path": "C:/Users/braul/Desktop/shorts_project/shorts/short_1.mp4"} \ No newline at end of file diff --git a/subtitles.srt b/subtitles.srt new file mode 100644 index 0000000..0ede093 --- /dev/null +++ b/subtitles.srt @@ -0,0 +1,3 @@ +1 +00:00:00,000 --> 00:00:02,500 +You're running \ No newline at end of file