import tkinter as tk from tkinter import ttk, filedialog, messagebox import threading import time import os import sys # Import the ShortsGeneratorGUI from shorts_generator2.py try: from shorts_generator2 import ShortsGeneratorGUI except ImportError: messagebox.showerror("Error", "Could not import ShortsGeneratorGUI from shorts_generator2.py") sys.exit(1) class ProgressWindow: def __init__(self, parent, title="Processing"): self.parent = parent self.window = tk.Toplevel(parent) self.window.title(title) self.window.geometry("450x180") self.window.minsize(400, 160) self.window.resizable(True, False) self.window.transient(parent) self.window.grab_set() # Modern colors self.colors = { 'bg_primary': '#1a1a1a', 'bg_secondary': '#2d2d2d', 'text_primary': '#ffffff', 'text_secondary': '#b8b8b8', 'accent_blue': '#007acc', 'accent_red': '#dc3545' } self.window.configure(bg=self.colors['bg_primary']) # Make window responsive self.window.columnconfigure(0, weight=1) # Center the window self.window.update_idletasks() x = (self.window.winfo_screenwidth() // 2) - (450 // 2) y = (self.window.winfo_screenheight() // 2) - (180 // 2) self.window.geometry(f"450x180+{x}+{y}") # Bind resize event self.window.bind('', self.on_window_resize) # Create modern progress interface main_frame = tk.Frame(self.window, bg=self.colors['bg_secondary'], relief="flat", bd=0) main_frame.pack(fill="both", expand=True, padx=20, pady=20) main_frame.columnconfigure(0, weight=1) # Title title_label = tk.Label(main_frame, text=title, font=('Segoe UI', 14, 'bold'), bg=self.colors['bg_secondary'], fg=self.colors['text_primary']) title_label.grid(row=0, column=0, sticky="ew", pady=(0, 15)) # Status self.status_label = tk.Label(main_frame, text="Initializing...", font=('Segoe UI', 10), bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], anchor="w") self.status_label.grid(row=1, column=0, sticky="ew", pady=(0, 5)) # Time info self.time_label = tk.Label(main_frame, text="Elapsed: 0.0s | Remaining: --s", font=('Segoe UI', 9), bg=self.colors['bg_secondary'], fg=self.colors['text_secondary'], anchor="w") self.time_label.grid(row=2, column=0, sticky="ew", pady=(0, 10)) # Modern progress bar styling style = ttk.Style() style.theme_use('clam') style.configure("Modern.Horizontal.TProgressbar", background=self.colors['accent_blue'], troughcolor=self.colors['bg_primary'], borderwidth=0, lightcolor=self.colors['accent_blue'], darkcolor=self.colors['accent_blue']) # Main progress bar self.progress_var = tk.DoubleVar() self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var, maximum=100, style="Modern.Horizontal.TProgressbar") self.progress_bar.grid(row=3, column=0, sticky="ew", pady=(0, 8)) # Detection progress bar (hidden by default) self.detection_label = tk.Label(main_frame, text="", font=('Segoe UI', 9), bg=self.colors['bg_secondary'], fg=self.colors['accent_blue'], anchor="w") self.detection_progress_var = tk.DoubleVar() self.detection_progress_bar = ttk.Progressbar(main_frame, variable=self.detection_progress_var, maximum=100, style="Modern.Horizontal.TProgressbar") # Modern cancel button self.cancel_btn = tk.Button(main_frame, text="Cancel", command=self.cancel, bg=self.colors['accent_red'], fg='white', font=('Segoe UI', 10, 'bold'), relief="flat", bd=0, pady=8, cursor="hand2") self.cancel_btn.grid(row=6, column=0, pady=(10, 0)) self.start_time = time.time() self.cancelled = False def on_window_resize(self, event): """Handle window resize events""" if event.widget == self.window: # Update progress bar length based on window width width = self.window.winfo_width() progress_length = max(250, width - 80) # Minimum 250px, with padding try: self.progress_bar.config(length=progress_length) self.detection_progress_bar.config(length=progress_length) except: pass def show_detection_progress(self): """Show the detection progress bar - thread safe""" def _show(): self.detection_label.grid(row=3, column=0, sticky="ew", pady=(3, 0)) self.detection_progress_bar.grid(row=4, column=0, sticky="ew", pady=(3, 5)) self.window.after(0, _show) def hide_detection_progress(self): """Hide the detection progress bar - thread safe""" def _hide(): self.detection_label.grid_remove() self.detection_progress_bar.grid_remove() self.window.after(0, _hide) def update_progress(self, message, percent): """Update main progress bar - thread safe""" def _update(): if self.cancelled: return self.status_label.config(text=message) self.progress_var.set(percent) elapsed = time.time() - self.start_time if percent > 0: remaining = (elapsed / percent) * (100 - percent) self.time_label.config(text=f"Elapsed: {elapsed:.1f}s | Remaining: {remaining:.1f}s") # Schedule the update on the main thread self.window.after(0, _update) def update_detection_progress(self, message, percent): """Update detection progress bar - thread safe""" def _update(): if self.cancelled: return self.detection_label.config(text=message) self.detection_progress_var.set(percent) # Schedule the update on the main thread self.window.after(0, _update) def cancel(self): """Cancel the current operation""" self.cancelled = True self.window.destroy() def close(self): """Close the progress window""" if not self.cancelled: self.window.destroy() class ClipSelectionWindow: def __init__(self, parent, clips, video_path, detection_mode): self.parent = parent self.clips = clips self.video_path = video_path self.detection_mode = detection_mode self.selected_clips = [] # Modern colors self.colors = { 'bg_primary': '#1a1a1a', 'bg_secondary': '#2d2d2d', 'bg_tertiary': '#3d3d3d', 'text_primary': '#ffffff', 'text_secondary': '#b8b8b8', 'accent_green': '#28a745', 'accent_red': '#dc3545', 'accent_blue': '#007acc', 'border': '#404040' } # Create modern window self.window = tk.Toplevel(parent.root) self.window.title("Select Clips to Generate") self.window.geometry("700x600") self.window.minsize(500, 400) self.window.resizable(True, True) self.window.transient(parent.root) self.window.grab_set() self.window.configure(bg=self.colors['bg_primary']) # Make window responsive self.window.rowconfigure(1, weight=1) self.window.columnconfigure(0, weight=1) # Center the window self.window.update_idletasks() x = (self.window.winfo_screenwidth() // 2) - (700 // 2) y = (self.window.winfo_screenheight() // 2) - (600 // 2) self.window.geometry(f"700x600+{x}+{y}") # Bind resize event self.window.bind('', self.on_window_resize) self.setup_gui() def setup_gui(self): # Header section header_frame = tk.Frame(self.window, bg=self.colors['bg_secondary'], relief="flat", bd=0) header_frame.grid(row=0, column=0, sticky="ew", padx=20, pady=(20, 0)) header_frame.columnconfigure(0, weight=1) # Modern title title_label = tk.Label(header_frame, text=f"🎯 Found {len(self.clips)} clips using {self.detection_mode} detection", font=('Segoe UI', 16, 'bold'), bg=self.colors['bg_secondary'], fg=self.colors['text_primary']) title_label.pack(pady=20) # Instructions with modern styling self.instruction_label = tk.Label(header_frame, text="Select the clips you want to generate by checking the boxes below:", font=('Segoe UI', 11), bg=self.colors['bg_secondary'], fg=self.colors['text_secondary'], wraplength=600) self.instruction_label.pack(pady=(0, 20)) # Main content area content_frame = tk.Frame(self.window, bg=self.colors['bg_primary']) content_frame.grid(row=1, column=0, sticky="nsew", padx=20, pady=10) content_frame.rowconfigure(0, weight=1) content_frame.columnconfigure(0, weight=1) # Modern clips list with card design list_frame = tk.Frame(content_frame, bg=self.colors['bg_secondary'], relief="flat", bd=0) list_frame.grid(row=0, column=0, sticky="nsew", padx=0, pady=0) list_frame.rowconfigure(0, weight=1) list_frame.columnconfigure(0, weight=1) # Scrollable canvas with modern scrollbar canvas = tk.Canvas(list_frame, bg=self.colors['bg_secondary'], highlightthickness=0) # Modern scrollbar styling scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview) scrollable_frame = tk.Frame(canvas, bg=self.colors['bg_secondary']) 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) # Modern clip cards self.clip_vars = [] for i, (start, end) in enumerate(self.clips): var = tk.BooleanVar(value=True) self.clip_vars.append(var) duration = end - start # Modern clip card clip_card = tk.Frame(scrollable_frame, bg=self.colors['bg_tertiary'], relief="flat", bd=1, highlightbackground=self.colors['border'], highlightthickness=1) clip_card.pack(fill="x", pady=8, padx=15) clip_card.columnconfigure(1, weight=1) # Modern checkbox checkbox = tk.Checkbutton(clip_card, variable=var, text="", width=2, bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'], selectcolor=self.colors['accent_blue'], activebackground=self.colors['bg_tertiary'], relief="flat", bd=0) checkbox.grid(row=0, column=0, padx=15, pady=15, sticky="w") # Clip info with modern typography info_frame = tk.Frame(clip_card, bg=self.colors['bg_tertiary']) info_frame.grid(row=0, column=1, sticky="ew", padx=(0, 15), pady=15) clip_title = tk.Label(info_frame, text=f"Clip {i+1}", font=('Segoe UI', 11, 'bold'), bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'], anchor="w") clip_title.pack(anchor="w") clip_details = tk.Label(info_frame, text=f"⏱️ {start:.1f}s - {end:.1f}s • Duration: {duration:.1f}s", font=('Segoe UI', 10), bg=self.colors['bg_tertiary'], fg=self.colors['text_secondary'], anchor="w") clip_details.pack(anchor="w", pady=(2, 0)) canvas.grid(row=0, column=0, sticky="nsew") scrollbar.grid(row=0, column=1, sticky="ns") # Modern action buttons action_frame = tk.Frame(self.window, bg=self.colors['bg_primary']) action_frame.grid(row=2, column=0, sticky="ew", padx=20, pady=(10, 20)) action_frame.columnconfigure(0, weight=1) action_frame.columnconfigure(1, weight=1) action_frame.columnconfigure(2, weight=1) action_frame.columnconfigure(3, weight=1) # Selection buttons select_all_btn = self.create_modern_button(action_frame, "✅ Select All", self.select_all, self.colors['accent_blue']) select_all_btn.grid(row=0, column=0, padx=(0, 5), sticky="ew") select_none_btn = self.create_modern_button(action_frame, "❌ Select None", self.select_none, self.colors['bg_tertiary']) select_none_btn.grid(row=0, column=1, padx=5, sticky="ew") # Action buttons cancel_btn = self.create_modern_button(action_frame, "Cancel", self.cancel, self.colors['accent_red']) cancel_btn.grid(row=0, column=2, padx=5, sticky="ew") generate_btn = self.create_modern_button(action_frame, "🎬 Generate Selected", self.generate_selected, self.colors['accent_green']) generate_btn.grid(row=0, column=3, padx=(5, 0), sticky="ew") def create_modern_button(self, parent, text, command, color): """Create a modern button for the clip selection window""" button = tk.Button(parent, text=text, command=command, bg=color, fg='white', font=('Segoe UI', 10, 'bold'), relief="flat", bd=0, pady=10, cursor="hand2") # Add hover effect original_color = color def on_enter(e): # Lighten color on hover button.config(bg=self.lighten_color(original_color, 0.2)) def on_leave(e): button.config(bg=original_color) button.bind("", on_enter) button.bind("", on_leave) return button def lighten_color(self, hex_color, factor): """Lighten a hex color by a factor (0.0 to 1.0)""" hex_color = hex_color.lstrip('#') rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) rgb = tuple(min(255, int(c + (255 - c) * factor)) for c in rgb) return f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}" def on_window_resize(self, event): """Handle window resize events""" if event.widget == self.window: # Get current window size width = self.window.winfo_width() # Adjust wrap length for instruction text wrap_length = max(300, width - 100) try: self.instruction_label.config(wraplength=wrap_length) except: pass def select_all(self): """Select all clips""" for var in self.clip_vars: var.set(True) def select_none(self): """Deselect all clips""" for var in self.clip_vars: var.set(False) def cancel(self): """Cancel selection""" self.window.destroy() def generate_selected(self): """Generate the selected clips""" # Get selected clips self.selected_clips = [] for i, var in enumerate(self.clip_vars): if var.get(): self.selected_clips.append(self.clips[i]) if not self.selected_clips: messagebox.showwarning("No Selection", "Please select at least one clip to generate.") return # Close selection window self.window.destroy() # Trigger generation with selected clips if hasattr(self.parent, 'generate_selected_clips'): self.parent.generate_selected_clips(self.selected_clips) else: messagebox.showerror("Error", "Generation method not available.") class MainApplication: def __init__(self): self.root = tk.Tk() self.root.title("AI Shorts Generator - Main Controller") self.root.geometry("800x600") # Wider window for horizontal layout self.root.minsize(700, 500) # Increased minimum width for horizontal layout # Modern color scheme self.colors = { 'bg_primary': '#1a1a1a', # Dark background 'bg_secondary': '#2d2d2d', # Card backgrounds 'bg_tertiary': '#3d3d3d', # Elevated elements 'accent_blue': '#007acc', # Primary blue 'accent_green': '#28a745', # Success green 'accent_orange': '#fd7e14', # Warning orange 'accent_purple': '#6f42c1', # Secondary purple 'accent_red': '#dc3545', # Error red 'text_primary': '#ffffff', # Primary text 'text_secondary': '#b8b8b8', # Secondary text 'text_muted': '#6c757d', # Muted text 'border': '#404040', # Border color 'hover': '#4a4a4a' # Hover state } self.root.configure(bg=self.colors['bg_primary']) # Modern fonts self.fonts = { 'title': ('Segoe UI', 18, 'bold'), 'heading': ('Segoe UI', 12, 'bold'), 'body': ('Segoe UI', 10), 'caption': ('Segoe UI', 9), 'button': ('Segoe UI', 10, 'bold') } # Make window responsive self.root.rowconfigure(0, weight=1) self.root.columnconfigure(0, weight=1) # Initialize the ShortsGeneratorGUI will be created when needed self.shorts_generator = None self.setup_gui() # Bind resize event for responsive updates self.root.bind('', self.on_window_resize) def get_shorts_generator(self): """Get or create a minimal ShortsGenerator instance when needed""" if self.shorts_generator is None: try: # Create a simple container class with just the attributes we need class MinimalShortsGenerator: def __init__(self): self.video_path = None self.output_folder = "shorts" self.max_clips = 3 self.threshold_db = -30 self.clip_duration = 5 self.detection_mode_var = tk.StringVar(value="loud") self.shorts_generator = MinimalShortsGenerator() print("✅ Minimal ShortsGenerator initialized successfully") except Exception as e: print(f"❌ Failed to initialize ShortsGenerator: {e}") messagebox.showerror("Initialization Error", f"Failed to initialize ShortsGenerator: {e}") return None return self.shorts_generator def setup_gui(self): """Setup the main GUI with modern horizontal design""" # Create main container that fills the window main_container = tk.Frame(self.root, bg=self.colors['bg_primary']) main_container.pack(fill="both", expand=True, padx=20, pady=20) # Modern title with gradient effect simulation title_frame = tk.Frame(main_container, bg=self.colors['bg_primary']) title_frame.pack(fill="x", pady=(0, 20)) title_label = tk.Label(title_frame, text="🎬 AI Shorts Generator", font=self.fonts['title'], bg=self.colors['bg_primary'], fg=self.colors['text_primary']) title_label.pack() subtitle_label = tk.Label(title_frame, text="Create viral content with AI-powered video analysis", font=self.fonts['caption'], bg=self.colors['bg_primary'], fg=self.colors['text_secondary']) subtitle_label.pack(pady=(5, 0)) # Create horizontal layout with left and right panels content_frame = tk.Frame(main_container, bg=self.colors['bg_primary']) content_frame.pack(fill="both", expand=True) # Left panel - Video Selection and Settings left_panel = tk.Frame(content_frame, bg=self.colors['bg_primary']) left_panel.pack(side="left", fill="both", expand=True, padx=(0, 15)) # Modern card-style file selection frame file_card = self.create_modern_card(left_panel, "📁 Video Selection") self.file_label = tk.Label(file_card, text="No video selected", font=self.fonts['body'], bg=self.colors['bg_tertiary'], fg=self.colors['text_secondary'], relief="flat", anchor="w", pady=12, padx=15, bd=1, highlightbackground=self.colors['border'], highlightthickness=1) self.file_label.pack(fill="x", pady=(0, 10)) # Modern button with hover effect select_btn = self.create_modern_button(file_card, "📁 Select Video File", self.select_video_file, self.colors['accent_blue']) select_btn.pack(fill="x", pady=5) # Modern settings card settings_card = self.create_modern_card(left_panel, "⚙️ Detection Settings") # Detection mode with modern styling mode_label = tk.Label(settings_card, text="Detection Mode:", font=self.fonts['heading'], bg=self.colors['bg_secondary'], fg=self.colors['text_primary']) mode_label.pack(anchor="w", pady=(0, 10)) self.detection_var = tk.StringVar(value="loud") detection_container = tk.Frame(settings_card, bg=self.colors['bg_secondary']) detection_container.pack(fill="x", pady=5) modes = [("🔊 Loud Moments", "loud"), ("🎬 Scene Changes", "scene"), ("🏃 Motion", "motion"), ("💬 Speech", "speech"), ("🎵 Audio Peaks", "peaks"), ("🎯 Combined", "combined")] # Create modern radio buttons in rows for i in range(0, len(modes), 3): # 3 per row row_frame = tk.Frame(detection_container, bg=self.colors['bg_secondary']) row_frame.pack(fill="x", pady=3) for j in range(3): if i + j < len(modes): text, value = modes[i + j] radio_frame = tk.Frame(row_frame, bg=self.colors['bg_tertiary'], relief="flat", bd=1) radio_frame.pack(side="left", fill="x", expand=True, padx=5) radio = tk.Radiobutton(radio_frame, text=text, variable=self.detection_var, value=value, bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'], font=self.fonts['body'], selectcolor=self.colors['accent_blue'], activebackground=self.colors['hover'], activeforeground=self.colors['text_primary'], relief="flat", bd=0) radio.pack(pady=8, padx=10) # Right panel - Actions and Controls right_panel = tk.Frame(content_frame, bg=self.colors['bg_primary']) right_panel.pack(side="right", fill="y", padx=(15, 0)) right_panel.config(width=300) # Fixed width for actions panel # Modern action buttons card button_card = self.create_modern_card(right_panel, "🚀 Actions") # Preview button self.preview_btn = self.create_modern_button(button_card, "🔍 Preview Clips", self.preview_clips_threaded, self.colors['accent_blue']) self.preview_btn.pack(fill="x", pady=5) # Generate button - primary action self.generate_btn = self.create_modern_button(button_card, "🎬 Generate All Clips", self.generate_shorts_threaded, self.colors['accent_green'], large=True) self.generate_btn.pack(fill="x", pady=5) # Secondary action buttons self.edit_btn = self.create_modern_button(button_card, "✏️ Edit Generated Shorts", self.open_editor, self.colors['accent_orange']) self.edit_btn.pack(fill="x", pady=5) self.thumbnail_btn = self.create_modern_button(button_card, "📸 Create Thumbnails", self.open_thumbnails, self.colors['accent_purple']) self.thumbnail_btn.pack(fill="x", pady=5) # Info tip with modern styling - moved to bottom tip_frame = tk.Frame(button_card, bg=self.colors['bg_secondary']) tip_frame.pack(fill="x", pady=(20, 0)) tip_icon = tk.Label(tip_frame, text="💡", font=self.fonts['body'], bg=self.colors['bg_secondary'], fg=self.colors['accent_orange']) tip_icon.pack(side="left", padx=(0, 8)) info_label = tk.Label(tip_frame, text="Tip: Use 'Preview Clips' to select specific clips for faster processing", font=self.fonts['caption'], fg=self.colors['text_muted'], bg=self.colors['bg_secondary'], wraplength=250, anchor="w") info_label.pack(side="left", fill="x", expand=True) # Modern status bar at the bottom status_frame = tk.Frame(main_container, bg=self.colors['bg_tertiary'], relief="flat", bd=1) status_frame.pack(fill="x", pady=(20, 0)) status_icon = tk.Label(status_frame, text="●", font=self.fonts['body'], bg=self.colors['bg_tertiary'], fg=self.colors['accent_green']) status_icon.pack(side="left", padx=15, pady=8) self.status_label = tk.Label(status_frame, text="Ready - Select a video to begin", font=self.fonts['caption'], fg=self.colors['text_secondary'], bg=self.colors['bg_tertiary'], wraplength=400, anchor="w") self.status_label.pack(side="left", fill="x", expand=True, pady=8, padx=(0, 15)) # Store detected clips for selection self.detected_clips = [] def create_modern_card(self, parent, title): """Create a modern card-style container""" card_frame = tk.Frame(parent, bg=self.colors['bg_secondary'], relief="flat", bd=0) card_frame.pack(fill="x", pady=10) # Card header header_frame = tk.Frame(card_frame, bg=self.colors['bg_secondary']) header_frame.pack(fill="x", padx=20, pady=(15, 5)) header_label = tk.Label(header_frame, text=title, font=self.fonts['heading'], bg=self.colors['bg_secondary'], fg=self.colors['text_primary']) header_label.pack(anchor="w") # Card content area content_frame = tk.Frame(card_frame, bg=self.colors['bg_secondary']) content_frame.pack(fill="both", expand=True, padx=20, pady=(5, 20)) return content_frame def create_modern_button(self, parent, text, command, color, large=False): """Create a modern button with hover effects""" font = self.fonts['button'] if not large else ('Segoe UI', 12, 'bold') pady = 12 if not large else 15 button = tk.Button(parent, text=text, command=command, bg=color, fg='white', font=font, relief="flat", bd=0, pady=pady, activebackground=self.adjust_color(color, -20), activeforeground='white', cursor="hand2") # Add hover effects def on_enter(e): button.config(bg=self.adjust_color(color, 15)) def on_leave(e): button.config(bg=color) button.bind("", on_enter) button.bind("", on_leave) return button def adjust_color(self, hex_color, adjustment): """Adjust color brightness for hover effects""" # Remove # if present hex_color = hex_color.lstrip('#') # Convert to RGB rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) # Adjust brightness adjusted = tuple(max(0, min(255, c + adjustment)) for c in rgb) # Convert back to hex return f"#{adjusted[0]:02x}{adjusted[1]:02x}{adjusted[2]:02x}" def on_window_resize(self, event): """Handle window resize events for responsive layout""" if event.widget == self.root: # Get current window size width = self.root.winfo_width() height = self.root.winfo_height() # Adjust font sizes based on window width if width < 450: title_font_size = 14 button_font_size = 10 info_wrap_length = 300 elif width < 550: title_font_size = 15 button_font_size = 11 info_wrap_length = 350 else: title_font_size = 16 button_font_size = 12 info_wrap_length = 400 # Update wraplength for text elements try: # Find and update info label for widget in self.root.winfo_children(): if isinstance(widget, tk.Frame): for subwidget in widget.winfo_children(): if isinstance(subwidget, tk.Frame): for subsubwidget in subwidget.winfo_children(): if isinstance(subsubwidget, tk.Label) and "Tip:" in str(subsubwidget.cget("text")): subsubwidget.config(wraplength=info_wrap_length) elif isinstance(subsubwidget, tk.Label) and "Ready" in str(subsubwidget.cget("text")): subsubwidget.config(wraplength=info_wrap_length) except: pass # Ignore any errors during dynamic updates def select_video_file(self): """Select video file""" filetypes = [ ("Video files", "*.mp4 *.avi *.mov *.mkv *.wmv *.flv *.webm"), ("All files", "*.*") ] file_path = filedialog.askopenfilename( title="Select Video File", filetypes=filetypes ) if file_path: # Update display filename = os.path.basename(file_path) self.file_label.config(text=filename) # Update shorts generator generator = self.get_shorts_generator() if generator: generator.video_path = file_path if hasattr(generator, 'video_label'): generator.video_label.config(text=os.path.basename(file_path)) self.status_label.config(text=f"Video loaded: {filename}") def preview_clips_threaded(self): """Run preview clips with progress window""" generator = self.get_shorts_generator() if not generator or not hasattr(generator, 'video_path') or not generator.video_path: messagebox.showwarning("No Video", "Please select a video file first.") return # Update settings in shorts generator self.update_shorts_generator_settings() # Create progress window progress_window = ProgressWindow(self.root, "Previewing Clips") # Show detection progress for heavy modes detection_mode = self.detection_var.get() if detection_mode in ["scene", "motion", "speech", "peaks", "combined"]: progress_window.show_detection_progress() def run_preview(): try: from shorts_generator2 import (detect_loud_moments, detect_scene_changes_with_progress, detect_motion_intensity_with_progress, detect_speech_emotion_with_progress, detect_audio_peaks_with_progress, detect_combined_intensity_with_progress, validate_video) generator = self.get_shorts_generator() if not generator or not hasattr(generator, 'video_path'): raise Exception("ShortsGeneratorGUI not properly initialized") video_path = generator.video_path clip_duration = 5 # Fixed clip duration since we removed the setting detection_mode = self.detection_var.get() # Thread-safe progress callbacks with cancellation checks def progress_callback(message, percent): if progress_window.cancelled: raise InterruptedError("Preview cancelled by user") self.root.after(0, lambda: progress_window.update_progress(message, percent)) def detection_callback(message, percent): if progress_window.cancelled: raise InterruptedError("Preview cancelled by user") self.root.after(0, lambda: progress_window.update_detection_progress(message, percent)) # Check for cancellation before starting if progress_window.cancelled: return # Validate video first self.root.after(0, lambda: progress_callback("Validating video...", 5)) validate_video(video_path, min_duration=clip_duration * 2) # Run detection based on mode self.root.after(0, lambda: progress_callback(f"Analyzing {detection_mode} moments...", 10)) if detection_mode == "loud": moments = detect_loud_moments(video_path, chunk_duration=clip_duration, threshold_db=-30) elif detection_mode == "scene": moments = detect_scene_changes_with_progress(video_path, chunk_duration=clip_duration, progress_callback=detection_callback) elif detection_mode == "motion": moments = detect_motion_intensity_with_progress(video_path, chunk_duration=clip_duration, progress_callback=detection_callback) elif detection_mode == "speech": moments = detect_speech_emotion_with_progress(video_path, chunk_duration=clip_duration, progress_callback=detection_callback) elif detection_mode == "peaks": moments = detect_audio_peaks_with_progress(video_path, chunk_duration=clip_duration, progress_callback=detection_callback) elif detection_mode == "combined": moments = detect_combined_intensity_with_progress(video_path, chunk_duration=clip_duration, progress_callback=detection_callback) else: moments = detect_loud_moments(video_path, chunk_duration=clip_duration, threshold_db=-30) # Final progress updates without cancellation checks (process is nearly complete) if not progress_window.cancelled: self.root.after(0, lambda: progress_window.update_progress("Analysis complete!", 90)) # Show results with selection window def show_results(): if progress_window.cancelled: return if moments: self.detected_clips = moments # Store for potential generation progress_window.close() # Show clip selection window ClipSelectionWindow(self, moments, video_path, detection_mode) else: progress_window.close() messagebox.showwarning("No Results", "No interesting moments found with current settings.") if not progress_window.cancelled: self.root.after(0, lambda: progress_window.update_progress("Preparing results...", 100)) self.root.after(500, show_results) except InterruptedError: # Preview was cancelled by user def show_cancelled(): progress_window.close() # Don't show an error message for user cancellation self.root.after(0, show_cancelled) except Exception as e: error_msg = str(e) def show_error(): progress_window.close() messagebox.showerror("Preview Error", f"An error occurred during preview:\n{error_msg}") self.root.after(0, show_error) # Start the preview in a background thread threading.Thread(target=run_preview, daemon=True).start() def generate_shorts_threaded(self): """Run generate shorts with progress window""" generator = self.get_shorts_generator() if not generator or not hasattr(generator, 'video_path') or not generator.video_path: messagebox.showwarning("No Video", "Please select a video file first.") return # Update settings self.update_shorts_generator_settings() # Create progress window progress_window = ProgressWindow(self.root, "Generating Shorts") # Show detection progress for heavy modes detection_mode = self.detection_var.get() if detection_mode in ["scene", "motion", "speech", "peaks", "combined"]: progress_window.show_detection_progress() def run_generation(): try: from shorts_generator2 import generate_shorts generator = self.get_shorts_generator() if not generator or not hasattr(generator, 'video_path'): raise Exception("ShortsGeneratorGUI not properly initialized") video_path = generator.video_path detection_mode = self.detection_var.get() clip_duration = 5 # Default duration # Thread-safe progress callbacks with cancellation checks def progress_callback(message, percent): if progress_window.cancelled: raise InterruptedError("Generation cancelled by user") self.root.after(0, lambda: progress_window.update_progress(message, percent)) def detection_callback(message, percent): if progress_window.cancelled: raise InterruptedError("Generation cancelled by user") self.root.after(0, lambda: progress_window.update_detection_progress(message, percent)) # Check for cancellation before starting if progress_window.cancelled: return # Run the actual generation (no max clips limit - user will select from preview) generate_shorts( video_path, max_clips=50, # Generate a reasonable number for preview/selection output_folder="shorts", progress_callback=progress_callback, detection_progress_callback=detection_callback, threshold_db=-30, clip_duration=5, detection_mode=detection_mode ) def show_success(): progress_window.close() messagebox.showinfo("Success", "Successfully generated shorts!\n\nCheck the 'shorts' folder for your videos.") self.root.after(0, show_success) except InterruptedError: # Generation was cancelled by user def show_cancelled(): progress_window.close() # Don't show an error message for user cancellation self.root.after(0, show_cancelled) except Exception as e: error_msg = str(e) def show_error(): progress_window.close() messagebox.showerror("Generation Error", f"An error occurred during generation:\n{error_msg}") self.root.after(0, show_error) # Start the generation in a background thread threading.Thread(target=run_generation, daemon=True).start() def generate_selected_clips(self, selected_clips): """Generate only the selected clips""" generator = self.get_shorts_generator() if not generator or not hasattr(generator, 'video_path') or not generator.video_path: messagebox.showerror("Error", "No video selected.") return # Create progress window progress_window = ProgressWindow(self.root, "Generating Selected Clips") def run_selected_generation(): try: generator = self.get_shorts_generator() if not generator or not hasattr(generator, 'video_path'): raise Exception("ShortsGeneratorGUI not properly initialized") video_path = generator.video_path # Thread-safe progress callback with cancellation checks def progress_callback(message, percent): if progress_window.cancelled: raise InterruptedError("Generation cancelled by user") self.root.after(0, lambda: progress_window.update_progress(message, percent)) # Check for cancellation before starting if progress_window.cancelled: return # Use a custom generation function for selected clips self.generate_clips_from_moments( video_path, selected_clips, progress_callback, progress_window ) def show_success(): progress_window.close() messagebox.showinfo("Success", f"Successfully generated {len(selected_clips)} selected clips!\n\nCheck the 'shorts' folder for your videos.") self.root.after(0, show_success) except InterruptedError: # Generation was cancelled by user def show_cancelled(): progress_window.close() # Don't show an error message for user cancellation self.root.after(0, show_cancelled) except Exception as e: error_msg = str(e) def show_error(): progress_window.close() messagebox.showerror("Generation Error", f"An error occurred during generation:\n{error_msg}") self.root.after(0, show_error) # Start the generation in a background thread threading.Thread(target=run_selected_generation, daemon=True).start() def generate_clips_from_moments(self, video_path, moments, progress_callback, progress_window): """Generate video clips from specific moments""" try: import os from moviepy.editor import VideoFileClip os.makedirs("shorts", exist_ok=True) total_clips = len(moments) for i, (start, end) in enumerate(moments): if progress_window.cancelled: break progress_callback(f"Processing clip {i+1}/{total_clips}...", (i/total_clips) * 90) # Create clip with VideoFileClip(video_path) as video: clip = video.subclipped(start, end) output_path = os.path.join("shorts", f"short_{i+1}.mp4") clip.write_videofile(output_path, verbose=False, logger=None) clip.close() progress_callback("All clips generated successfully!", 100) except ImportError as e: # MoviePy not available, use alternative approach progress_callback("Using alternative generation method...", 10) # Check for cancellation before starting if progress_window.cancelled: return # Import and use the existing generate_shorts function from shorts_generator2 import generate_shorts # Create a temporary callback that updates our progress window and checks for cancellation def alt_progress_callback(message, percent): if progress_window.cancelled: # Try to indicate cancellation to the calling function raise InterruptedError("Generation cancelled by user") progress_callback(message, percent) def alt_detection_callback(message, percent): if progress_window.cancelled: raise InterruptedError("Generation cancelled by user") progress_callback(f"Detection: {message}", percent) try: # Use generate_shorts with the exact number of selected clips # This ensures we generate exactly what the user selected generate_shorts( video_path, max_clips=len(moments), # Generate exactly the number of selected clips output_folder="shorts", progress_callback=alt_progress_callback, detection_progress_callback=alt_detection_callback, threshold_db=-30, clip_duration=5, detection_mode="loud" # Default detection mode ) except InterruptedError: # Generation was cancelled progress_callback("Generation cancelled by user", 0) return except Exception as e: progress_callback(f"Error: {str(e)}", 100) raise def update_shorts_generator_settings(self): """Update the shorts generator with current settings""" generator = self.get_shorts_generator() if generator and hasattr(generator, 'detection_mode_var'): generator.detection_mode_var.set(self.detection_var.get()) def open_editor(self): """Open the shorts editor""" try: # Import and create the editor directly from shorts_generator2 import ShortsEditorGUI # Get the output folder from generator if available, otherwise use default generator = self.get_shorts_generator() output_folder = getattr(generator, 'output_folder', 'shorts') if generator else 'shorts' # Create and open the editor editor = ShortsEditorGUI(self.root, output_folder) editor.open_editor() except Exception as e: print(f"Editor Error: {e}") import traceback traceback.print_exc() messagebox.showerror("Editor Error", f"Could not open editor: {e}") def open_thumbnails(self): """Open the thumbnail editor""" try: import os import glob # Check if there are any video files to work with video_files = [] # Check for original video generator = self.get_shorts_generator() if generator and hasattr(generator, 'video_path') and generator.video_path: video_files.append(("Original Video", generator.video_path)) # Check for generated shorts output_folder = getattr(generator, 'output_folder', 'shorts') if generator else 'shorts' if os.path.exists(output_folder): shorts = glob.glob(os.path.join(output_folder, "*.mp4")) for short in shorts: video_files.append((os.path.basename(short), short)) if not video_files: messagebox.showinfo("No Videos Found", "Please select a video or generate some shorts first!") return # If only one video, open it directly if len(video_files) == 1: selected_video = video_files[0][1] else: # Let user choose which video to edit choice_window = tk.Toplevel(self.root) choice_window.title("Select Video for Thumbnail") choice_window.geometry("400x300") choice_window.transient(self.root) choice_window.grab_set() choice_window.configure(bg=self.colors['bg_primary']) tk.Label(choice_window, text="📸 Select Video for Thumbnail Creation", font=("Segoe UI", 12, "bold"), bg=self.colors['bg_primary'], fg=self.colors['text_primary']).pack(pady=20) selected_video = None def on_video_select(video_path): nonlocal selected_video selected_video = video_path choice_window.destroy() # Create list of videos with modern styling for display_name, video_path in video_files: btn = tk.Button(choice_window, text=f"📹 {display_name}", command=lambda vp=video_path: on_video_select(vp), font=("Segoe UI", 10), pady=8, width=40, bg=self.colors['accent_blue'], fg='white', relief="flat", bd=0, cursor="hand2") btn.pack(pady=3, padx=20, fill="x") cancel_btn = tk.Button(choice_window, text="Cancel", command=choice_window.destroy, font=("Segoe UI", 10), pady=8, bg=self.colors['accent_red'], fg='white', relief="flat", bd=0, cursor="hand2") cancel_btn.pack(pady=15) # Wait for selection choice_window.wait_window() if not selected_video: return # Import and open thumbnail editor from thumbnail_editor import open_thumbnail_editor open_thumbnail_editor(selected_video) except Exception as e: print(f"Thumbnail Error: {e}") import traceback traceback.print_exc() messagebox.showerror("Thumbnail Error", f"Could not open thumbnail editor: {e}") def run(self): """Start the main application""" self.root.mainloop() if __name__ == "__main__": app = MainApplication() app.run()