""" Professional Video Editor for Generated Shorts Standalone application for editing video clips with timeline controls and video synchronization """ import tkinter as tk from tkinter import ttk, filedialog, messagebox import cv2 import numpy as np import os import threading import time from datetime import datetime from PIL import Image, ImageTk # Try to import MoviePy, handle if not available try: from moviepy.editor import VideoFileClip, TextClip, CompositeVideoClip from moviepy.video.fx import FadeIn, FadeOut, Resize from moviepy.audio.fx import MultiplyVolume MOVIEPY_AVAILABLE = True except ImportError: print("⚠️ MoviePy not available - using OpenCV backend for video processing") MOVIEPY_AVAILABLE = False # Create dummy classes for compatibility class VideoFileClip: def __init__(self, *args, **kwargs): raise ImportError("MoviePy not available") class TextClip: def __init__(self, *args, **kwargs): raise ImportError("MoviePy not available") class CompositeVideoClip: def __init__(self, *args, **kwargs): raise ImportError("MoviePy not available") class ShortsEditorGUI: """Professional video editing interface with timeline controls and real-time preview""" def __init__(self, parent=None, shorts_folder="shorts"): self.parent = parent self.shorts_folder = shorts_folder # Video state self.current_video = None self.current_clip = None self.current_time = 0.0 self.video_duration = 0.0 self.is_playing = False self.timeline_is_playing = False self.play_thread = None # Timeline state self.timeline_position = 0.0 self.timeline_scale = 1.0 # Pixels per second self.timeline_width = 800 # Professional timeline features self.timeline_clips = [] self.selected_clip = None self.markers = [] # Multi-track system self.tracks = { 'video_1': {'y_offset': 40, 'height': 60, 'color': '#3498db', 'name': 'Video 1', 'muted': False, 'locked': False, 'solo': False, 'visible': True}, 'video_2': {'y_offset': 105, 'height': 60, 'color': '#2ecc71', 'name': 'Video 2', 'muted': False, 'locked': False, 'solo': False, 'visible': True}, 'audio_1': {'y_offset': 170, 'height': 40, 'color': '#e74c3c', 'name': 'Audio 1', 'muted': False, 'locked': False, 'solo': False, 'visible': True}, 'audio_2': {'y_offset': 215, 'height': 40, 'color': '#f39c12', 'name': 'Audio 2', 'muted': False, 'locked': False, 'solo': False, 'visible': True}, 'text_1': {'y_offset': 260, 'height': 35, 'color': '#9b59b6', 'name': 'Text/Graphics', 'muted': False, 'locked': False, 'solo': False, 'visible': True} } # Timeline interaction state self.dragging_clip = None self.drag_start_x = None self.drag_start_time = None self.drag_offset = 0 self.snap_enabled = True self.magnetic_timeline = True self.grid_size = 1.0 # Snap grid in seconds # Timeline editing modes self.edit_mode = 'select' # 'select', 'cut', 'trim', 'ripple' # Visual enhancements self.show_thumbnails = True self.show_waveforms = True self.clip_thumbnails = {} self.audio_waveforms = {} # Track widgets for UI self.track_widgets = {} # Modern color scheme self.colors = { 'bg_primary': '#1a1a1a', 'bg_secondary': '#2d2d2d', 'bg_tertiary': '#3d3d3d', 'text_primary': '#ffffff', 'text_secondary': '#b8b8b8', 'accent_blue': '#007acc', 'accent_green': '#28a745', 'accent_orange': '#fd7e14', 'accent_red': '#dc3545', 'border': '#404040' } # Modern fonts self.fonts = { 'title': ('Segoe UI', 16, 'bold'), 'heading': ('Segoe UI', 11, 'bold'), 'body': ('Segoe UI', 10), 'caption': ('Segoe UI', 9), 'button': ('Segoe UI', 10, 'bold') } def open_editor(self): """Open the video editor window""" # Create editor window self.editor_window = tk.Toplevel(self.parent) if self.parent else tk.Tk() self.editor_window.title("Professional Shorts Editor") self.editor_window.geometry("1200x800") self.editor_window.minsize(900, 600) self.editor_window.configure(bg=self.colors['bg_primary']) # Make window responsive self.editor_window.rowconfigure(1, weight=1) self.editor_window.columnconfigure(0, weight=1) # Create interface self.create_editor_interface() # Start the editor if not self.parent: self.editor_window.mainloop() def create_editor_interface(self): """Create the main editor interface""" # Header with file selection header_frame = tk.Frame(self.editor_window, bg=self.colors['bg_secondary'], height=60) header_frame.pack(fill="x", padx=10, pady=(10, 0)) header_frame.pack_propagate(False) # Title title_label = tk.Label(header_frame, text="✏️ Professional Shorts Editor", font=self.fonts['title'], bg=self.colors['bg_secondary'], fg=self.colors['text_primary']) title_label.pack(side="left", padx=20, pady=15) # File selection file_frame = tk.Frame(header_frame, bg=self.colors['bg_secondary']) file_frame.pack(side="right", padx=20, pady=15) self.current_file_label = tk.Label(file_frame, text="No video selected", font=self.fonts['body'], bg=self.colors['bg_tertiary'], fg=self.colors['text_secondary'], padx=15, pady=8) self.current_file_label.pack(side="left", padx=(0, 10)) select_btn = tk.Button(file_frame, text="πŸ“ Select Video", command=self.select_video_file, font=self.fonts['button'], bg=self.colors['accent_blue'], fg='white', padx=20, pady=8, relief="flat", bd=0, cursor="hand2") select_btn.pack(side="left") # Main content area main_frame = tk.Frame(self.editor_window, bg=self.colors['bg_primary']) main_frame.pack(fill="both", expand=True, padx=10, pady=10) main_frame.rowconfigure(0, weight=1) main_frame.columnconfigure(0, weight=2) main_frame.columnconfigure(1, weight=1) # Left panel - Video player and timeline player_frame = tk.Frame(main_frame, bg=self.colors['bg_secondary']) player_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 5)) player_frame.rowconfigure(0, weight=1) player_frame.rowconfigure(1, weight=0) player_frame.columnconfigure(0, weight=1) # Video display area video_container = tk.Frame(player_frame, bg=self.colors['bg_tertiary']) video_container.grid(row=0, column=0, sticky="nsew", padx=15, pady=15) video_container.rowconfigure(0, weight=1) video_container.columnconfigure(0, weight=1) # Video canvas self.video_canvas = tk.Canvas(video_container, bg='black', highlightthickness=0) self.video_canvas.grid(row=0, column=0, sticky="nsew") # Professional Timeline Workspace timeline_workspace = tk.Frame(player_frame, bg=self.colors['bg_secondary'], height=350) timeline_workspace.grid(row=1, column=0, sticky="ew", padx=15, pady=(0, 15)) timeline_workspace.pack_propagate(False) # Timeline header with editing tools header_frame = tk.Frame(timeline_workspace, bg=self.colors['bg_secondary']) header_frame.pack(fill="x", pady=(10, 5)) # Left side - Timeline controls left_controls = tk.Frame(header_frame, bg=self.colors['bg_secondary']) left_controls.pack(side="left") # Editing mode selector tk.Label(left_controls, text="Mode:", font=self.fonts['caption'], bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(side="left") self.mode_var = tk.StringVar(value="select") mode_combo = ttk.Combobox(left_controls, textvariable=self.mode_var, width=8, values=["select", "cut", "trim", "ripple"], state="readonly") mode_combo.pack(side="left", padx=(5, 10)) mode_combo.bind('<>', self.on_mode_change) # Snap and magnetic timeline toggles self.snap_var = tk.BooleanVar(value=True) snap_check = tk.Checkbutton(left_controls, text="Snap", variable=self.snap_var, bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], selectcolor=self.colors['accent_blue'], command=self.toggle_snap) snap_check.pack(side="left", padx=5) self.magnetic_var = tk.BooleanVar(value=True) magnetic_check = tk.Checkbutton(left_controls, text="Magnetic", variable=self.magnetic_var, bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], selectcolor=self.colors['accent_blue'], command=self.toggle_magnetic) magnetic_check.pack(side="left", padx=5) # Center - Playback controls center_controls = tk.Frame(header_frame, bg=self.colors['bg_secondary']) center_controls.pack(side="left", padx=20) self.timeline_play_btn = tk.Button(center_controls, text="▢️", command=self.timeline_play, bg=self.colors['accent_green'], fg='white', font=('Arial', 12, 'bold'), width=3, height=1, relief="flat", bd=0, cursor="hand2") self.timeline_play_btn.pack(side="left", padx=2) self.timeline_pause_btn = tk.Button(center_controls, text="⏸️", command=self.timeline_pause, bg=self.colors['accent_orange'], fg='white', font=('Arial', 12, 'bold'), width=3, height=1, relief="flat", bd=0, cursor="hand2") self.timeline_pause_btn.pack(side="left", padx=2) self.timeline_stop_btn = tk.Button(center_controls, text="⏹️", command=self.timeline_stop, bg=self.colors['accent_red'], fg='white', font=('Arial', 12, 'bold'), width=3, height=1, relief="flat", bd=0, cursor="hand2") self.timeline_stop_btn.pack(side="left", padx=2) # Right side - Zoom and time display right_controls = tk.Frame(header_frame, bg=self.colors['bg_secondary']) right_controls.pack(side="right") # Zoom control tk.Label(right_controls, text="Zoom:", font=self.fonts['caption'], bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(side="left") self.zoom_var = tk.DoubleVar(value=1.0) zoom_scale = tk.Scale(right_controls, from_=0.1, to=5.0, resolution=0.1, orient="horizontal", variable=self.zoom_var, command=self.on_zoom_change, length=150, bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], highlightthickness=0, troughcolor=self.colors['bg_tertiary']) zoom_scale.pack(side="left", padx=10) # Time display self.time_display = tk.Label(right_controls, text="00:00 / 00:00", font=self.fonts['body'], bg=self.colors['bg_secondary'], fg=self.colors['text_primary']) self.time_display.pack(side="left", padx=20) # Main timeline container timeline_container = tk.Frame(timeline_workspace, bg=self.colors['bg_tertiary']) timeline_container.pack(fill="both", expand=True, pady=5) # Track labels panel (left side) self.track_panel = tk.Frame(timeline_container, bg=self.colors['bg_secondary'], width=120) self.track_panel.pack(side="left", fill="y") self.track_panel.pack_propagate(False) # Timeline canvas with scrollbars canvas_frame = tk.Frame(timeline_container, bg=self.colors['bg_tertiary']) canvas_frame.pack(side="right", fill="both", expand=True) # Create canvas with scrollbars self.timeline_canvas = tk.Canvas(canvas_frame, bg='#1a1a1a', highlightthickness=0, scrollregion=(0, 0, 2000, 300)) # Scrollbars h_scrollbar = ttk.Scrollbar(canvas_frame, orient="horizontal", command=self.timeline_canvas.xview) v_scrollbar = ttk.Scrollbar(canvas_frame, orient="vertical", command=self.timeline_canvas.yview) self.timeline_canvas.configure(xscrollcommand=h_scrollbar.set, yscrollcommand=v_scrollbar.set) # Pack scrollbars and canvas h_scrollbar.pack(side="bottom", fill="x") v_scrollbar.pack(side="right", fill="y") self.timeline_canvas.pack(side="left", fill="both", expand=True) # Bind professional timeline events self.timeline_canvas.bind("", self.timeline_click) self.timeline_canvas.bind("", self.timeline_drag) self.timeline_canvas.bind("", self.on_timeline_drag_end) self.timeline_canvas.bind("", self.on_timeline_right_click) self.timeline_canvas.bind("", self.on_timeline_double_click) # Create track controls self.create_track_controls() # Initialize sample clips for demonstration self.create_sample_timeline_content() # Right panel - Tools and effects tools_frame = tk.Frame(main_frame, bg=self.colors['bg_secondary']) tools_frame.grid(row=0, column=1, sticky="nsew", padx=(5, 0)) # Tools header tools_header = tk.Label(tools_frame, text="πŸ› οΈ Editing Tools", font=self.fonts['heading'], bg=self.colors['bg_secondary'], fg=self.colors['text_primary']) tools_header.pack(pady=(15, 10)) # Create tool sections self.create_basic_tools(tools_frame) self.create_effects_tools(tools_frame) self.create_export_tools(tools_frame) # Initialize timeline self.update_timeline() def create_track_controls(self): """Create professional track control panel""" # Clear existing track controls for widget in self.track_panel.winfo_children(): widget.destroy() # Header header = tk.Label(self.track_panel, text="TRACKS", font=('Arial', 9, 'bold'), bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']) header.pack(fill="x", pady=(5, 10)) # Create controls for each track for track_id, track_info in self.tracks.items(): self.create_track_control(track_id, track_info) def create_track_control(self, track_id, track_info): """Create control panel for a single track""" # Track frame track_frame = tk.Frame(self.track_panel, bg=self.colors['bg_secondary'], height=track_info['height'], relief="raised", bd=1) track_frame.pack(fill="x", pady=1) track_frame.pack_propagate(False) # Track name name_label = tk.Label(track_frame, text=track_info['name'], font=('Arial', 8, 'bold'), bg=self.colors['bg_secondary'], fg=track_info['color']) name_label.pack(anchor="w", padx=5, pady=2) # Control buttons frame controls = tk.Frame(track_frame, bg=self.colors['bg_secondary']) controls.pack(fill="x", padx=5) # Mute button mute_text = "πŸ”‡" if track_info['muted'] else "πŸ”Š" mute_btn = tk.Button(controls, text=mute_text, width=2, height=1, bg=self.colors['accent_red'] if track_info['muted'] else self.colors['bg_tertiary'], fg='white', font=('Arial', 8), relief="flat", bd=0, command=lambda: self.toggle_track_mute(track_id)) mute_btn.pack(side="left", padx=1) # Solo button solo_text = "S" solo_btn = tk.Button(controls, text=solo_text, width=2, height=1, bg=self.colors['accent_orange'] if track_info['solo'] else self.colors['bg_tertiary'], fg='white', font=('Arial', 8, 'bold'), relief="flat", bd=0, command=lambda: self.toggle_track_solo(track_id)) solo_btn.pack(side="left", padx=1) # Lock button lock_text = "πŸ”’" if track_info['locked'] else "πŸ”“" lock_btn = tk.Button(controls, text=lock_text, width=2, height=1, bg=self.colors['accent_blue'] if track_info['locked'] else self.colors['bg_tertiary'], fg='white', font=('Arial', 8), relief="flat", bd=0, command=lambda: self.toggle_track_lock(track_id)) lock_btn.pack(side="left", padx=1) # Store track widgets for updates self.track_widgets[track_id] = { 'frame': track_frame, 'mute_btn': mute_btn, 'solo_btn': solo_btn, 'lock_btn': lock_btn } def create_sample_timeline_content(self): """Create sample timeline content for demonstration""" if self.current_video and self.video_duration > 0: # Create a sample clip representing the loaded video sample_clip = { 'id': 1, 'name': os.path.basename(self.current_video) if self.current_video else 'Sample Video', 'start_time': 0, 'end_time': min(self.video_duration, 10), # Cap at 10 seconds for demo 'track': 'video_1', 'color': self.tracks['video_1']['color'], 'file_path': self.current_video, 'type': 'video' } self.timeline_clips = [sample_clip] # Add sample markers self.markers = [ {'time': 2.0, 'name': 'Intro End', 'color': '#ffeb3b'}, {'time': 5.0, 'name': 'Mid Point', 'color': '#4caf50'}, {'time': 8.0, 'name': 'Outro Start', 'color': '#f44336'} ] self.update_timeline() # Professional timeline interaction methods def on_mode_change(self, event=None): """Handle editing mode change""" self.edit_mode = self.mode_var.get() print(f"🎬 Editing mode changed to: {self.edit_mode}") # Update cursor based on mode cursor_map = { 'select': 'hand2', 'cut': 'crosshair', 'trim': 'sb_h_double_arrow', 'ripple': 'fleur' } self.timeline_canvas.configure(cursor=cursor_map.get(self.edit_mode, 'hand2')) def toggle_snap(self): """Toggle snap to grid""" self.snap_enabled = self.snap_var.get() print(f"🧲 Snap enabled: {self.snap_enabled}") def toggle_magnetic(self): """Toggle magnetic timeline""" self.magnetic_timeline = self.magnetic_var.get() print(f"🧲 Magnetic timeline: {self.magnetic_timeline}") def toggle_track_mute(self, track_id): """Toggle track mute""" self.tracks[track_id]['muted'] = not self.tracks[track_id]['muted'] self.update_track_controls() print(f"πŸ”‡ Track {track_id} muted: {self.tracks[track_id]['muted']}") def toggle_track_solo(self, track_id): """Toggle track solo""" self.tracks[track_id]['solo'] = not self.tracks[track_id]['solo'] self.update_track_controls() print(f"🎡 Track {track_id} solo: {self.tracks[track_id]['solo']}") def toggle_track_lock(self, track_id): """Toggle track lock""" self.tracks[track_id]['locked'] = not self.tracks[track_id]['locked'] self.update_track_controls() print(f"πŸ”’ Track {track_id} locked: {self.tracks[track_id]['locked']}") def update_track_controls(self): """Update track control button states""" for track_id, widgets in self.track_widgets.items(): track_info = self.tracks[track_id] # Update mute button mute_text = "πŸ”‡" if track_info['muted'] else "πŸ”Š" mute_color = self.colors['accent_red'] if track_info['muted'] else self.colors['bg_tertiary'] widgets['mute_btn'].configure(text=mute_text, bg=mute_color) # Update solo button solo_color = self.colors['accent_orange'] if track_info['solo'] else self.colors['bg_tertiary'] widgets['solo_btn'].configure(bg=solo_color) # Update lock button lock_text = "πŸ”’" if track_info['locked'] else "πŸ”“" lock_color = self.colors['accent_blue'] if track_info['locked'] else self.colors['bg_tertiary'] widgets['lock_btn'].configure(text=lock_text, bg=lock_color) def on_zoom_change(self, value): """Handle timeline zoom change""" zoom_level = float(value) self.timeline_scale = 50 * zoom_level # Base scale of 50 pixels per second self.update_timeline() print(f"πŸ” Timeline zoom: {zoom_level:.1f}x") # Initialize timeline self.update_timeline() def create_basic_tools(self, parent): """Create basic editing tools""" basic_frame = tk.LabelFrame(parent, text="Basic Editing", font=self.fonts['heading'], bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], relief="flat", bd=1) basic_frame.pack(fill="x", padx=15, pady=5) # Trim controls trim_frame = tk.Frame(basic_frame, bg=self.colors['bg_secondary']) trim_frame.pack(fill="x", padx=10, pady=5) tk.Label(trim_frame, text="Trim Video:", font=self.fonts['body'], bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).pack(anchor="w") trim_controls = tk.Frame(trim_frame, bg=self.colors['bg_secondary']) trim_controls.pack(fill="x", pady=5) tk.Label(trim_controls, text="Start:", font=self.fonts['caption'], bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(side="left") self.trim_start_var = tk.DoubleVar(value=0.0) trim_start_spin = tk.Spinbox(trim_controls, from_=0, to=999, increment=0.1, textvariable=self.trim_start_var, width=8, font=self.fonts['caption']) trim_start_spin.pack(side="left", padx=5) tk.Label(trim_controls, text="End:", font=self.fonts['caption'], bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(side="left", padx=(10, 0)) self.trim_end_var = tk.DoubleVar(value=10.0) trim_end_spin = tk.Spinbox(trim_controls, from_=0, to=999, increment=0.1, textvariable=self.trim_end_var, width=8, font=self.fonts['caption']) trim_end_spin.pack(side="left", padx=5) trim_btn = tk.Button(basic_frame, text="βœ‚οΈ Apply Trim", command=self.apply_trim, bg=self.colors['accent_blue'], fg='white', font=self.fonts['button'], relief="flat", bd=0, cursor="hand2") trim_btn.pack(fill="x", padx=10, pady=5) # Speed controls speed_frame = tk.Frame(basic_frame, bg=self.colors['bg_secondary']) speed_frame.pack(fill="x", padx=10, pady=5) tk.Label(speed_frame, text="Speed:", font=self.fonts['body'], bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).pack(anchor="w") self.speed_var = tk.DoubleVar(value=1.0) speed_scale = tk.Scale(speed_frame, from_=0.25, to=3.0, resolution=0.25, orient="horizontal", variable=self.speed_var, bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], highlightthickness=0, troughcolor=self.colors['bg_tertiary']) speed_scale.pack(fill="x", pady=5) speed_btn = tk.Button(basic_frame, text="⚑ Apply Speed", command=self.apply_speed, bg=self.colors['accent_green'], fg='white', font=self.fonts['button'], relief="flat", bd=0, cursor="hand2") speed_btn.pack(fill="x", padx=10, pady=5) # Volume controls volume_frame = tk.Frame(basic_frame, bg=self.colors['bg_secondary']) volume_frame.pack(fill="x", padx=10, pady=5) tk.Label(volume_frame, text="Volume:", font=self.fonts['body'], bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).pack(anchor="w") self.volume_var = tk.DoubleVar(value=1.0) volume_scale = tk.Scale(volume_frame, from_=0.0, to=2.0, resolution=0.1, orient="horizontal", variable=self.volume_var, bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], highlightthickness=0, troughcolor=self.colors['bg_tertiary']) volume_scale.pack(fill="x", pady=5) volume_btn = tk.Button(basic_frame, text="πŸ”Š Apply Volume", command=self.apply_volume, bg=self.colors['accent_orange'], fg='white', font=self.fonts['button'], relief="flat", bd=0, cursor="hand2") volume_btn.pack(fill="x", padx=10, pady=5) def create_effects_tools(self, parent): """Create effects tools""" effects_frame = tk.LabelFrame(parent, text="Effects", font=self.fonts['heading'], bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], relief="flat", bd=1) effects_frame.pack(fill="x", padx=15, pady=5) # Fade effects fade_btn = tk.Button(effects_frame, text="πŸŒ… Add Fade In/Out", command=self.apply_fade, bg=self.colors['accent_blue'], fg='white', font=self.fonts['button'], relief="flat", bd=0, cursor="hand2") fade_btn.pack(fill="x", padx=10, pady=5) # Text overlay text_frame = tk.Frame(effects_frame, bg=self.colors['bg_secondary']) text_frame.pack(fill="x", padx=10, pady=5) tk.Label(text_frame, text="Text Overlay:", font=self.fonts['body'], bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).pack(anchor="w") self.text_var = tk.StringVar(value="Sample Text") text_entry = tk.Entry(text_frame, textvariable=self.text_var, font=self.fonts['body']) text_entry.pack(fill="x", pady=5) text_btn = tk.Button(effects_frame, text="πŸ“ Add Text", command=self.apply_text, bg=self.colors['accent_green'], fg='white', font=self.fonts['button'], relief="flat", bd=0, cursor="hand2") text_btn.pack(fill="x", padx=10, pady=5) def create_export_tools(self, parent): """Create export tools""" export_frame = tk.LabelFrame(parent, text="Export", font=self.fonts['heading'], bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], relief="flat", bd=1) export_frame.pack(fill="x", padx=15, pady=5) # Reset button reset_btn = tk.Button(export_frame, text="πŸ”„ Reset", command=self.reset_video, bg=self.colors['accent_red'], fg='white', font=self.fonts['button'], relief="flat", bd=0, cursor="hand2") reset_btn.pack(fill="x", padx=10, pady=5) # Export button export_btn = tk.Button(export_frame, text="πŸ’Ύ Export Video", command=self.export_video, bg=self.colors['accent_green'], fg='white', font=self.fonts['button'], relief="flat", bd=0, cursor="hand2") export_btn.pack(fill="x", padx=10, pady=5) def select_video_file(self): """Select a video file to edit""" # Check for videos in shorts folder first if os.path.exists(self.shorts_folder): video_files = [f for f in os.listdir(self.shorts_folder) if f.lower().endswith(('.mp4', '.avi', '.mov', '.mkv'))] if video_files: # Show selection dialog for shorts choice_window = tk.Toplevel(self.editor_window) choice_window.title("Select Video to Edit") choice_window.geometry("400x300") choice_window.configure(bg=self.colors['bg_primary']) choice_window.transient(self.editor_window) choice_window.grab_set() tk.Label(choice_window, text="Select a video to edit:", font=self.fonts['heading'], bg=self.colors['bg_primary'], fg=self.colors['text_primary']).pack(pady=10) selected_file = None def select_file(filename): nonlocal selected_file selected_file = os.path.join(self.shorts_folder, filename) choice_window.destroy() # List videos for video_file in video_files: btn = tk.Button(choice_window, text=f"πŸ“Ή {video_file}", command=lambda f=video_file: select_file(f), bg=self.colors['accent_blue'], fg='white', font=self.fonts['button'], relief="flat", bd=0, cursor="hand2") btn.pack(fill="x", padx=20, pady=2) # Browse button browse_btn = tk.Button(choice_window, text="πŸ“ Browse Other Files", command=lambda: self.browse_video_file(choice_window), bg=self.colors['accent_orange'], fg='white', font=self.fonts['button'], relief="flat", bd=0, cursor="hand2") browse_btn.pack(fill="x", padx=20, pady=10) choice_window.wait_window() if selected_file: self.load_video(selected_file) else: self.browse_video_file() else: self.browse_video_file() def browse_video_file(self, parent_window=None): """Browse for video file""" filetypes = [ ("Video files", "*.mp4 *.avi *.mov *.mkv *.wmv *.flv *.webm"), ("All files", "*.*") ] file_path = filedialog.askopenfilename( title="Select Video File", filetypes=filetypes, parent=parent_window or self.editor_window ) if file_path: if parent_window: parent_window.destroy() self.load_video(file_path) def load_video(self, video_path): """Load a video for editing""" try: # Clean up previous video if hasattr(self, 'current_clip') and self.current_clip: if MOVIEPY_AVAILABLE: self.current_clip.close() else: if hasattr(self.current_clip, 'release'): self.current_clip.release() # Load new video self.current_video = video_path if MOVIEPY_AVAILABLE: # Use MoviePy for full functionality self.current_clip = VideoFileClip(video_path) self.video_duration = self.current_clip.duration self.current_time = 0.0 # Display first frame self.display_frame_at_time(0.0) else: # Use OpenCV for basic functionality self.current_clip = cv2.VideoCapture(video_path) if not self.current_clip.isOpened(): raise Exception("Could not open video file") # Get video properties fps = self.current_clip.get(cv2.CAP_PROP_FPS) frame_count = self.current_clip.get(cv2.CAP_PROP_FRAME_COUNT) self.video_duration = frame_count / fps if fps > 0 else 0 self.current_time = 0.0 # Display first frame self.display_frame_at_time_opencv(0.0) # Update UI filename = os.path.basename(video_path) self.current_file_label.config(text=filename) # Update trim controls self.trim_start_var.set(0.0) self.trim_end_var.set(self.video_duration) # Update timeline self.update_timeline() self.update_time_display() backend = "MoviePy" if MOVIEPY_AVAILABLE else "OpenCV" print(f"βœ… Loaded video: {filename} ({self.video_duration:.1f}s) using {backend}") if not MOVIEPY_AVAILABLE: messagebox.showinfo("Limited Functionality", "Video editor is running with limited functionality.\n" + "Only basic playback and timeline controls are available.\n" + "For full editing features, install MoviePy:\n" + "pip install moviepy") except Exception as e: messagebox.showerror("Load Error", f"Could not load video: {e}") def display_frame_at_time_opencv(self, time_sec): """Display a specific frame using OpenCV""" if not self.current_clip or not hasattr(self.current_clip, 'get'): return try: # Calculate frame number fps = self.current_clip.get(cv2.CAP_PROP_FPS) frame_number = int(time_sec * fps) # Set video position self.current_clip.set(cv2.CAP_PROP_POS_FRAMES, frame_number) # Read frame ret, frame = self.current_clip.read() if not ret: return # Convert BGR to RGB frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # Convert to PIL Image pil_image = Image.fromarray(frame_rgb) # Resize to fit canvas while maintaining aspect ratio canvas_width = self.video_canvas.winfo_width() canvas_height = self.video_canvas.winfo_height() if canvas_width > 1 and canvas_height > 1: # Calculate scaling to fit canvas scale_w = canvas_width / pil_image.width scale_h = canvas_height / pil_image.height scale = min(scale_w, scale_h) new_width = int(pil_image.width * scale) new_height = int(pil_image.height * scale) pil_image = pil_image.resize((new_width, new_height), Image.Resampling.LANCZOS) # Convert to PhotoImage photo = ImageTk.PhotoImage(pil_image) # Clear canvas and display image self.video_canvas.delete("all") x = canvas_width // 2 y = canvas_height // 2 self.video_canvas.create_image(x, y, image=photo) # Keep reference to prevent garbage collection self.video_canvas.image = photo except Exception as e: print(f"Frame display error: {e}") def display_frame_at_time(self, time_sec): """Display a specific frame from the video""" if not self.current_clip: return if MOVIEPY_AVAILABLE: self.display_frame_at_time_moviepy(time_sec) else: self.display_frame_at_time_opencv(time_sec) def display_frame_at_time_moviepy(self, time_sec): """Display a specific frame using MoviePy""" try: # Get frame at specified time time_sec = max(0, min(time_sec, self.video_duration)) frame = self.current_clip.get_frame(time_sec) # Convert to PIL Image if frame.dtype != np.uint8: frame = (frame * 255).astype(np.uint8) pil_image = Image.fromarray(frame) # Resize to fit canvas while maintaining aspect ratio canvas_width = self.video_canvas.winfo_width() canvas_height = self.video_canvas.winfo_height() if canvas_width > 1 and canvas_height > 1: # Calculate scaling to fit canvas scale_w = canvas_width / pil_image.width scale_h = canvas_height / pil_image.height scale = min(scale_w, scale_h) new_width = int(pil_image.width * scale) new_height = int(pil_image.height * scale) pil_image = pil_image.resize((new_width, new_height), Image.Resampling.LANCZOS) # Convert to PhotoImage photo = ImageTk.PhotoImage(pil_image) # Clear canvas and display image self.video_canvas.delete("all") x = canvas_width // 2 y = canvas_height // 2 self.video_canvas.create_image(x, y, image=photo) # Keep reference to prevent garbage collection self.video_canvas.image = photo except Exception as e: print(f"Frame display error: {e}") def update_timeline(self): """Update the timeline display""" if not self.timeline_canvas.winfo_exists(): return self.timeline_canvas.delete("all") if not self.current_clip: return canvas_width = self.timeline_canvas.winfo_width() canvas_height = self.timeline_canvas.winfo_height() if canvas_width <= 1: return # Calculate timeline scale self.timeline_scale = (canvas_width - 40) / max(self.video_duration, 1) # Draw timeline background self.timeline_canvas.create_rectangle(20, 20, canvas_width - 20, canvas_height - 20, fill=self.colors['bg_primary'], outline=self.colors['border']) # Draw time markers for i in range(0, int(self.video_duration) + 1): x = 20 + i * self.timeline_scale if x < canvas_width - 20: self.timeline_canvas.create_line(x, 20, x, canvas_height - 20, fill=self.colors['border'], width=1) # Time labels if i % 2 == 0: # Every 2 seconds self.timeline_canvas.create_text(x, canvas_height - 35, text=f"{i}s", fill=self.colors['text_secondary'], font=self.fonts['caption']) # Draw playhead playhead_x = 20 + self.current_time * self.timeline_scale self.timeline_canvas.create_line(playhead_x, 20, playhead_x, canvas_height - 20, fill=self.colors['accent_blue'], width=3) # Draw playhead handle self.timeline_canvas.create_oval(playhead_x - 5, 15, playhead_x + 5, 25, fill=self.colors['accent_blue'], outline='white') def timeline_click(self, event): """Handle timeline click""" if not self.current_clip: return canvas_width = self.timeline_canvas.winfo_width() click_x = event.x # Convert click position to time relative_x = click_x - 20 if relative_x >= 0 and relative_x <= canvas_width - 40: clicked_time = relative_x / self.timeline_scale clicked_time = max(0, min(clicked_time, self.video_duration)) # Update current time and display self.current_time = clicked_time self.display_frame_at_time(self.current_time) self.update_timeline() self.update_time_display() def timeline_drag(self, event): """Handle timeline dragging""" self.timeline_click(event) # Same behavior as click for now def on_timeline_drag_end(self, event): """End timeline drag operation""" if hasattr(self, 'dragging_clip') and self.dragging_clip: print(f"🎬 Moved clip '{self.dragging_clip['name']}' to {self.dragging_clip['start_time']:.2f}s") # Clear drag state if hasattr(self, 'dragging_clip'): self.dragging_clip = None if hasattr(self, 'drag_start_x'): self.drag_start_x = None if hasattr(self, 'drag_start_time'): self.drag_start_time = None if hasattr(self, 'drag_offset'): self.drag_offset = 0 def on_timeline_right_click(self, event): """Handle right-click context menu""" try: canvas_x = self.timeline_canvas.canvasx(event.x) canvas_y = self.timeline_canvas.canvasy(event.y) clicked_clip = self.get_clip_at_position(canvas_x, canvas_y) if hasattr(self, 'get_clip_at_position') else None # Create context menu context_menu = tk.Menu(self.root, tearoff=0, bg=self.colors['bg_secondary'], fg=self.colors['text_primary']) if clicked_clip: # Clip context menu self.selected_clip = clicked_clip context_menu.add_command(label=f"Cut '{clicked_clip['name']}'", command=lambda: self.cut_clip_at_playhead()) context_menu.add_command(label=f"Delete '{clicked_clip['name']}'", command=lambda: self.delete_clip(clicked_clip)) context_menu.add_separator() context_menu.add_command(label="Duplicate Clip", command=lambda: self.duplicate_clip(clicked_clip)) context_menu.add_command(label="Properties", command=lambda: self.show_clip_properties(clicked_clip)) else: # Timeline context menu click_time = canvas_x / self.timeline_scale if hasattr(self, 'timeline_scale') else 0 context_menu.add_command(label="Add Marker", command=lambda: self.add_marker_at_time(click_time)) context_menu.add_command(label="Zoom to Fit", command=self.zoom_to_fit) try: context_menu.tk_popup(event.x_root, event.y_root) finally: context_menu.grab_release() except Exception as e: print(f"Context menu error: {e}") def on_timeline_double_click(self, event): """Handle timeline double-click""" try: canvas_x = self.timeline_canvas.canvasx(event.x) canvas_y = self.timeline_canvas.canvasy(event.y) clicked_clip = self.get_clip_at_position(canvas_x, canvas_y) if hasattr(self, 'get_clip_at_position') else None if clicked_clip: self.show_clip_properties(clicked_clip) else: # Add marker on double-click click_time = canvas_x / self.timeline_scale if hasattr(self, 'timeline_scale') else 0 self.add_marker_at_time(click_time) except Exception as e: print(f"Double-click error: {e}") def update_time_display(self): """Update the time display""" current_min = int(self.current_time // 60) current_sec = int(self.current_time % 60) total_min = int(self.video_duration // 60) total_sec = int(self.video_duration % 60) time_text = f"{current_min:02d}:{current_sec:02d} / {total_min:02d}:{total_sec:02d}" self.time_display.config(text=time_text) def timeline_play(self): """Start timeline playback""" if not self.current_clip: return self.timeline_is_playing = True self.play_video() # Start actual video playback self._start_timeline_playback() def timeline_pause(self): """Pause timeline playback""" self.timeline_is_playing = False self.pause_video() # Pause actual video def timeline_stop(self): """Stop timeline playback""" self.timeline_is_playing = False self.stop_video() # Stop actual video self.current_time = 0.0 self.display_frame_at_time(0.0) self.update_timeline() self.update_time_display() def _start_timeline_playback(self): """Start the timeline playback loop""" def playback_loop(): while self.timeline_is_playing and self.current_time < self.video_duration: if not self.is_playing: # Sync with video player state break # Update timeline display self.editor_window.after(0, self.update_timeline) self.editor_window.after(0, self.update_time_display) time.sleep(1/30) # 30 FPS update rate # Playback finished self.timeline_is_playing = False if not hasattr(self, 'timeline_thread') or not self.timeline_thread.is_alive(): self.timeline_thread = threading.Thread(target=playback_loop, daemon=True) self.timeline_thread.start() def play_video(self): """Start video playback""" if not self.current_clip or self.is_playing: return self.is_playing = True def play_thread(): start_time = time.time() start_video_time = self.current_time while self.is_playing and self.current_time < self.video_duration: try: # Calculate current video time elapsed = time.time() - start_time self.current_time = start_video_time + elapsed if self.current_time >= self.video_duration: self.current_time = self.video_duration self.is_playing = False break # Update display self.display_frame_at_time(self.current_time) self.update_time_display() # Frame rate control (approximately 30 FPS) time.sleep(1/30) except Exception as e: print(f"⚠️ Playback error: {e}") break # Playback finished self.is_playing = False self.play_thread = threading.Thread(target=play_thread, daemon=True) self.play_thread.start() def pause_video(self): """Pause video playback""" self.is_playing = False def stop_video(self): """Stop video and return to beginning""" self.is_playing = False self.current_time = 0.0 self.display_frame_at_time(0.0) def apply_trim(self): """Apply trim to the video""" if not MOVIEPY_AVAILABLE: messagebox.showwarning("Feature Unavailable", "Trim feature requires MoviePy.\nInstall with: pip install moviepy") return if not self.current_clip: messagebox.showwarning("No Video", "Please load a video first.") return start_time = self.trim_start_var.get() end_time = self.trim_end_var.get() if start_time >= end_time: messagebox.showerror("Invalid Range", "Start time must be less than end time.") return if end_time > self.video_duration: messagebox.showerror("Invalid Range", f"End time cannot exceed video duration ({self.video_duration:.1f}s).") return try: # Apply trim self.current_clip = self.current_clip.subclipped(start_time, end_time) self.video_duration = self.current_clip.duration self.current_time = 0.0 # Update UI self.trim_end_var.set(self.video_duration) self.display_frame_at_time(0.0) self.update_timeline() self.update_time_display() messagebox.showinfo("Success", f"Video trimmed to {start_time:.1f}s - {end_time:.1f}s") except Exception as e: messagebox.showerror("Trim Error", f"Could not trim video: {e}") def apply_speed(self): """Apply speed change to the video""" if not MOVIEPY_AVAILABLE: messagebox.showwarning("Feature Unavailable", "Speed change requires MoviePy.\nInstall with: pip install moviepy") return if not self.current_clip: messagebox.showwarning("No Video", "Please load a video first.") return speed_factor = self.speed_var.get() try: if speed_factor > 1: # Speed up self.current_clip = self.current_clip.with_fps(self.current_clip.fps * speed_factor) self.current_clip = self.current_clip.subclipped(0, self.current_clip.duration / speed_factor) else: # Slow down self.current_clip = self.current_clip.with_fps(self.current_clip.fps * speed_factor) self.video_duration = self.current_clip.duration self.current_time = 0.0 # Update UI self.trim_end_var.set(self.video_duration) self.display_frame_at_time(0.0) self.update_timeline() self.update_time_display() messagebox.showinfo("Success", f"Speed changed to {speed_factor:.1f}x") except Exception as e: messagebox.showerror("Speed Error", f"Could not change speed: {e}") def apply_volume(self): """Apply volume adjustment""" if not MOVIEPY_AVAILABLE: messagebox.showwarning("Feature Unavailable", "Volume adjustment requires MoviePy.\nInstall with: pip install moviepy") return if not self.current_clip: messagebox.showwarning("No Video", "Please load a video first.") return if not self.current_clip.audio: messagebox.showwarning("No Audio", "This video has no audio track.") return volume_factor = self.volume_var.get() try: self.current_clip = self.current_clip.with_effects([MultiplyVolume(volume_factor)]) messagebox.showinfo("Success", f"Volume adjusted to {volume_factor:.1f}x") except Exception as e: messagebox.showerror("Volume Error", f"Could not adjust volume: {e}") def apply_fade(self): """Apply fade in/out effects""" if not MOVIEPY_AVAILABLE: messagebox.showwarning("Feature Unavailable", "Fade effects require MoviePy.\nInstall with: pip install moviepy") return if not self.current_clip: messagebox.showwarning("No Video", "Please load a video first.") return try: fade_duration = min(1.0, self.video_duration / 4) # Max 1 second or 1/4 of video self.current_clip = self.current_clip.with_effects([ FadeIn(fade_duration), FadeOut(fade_duration) ]) messagebox.showinfo("Success", f"Fade effects applied ({fade_duration:.1f}s)") except Exception as e: messagebox.showerror("Fade Error", f"Could not apply fade effects: {e}") def apply_text(self): """Apply text overlay""" if not MOVIEPY_AVAILABLE: messagebox.showwarning("Feature Unavailable", "Text overlay requires MoviePy.\nInstall with: pip install moviepy") return if not self.current_clip: messagebox.showwarning("No Video", "Please load a video first.") return text = self.text_var.get().strip() if not text: messagebox.showwarning("No Text", "Please enter text to overlay.") return try: # Create text clip text_clip = TextClip(text, fontsize=50, color='white', font='Arial-Bold') text_clip = text_clip.with_duration(self.current_clip.duration) text_clip = text_clip.with_position(('center', 'bottom')) # Composite with video self.current_clip = CompositeVideoClip([self.current_clip, text_clip]) messagebox.showinfo("Success", f"Text overlay added: '{text}'") except Exception as e: messagebox.showerror("Text Error", f"Could not add text overlay: {e}") def reset_video(self): """Reset video to original state""" if not self.current_video: messagebox.showwarning("No Video", "No video loaded.") return if messagebox.askyesno("Reset Video", "Reset all changes and reload original video?"): self.load_video(self.current_video) def export_video(self): """Export the edited video""" if not MOVIEPY_AVAILABLE: messagebox.showwarning("Feature Unavailable", "Video export requires MoviePy.\nInstall with: pip install moviepy") return if not self.current_clip: messagebox.showwarning("No Video", "Please load a video first.") return # Get output filename timestamp = datetime.now().strftime("%H%M%S") default_name = f"edited_video_{timestamp}.mp4" output_path = filedialog.asksaveasfilename( title="Save Edited Video", defaultextension=".mp4", filetypes=[("MP4 files", "*.mp4"), ("All files", "*.*")], initialname=default_name ) if not output_path: return # Create progress window progress_window = tk.Toplevel(self.editor_window) progress_window.title("Exporting Video") progress_window.geometry("400x150") progress_window.configure(bg=self.colors['bg_primary']) progress_window.transient(self.editor_window) progress_window.grab_set() progress_label = tk.Label(progress_window, text="Exporting video...", font=self.fonts['body'], bg=self.colors['bg_primary'], fg=self.colors['text_primary']) progress_label.pack(pady=20) progress_bar = ttk.Progressbar(progress_window, mode='indeterminate') progress_bar.pack(pady=10, padx=20, fill="x") progress_bar.start() def export_thread(): try: # Export video self.current_clip.write_videofile( output_path, codec="libx264", audio_codec="aac", verbose=False, logger=None ) def show_success(): progress_window.destroy() messagebox.showinfo("Export Complete", f"Video exported successfully!\n\nSaved to: {output_path}") self.editor_window.after(0, show_success) except Exception as e: def show_error(): progress_window.destroy() messagebox.showerror("Export Error", f"Could not export video: {e}") self.editor_window.after(0, show_error) # Start export in background thread threading.Thread(target=export_thread, daemon=True).start() # Professional timeline helper methods def get_clip_at_position(self, x, y): """Get the clip at the given canvas position""" time_pos = x / self.timeline_scale for clip in self.timeline_clips: if clip['start_time'] <= time_pos <= clip['end_time']: # Check if Y position is within the clip's track track_info = self.tracks[clip['track']] if track_info['y_offset'] <= y <= track_info['y_offset'] + track_info['height']: return clip return None def snap_to_grid(self, time_value): """Snap time value to grid""" if self.snap_enabled and self.grid_size > 0: return round(time_value / self.grid_size) * self.grid_size return time_value def magnetic_snap(self, new_time, dragging_clip): """Apply magnetic timeline snapping to other clips""" if not self.magnetic_timeline: return new_time snap_distance = 0.2 # 200ms snap distance clip_duration = dragging_clip['end_time'] - dragging_clip['start_time'] for clip in self.timeline_clips: if clip == dragging_clip or clip['track'] != dragging_clip['track']: continue # Snap to start of other clips if abs(new_time - clip['start_time']) < snap_distance: return clip['start_time'] # Snap to end of other clips if abs(new_time - clip['end_time']) < snap_distance: return clip['end_time'] # Snap end of dragging clip to start of other clips if abs((new_time + clip_duration) - clip['start_time']) < snap_distance: return clip['start_time'] - clip_duration return new_time def cut_clip_at_position(self, clip, cut_time): """Cut a clip at the specified time""" if cut_time <= clip['start_time'] or cut_time >= clip['end_time']: return # Create two new clips first_clip = clip.copy() first_clip['id'] = len(self.timeline_clips) + 1 first_clip['end_time'] = cut_time first_clip['name'] = f"{clip['name']} (1)" second_clip = clip.copy() second_clip['id'] = len(self.timeline_clips) + 2 second_clip['start_time'] = cut_time second_clip['name'] = f"{clip['name']} (2)" # Remove original clip and add new ones self.timeline_clips.remove(clip) self.timeline_clips.extend([first_clip, second_clip]) self.selected_clip = first_clip self.update_timeline() print(f"βœ‚οΈ Cut clip at {cut_time:.2f}s") def cut_clip_at_playhead(self): """Cut selected clip at current playhead position""" if self.selected_clip: self.cut_clip_at_position(self.selected_clip, self.current_time) def delete_clip(self, clip): """Delete a clip from timeline""" if clip in self.timeline_clips: self.timeline_clips.remove(clip) if self.selected_clip == clip: self.selected_clip = None self.update_timeline() print(f"πŸ—‘οΈ Deleted clip: {clip['name']}") def duplicate_clip(self, clip): """Duplicate a clip""" new_clip = clip.copy() new_clip['id'] = len(self.timeline_clips) + 1 new_clip['name'] = f"{clip['name']} (Copy)" # Place after original clip duration = clip['end_time'] - clip['start_time'] new_clip['start_time'] = clip['end_time'] new_clip['end_time'] = clip['end_time'] + duration self.timeline_clips.append(new_clip) self.selected_clip = new_clip self.update_timeline() print(f"πŸ“„ Duplicated clip: {new_clip['name']}") def add_marker_at_time(self, time): """Add a marker at specified time""" marker = { 'time': time, 'name': f"Marker {len(self.markers) + 1}", 'color': '#ffeb3b' } self.markers.append(marker) self.update_timeline() print(f"πŸ“ Added marker at {time:.2f}s") def show_clip_properties(self, clip): """Show clip properties dialog""" props_window = tk.Toplevel(self.root) props_window.title(f"Clip Properties - {clip['name']}") props_window.configure(bg=self.colors['bg_primary']) props_window.geometry("400x300") # Clip info tk.Label(props_window, text=f"Clip: {clip['name']}", font=self.fonts['heading'], bg=self.colors['bg_primary'], fg=self.colors['text_primary']).pack(pady=10) info_frame = tk.Frame(props_window, bg=self.colors['bg_primary']) info_frame.pack(fill='x', padx=20) # Duration, start time, etc. duration = clip['end_time'] - clip['start_time'] tk.Label(info_frame, text=f"Duration: {duration:.2f}s", bg=self.colors['bg_primary'], fg=self.colors['text_secondary']).pack(anchor='w') tk.Label(info_frame, text=f"Start: {clip['start_time']:.2f}s", bg=self.colors['bg_primary'], fg=self.colors['text_secondary']).pack(anchor='w') tk.Label(info_frame, text=f"End: {clip['end_time']:.2f}s", bg=self.colors['bg_primary'], fg=self.colors['text_secondary']).pack(anchor='w') tk.Label(info_frame, text=f"Track: {clip['track']}", bg=self.colors['bg_primary'], fg=self.colors['text_secondary']).pack(anchor='w') def zoom_to_fit(self): """Zoom timeline to fit all content""" if not self.timeline_clips: return # Find the last clip end time max_time = max(clip['end_time'] for clip in self.timeline_clips) canvas_width = self.timeline_canvas.winfo_width() if max_time > 0 and canvas_width > 100: zoom_level = (canvas_width - 100) / (max_time * 50) # 50 is base scale self.zoom_var.set(max(0.1, min(5.0, zoom_level))) self.on_zoom_change(zoom_level) print(f"πŸ” Zoomed to fit content ({zoom_level:.1f}x)") def open_shorts_editor(shorts_folder="shorts"): """Open the shorts editor as a standalone application""" editor = ShortsEditorGUI(shorts_folder=shorts_folder) editor.open_editor() if __name__ == "__main__": # Run as standalone application open_shorts_editor()