diff --git a/video_editor (3).py b/video_editor (3).py deleted file mode 100644 index 1a07355..0000000 --- a/video_editor (3).py +++ /dev/null @@ -1,2165 +0,0 @@ -""" -Professional Video Editor for Generated Shorts -Author: Dario Pascoal - -Description: This is a comprehensive video editing application designed specifically for editing -short-form video content. The application provides a professional timeline-based interface -similar to industry-standard video editing software, with features including: - -- Multi-track timeline with visual track roads for professional editing workflow -- Real-time video preview with frame-accurate scrubbing -- Professional editing tools: trim, speed adjustment, volume control, fade effects -- Text overlay capabilities with customizable styling -- Export functionality with multiple format support -- Tabbed interface organizing tools into logical categories -- Dark theme optimized for video editing work -- Support for multiple video formats (MP4, AVI, MOV, MKV, etc.) - -The application is built using Python's Tkinter for the GUI, OpenCV for basic video processing, -and optionally MoviePy for advanced video editing features. It's designed to be educational, -showing how professional video editing interfaces work while remaining accessible to beginners. - -Technical Architecture: -- Uses threading for non-blocking video playback and processing -- Implements canvas-based timeline with precise time-to-pixel calculations -- Supports both MoviePy (full features) and OpenCV (basic features) backends -- Maintains professional video editing workflow patterns -- Provides real-time preview updates during editing operations - -This file serves as the main entry point for the video editing functionality and can be -integrated into larger applications or run as a standalone tool. -""" - -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 - - This class provides a complete video editing interface with timeline controls, - real-time preview, and professional editing tools. It's designed to mimic the - workflow of professional video editing software while remaining accessible - to beginners. - - Key Features: - - Multi-track timeline with visual separation (road lines) - - Frame-accurate video scrubbing and playback - - Professional editing tools (trim, speed, volume, effects) - - Real-time preview with synchronized timeline - - Tabbed tool interface for organized workflow - - Support for multiple video formats - - Export functionality with timestamp-based naming - - The interface uses a dark theme optimized for video editing work and provides - both mouse and keyboard controls for efficient editing workflow. - - Attributes: - parent: Parent window or None for standalone operation - shorts_folder: Directory path for video files - current_video: Path to currently loaded video file - current_clip: Video clip object (MoviePy or OpenCV) - timeline_*: Various timeline state and control variables - colors: Dictionary defining the dark theme color scheme - fonts: Dictionary defining typography for different UI elements - """ - - def __init__(self, parent=None, shorts_folder="shorts"): - """ - Initialize the Professional Video Editor Interface - - Sets up all the necessary state variables, UI theme, and data structures - needed for professional video editing. This includes timeline management, - video playback state, track configuration, and UI styling. - - The initialization process: - 1. Stores parent window reference and shorts folder path - 2. Initializes video playback state variables - 3. Sets up timeline state and interaction controls - 4. Configures professional editing features (snap, magnetic timeline) - 5. Defines track structure for multi-track editing - 6. Establishes dark theme color scheme optimized for video work - 7. Sets up typography for different UI elements - - Args: - parent: Parent tkinter window (None for standalone operation) - shorts_folder: String path to directory containing video files - """ - # Store parent window reference and video directory path - self.parent = parent - self.shorts_folder = shorts_folder - - # Video playback state management - # These variables track the current state of video loading and playback - self.current_video = None # File path to currently loaded video - self.current_clip = None # Video clip object (MoviePy VideoFileClip or OpenCV VideoCapture) - self.current_time = 0.0 # Current playback position in seconds - self.video_duration = 0.0 # Total video duration in seconds - self.is_playing = False # Whether video is currently playing - self.timeline_is_playing = False # Whether timeline playback is active - self.play_thread = None # Background thread for video playback - - # Timeline display and interaction state - # Controls how the timeline is rendered and how users interact with it - self.timeline_position = 0.0 # Current scroll position of timeline view - self.timeline_scale = 1.0 # Zoom level: pixels per second of video - self.timeline_width = 800 # Width of timeline canvas in pixels - - # Professional timeline editing features - # These lists store timeline content and user-created elements - self.timeline_clips = [] # List of video/audio clips on timeline - self.selected_clip = None # Currently selected clip for editing - self.markers = [] # User-placed timeline markers for navigation - - # Multi-track system for professional video editing workflow - # Each track has its own properties and can hold different types of media - # This structure allows for layered editing with video and audio tracks - self.tracks = { - # Video tracks - higher tracks appear on top in the final composition - 'video_1': {'y_offset': 40, 'height': 60, 'color': '#3498db', 'name': 'Video 1', - 'muted': False, 'locked': False, 'solo': False, 'visible': True, 'type': 'video'}, - 'video_2': {'y_offset': 105, 'height': 60, 'color': '#2ecc71', 'name': 'Video 2', - 'muted': False, 'locked': False, 'solo': False, 'visible': True, 'type': 'video'}, - 'video_3': {'y_offset': 170, 'height': 60, 'color': '#9b59b6', 'name': 'Video 3', - 'muted': False, 'locked': False, 'solo': False, 'visible': True, 'type': 'video'}, - # Audio tracks - for background music, sound effects, voiceovers - 'audio_1': {'y_offset': 235, 'height': 40, 'color': '#e74c3c', 'name': 'Audio 1', - 'muted': False, 'locked': False, 'solo': False, 'visible': True, 'type': 'audio'}, - 'audio_2': {'y_offset': 280, 'height': 40, 'color': '#f39c12', 'name': 'Audio 2', - 'muted': False, 'locked': False, 'solo': False, 'visible': True, 'type': 'audio'} - } - - # Timeline interaction state for drag-and-drop editing - # These variables track the current state of user interactions with timeline elements - self.dragging_clip = None # Clip currently being dragged by user - self.drag_start_x = None # Mouse X position when drag started - self.drag_start_time = None # Original time position of dragged clip - self.drag_offset = 0 # Offset from clip start to mouse position - self.snap_enabled = True # Whether clips snap to grid/other clips - self.magnetic_timeline = True # Whether clips magnetically attract to each other - self.grid_size = 1.0 # Snap grid size in seconds (for precise alignment) - - # Timeline editing modes for different types of operations - # This affects how mouse interactions behave on the timeline - self.edit_mode = 'select' # Current editing mode: 'select', 'cut', 'trim', 'ripple' - - # Visual enhancement options for better user experience - # These can be toggled on/off based on performance and user preference - self.show_thumbnails = True # Show video thumbnail previews on timeline clips - self.show_waveforms = True # Show audio waveform visualization - - # Cached visual data for performance optimization - # Storing thumbnails and waveforms prevents repeated generation - self.clip_thumbnails = {} # Dictionary storing thumbnail images by clip ID - self.audio_waveforms = {} # Dictionary storing waveform data by clip ID - - # Track control widget references for dynamic updates - # This allows us to update track control buttons when states change - self.track_widgets = {} # Dictionary storing widget references by track ID - - # Professional dark color scheme optimized for video editing - # Dark themes reduce eye strain during long editing sessions and help - # focus attention on the video content rather than the interface - self.colors = { - 'bg_primary': '#1a1a1a', # Main background - darkest for maximum contrast - 'bg_secondary': '#2d2d2d', # Secondary panels - slightly lighter - 'bg_tertiary': '#3d3d3d', # Buttons and controls - interactive elements - 'text_primary': '#ffffff', # Main text - high contrast for readability - 'text_secondary': '#b8b8b8', # Secondary text - lower contrast for hierarchy - 'accent_blue': '#007acc', # Primary actions - professional blue - 'accent_green': '#28a745', # Success/positive actions - natural green - 'accent_orange': '#fd7e14', # Warning/attention - energetic orange - 'accent_red': '#dc3545', # Destructive/negative actions - clear red - 'border': '#404040' # Subtle borders - defines sections without distraction - } - - # Typography system for consistent text hierarchy - # Font choices prioritize readability and professional appearance - # Segoe UI provides excellent readability across different screen sizes - self.fonts = { - 'title': ('Segoe UI', 16, 'bold'), # Main window titles and headers - 'heading': ('Segoe UI', 11, 'bold'), # Section headings and tool categories - 'body': ('Segoe UI', 10), # Regular text and input fields - 'caption': ('Segoe UI', 9), # Small labels and descriptions - 'button': ('Segoe UI', 10, 'bold') # Button text for clear action indicators - } - - def open_editor(self): - """ - Open the Professional Video Editor Window - - Creates and displays the main video editing interface window. This method - sets up the window properties, makes it responsive, and initializes the - complete editor interface. - - The window creation process: - 1. Creates either a new Tkinter window (standalone) or Toplevel window (child) - 2. Sets window title, size, and minimum dimensions for usability - 3. Applies the dark theme background color - 4. Configures responsive layout with proper weight distribution - 5. Calls create_editor_interface() to build the complete UI - 6. Starts the main event loop if running standalone - - Window Properties: - - Title: "Professional Shorts Editor" - - Size: 1200x800 pixels (optimal for video editing workflow) - - Minimum: 900x600 pixels (ensures UI remains functional when resized) - - Theme: Dark background optimized for video editing - - Layout: Responsive with proper weight distribution for resizing - """ - # Create the main editor window - either standalone or child window - # If parent exists, create as child window; otherwise create root 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") # Optimal size for video editing - self.editor_window.minsize(900, 600) # Minimum usable size - self.editor_window.configure(bg=self.colors['bg_primary']) # Apply dark theme - - # Configure responsive layout for window resizing - # Row 1 gets all extra vertical space (main content area) - # Column 0 gets all extra horizontal space (full width utilization) - self.editor_window.rowconfigure(1, weight=1) - self.editor_window.columnconfigure(0, weight=1) - - # Build the complete editor interface - self.create_editor_interface() - - # Start the application main loop if running as standalone application - if not self.parent: - self.editor_window.mainloop() - - def create_editor_interface(self): - """ - Create the Complete Professional Video Editor Interface - - Builds the entire user interface for the video editor, organizing it into - logical sections that mirror professional video editing software. The - interface uses a three-panel layout with header, main content, and tools. - - Interface Structure: - 1. Header: Title, current file display, and file selection controls - 2. Main Content: Split into left panel (video/timeline) and right panel (tools) - 3. Left Panel: Video preview, playback controls, and professional timeline - 4. Right Panel: Tabbed tool interface with editing functions - 5. Timeline: Multi-track workspace with road lines and interactive controls - - The layout is designed to be intuitive for users familiar with professional - video editing software while remaining accessible to beginners. Dark theme - reduces eye strain during long editing sessions. - """ - # Header section - contains title and file management controls - # Fixed height prevents layout shifts when content changes - 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) # Maintain fixed height - - # Application title with professional icon and branding - 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 area - shows current file and provides selection control - file_frame = tk.Frame(header_frame, bg=self.colors['bg_secondary']) - file_frame.pack(side="right", padx=20, pady=15) - - # Current file display - shows filename or "No video selected" status - 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)) - - # File selection button - opens file browser or shows available videos - 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 - contains the primary editing workspace - # Uses a two-column grid layout with weighted columns for responsive design - # Left column (weight=2) is larger for video preview and timeline - # Right column (weight=1) is smaller for tools and controls - 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) # Single row takes all vertical space - main_frame.columnconfigure(0, weight=2) # Left column gets 2/3 of width - main_frame.columnconfigure(1, weight=1) # Right column gets 1/3 of width - - # Left panel - Video preview and timeline workspace - # This is the main editing area where users see their video and timeline - # Organized vertically with video preview on top and timeline below - 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) # Video preview area (expandable) - player_frame.rowconfigure(1, weight=0) # Timeline area (fixed height) - player_frame.columnconfigure(0, weight=1) # Full width utilization - - # Video Display Area - The main preview window for video content - # This section provides real-time video preview with proper aspect ratio maintenance - # Dark border provides visual separation and professional appearance - 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 canvas expands to fill space - video_container.columnconfigure(0, weight=1) # Full width utilization - - # Video canvas - displays the actual video frames - # Black background provides professional video preview appearance - # No highlight thickness removes the default canvas border for cleaner look - self.video_canvas = tk.Canvas(video_container, bg='black', highlightthickness=0) - self.video_canvas.grid(row=0, column=0, sticky="nsew") - - # Professional Timeline Workspace - Multi-track editing environment - # Fixed height prevents timeline from shrinking when window is resized - # This is where users perform the majority of their editing work - timeline_workspace = tk.Frame(player_frame, bg=self.colors['bg_secondary'], height=380) - timeline_workspace.grid(row=1, column=0, sticky="ew", padx=15, pady=(0, 15)) - timeline_workspace.pack_propagate(False) # Maintain fixed height for consistent layout - - # Timeline header - contains editing mode controls and timeline options - # These controls affect how the user interacts with timeline elements - header_frame = tk.Frame(timeline_workspace, bg=self.colors['bg_secondary']) - header_frame.pack(fill="x", pady=(10, 5)) - - # Left side timeline controls - editing modes and interaction options - left_controls = tk.Frame(header_frame, bg=self.colors['bg_secondary']) - left_controls.pack(side="left") - - # Editing mode selector - determines how mouse interactions behave - # Different modes enable different types of editing operations - tk.Label(left_controls, text="Mode:", font=self.fonts['caption'], - bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(side="left") - - # Mode selection dropdown with professional editing modes - # select: Default mode for selecting and moving clips - # cut: Razor tool for splitting clips at specific points - # trim: For adjusting clip start/end points - # ripple: Moves clips and automatically adjusts following clips - 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) - - # Professional timeline assistance features - # These options help users align and position clips precisely - 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) - - # Magnetic timeline - clips automatically attract to align with other clips - # This feature helps maintain precise timing relationships between clips - 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 - Professional playback controls for timeline - # These controls manage video playback synchronized with timeline position - # Color-coded for immediate recognition: Green=Play, Orange=Pause, Red=Stop - center_controls = tk.Frame(header_frame, bg=self.colors['bg_secondary']) - center_controls.pack(side="left", padx=20) - - # Play button - starts video playback from current timeline position - 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) - - # Pause button - temporarily stops playback, maintains current position - 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) - - # Stop button - stops playback and returns to beginning of timeline - 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, 400)) - - # Scrollbars - h_scrollbar = ttk.Scrollbar(canvas_frame, orient="horizontal", command=self.timeline_canvas.xview) - v_scrollbar = ttk.Scrollbar(canvas_frame, orient="vertical", command=self.sync_vertical_scroll) - 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() - - # Bind canvas resize to redraw road lines - self.track_panel.bind("", self.on_track_panel_resize) - - # 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 tabbed interface for tools - self.create_tabbed_tools(tools_frame) - - # Initialize timeline - self.update_timeline() - - def create_tabbed_tools(self, parent): - """Create tabbed interface for editing tools""" - import tkinter.ttk as ttk - - # Create notebook for tabs - self.tools_notebook = ttk.Notebook(parent) - self.tools_notebook.pack(fill="both", expand=True, padx=10, pady=5) - - # Configure notebook style for dark theme - style = ttk.Style() - - # Set theme and configure colors - try: - style.theme_use('clam') # Use clam theme as base for better customization - except: - pass # Fall back to default theme if clam isn't available - - # Configure the notebook (main container) - style.configure('TNotebook', - background=self.colors['bg_secondary'], - borderwidth=0, - tabmargins=[2, 5, 2, 0]) - - # Configure the tabs themselves with consistent sizing - style.configure('TNotebook.Tab', - background=self.colors['bg_tertiary'], - foreground=self.colors['text_primary'], - padding=[15, 8, 15, 8], # Explicit left, top, right, bottom padding - borderwidth=0, # Remove border to prevent size changes - focuscolor='none', - relief='flat') - - # Configure selected tab with same padding to prevent shrinking - style.map('TNotebook.Tab', - background=[('selected', self.colors['accent_blue']), - ('active', self.colors['bg_primary']), - ('!active', self.colors['bg_tertiary'])], - foreground=[('selected', 'white'), - ('active', self.colors['text_primary']), - ('!active', self.colors['text_primary'])], - padding=[('selected', [15, 8, 15, 8]), # Same padding for selected - ('active', [15, 8, 15, 8]), # Same padding for active - ('!active', [15, 8, 15, 8])], # Same padding for inactive - borderwidth=[('selected', 0), # No border changes - ('active', 0), - ('!active', 0)], - relief=[('selected', 'flat'), # Consistent relief - ('active', 'flat'), - ('!active', 'flat')]) - - # Basic Editing Tab - basic_frame = tk.Frame(self.tools_notebook, bg=self.colors['bg_secondary']) - self.tools_notebook.add(basic_frame, text="Basic Editing") - self.create_basic_tools(basic_frame) - - # Video Effects Tab - video_effects_frame = tk.Frame(self.tools_notebook, bg=self.colors['bg_secondary']) - self.tools_notebook.add(video_effects_frame, text="Video Effects") - self.create_video_effects_tools(video_effects_frame) - - # Audio Effects Tab - audio_effects_frame = tk.Frame(self.tools_notebook, bg=self.colors['bg_secondary']) - self.tools_notebook.add(audio_effects_frame, text="Audio Effects") - self.create_audio_effects_tools(audio_effects_frame) - - # Export Tab - export_frame = tk.Frame(self.tools_notebook, bg=self.colors['bg_secondary']) - self.tools_notebook.add(export_frame, text="Export") - self.create_export_tools(export_frame) - - def create_track_controls(self): - """Create professional track control panel with road lines""" - # Clear existing track controls - for widget in self.track_panel.winfo_children(): - widget.destroy() - - # Create road line background canvas - self.track_road_canvas = tk.Canvas(self.track_panel, - bg=self.colors['bg_secondary'], - highlightthickness=0, - scrollregion=(0, 0, 120, 400)) - self.track_road_canvas.pack(fill="both", expand=True) - - # Draw road lines for track separation - self.draw_track_road_lines() - - # Create controls for each track - for track_id, track_info in self.tracks.items(): - self.create_track_control(track_id, track_info) - - def draw_track_road_lines(self): - """Draw road lines for track visual separation""" - # Clear existing lines - self.track_road_canvas.delete("road_lines") - - # Get canvas dimensions - canvas_width = self.track_panel.winfo_width() or 120 - canvas_height = self.track_panel.winfo_height() or 320 - - # Draw horizontal road lines for each track - for track_id, track_info in self.tracks.items(): - y_pos = track_info['y_offset'] - track_height = track_info['height'] - - # Top line of track - self.track_road_canvas.create_line( - 0, y_pos, canvas_width, y_pos, - fill=self.colors['border'], width=1, tags="road_lines" - ) - - # Bottom line of track - self.track_road_canvas.create_line( - 0, y_pos + track_height, canvas_width, y_pos + track_height, - fill=self.colors['border'], width=1, tags="road_lines" - ) - - # Track type indicator line (left edge with track color) - self.track_road_canvas.create_line( - 0, y_pos, 0, y_pos + track_height, - fill=track_info['color'], width=3, tags="road_lines" - ) - - # Center dashed line for alignment reference - center_x = canvas_width // 2 - for y in range(0, int(canvas_height), 10): - self.track_road_canvas.create_line( - center_x, y, center_x, y + 5, - fill=self.colors['text_secondary'], width=1, - tags="road_lines", dash=(2, 3) - ) - - def on_track_panel_resize(self, event): - """Handle track panel resize to redraw road lines""" - if hasattr(self, 'track_road_canvas'): - # Small delay to ensure canvas size is updated - self.editor_window.after(10, self.draw_track_road_lines) - - def sync_vertical_scroll(self, *args): - """Synchronize vertical scrolling between track panel and timeline canvas""" - # Scroll both canvases together - self.timeline_canvas.yview(*args) - if hasattr(self, 'track_road_canvas'): - self.track_road_canvas.yview(*args) - - def create_track_control(self, track_id, track_info): - """Create control panel for a single track positioned on road lines""" - # Calculate precise positioning based on track offset - y_position = track_info['y_offset'] - track_height = track_info['height'] - - # Create control buttons frame positioned on canvas - controls_frame = tk.Frame(self.track_road_canvas, bg=self.colors['bg_secondary']) - - # Control buttons container - controls = tk.Frame(controls_frame, bg=self.colors['bg_secondary']) - controls.pack(padx=5, pady=2) - - # Track name label (small, top of controls) - name_label = tk.Label(controls, text=track_info['name'], - font=('Arial', 7, 'bold'), bg=self.colors['bg_secondary'], - fg=track_info['color']) - name_label.pack(anchor="center") - - # Button container - button_container = tk.Frame(controls, bg=self.colors['bg_secondary']) - button_container.pack() - - # Mute button - mute_text = "πŸ”‡" if track_info['muted'] else "πŸ”Š" - mute_btn = tk.Button(button_container, 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', 7), 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(button_container, 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', 7, '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(button_container, 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', 7), relief="flat", bd=0, - command=lambda: self.toggle_track_lock(track_id)) - lock_btn.pack(side="left", padx=1) - - # Position the controls frame on the canvas - canvas_width = 120 - control_y = y_position + (track_height // 2) - 20 # Center vertically in track - - self.track_road_canvas.create_window( - canvas_width // 2, control_y, - window=controls_frame, anchor="center" - ) - - # Store track widgets for updates - self.track_widgets[track_id] = { - 'frame': controls_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""" - # Create scrollable frame for tools - tools_canvas = tk.Canvas(parent, bg=self.colors['bg_secondary'], highlightthickness=0) - scrollbar = tk.Scrollbar(parent, orient="vertical", command=tools_canvas.yview) - scrollable_frame = tk.Frame(tools_canvas, bg=self.colors['bg_secondary']) - - scrollable_frame.bind( - "", - lambda e: tools_canvas.configure(scrollregion=tools_canvas.bbox("all")) - ) - - tools_canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") - tools_canvas.configure(yscrollcommand=scrollbar.set) - - tools_canvas.pack(side="left", fill="both", expand=True) - scrollbar.pack(side="right", fill="y") - - # Trim controls - trim_frame = tk.LabelFrame(scrollable_frame, text="βœ‚οΈ Trim Video", font=self.fonts['body'], - bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], - relief="flat", bd=1) - trim_frame.pack(fill="x", padx=10, pady=5) - - 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(trim_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.LabelFrame(scrollable_frame, text="⚑ Speed Control", font=self.fonts['body'], - bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], - relief="flat", bd=1) - speed_frame.pack(fill="x", padx=10, pady=5) - - 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, padx=10) - - speed_btn = tk.Button(speed_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.LabelFrame(scrollable_frame, text="πŸ”Š Volume Control", font=self.fonts['body'], - bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], - relief="flat", bd=1) - volume_frame.pack(fill="x", padx=10, pady=5) - - 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, padx=10) - - volume_btn = tk.Button(volume_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) - - # Resize controls - resize_frame = tk.LabelFrame(scrollable_frame, text="πŸ“ Resize Video", font=self.fonts['body'], - bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], - relief="flat", bd=1) - resize_frame.pack(fill="x", padx=10, pady=5) - - # Preset sizes - preset_frame = tk.Frame(resize_frame, bg=self.colors['bg_secondary']) - preset_frame.pack(fill="x", padx=10, pady=5) - - tk.Label(preset_frame, text="Presets:", font=self.fonts['caption'], - bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w") - - presets = [("9:16 (1080x1920)", 1080, 1920), ("16:9 (1920x1080)", 1920, 1080), ("1:1 (1080x1080)", 1080, 1080)] - for name, width, height in presets: - btn = tk.Button(preset_frame, text=name, - command=lambda w=width, h=height: self.apply_resize(w, h), - bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'], - font=self.fonts['caption'], relief="flat", bd=0, cursor="hand2") - btn.pack(fill="x", pady=2) - - def create_video_effects_tools(self, parent): - """Create video effects tools""" - # Create scrollable frame for video effects - effects_canvas = tk.Canvas(parent, bg=self.colors['bg_secondary'], highlightthickness=0) - scrollbar_effects = tk.Scrollbar(parent, orient="vertical", command=effects_canvas.yview) - scrollable_effects_frame = tk.Frame(effects_canvas, bg=self.colors['bg_secondary']) - - scrollable_effects_frame.bind( - "", - lambda e: effects_canvas.configure(scrollregion=effects_canvas.bbox("all")) - ) - - effects_canvas.create_window((0, 0), window=scrollable_effects_frame, anchor="nw") - effects_canvas.configure(yscrollcommand=scrollbar_effects.set) - - effects_canvas.pack(side="left", fill="both", expand=True) - scrollbar_effects.pack(side="right", fill="y") - - # Fade effects - fade_frame = tk.LabelFrame(scrollable_effects_frame, text="πŸŒ… Fade Effects", font=self.fonts['body'], - bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], - relief="flat", bd=1) - fade_frame.pack(fill="x", padx=10, pady=5) - - fade_btn = tk.Button(fade_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.LabelFrame(scrollable_effects_frame, text="πŸ“ Text Overlay", font=self.fonts['body'], - bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], - relief="flat", bd=1) - text_frame.pack(fill="x", padx=10, pady=5) - - text_controls = tk.Frame(text_frame, bg=self.colors['bg_secondary']) - text_controls.pack(fill="x", padx=10, pady=5) - - tk.Label(text_controls, text="Text:", font=self.fonts['caption'], - bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w") - - self.text_var = tk.StringVar(value="Sample Text") - text_entry = tk.Entry(text_controls, textvariable=self.text_var, font=self.fonts['body'], - width=25) - text_entry.pack(fill="x", pady=5) - - text_btn = tk.Button(text_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_audio_effects_tools(self, parent): - """Create audio effects tools""" - # Create scrollable frame for audio effects - audio_canvas = tk.Canvas(parent, bg=self.colors['bg_secondary'], highlightthickness=0) - scrollbar_audio = tk.Scrollbar(parent, orient="vertical", command=audio_canvas.yview) - scrollable_audio_frame = tk.Frame(audio_canvas, bg=self.colors['bg_secondary']) - - scrollable_audio_frame.bind( - "", - lambda e: audio_canvas.configure(scrollregion=audio_canvas.bbox("all")) - ) - - audio_canvas.create_window((0, 0), window=scrollable_audio_frame, anchor="nw") - audio_canvas.configure(yscrollcommand=scrollbar_audio.set) - - audio_canvas.pack(side="left", fill="both", expand=True) - scrollbar_audio.pack(side="right", fill="y") - - # Volume controls (detailed) - volume_frame = tk.LabelFrame(scrollable_audio_frame, text="πŸ”Š Volume Controls", font=self.fonts['body'], - bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], - relief="flat", bd=1) - volume_frame.pack(fill="x", padx=10, pady=5) - - # Volume slider with fine control - self.audio_volume_var = tk.DoubleVar(value=1.0) - volume_scale = tk.Scale(volume_frame, from_=0.0, to=3.0, resolution=0.01, - orient="horizontal", variable=self.audio_volume_var, - bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], - highlightthickness=0, length=250) - volume_scale.pack(fill="x", pady=5, padx=10) - - volume_btn = tk.Button(volume_frame, text="πŸ”Š Apply Volume", command=self.apply_volume, - bg=self.colors['accent_blue'], fg='white', font=self.fonts['button'], - relief="flat", bd=0, cursor="hand2") - volume_btn.pack(fill="x", padx=10, pady=5) - - # Audio info - info_frame = tk.LabelFrame(scrollable_audio_frame, text="ℹ️ Audio Information", font=self.fonts['body'], - bg=self.colors['bg_secondary'], fg=self.colors['text_primary'], - relief="flat", bd=1) - info_frame.pack(fill="x", padx=10, pady=5) - - info_text = tk.Label(info_frame, - text="Audio effects require external tools\nor MoviePy installation.\n\nFor advanced audio editing:\nβ€’ Audacity (free)\nβ€’ FFmpeg\nβ€’ pip install moviepy", - font=self.fonts['caption'], bg=self.colors['bg_secondary'], - fg=self.colors['text_secondary'], justify="left") - info_text.pack(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 track road lines - self.draw_timeline_track_roads(canvas_width, canvas_height) - - # 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 draw_timeline_track_roads(self, canvas_width, canvas_height): - """Draw track road lines on timeline canvas""" - left_margin = 20 - right_margin = 20 - - # Draw horizontal road lines for each track - for track_id, track_info in self.tracks.items(): - y_pos = track_info['y_offset'] - track_height = track_info['height'] - - # Top line of track - self.timeline_canvas.create_line( - left_margin, y_pos, canvas_width - right_margin, y_pos, - fill=self.colors['border'], width=1, tags="track_roads" - ) - - # Bottom line of track - self.timeline_canvas.create_line( - left_margin, y_pos + track_height, canvas_width - right_margin, y_pos + track_height, - fill=self.colors['border'], width=1, tags="track_roads" - ) - - # Track type indicator line (left edge with track color) - self.timeline_canvas.create_line( - left_margin, y_pos, left_margin, y_pos + track_height, - fill=track_info['color'], width=4, tags="track_roads" - ) - - # Track name label on the left - self.timeline_canvas.create_text( - left_margin + 50, y_pos + track_height // 2, - text=track_info['name'], fill=track_info['color'], - font=('Arial', 8, 'bold'), anchor="center", tags="track_roads" - ) - - 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 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: - if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'subclipped'): - # Apply trim using MoviePy - 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") - else: - # OpenCV mode - export trimmed video - if not self.current_video: - messagebox.showwarning("No Video", "No video file loaded.") - return - - timestamp = datetime.now().strftime("%H%M%S") - base_name = os.path.splitext(os.path.basename(self.current_video))[0] - output_path = os.path.join(os.path.dirname(self.current_video), - f"{base_name}_trimmed_{timestamp}.mp4") - - self.apply_trim_opencv(self.current_video, output_path, start_time, end_time) - self.load_video(output_path) - messagebox.showinfo("Success", f"Video trimmed ({start_time:.1f}s to {end_time:.1f}s) and saved as:\n{os.path.basename(output_path)}") - - except Exception as e: - messagebox.showerror("Trim Error", f"Could not trim video: {e}") - - def apply_trim_opencv(self, input_path, output_path, start_time, end_time): - """Apply trim using OpenCV""" - cap = cv2.VideoCapture(input_path) - fps = cap.get(cv2.CAP_PROP_FPS) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - - start_frame = int(start_time * fps) - end_frame = int(end_time * fps) - - fourcc = cv2.VideoWriter_fourcc(*'mp4v') - out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) - - # Set to start frame - cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame) - - frame_count = start_frame - while frame_count < end_frame: - ret, frame = cap.read() - if not ret: - break - - out.write(frame) - frame_count += 1 - - cap.release() - out.release() - - def apply_speed(self): - """Apply speed change to the video""" - if not self.current_clip: - messagebox.showwarning("No Video", "Please load a video first.") - return - - speed_factor = self.speed_var.get() - - try: - if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'with_fps'): - # Apply speed change using MoviePy - 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") - else: - # OpenCV mode - export video with speed change - if not self.current_video: - messagebox.showwarning("No Video", "No video file loaded.") - return - - timestamp = datetime.now().strftime("%H%M%S") - base_name = os.path.splitext(os.path.basename(self.current_video))[0] - output_path = os.path.join(os.path.dirname(self.current_video), - f"{base_name}_speed_{speed_factor}x_{timestamp}.mp4") - - self.apply_speed_opencv(self.current_video, output_path, speed_factor) - self.load_video(output_path) - messagebox.showinfo("Success", f"Speed changed to {speed_factor:.1f}x and saved as:\n{os.path.basename(output_path)}") - - except Exception as e: - messagebox.showerror("Speed Error", f"Could not change speed: {e}") - - def apply_speed_opencv(self, input_path, output_path, speed_factor): - """Apply speed change using OpenCV""" - cap = cv2.VideoCapture(input_path) - fps = cap.get(cv2.CAP_PROP_FPS) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - - # Calculate new FPS for speed change - new_fps = fps * speed_factor - - fourcc = cv2.VideoWriter_fourcc(*'mp4v') - out = cv2.VideoWriter(output_path, fourcc, new_fps, (width, height)) - - while True: - ret, frame = cap.read() - if not ret: - break - - out.write(frame) - - cap.release() - out.release() - - def apply_volume(self): - """Apply volume adjustment""" - if not self.current_clip: - messagebox.showwarning("No Video", "Please load a video first.") - return - - # Check for volume factor from either basic or audio effects tab - volume_factor = 1.0 - if hasattr(self, 'audio_volume_var'): - volume_factor = self.audio_volume_var.get() - elif hasattr(self, 'volume_var'): - volume_factor = self.volume_var.get() - - try: - if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'audio'): - # Use MoviePy for volume adjustment - if not self.current_clip.audio: - messagebox.showwarning("No Audio", "This video has no audio track.") - return - - self.current_clip = self.current_clip.with_effects([MultiplyVolume(volume_factor)]) - messagebox.showinfo("Success", f"Volume adjusted to {volume_factor:.1f}x") - else: - # OpenCV mode - show helpful message about audio processing - if not self.current_video: - messagebox.showwarning("No Video", "No video file loaded.") - return - - messagebox.showinfo("Audio Processing", - f"Volume adjustment to {volume_factor:.1f}x noted.\n\n" - "Audio processing requires external tools.\n" - "For audio editing, consider using:\n" - "β€’ Audacity (free audio editor)\n" - "β€’ FFmpeg command line\n" - "β€’ Install MoviePy: pip install moviepy") - - 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 self.current_clip: - messagebox.showwarning("No Video", "Please load a video first.") - return - - try: - if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'fadein'): - # Use MoviePy for advanced fade effects - fade_duration = min(1.0, self.video_duration / 4) - - if hasattr(self.current_clip, 'fadein') and hasattr(self.current_clip, 'fadeout'): - self.current_clip = self.current_clip.fadein(fade_duration).fadeout(fade_duration) - messagebox.showinfo("Success", f"Fade effects applied ({fade_duration:.1f}s)") - else: - messagebox.showinfo("Not Available", "Fade effects not available in this MoviePy version") - else: - # OpenCV mode - export video with fade effect - if not self.current_video: - messagebox.showwarning("No Video", "No video file loaded.") - return - - timestamp = datetime.now().strftime("%H%M%S") - base_name = os.path.splitext(os.path.basename(self.current_video))[0] - output_path = os.path.join(os.path.dirname(self.current_video), - f"{base_name}_faded_{timestamp}.mp4") - - self.apply_fade_opencv(self.current_video, output_path) - self.load_video(output_path) - messagebox.showinfo("Success", f"Fade effects applied and saved as:\n{os.path.basename(output_path)}") - - except Exception as e: - messagebox.showerror("Fade Error", f"Could not apply fade effects: {e}") - - def apply_fade_opencv(self, input_path, output_path): - """Apply fade effects using OpenCV""" - cap = cv2.VideoCapture(input_path) - fps = cap.get(cv2.CAP_PROP_FPS) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - - fourcc = cv2.VideoWriter_fourcc(*'mp4v') - out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) - - # Calculate fade frames (1 second fade in/out) - fade_frames = min(int(fps), total_frames // 4) - - frame_count = 0 - while True: - ret, frame = cap.read() - if not ret: - break - - # Apply fade in - if frame_count < fade_frames: - alpha = frame_count / fade_frames - frame = cv2.convertScaleAbs(frame, alpha=alpha, beta=0) - - # Apply fade out - elif frame_count >= total_frames - fade_frames: - alpha = (total_frames - frame_count) / fade_frames - frame = cv2.convertScaleAbs(frame, alpha=alpha, beta=0) - - out.write(frame) - frame_count += 1 - - cap.release() - out.release() - - def apply_text(self): - """Apply text overlay""" - if not self.current_clip: - messagebox.showwarning("No Video", "Please load a video first.") - return - - # Get text from the text variable - text_content = self.text_var.get().strip() if hasattr(self, 'text_var') and self.text_var.get().strip() else "Sample Text" - - if not text_content: - messagebox.showwarning("No Text", "Please enter text to overlay.") - return - - try: - if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'duration'): - # Use MoviePy for text overlay - text_clip = TextClip(text_content, 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_content}'") - else: - # OpenCV mode - export video with text overlay - if not self.current_video: - messagebox.showwarning("No Video", "No video file loaded.") - return - - timestamp = datetime.now().strftime("%H%M%S") - base_name = os.path.splitext(os.path.basename(self.current_video))[0] - output_path = os.path.join(os.path.dirname(self.current_video), - f"{base_name}_with_text_{timestamp}.mp4") - - self.apply_text_opencv(self.current_video, output_path, text_content) - self.load_video(output_path) - messagebox.showinfo("Success", f"Text '{text_content}' added and saved as:\n{os.path.basename(output_path)}") - - except Exception as e: - messagebox.showerror("Text Error", f"Could not add text overlay: {e}") - - def apply_text_opencv(self, input_path, output_path, text): - """Apply text overlay using OpenCV""" - cap = cv2.VideoCapture(input_path) - fps = cap.get(cv2.CAP_PROP_FPS) - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - - fourcc = cv2.VideoWriter_fourcc(*'mp4v') - out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) - - # Text settings - font = cv2.FONT_HERSHEY_SIMPLEX - font_scale = min(width, height) / 800 # Scale font based on video size - color = (255, 255, 255) # White color - thickness = max(1, int(font_scale * 2)) - - # Get text size for positioning - (text_width, text_height), baseline = cv2.getTextSize(text, font, font_scale, thickness) - x = (width - text_width) // 2 - y = height - 50 # Bottom position - - while True: - ret, frame = cap.read() - if not ret: - break - - # Add black outline for better visibility - cv2.putText(frame, text, (x-2, y-2), font, font_scale, (0, 0, 0), thickness+2, cv2.LINE_AA) - cv2.putText(frame, text, (x+2, y+2), font, font_scale, (0, 0, 0), thickness+2, cv2.LINE_AA) - - # Add white text - cv2.putText(frame, text, (x, y), font, font_scale, color, thickness, cv2.LINE_AA) - - out.write(frame) - - cap.release() - out.release() - - def apply_resize(self, target_width, target_height): - """Apply resize to video""" - if not self.current_clip: - messagebox.showwarning("No Video", "Please load a video first.") - return - - try: - if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'resize'): - # Use MoviePy for resizing - self.current_clip = self.current_clip.resize((target_width, target_height)) - messagebox.showinfo("Success", f"Video resized to {target_width}x{target_height}") - else: - # OpenCV mode - export resized video - if not self.current_video: - messagebox.showwarning("No Video", "No video file loaded.") - return - - timestamp = datetime.now().strftime("%H%M%S") - base_name = os.path.splitext(os.path.basename(self.current_video))[0] - output_path = os.path.join(os.path.dirname(self.current_video), - f"{base_name}_resized_{target_width}x{target_height}_{timestamp}.mp4") - - self.apply_resize_opencv(self.current_video, output_path, target_width, target_height) - self.load_video(output_path) - messagebox.showinfo("Success", f"Video resized to {target_width}x{target_height} and saved as:\n{os.path.basename(output_path)}") - - except Exception as e: - messagebox.showerror("Resize Error", f"Could not resize video: {e}") - - def apply_resize_opencv(self, input_path, output_path, target_width, target_height): - """Apply resize using OpenCV""" - cap = cv2.VideoCapture(input_path) - fps = cap.get(cv2.CAP_PROP_FPS) - - fourcc = cv2.VideoWriter_fourcc(*'mp4v') - out = cv2.VideoWriter(output_path, fourcc, fps, (target_width, target_height)) - - while True: - ret, frame = cap.read() - if not ret: - break - - # Resize frame - resized_frame = cv2.resize(frame, (target_width, target_height)) - out.write(resized_frame) - - cap.release() - out.release() - - 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()