diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..7f62ddf --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,330 @@ +# Coding pattern preferences + +- Always prefer simple solutions +- Avoid duplication of code whenever possible, which means checking for other areas of the codebase that might already have similar code and functionality +- Write code that takes into account the different environments: dev, test, and prod +- You are careful to only make changes that are requested or you are confident are well understood and related to the change being requested +- When fixing an issue or bug, do not introduce a new pattern or technology without first exhausting all options for the existing implementation. And if you finally do this, make sure to remove the old implementation afterwards so we don’t have duplicate logic. +- Keep the codebase very clean and organized +- Avoid having files over 500 lines of code. Refactor at that point. +- Mocking data is only needed for tests, never mock data for dev or prod +- Never add stubbing or fake data patterns to code that affects the dev or prod environments +- Never overwrite my .env file without first asking and confirming +- Never ask the user to provide the content of a specific file. Just open the file and check it yourself. +- Never create multiple files for sql execution. Always use a single file for all sql execution in a given migration. +- Never fix the symptoms of a problem, always fix the root cause of the problem. + +# File Documentation Completion Requirements + +**MANDATORY: When updating a file to "match the new copilot instructions", you MUST:** + +1. **Complete the ENTIRE file** - Document every method, function, class, and significant code block +2. **Check for syntax errors** - Always verify the file compiles without errors after changes +3. **Test incrementally** - Use the get_errors tool after each major change to catch issues early +4. **Document systematically** - Go through the file from top to bottom, ensuring no method is left undocumented +5. **Maintain functionality** - Never break existing functionality while adding documentation +6. **Use proper comment syntax** - Always use the correct comment format for the programming language +7. **Validate completeness** - Before considering the task complete, review the entire file to ensure every function has documentation + +**File Documentation Checklist:** + +- [ ] File header with comprehensive description +- [ ] Every class documented with JSDoc +- [ ] Every method/function documented with JSDoc +- [ ] Every significant code block has educational comments +- [ ] Business context explained for complex logic +- [ ] Technical implementation details provided +- [ ] Error handling approaches documented +- [ ] Integration points with other modules explained +- [ ] No syntax errors remain in the file +- [ ] File compiles and functions properly + +**If a file is large (>1000 lines):** + +- Work in sections but complete ALL sections +- Add section dividers with clear documentation +- Use get_errors tool frequently to catch issues +- Test syntax after each major section +- Document the overall file architecture in the header + +**Quality Standards:** + +- Documentation should be educational and explain both WHAT and WHY +- Include business context for complex features +- Explain integration points and dependencies +- Use examples where helpful +- Write for developers who are new to the codebase + +# Coding workflow preferences + +- Focus on the areas of code relevant to the task +- Always use a surgical approach to code changes either removing or adding code while preserving all other functionalities. +- Do not touch code that is unrelated to the task +- Avoid making major changes to the patterns and architecture of how a feature works, after it has shown to work well, unless explicitly instructed +- Always think about what other methods and areas of code might be affected by code changes + +## Configuration File Documentation + +### For Configuration Files (package.json, tsconfig.json, etc.): + +```javascript +/** + * [Configuration Purpose] + * Author: Dario Pascoal + * + * Description: [What this configuration controls and its main purpose] + * + * Important Notes: [Any critical information about changes or compatibility] + */ +``` + +## Version Control Guidelines + +### Commit Message Standards + +Follow conventional commit format: + +- **feat**: New features or functionality +- **fix**: Bug fixes and corrections +- **docs**: Documentation updates (README, comments, etc.) +- **style**: Code formatting and style changes +- **refactor**: Code restructuring without functionality changes +- **test**: Adding or updating tests +- **chore**: Maintenance tasks, dependency updates + +Examples: + +- `feat: add SAP connection configuration panel` +- `fix: resolve memory leak in VBS process management` +- `docs: update README with new installation requirements` + +# Code Documentation Requirements + +## File Headers + +Every source code file should include a comprehensive header: + +### For TypeScript/JavaScript files: + +```javascript +/** + * [File Purpose/Component Name] + * Author: Dario Pascoal + * + * Description: [Detailed explanation of what this file does, its main purpose, + * and how it fits into the overall system. Explain it as if teaching someone + * who is new to programming.] + */ +``` + +### For HTML/CSS files: + +```css +/** + * [File Purpose/Component Name] + * Author: Dario Pascoal + * + * Description: [Detailed explanation of what this file does, its main purpose, + * and how it fits into the overall system. Explain it as if teaching someone + * who is new to programming.] + */ +``` + +### For VBScript files (.vbs): + +```vb +' [File Purpose/Component Name] +' Author: Dario Pascoal +' +' Description: [Detailed explanation of what this file does, its main purpose, +' and how it fits into the overall system. Explain it as if teaching someone +' who is new to programming.] +' +' [Additional sections as needed: Prerequisites, Parameters, Returns, etc.] +``` + +### For PowerShell files (.ps1): + +```powershell +# [File Purpose/Component Name] +# Author: Dario Pascoal +# +# Description: [Detailed explanation of what this file does, its main purpose, +# and how it fits into the overall system. Explain it as if teaching someone +# who is new to programming.] +# +# [Additional sections as needed: Prerequisites, Parameters, Returns, etc.] +``` + +### CRITICAL: Language-Specific Comment Formatting + +**ALWAYS use the correct comment syntax for each programming language:** + +- **JavaScript/TypeScript**: Use `/** */` for file headers and `/* */` or `//` for inline comments +- **VBScript (.vbs)**: Use single quotes `'` for ALL comments - NEVER use `/** */` or `/* */` +- **PowerShell (.ps1)**: Use hash symbol `#` for ALL comments +- **HTML/CSS**: Use `/* */` for comments +- **Python**: Use `#` for comments and `"""` for docstrings +- **Batch files (.bat/.cmd)**: Use `REM` or `::` for comments + +**This is mandatory** - using incorrect comment syntax will cause syntax errors and prevent scripts from executing properly. + +## Function Documentation + +Document functions with comprehensive JSDoc comments that explain both what and how: + +```javascript +/** + * [Clear description of what the function does and why it exists] + * + * [Detailed explanation of how the function works, step by step, + * written for someone learning to code] + * + * @param {Type} paramName - Detailed explanation of what this parameter is, + * what format it should be in, and how it's used + * @returns {Type} Detailed explanation of what gets returned and when + */ +``` + +## Essential Comments - Write for Beginners + +Focus comments on explaining code as if teaching someone new to programming: + +- **Business logic**: Explain WHY certain decisions were made and WHAT the business requirement is +- **Complex algorithms**: Step-by-step explanation of HOW the code works +- **Integration points**: Explain HOW code connects to external systems and WHAT data flows between them +- **Non-obvious code**: Explain WHAT isn't immediately clear and WHY it works that way +- **Workarounds**: Explain WHAT the problem was and HOW this solution addresses it +- **Data structures**: Explain WHAT kind of data is stored and HOW it's organized +- **Control flow**: Explain WHAT conditions trigger different code paths and WHY + +## Detailed Comment Guidelines + +### Inline Comments - Explain the "What" and "Why" + +Write comments that help a beginner understand: + +- What each significant line or block of code is doing +- Why certain approaches were chosen +- What the expected input/output is at each step +- How different parts of the code work together +- What would happen if certain conditions are met + +Example: + +```javascript +// Check if the user has permission to access this feature +// This prevents unauthorized users from seeing sensitive data +if (user.hasPermission("admin")) { + // Load the admin dashboard with all management tools + // This includes user management, system settings, and reports + loadAdminDashboard(); +} else { + // Show a basic user dashboard with limited functionality + // Regular users only see their own data and basic features + loadUserDashboard(); +} +``` + +### Complex Logic Comments + +For any complex business logic, algorithms, or multi-step processes: + +```javascript +/** + * PROCESS EXPLANATION: + * + * This function handles the complete user login workflow: + * 1. First, it validates the username and password format + * 2. Then it checks the credentials against the database + * 3. If successful, it creates a secure session token + * 4. Finally, it redirects the user to their appropriate dashboard + * + * The reason we do this in multiple steps is to provide better + * error messages to the user and to log security events properly. + */ +``` + +## Comment Quality Standards + +- **Be educational**: Write as if teaching a programming student +- **Explain assumptions**: State what the code assumes about inputs/environment +- **Describe data flow**: Explain what data goes in and what comes out +- **Clarify complex conditions**: Break down complicated if/else logic +- **Document error cases**: Explain what can go wrong and how it's handled +- **Use plain language**: Avoid jargon, explain technical terms when used +- **Provide context**: Explain how this code fits into the bigger picture# README Maintenance Requirements + +## MANDATORY: README Updates + +The project README.md file MUST be updated whenever making significant changes to the codebase. This ensures the documentation stays synchronized with the actual implementation. + +### When to Update README: + +**ALWAYS update the README when:** + +- Adding new features or functionality +- Changing installation or setup procedures +- Modifying configuration requirements +- Adding new dependencies or technologies +- Changing project structure or architecture +- Adding new scripts or build processes +- Modifying environment variables or configuration files +- Updating system requirements or compatibility +- Adding new command-line interfaces or APIs +- Changing deployment procedures + +### README Sections to Maintain: + +1. **Project Description**: Keep the main purpose and features up-to-date +2. **Technology Stack**: Update when adding new technologies or frameworks +3. **Installation Instructions**: Verify and update setup steps +4. **Configuration**: Document new settings, environment variables, or config files +5. **Usage Examples**: Add examples for new features +6. **API Documentation**: Update when adding new endpoints or methods +7. **Development Setup**: Keep development environment instructions current +8. **Build and Deployment**: Update build scripts and deployment procedures +9. **Troubleshooting**: Add common issues and solutions +10. **Contributing Guidelines**: Update development and contribution processes + +### README Update Standards: + +- **Be Comprehensive**: Include all necessary information for new team members +- **Keep Examples Current**: Ensure all code examples work with the current version +- **Update Screenshots**: Replace outdated UI screenshots when interface changes +- **Maintain Accuracy**: Verify all instructions work on a clean environment +- **Version Information**: Update version numbers and compatibility information +- **Link Validation**: Ensure all links are current and functional + +### README Quality Checklist: + +Before committing code changes, verify: + +- [ ] README reflects all new features and changes +- [ ] Installation instructions are accurate and complete +- [ ] All code examples are tested and working +- [ ] Configuration documentation is up-to-date +- [ ] System requirements are current +- [ ] Links and references are valid +- [ ] Screenshots and diagrams reflect current state +- [ ] Troubleshooting section addresses known issues + +### Automatic README Triggers: + +**High Priority Updates** - Always update README for: + +- New major features or modules +- Changes to package.json dependencies +- Environment variable additions/changes +- New configuration files or formats +- Changes to build/deployment processes +- New user-facing functionality + +**Medium Priority Updates** - Consider updating README for: + +- Internal architecture changes that affect setup +- New development tools or workflows +- Performance improvements with user impact +- Security enhancements with configuration changes + +The README should serve as the single source of truth for project onboarding, setup, and usage. Keep it comprehensive, accurate, and user-friendly. diff --git a/video_editor (3).py b/video_editor (3).py new file mode 100644 index 0000000..1a07355 --- /dev/null +++ b/video_editor (3).py @@ -0,0 +1,2165 @@ +""" +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() diff --git a/video_editor.py b/video_editor.py index 30d92de..1a774cc 100644 --- a/video_editor.py +++ b/video_editor.py @@ -1,6 +1,33 @@ """ Professional Video Editor for Generated Shorts -Standalone application for editing video clips with timeline controls and video synchronization +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 @@ -35,196 +62,336 @@ except ImportError: raise ImportError("MoviePy not available") class ShortsEditorGUI: - """Professional video editing interface with timeline controls and real-time preview""" + """ + 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 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 + # 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 state - self.timeline_position = 0.0 - self.timeline_scale = 1.0 # Pixels per second - self.timeline_width = 800 + # 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 features - self.timeline_clips = [] - self.selected_clip = None - self.markers = [] + # 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 + # 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_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_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'} + # 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 - 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 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 - self.edit_mode = 'select' # 'select', 'cut', 'trim', 'ripple' + # 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 enhancements - self.show_thumbnails = True - self.show_waveforms = True - self.clip_thumbnails = {} - self.audio_waveforms = {} + # 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 - # Track widgets for UI - self.track_widgets = {} + # 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 - # Modern color scheme + # 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', - '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' + '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 } - # Modern fonts + # 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'), - 'heading': ('Segoe UI', 11, 'bold'), - 'body': ('Segoe UI', 10), - 'caption': ('Segoe UI', 9), - 'button': ('Segoe UI', 10, 'bold') + '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 video editor window""" - # Create editor window + """ + 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") - self.editor_window.minsize(900, 600) - self.editor_window.configure(bg=self.colors['bg_primary']) + self.editor_window.geometry("1200x900") # Increased height for Audio 2 visibility + self.editor_window.minsize(900, 700) # Increased minimum size + self.editor_window.configure(bg=self.colors['bg_primary']) # Apply dark theme - # Make window responsive + # 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) - # Create interface + # Build the complete editor interface self.create_editor_interface() - # Start the editor + # 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 main editor interface""" - # Header with file selection + """ + 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) + header_frame.pack_propagate(False) # Maintain fixed height - # Title + # 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 + # 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 + # 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) - main_frame.columnconfigure(0, weight=2) - main_frame.columnconfigure(1, weight=1) + 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 player and timeline + # 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) - player_frame.rowconfigure(1, weight=0) - player_frame.columnconfigure(0, weight=1) + 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 + # 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_container.columnconfigure(0, weight=1) + video_container.rowconfigure(0, weight=1) # Video canvas expands to fill space + video_container.columnconfigure(0, weight=1) # Full width utilization - # Video canvas + # 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 - timeline_workspace = tk.Frame(player_frame, bg=self.colors['bg_secondary'], height=380) + # 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=428) timeline_workspace.grid(row=1, column=0, sticky="ew", padx=15, pady=(0, 15)) - timeline_workspace.pack_propagate(False) + timeline_workspace.pack_propagate(False) # Maintain fixed height for consistent layout - # Timeline header with editing tools + # 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)) + header_frame.pack(fill="x", pady=(5, 5)) - # Left side - Timeline controls + # 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 + # 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) - # Snap and magnetic timeline toggles + # 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 - Playback controls + # 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', @@ -232,6 +399,7 @@ class ShortsEditorGUI: 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', @@ -239,6 +407,7 @@ class ShortsEditorGUI: 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', @@ -281,19 +450,24 @@ class ShortsEditorGUI: canvas_frame = tk.Frame(timeline_container, bg=self.colors['bg_tertiary']) canvas_frame.pack(side="right", fill="both", expand=True) - # Create canvas with scrollbars + # Create canvas without internal 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 canvas to fill the frame completely + self.timeline_canvas.pack(fill="both", expand=True) - # 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) + # Create external horizontal scrollbar for timeline control + # Place it outside the timeline in the workspace area + external_scrollbar_frame = tk.Frame(timeline_workspace, bg=self.colors['bg_primary'], height=20) + external_scrollbar_frame.pack(side="bottom", fill="x", padx=5, pady=2) + external_scrollbar_frame.pack_propagate(False) + + # Horizontal scrollbar outside timeline but controlling it + self.external_h_scrollbar = ttk.Scrollbar(external_scrollbar_frame, orient="horizontal", + command=self.timeline_canvas.xview) + self.timeline_canvas.configure(xscrollcommand=self.external_h_scrollbar.set) + self.external_h_scrollbar.pack(fill="x", expand=True) # Bind professional timeline events self.timeline_canvas.bind("", self.timeline_click) @@ -440,13 +614,13 @@ class ShortsEditorGUI: # 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" + fill=self.colors['border'], width=3, 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" + fill=track_info['color'], width=1, tags="road_lines" ) # Center dashed line for alignment reference @@ -465,11 +639,9 @@ class ShortsEditorGUI: 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) + """Vertical scrolling disabled - no scrollbars for up/down movement""" + # Vertical scrolling removed for cleaner interface + pass def create_track_control(self, track_id, track_info): """Create control panel for a single track positioned on road lines"""