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("400x160") self.window.resizable(False, False) self.window.transient(parent) self.window.grab_set() # Center the window self.window.update_idletasks() x = (self.window.winfo_screenwidth() // 2) - (400 // 2) y = (self.window.winfo_screenheight() // 2) - (160 // 2) self.window.geometry(f"400x160+{x}+{y}") # Create progress widgets self.status_label = tk.Label(self.window, text="Initializing...", anchor="w", font=("Arial", 10)) self.status_label.pack(fill="x", padx=15, pady=(15,5)) self.time_label = tk.Label(self.window, text="Elapsed: 0.0s | Remaining: --s", anchor="w", font=("Arial", 9), fg="gray") self.time_label.pack(fill="x", padx=15, pady=(0,5)) # Main progress bar self.progress_var = tk.DoubleVar() self.progress_bar = ttk.Progressbar(self.window, variable=self.progress_var, maximum=100, length=370) self.progress_bar.pack(fill="x", padx=15, pady=(5,3)) # Detection progress bar (hidden by default) self.detection_label = tk.Label(self.window, text="", anchor="w", font=("Arial", 9), fg="blue") self.detection_progress_var = tk.DoubleVar() self.detection_progress_bar = ttk.Progressbar(self.window, variable=self.detection_progress_var, maximum=100, length=370) # Cancel button self.cancel_btn = tk.Button(self.window, text="Cancel", command=self.cancel) self.cancel_btn.pack(pady=(5,15)) self.start_time = time.time() self.cancelled = False def show_detection_progress(self): """Show the detection progress bar - thread safe""" def _show(): self.detection_label.pack(fill="x", padx=15, pady=(3,0)) self.detection_progress_bar.pack(fill="x", padx=15, 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.pack_forget() self.detection_progress_bar.pack_forget() 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 = [] # Create window with parent's root self.window = tk.Toplevel(parent.root) self.window.title("Select Clips to Generate") self.window.geometry("600x500") self.window.resizable(True, True) self.window.transient(parent.root) self.window.grab_set() # Center the window self.window.update_idletasks() x = (self.window.winfo_screenwidth() // 2) - (600 // 2) y = (self.window.winfo_screenheight() // 2) - (500 // 2) self.window.geometry(f"600x500+{x}+{y}") self.setup_gui() def setup_gui(self): # Title title_label = tk.Label(self.window, text=f"Found {len(self.clips)} clips using {self.detection_mode} detection", font=("Arial", 12, "bold")) title_label.pack(pady=10) # Instructions instruction_label = tk.Label(self.window, text="Select the clips you want to generate (check the boxes):", font=("Arial", 10)) instruction_label.pack(pady=(0,10)) # Clips list frame with scrollbar list_frame = tk.Frame(self.window) list_frame.pack(fill="both", expand=True, padx=20, pady=10) # Scrollable frame canvas = tk.Canvas(list_frame) scrollbar = ttk.Scrollbar(list_frame, 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) # Clip checkboxes self.clip_vars = [] for i, (start, end) in enumerate(self.clips): var = tk.BooleanVar(value=True) # All selected by default self.clip_vars.append(var) duration = end - start clip_frame = tk.Frame(scrollable_frame, relief="ridge", bd=1) clip_frame.pack(fill="x", pady=2, padx=5) checkbox = tk.Checkbutton(clip_frame, variable=var, text="", width=2) checkbox.pack(side="left", padx=5) info_label = tk.Label(clip_frame, text=f"Clip {i+1}: {start:.1f}s - {end:.1f}s (Duration: {duration:.1f}s)", font=("Arial", 10), anchor="w") info_label.pack(side="left", fill="x", expand=True, padx=5) canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") # Selection buttons button_frame = tk.Frame(self.window) button_frame.pack(fill="x", padx=20, pady=10) select_all_btn = tk.Button(button_frame, text="Select All", command=self.select_all) select_all_btn.pack(side="left", padx=5) select_none_btn = tk.Button(button_frame, text="Select None", command=self.select_none) select_none_btn.pack(side="left", padx=5) # Generate button action_frame = tk.Frame(self.window) action_frame.pack(fill="x", padx=20, pady=10) cancel_btn = tk.Button(action_frame, text="Cancel", command=self.cancel, bg="#f44336", fg="white") cancel_btn.pack(side="right", padx=5) generate_selected_btn = tk.Button(action_frame, text="Generate Selected Clips", command=self.generate_selected, bg="#4CAF50", fg="white", font=("Arial", 10, "bold")) generate_selected_btn.pack(side="right", padx=5) 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("500x600") self.root.configure(bg="#f0f0f0") # Initialize the ShortsGeneratorGUI (but don't show its window) self.shorts_generator = None self.init_shorts_generator() self.setup_gui() def init_shorts_generator(self): """Initialize the ShortsGeneratorGUI without showing its window""" try: # Create a hidden root for ShortsGeneratorGUI hidden_root = tk.Tk() hidden_root.withdraw() # Hide the window # Create ShortsGeneratorGUI instance self.shorts_generator = ShortsGeneratorGUI(hidden_root) # Don't show the original window hidden_root.withdraw() except Exception as e: messagebox.showerror("Initialization Error", f"Failed to initialize ShortsGeneratorGUI: {e}") self.shorts_generator = None def setup_gui(self): """Setup the main GUI""" # Title title_label = tk.Label(self.root, text="🎬 AI Shorts Generator", font=("Arial", 16, "bold"), bg="#f0f0f0", fg="#2c3e50") title_label.pack(pady=20) # File selection frame file_frame = tk.Frame(self.root, bg="#f0f0f0") file_frame.pack(pady=10, padx=20, fill="x") tk.Label(file_frame, text="Selected Video:", font=("Arial", 10, "bold"), bg="#f0f0f0").pack(anchor="w") self.file_label = tk.Label(file_frame, text="No video selected", font=("Arial", 9), bg="white", relief="sunken", anchor="w", pady=5, padx=10) self.file_label.pack(fill="x", pady=(5,10)) # File selection button select_btn = tk.Button(file_frame, text="📁 Select Video File", command=self.select_video_file, bg="#3498db", fg="white", font=("Arial", 10, "bold"), pady=5) select_btn.pack(pady=5) # Settings frame (simplified) settings_frame = tk.LabelFrame(self.root, text="Quick Settings", font=("Arial", 10, "bold"), bg="#f0f0f0", padx=10, pady=10) settings_frame.pack(pady=10, padx=20, fill="x") # Detection mode tk.Label(settings_frame, text="Detection Mode:", bg="#f0f0f0").pack(anchor="w") self.detection_var = tk.StringVar(value="loud") detection_frame = tk.Frame(settings_frame, bg="#f0f0f0") detection_frame.pack(fill="x", pady=5) modes = [("Loud Moments", "loud"), ("Scene Changes", "scene"), ("Motion", "motion"), ("Speech", "speech"), ("Audio Peaks", "peaks"), ("Combined", "combined")] for i, (text, value) in enumerate(modes): if i % 3 == 0: row_frame = tk.Frame(detection_frame, bg="#f0f0f0") row_frame.pack(fill="x") tk.Radiobutton(row_frame, text=text, variable=self.detection_var, value=value, bg="#f0f0f0").pack(side="left", padx=10) # Main action buttons button_frame = tk.Frame(self.root, bg="#f0f0f0") button_frame.pack(pady=20, padx=20, fill="x") # Preview Clips Button self.preview_btn = tk.Button(button_frame, text="🔍 Preview Clips", command=self.preview_clips_threaded, bg="#2196F3", fg="white", font=("Arial", 11, "bold"), pady=8) self.preview_btn.pack(fill="x", pady=5) # Generate Shorts Button self.generate_btn = tk.Button(button_frame, text="🎬 Generate All Detected Clips", command=self.generate_shorts_threaded, bg="#4CAF50", fg="white", font=("Arial", 12, "bold"), pady=10) self.generate_btn.pack(fill="x", pady=5) # Info label info_label = tk.Label(button_frame, text="💡 Tip: Use 'Preview Clips' to select specific clips for faster processing", font=("Arial", 9), fg="gray", bg="#f0f0f0") info_label.pack(pady=(5,10)) # Edit Generated Shorts Button self.edit_btn = tk.Button(button_frame, text="✏️ Edit Generated Shorts", command=self.open_editor, bg="#FF9800", fg="white", font=("Arial", 11, "bold"), pady=8) self.edit_btn.pack(fill="x", pady=5) # Create Thumbnails Button self.thumbnail_btn = tk.Button(button_frame, text="📸 Create Thumbnails", command=self.open_thumbnails, bg="#9C27B0", fg="white", font=("Arial", 11, "bold"), pady=8) self.thumbnail_btn.pack(fill="x", pady=5) # Status label self.status_label = tk.Label(self.root, text="Ready - Select a video to begin", font=("Arial", 9), fg="gray", bg="#f0f0f0") self.status_label.pack(pady=(20,10)) # Store detected clips for selection self.detected_clips = [] 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 if self.shorts_generator: self.shorts_generator.video_path = file_path self.shorts_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""" if not self.shorts_generator or not self.shorts_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) video_path = self.shorts_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""" if not self.shorts_generator or not self.shorts_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 video_path = self.shorts_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""" if not self.shorts_generator or not self.shorts_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: video_path = self.shorts_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""" if self.shorts_generator: self.shorts_generator.detection_mode_var.set(self.detection_var.get()) def open_editor(self): """Open the shorts editor""" print("DEBUG: open_editor called") if self.shorts_generator: print("DEBUG: shorts_generator exists") try: print("DEBUG: Attempting to call open_shorts_editor()") if hasattr(self.shorts_generator, 'open_shorts_editor'): print("DEBUG: open_shorts_editor method exists") print(f"DEBUG: shorts_generator root: {self.shorts_generator.root}") print(f"DEBUG: shorts_generator output_folder: {getattr(self.shorts_generator, 'output_folder', 'NOT SET')}") # Create the editor with the main window as parent instead of hidden root from shorts_generator2 import ShortsEditorGUI editor = ShortsEditorGUI(self.root, self.shorts_generator.output_folder) editor.open_editor() print("DEBUG: Editor opened successfully") else: print("DEBUG: open_shorts_editor method does NOT exist") messagebox.showerror("Editor Error", "The open_shorts_editor method is not available in ShortsGeneratorGUI") except Exception as e: print(f"DEBUG: Exception in open_editor: {e}") import traceback traceback.print_exc() messagebox.showerror("Editor Error", f"Could not open editor: {e}") else: print("DEBUG: shorts_generator is None") messagebox.showerror("Editor Error", "ShortsGeneratorGUI is not initialized") def open_thumbnails(self): """Open the thumbnail editor""" print("DEBUG: open_thumbnails called") if self.shorts_generator: print("DEBUG: shorts_generator exists") try: print("DEBUG: Attempting to call open_thumbnail_editor()") if hasattr(self.shorts_generator, 'open_thumbnail_editor'): print("DEBUG: open_thumbnail_editor method exists") # Call the method directly but handle the parent window issue # Let's import and call the thumbnail editor function directly import os import glob # Check if there are any video files to work with video_files = [] # Check for original video if self.shorts_generator.video_path: video_files.append(("Original Video", self.shorts_generator.video_path)) # Check for generated shorts if os.path.exists(self.shorts_generator.output_folder): shorts = glob.glob(os.path.join(self.shorts_generator.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() tk.Label(choice_window, text="📸 Select Video for Thumbnail Creation", font=("Arial", 12, "bold")).pack(pady=10) selected_video = None def on_video_select(video_path): nonlocal selected_video selected_video = video_path choice_window.destroy() # Create list of videos 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=("Arial", 10), pady=5, width=40) btn.pack(pady=2, padx=20, fill="x") tk.Button(choice_window, text="Cancel", command=choice_window.destroy).pack(pady=10) # 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) print("DEBUG: Thumbnail editor opened successfully") else: print("DEBUG: open_thumbnail_editor method does NOT exist") messagebox.showerror("Thumbnail Error", "The open_thumbnail_editor method is not available in ShortsGeneratorGUI") except Exception as e: print(f"DEBUG: Exception in open_thumbnails: {e}") import traceback traceback.print_exc() messagebox.showerror("Thumbnail Error", f"Could not open thumbnail editor: {e}") else: print("DEBUG: shorts_generator is None") messagebox.showerror("Thumbnail Error", "ShortsGeneratorGUI is not initialized") def run(self): """Start the main application""" self.root.mainloop() if __name__ == "__main__": app = MainApplication() app.run()