Add Professional Video Editor with timeline controls and real-time preview
- Implemented a standalone video editor for editing generated shorts. - Integrated OpenCV for basic video playback and MoviePy for advanced editing features. - Added functionalities including video trimming, speed control, volume adjustment, fade effects, and text overlays. - Created a modern GUI using Tkinter with responsive design and a professional color scheme. - Included detailed README documentation outlining features, usage, and installation requirements.
This commit is contained in:
parent
caf64b9815
commit
809e768cae
4
Main.py
4
Main.py
@ -1066,8 +1066,8 @@ class MainApplication:
|
||||
def open_editor(self):
|
||||
"""Open the shorts editor"""
|
||||
try:
|
||||
# Import and create the editor directly
|
||||
from shorts_generator2 import ShortsEditorGUI
|
||||
# Import and create the editor from the new video_editor.py file
|
||||
from video_editor import ShortsEditorGUI
|
||||
|
||||
# Get the output folder from generator if available, otherwise use default
|
||||
generator = self.get_shorts_generator()
|
||||
|
||||
85
VIDEO_EDITOR_README.md
Normal file
85
VIDEO_EDITOR_README.md
Normal file
@ -0,0 +1,85 @@
|
||||
# Professional Video Editor
|
||||
|
||||
A standalone video editor with timeline controls and real-time preview, designed specifically for editing generated shorts.
|
||||
|
||||
## Features
|
||||
|
||||
### ✅ Working Features (OpenCV Backend)
|
||||
- **🎬 Video Playback**: Load and play video files with timeline controls
|
||||
- **📺 Real-time Preview**: Professional video player with frame-by-frame seeking
|
||||
- **⏯️ Timeline Controls**: Play, Pause, Stop buttons with synchronized video playback
|
||||
- **🕒 Time Display**: Current time and total duration with precise seeking
|
||||
- **📊 Interactive Timeline**: Click and drag to seek to specific time positions
|
||||
- **🎯 Frame-accurate Seeking**: Navigate to exact frames using the timeline
|
||||
|
||||
### 🔧 Advanced Features (Requires MoviePy)
|
||||
- **✂️ Video Trimming**: Cut videos to specific time ranges
|
||||
- **⚡ Speed Control**: Adjust playback speed (0.25x to 3.0x)
|
||||
- **🔊 Volume Adjustment**: Control audio levels (0x to 2.0x)
|
||||
- **🌅 Fade Effects**: Add professional fade in/out transitions
|
||||
- **📝 Text Overlays**: Add custom text with positioning
|
||||
- **💾 Video Export**: Save edited videos in MP4 format
|
||||
|
||||
## Usage
|
||||
|
||||
### From Main Application
|
||||
1. Run `python Main.py`
|
||||
2. Click "✏️ Edit Generated Shorts" button
|
||||
3. Select a video from your shorts folder or browse for any video file
|
||||
|
||||
### Standalone Mode
|
||||
1. Run `python video_editor.py`
|
||||
2. The editor will open directly
|
||||
|
||||
## Installation Requirements
|
||||
|
||||
### Basic Functionality (OpenCV)
|
||||
```bash
|
||||
pip install opencv-python pillow
|
||||
```
|
||||
|
||||
### Full Functionality (MoviePy)
|
||||
```bash
|
||||
pip install moviepy opencv-python pillow
|
||||
```
|
||||
|
||||
## How to Use
|
||||
|
||||
1. **Load Video**: Click "📁 Select Video" to choose a video file
|
||||
2. **Navigate**: Use timeline controls (Play/Pause/Stop) or click on timeline to seek
|
||||
3. **Edit** (if MoviePy available):
|
||||
- Adjust trim start/end times and click "✂️ Apply Trim"
|
||||
- Change speed with slider and click "⚡ Apply Speed"
|
||||
- Adjust volume and click "🔊 Apply Volume"
|
||||
- Add text overlay and click "📝 Add Text"
|
||||
- Apply fade effects with "🌅 Add Fade In/Out"
|
||||
4. **Export**: Click "💾 Export Video" to save your changes
|
||||
|
||||
## Timeline Controls
|
||||
|
||||
- **▶️ Play**: Start video playback and timeline animation
|
||||
- **⏸️ Pause**: Pause both video and timeline
|
||||
- **⏹️ Stop**: Stop and return to beginning
|
||||
- **Timeline Click**: Seek to specific time position
|
||||
- **Time Display**: Shows current time / total duration
|
||||
|
||||
## Notes
|
||||
|
||||
- The editor automatically detects available libraries and adjusts functionality
|
||||
- Without MoviePy, you get a professional video player with timeline controls
|
||||
- With MoviePy, you get full editing capabilities
|
||||
- All timeline controls are synchronized with actual video playback
|
||||
- The interface is responsive and works with different window sizes
|
||||
|
||||
## File Support
|
||||
|
||||
Supports common video formats: MP4, AVI, MOV, MKV, WMV, FLV, WEBM
|
||||
|
||||
## Architecture
|
||||
|
||||
The editor is built with:
|
||||
- **Tkinter**: Modern GUI with professional styling
|
||||
- **OpenCV**: Video loading and frame display (always available)
|
||||
- **MoviePy**: Advanced video editing features (optional)
|
||||
- **PIL**: Image processing and display
|
||||
- **Threading**: Non-blocking video playback and timeline updates
|
||||
2522
shorts_generator2.py
2522
shorts_generator2.py
File diff suppressed because it is too large
Load Diff
999
video_editor.py
Normal file
999
video_editor.py
Normal file
@ -0,0 +1,999 @@
|
||||
"""
|
||||
Professional Video Editor for Generated Shorts
|
||||
Standalone application for editing video clips with timeline controls and video synchronization
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
import cv2
|
||||
import numpy as np
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
# Try to import MoviePy, handle if not available
|
||||
try:
|
||||
from moviepy.editor import VideoFileClip, TextClip, CompositeVideoClip
|
||||
from moviepy.video.fx import FadeIn, FadeOut, Resize
|
||||
from moviepy.audio.fx import MultiplyVolume
|
||||
MOVIEPY_AVAILABLE = True
|
||||
except ImportError:
|
||||
print("⚠️ MoviePy not available - using OpenCV backend for video processing")
|
||||
MOVIEPY_AVAILABLE = False
|
||||
|
||||
# Create dummy classes for compatibility
|
||||
class VideoFileClip:
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise ImportError("MoviePy not available")
|
||||
class TextClip:
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise ImportError("MoviePy not available")
|
||||
class CompositeVideoClip:
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise ImportError("MoviePy not available")
|
||||
|
||||
class ShortsEditorGUI:
|
||||
"""Professional video editing interface with timeline controls and real-time preview"""
|
||||
|
||||
def __init__(self, parent=None, shorts_folder="shorts"):
|
||||
self.parent = parent
|
||||
self.shorts_folder = shorts_folder
|
||||
|
||||
# Video state
|
||||
self.current_video = None
|
||||
self.current_clip = None
|
||||
self.current_time = 0.0
|
||||
self.video_duration = 0.0
|
||||
self.is_playing = False
|
||||
self.timeline_is_playing = False
|
||||
self.play_thread = None
|
||||
|
||||
# Timeline state
|
||||
self.timeline_position = 0.0
|
||||
self.timeline_scale = 1.0 # Pixels per second
|
||||
self.timeline_width = 800
|
||||
|
||||
# Modern color scheme
|
||||
self.colors = {
|
||||
'bg_primary': '#1a1a1a',
|
||||
'bg_secondary': '#2d2d2d',
|
||||
'bg_tertiary': '#3d3d3d',
|
||||
'text_primary': '#ffffff',
|
||||
'text_secondary': '#b8b8b8',
|
||||
'accent_blue': '#007acc',
|
||||
'accent_green': '#28a745',
|
||||
'accent_orange': '#fd7e14',
|
||||
'accent_red': '#dc3545',
|
||||
'border': '#404040'
|
||||
}
|
||||
|
||||
# Modern fonts
|
||||
self.fonts = {
|
||||
'title': ('Segoe UI', 16, 'bold'),
|
||||
'heading': ('Segoe UI', 11, 'bold'),
|
||||
'body': ('Segoe UI', 10),
|
||||
'caption': ('Segoe UI', 9),
|
||||
'button': ('Segoe UI', 10, 'bold')
|
||||
}
|
||||
|
||||
def open_editor(self):
|
||||
"""Open the video editor window"""
|
||||
# Create editor window
|
||||
self.editor_window = tk.Toplevel(self.parent) if self.parent else tk.Tk()
|
||||
self.editor_window.title("Professional Shorts Editor")
|
||||
self.editor_window.geometry("1200x800")
|
||||
self.editor_window.minsize(900, 600)
|
||||
self.editor_window.configure(bg=self.colors['bg_primary'])
|
||||
|
||||
# Make window responsive
|
||||
self.editor_window.rowconfigure(1, weight=1)
|
||||
self.editor_window.columnconfigure(0, weight=1)
|
||||
|
||||
# Create interface
|
||||
self.create_editor_interface()
|
||||
|
||||
# Start the editor
|
||||
if not self.parent:
|
||||
self.editor_window.mainloop()
|
||||
|
||||
def create_editor_interface(self):
|
||||
"""Create the main editor interface"""
|
||||
# Header with file selection
|
||||
header_frame = tk.Frame(self.editor_window, bg=self.colors['bg_secondary'], height=60)
|
||||
header_frame.pack(fill="x", padx=10, pady=(10, 0))
|
||||
header_frame.pack_propagate(False)
|
||||
|
||||
# Title
|
||||
title_label = tk.Label(header_frame, text="✏️ Professional Shorts Editor",
|
||||
font=self.fonts['title'], bg=self.colors['bg_secondary'],
|
||||
fg=self.colors['text_primary'])
|
||||
title_label.pack(side="left", padx=20, pady=15)
|
||||
|
||||
# File selection
|
||||
file_frame = tk.Frame(header_frame, bg=self.colors['bg_secondary'])
|
||||
file_frame.pack(side="right", padx=20, pady=15)
|
||||
|
||||
self.current_file_label = tk.Label(file_frame, text="No video selected",
|
||||
font=self.fonts['body'], bg=self.colors['bg_tertiary'],
|
||||
fg=self.colors['text_secondary'], padx=15, pady=8)
|
||||
self.current_file_label.pack(side="left", padx=(0, 10))
|
||||
|
||||
select_btn = tk.Button(file_frame, text="📁 Select Video",
|
||||
command=self.select_video_file, font=self.fonts['button'],
|
||||
bg=self.colors['accent_blue'], fg='white', padx=20, pady=8,
|
||||
relief="flat", bd=0, cursor="hand2")
|
||||
select_btn.pack(side="left")
|
||||
|
||||
# Main content area
|
||||
main_frame = tk.Frame(self.editor_window, bg=self.colors['bg_primary'])
|
||||
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
main_frame.rowconfigure(0, weight=1)
|
||||
main_frame.columnconfigure(0, weight=2)
|
||||
main_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# Left panel - Video player and timeline
|
||||
player_frame = tk.Frame(main_frame, bg=self.colors['bg_secondary'])
|
||||
player_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 5))
|
||||
player_frame.rowconfigure(0, weight=1)
|
||||
player_frame.rowconfigure(1, weight=0)
|
||||
player_frame.columnconfigure(0, weight=1)
|
||||
|
||||
# Video display area
|
||||
video_container = tk.Frame(player_frame, bg=self.colors['bg_tertiary'])
|
||||
video_container.grid(row=0, column=0, sticky="nsew", padx=15, pady=15)
|
||||
video_container.rowconfigure(0, weight=1)
|
||||
video_container.columnconfigure(0, weight=1)
|
||||
|
||||
# Video canvas
|
||||
self.video_canvas = tk.Canvas(video_container, bg='black', highlightthickness=0)
|
||||
self.video_canvas.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
# Timeline workspace
|
||||
timeline_workspace = tk.Frame(player_frame, bg=self.colors['bg_secondary'], height=200)
|
||||
timeline_workspace.grid(row=1, column=0, sticky="ew", padx=15, pady=(0, 15))
|
||||
timeline_workspace.pack_propagate(False)
|
||||
|
||||
# Timeline Controls
|
||||
controls_frame = tk.Frame(timeline_workspace, bg=self.colors['bg_secondary'])
|
||||
controls_frame.pack(fill="x", pady=(10, 0))
|
||||
|
||||
# Timeline control buttons
|
||||
btn_frame = tk.Frame(controls_frame, bg=self.colors['bg_secondary'])
|
||||
btn_frame.pack(side="left")
|
||||
|
||||
self.timeline_play_btn = tk.Button(btn_frame, text="▶️ Play",
|
||||
command=self.timeline_play,
|
||||
bg=self.colors['accent_green'], fg='white',
|
||||
font=self.fonts['button'], padx=15, pady=5,
|
||||
relief="flat", bd=0, cursor="hand2")
|
||||
self.timeline_play_btn.pack(side="left", padx=(0, 5))
|
||||
|
||||
self.timeline_pause_btn = tk.Button(btn_frame, text="⏸️ Pause",
|
||||
command=self.timeline_pause,
|
||||
bg=self.colors['accent_orange'], fg='white',
|
||||
font=self.fonts['button'], padx=15, pady=5,
|
||||
relief="flat", bd=0, cursor="hand2")
|
||||
self.timeline_pause_btn.pack(side="left", padx=5)
|
||||
|
||||
self.timeline_stop_btn = tk.Button(btn_frame, text="⏹️ Stop",
|
||||
command=self.timeline_stop,
|
||||
bg=self.colors['accent_red'], fg='white',
|
||||
font=self.fonts['button'], padx=15, pady=5,
|
||||
relief="flat", bd=0, cursor="hand2")
|
||||
self.timeline_stop_btn.pack(side="left", padx=5)
|
||||
|
||||
# Time display
|
||||
self.time_display = tk.Label(controls_frame, text="00:00 / 00:00",
|
||||
font=self.fonts['body'], bg=self.colors['bg_secondary'],
|
||||
fg=self.colors['text_primary'])
|
||||
self.time_display.pack(side="right", padx=20)
|
||||
|
||||
# Timeline canvas
|
||||
timeline_frame = tk.Frame(timeline_workspace, bg=self.colors['bg_tertiary'])
|
||||
timeline_frame.pack(fill="both", expand=True, pady=10)
|
||||
|
||||
self.timeline_canvas = tk.Canvas(timeline_frame, bg=self.colors['bg_tertiary'],
|
||||
height=120, highlightthickness=1,
|
||||
highlightbackground=self.colors['border'])
|
||||
self.timeline_canvas.pack(side="left", fill="both", expand=True)
|
||||
|
||||
# Bind timeline events
|
||||
self.timeline_canvas.bind("<Button-1>", self.timeline_click)
|
||||
self.timeline_canvas.bind("<B1-Motion>", self.timeline_drag)
|
||||
|
||||
# Right panel - Tools and effects
|
||||
tools_frame = tk.Frame(main_frame, bg=self.colors['bg_secondary'])
|
||||
tools_frame.grid(row=0, column=1, sticky="nsew", padx=(5, 0))
|
||||
|
||||
# Tools header
|
||||
tools_header = tk.Label(tools_frame, text="🛠️ Editing Tools",
|
||||
font=self.fonts['heading'], bg=self.colors['bg_secondary'],
|
||||
fg=self.colors['text_primary'])
|
||||
tools_header.pack(pady=(15, 10))
|
||||
|
||||
# Create tool sections
|
||||
self.create_basic_tools(tools_frame)
|
||||
self.create_effects_tools(tools_frame)
|
||||
self.create_export_tools(tools_frame)
|
||||
|
||||
# Initialize timeline
|
||||
self.update_timeline()
|
||||
|
||||
def create_basic_tools(self, parent):
|
||||
"""Create basic editing tools"""
|
||||
basic_frame = tk.LabelFrame(parent, text="Basic Editing", font=self.fonts['heading'],
|
||||
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
|
||||
relief="flat", bd=1)
|
||||
basic_frame.pack(fill="x", padx=15, pady=5)
|
||||
|
||||
# Trim controls
|
||||
trim_frame = tk.Frame(basic_frame, bg=self.colors['bg_secondary'])
|
||||
trim_frame.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
tk.Label(trim_frame, text="Trim Video:", font=self.fonts['body'],
|
||||
bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).pack(anchor="w")
|
||||
|
||||
trim_controls = tk.Frame(trim_frame, bg=self.colors['bg_secondary'])
|
||||
trim_controls.pack(fill="x", pady=5)
|
||||
|
||||
tk.Label(trim_controls, text="Start:", font=self.fonts['caption'],
|
||||
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(side="left")
|
||||
|
||||
self.trim_start_var = tk.DoubleVar(value=0.0)
|
||||
trim_start_spin = tk.Spinbox(trim_controls, from_=0, to=999, increment=0.1,
|
||||
textvariable=self.trim_start_var, width=8,
|
||||
font=self.fonts['caption'])
|
||||
trim_start_spin.pack(side="left", padx=5)
|
||||
|
||||
tk.Label(trim_controls, text="End:", font=self.fonts['caption'],
|
||||
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(side="left", padx=(10, 0))
|
||||
|
||||
self.trim_end_var = tk.DoubleVar(value=10.0)
|
||||
trim_end_spin = tk.Spinbox(trim_controls, from_=0, to=999, increment=0.1,
|
||||
textvariable=self.trim_end_var, width=8,
|
||||
font=self.fonts['caption'])
|
||||
trim_end_spin.pack(side="left", padx=5)
|
||||
|
||||
trim_btn = tk.Button(basic_frame, text="✂️ Apply Trim", command=self.apply_trim,
|
||||
bg=self.colors['accent_blue'], fg='white', font=self.fonts['button'],
|
||||
relief="flat", bd=0, cursor="hand2")
|
||||
trim_btn.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
# Speed controls
|
||||
speed_frame = tk.Frame(basic_frame, bg=self.colors['bg_secondary'])
|
||||
speed_frame.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
tk.Label(speed_frame, text="Speed:", font=self.fonts['body'],
|
||||
bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).pack(anchor="w")
|
||||
|
||||
self.speed_var = tk.DoubleVar(value=1.0)
|
||||
speed_scale = tk.Scale(speed_frame, from_=0.25, to=3.0, resolution=0.25,
|
||||
orient="horizontal", variable=self.speed_var,
|
||||
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
|
||||
highlightthickness=0, troughcolor=self.colors['bg_tertiary'])
|
||||
speed_scale.pack(fill="x", pady=5)
|
||||
|
||||
speed_btn = tk.Button(basic_frame, text="⚡ Apply Speed", command=self.apply_speed,
|
||||
bg=self.colors['accent_green'], fg='white', font=self.fonts['button'],
|
||||
relief="flat", bd=0, cursor="hand2")
|
||||
speed_btn.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
# Volume controls
|
||||
volume_frame = tk.Frame(basic_frame, bg=self.colors['bg_secondary'])
|
||||
volume_frame.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
tk.Label(volume_frame, text="Volume:", font=self.fonts['body'],
|
||||
bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).pack(anchor="w")
|
||||
|
||||
self.volume_var = tk.DoubleVar(value=1.0)
|
||||
volume_scale = tk.Scale(volume_frame, from_=0.0, to=2.0, resolution=0.1,
|
||||
orient="horizontal", variable=self.volume_var,
|
||||
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
|
||||
highlightthickness=0, troughcolor=self.colors['bg_tertiary'])
|
||||
volume_scale.pack(fill="x", pady=5)
|
||||
|
||||
volume_btn = tk.Button(basic_frame, text="🔊 Apply Volume", command=self.apply_volume,
|
||||
bg=self.colors['accent_orange'], fg='white', font=self.fonts['button'],
|
||||
relief="flat", bd=0, cursor="hand2")
|
||||
volume_btn.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
def create_effects_tools(self, parent):
|
||||
"""Create effects tools"""
|
||||
effects_frame = tk.LabelFrame(parent, text="Effects", font=self.fonts['heading'],
|
||||
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
|
||||
relief="flat", bd=1)
|
||||
effects_frame.pack(fill="x", padx=15, pady=5)
|
||||
|
||||
# Fade effects
|
||||
fade_btn = tk.Button(effects_frame, text="🌅 Add Fade In/Out", command=self.apply_fade,
|
||||
bg=self.colors['accent_blue'], fg='white', font=self.fonts['button'],
|
||||
relief="flat", bd=0, cursor="hand2")
|
||||
fade_btn.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
# Text overlay
|
||||
text_frame = tk.Frame(effects_frame, bg=self.colors['bg_secondary'])
|
||||
text_frame.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
tk.Label(text_frame, text="Text Overlay:", font=self.fonts['body'],
|
||||
bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).pack(anchor="w")
|
||||
|
||||
self.text_var = tk.StringVar(value="Sample Text")
|
||||
text_entry = tk.Entry(text_frame, textvariable=self.text_var, font=self.fonts['body'])
|
||||
text_entry.pack(fill="x", pady=5)
|
||||
|
||||
text_btn = tk.Button(effects_frame, text="📝 Add Text", command=self.apply_text,
|
||||
bg=self.colors['accent_green'], fg='white', font=self.fonts['button'],
|
||||
relief="flat", bd=0, cursor="hand2")
|
||||
text_btn.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
def create_export_tools(self, parent):
|
||||
"""Create export tools"""
|
||||
export_frame = tk.LabelFrame(parent, text="Export", font=self.fonts['heading'],
|
||||
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
|
||||
relief="flat", bd=1)
|
||||
export_frame.pack(fill="x", padx=15, pady=5)
|
||||
|
||||
# Reset button
|
||||
reset_btn = tk.Button(export_frame, text="🔄 Reset", command=self.reset_video,
|
||||
bg=self.colors['accent_red'], fg='white', font=self.fonts['button'],
|
||||
relief="flat", bd=0, cursor="hand2")
|
||||
reset_btn.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
# Export button
|
||||
export_btn = tk.Button(export_frame, text="💾 Export Video", command=self.export_video,
|
||||
bg=self.colors['accent_green'], fg='white', font=self.fonts['button'],
|
||||
relief="flat", bd=0, cursor="hand2")
|
||||
export_btn.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
def select_video_file(self):
|
||||
"""Select a video file to edit"""
|
||||
# Check for videos in shorts folder first
|
||||
if os.path.exists(self.shorts_folder):
|
||||
video_files = [f for f in os.listdir(self.shorts_folder)
|
||||
if f.lower().endswith(('.mp4', '.avi', '.mov', '.mkv'))]
|
||||
|
||||
if video_files:
|
||||
# Show selection dialog for shorts
|
||||
choice_window = tk.Toplevel(self.editor_window)
|
||||
choice_window.title("Select Video to Edit")
|
||||
choice_window.geometry("400x300")
|
||||
choice_window.configure(bg=self.colors['bg_primary'])
|
||||
choice_window.transient(self.editor_window)
|
||||
choice_window.grab_set()
|
||||
|
||||
tk.Label(choice_window, text="Select a video to edit:",
|
||||
font=self.fonts['heading'], bg=self.colors['bg_primary'],
|
||||
fg=self.colors['text_primary']).pack(pady=10)
|
||||
|
||||
selected_file = None
|
||||
|
||||
def select_file(filename):
|
||||
nonlocal selected_file
|
||||
selected_file = os.path.join(self.shorts_folder, filename)
|
||||
choice_window.destroy()
|
||||
|
||||
# List videos
|
||||
for video_file in video_files:
|
||||
btn = tk.Button(choice_window, text=f"📹 {video_file}",
|
||||
command=lambda f=video_file: select_file(f),
|
||||
bg=self.colors['accent_blue'], fg='white',
|
||||
font=self.fonts['button'], relief="flat", bd=0,
|
||||
cursor="hand2")
|
||||
btn.pack(fill="x", padx=20, pady=2)
|
||||
|
||||
# Browse button
|
||||
browse_btn = tk.Button(choice_window, text="📁 Browse Other Files",
|
||||
command=lambda: self.browse_video_file(choice_window),
|
||||
bg=self.colors['accent_orange'], fg='white',
|
||||
font=self.fonts['button'], relief="flat", bd=0,
|
||||
cursor="hand2")
|
||||
browse_btn.pack(fill="x", padx=20, pady=10)
|
||||
|
||||
choice_window.wait_window()
|
||||
|
||||
if selected_file:
|
||||
self.load_video(selected_file)
|
||||
else:
|
||||
self.browse_video_file()
|
||||
else:
|
||||
self.browse_video_file()
|
||||
|
||||
def browse_video_file(self, parent_window=None):
|
||||
"""Browse for video file"""
|
||||
filetypes = [
|
||||
("Video files", "*.mp4 *.avi *.mov *.mkv *.wmv *.flv *.webm"),
|
||||
("All files", "*.*")
|
||||
]
|
||||
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="Select Video File",
|
||||
filetypes=filetypes,
|
||||
parent=parent_window or self.editor_window
|
||||
)
|
||||
|
||||
if file_path:
|
||||
if parent_window:
|
||||
parent_window.destroy()
|
||||
self.load_video(file_path)
|
||||
|
||||
def load_video(self, video_path):
|
||||
"""Load a video for editing"""
|
||||
try:
|
||||
# Clean up previous video
|
||||
if hasattr(self, 'current_clip') and self.current_clip:
|
||||
if MOVIEPY_AVAILABLE:
|
||||
self.current_clip.close()
|
||||
else:
|
||||
if hasattr(self.current_clip, 'release'):
|
||||
self.current_clip.release()
|
||||
|
||||
# Load new video
|
||||
self.current_video = video_path
|
||||
|
||||
if MOVIEPY_AVAILABLE:
|
||||
# Use MoviePy for full functionality
|
||||
self.current_clip = VideoFileClip(video_path)
|
||||
self.video_duration = self.current_clip.duration
|
||||
self.current_time = 0.0
|
||||
|
||||
# Display first frame
|
||||
self.display_frame_at_time(0.0)
|
||||
else:
|
||||
# Use OpenCV for basic functionality
|
||||
self.current_clip = cv2.VideoCapture(video_path)
|
||||
if not self.current_clip.isOpened():
|
||||
raise Exception("Could not open video file")
|
||||
|
||||
# Get video properties
|
||||
fps = self.current_clip.get(cv2.CAP_PROP_FPS)
|
||||
frame_count = self.current_clip.get(cv2.CAP_PROP_FRAME_COUNT)
|
||||
self.video_duration = frame_count / fps if fps > 0 else 0
|
||||
self.current_time = 0.0
|
||||
|
||||
# Display first frame
|
||||
self.display_frame_at_time_opencv(0.0)
|
||||
|
||||
# Update UI
|
||||
filename = os.path.basename(video_path)
|
||||
self.current_file_label.config(text=filename)
|
||||
|
||||
# Update trim controls
|
||||
self.trim_start_var.set(0.0)
|
||||
self.trim_end_var.set(self.video_duration)
|
||||
|
||||
# Update timeline
|
||||
self.update_timeline()
|
||||
self.update_time_display()
|
||||
|
||||
backend = "MoviePy" if MOVIEPY_AVAILABLE else "OpenCV"
|
||||
print(f"✅ Loaded video: {filename} ({self.video_duration:.1f}s) using {backend}")
|
||||
|
||||
if not MOVIEPY_AVAILABLE:
|
||||
messagebox.showinfo("Limited Functionality",
|
||||
"Video editor is running with limited functionality.\n" +
|
||||
"Only basic playback and timeline controls are available.\n" +
|
||||
"For full editing features, install MoviePy:\n" +
|
||||
"pip install moviepy")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Load Error", f"Could not load video: {e}")
|
||||
|
||||
def display_frame_at_time_opencv(self, time_sec):
|
||||
"""Display a specific frame using OpenCV"""
|
||||
if not self.current_clip or not hasattr(self.current_clip, 'get'):
|
||||
return
|
||||
|
||||
try:
|
||||
# Calculate frame number
|
||||
fps = self.current_clip.get(cv2.CAP_PROP_FPS)
|
||||
frame_number = int(time_sec * fps)
|
||||
|
||||
# Set video position
|
||||
self.current_clip.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
|
||||
|
||||
# Read frame
|
||||
ret, frame = self.current_clip.read()
|
||||
if not ret:
|
||||
return
|
||||
|
||||
# Convert BGR to RGB
|
||||
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
|
||||
# Convert to PIL Image
|
||||
pil_image = Image.fromarray(frame_rgb)
|
||||
|
||||
# Resize to fit canvas while maintaining aspect ratio
|
||||
canvas_width = self.video_canvas.winfo_width()
|
||||
canvas_height = self.video_canvas.winfo_height()
|
||||
|
||||
if canvas_width > 1 and canvas_height > 1:
|
||||
# Calculate scaling to fit canvas
|
||||
scale_w = canvas_width / pil_image.width
|
||||
scale_h = canvas_height / pil_image.height
|
||||
scale = min(scale_w, scale_h)
|
||||
|
||||
new_width = int(pil_image.width * scale)
|
||||
new_height = int(pil_image.height * scale)
|
||||
|
||||
pil_image = pil_image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
|
||||
# Convert to PhotoImage
|
||||
photo = ImageTk.PhotoImage(pil_image)
|
||||
|
||||
# Clear canvas and display image
|
||||
self.video_canvas.delete("all")
|
||||
x = canvas_width // 2
|
||||
y = canvas_height // 2
|
||||
self.video_canvas.create_image(x, y, image=photo)
|
||||
|
||||
# Keep reference to prevent garbage collection
|
||||
self.video_canvas.image = photo
|
||||
|
||||
except Exception as e:
|
||||
print(f"Frame display error: {e}")
|
||||
|
||||
def display_frame_at_time(self, time_sec):
|
||||
"""Display a specific frame from the video"""
|
||||
if not self.current_clip:
|
||||
return
|
||||
|
||||
if MOVIEPY_AVAILABLE:
|
||||
self.display_frame_at_time_moviepy(time_sec)
|
||||
else:
|
||||
self.display_frame_at_time_opencv(time_sec)
|
||||
|
||||
def display_frame_at_time_moviepy(self, time_sec):
|
||||
"""Display a specific frame using MoviePy"""
|
||||
try:
|
||||
# Get frame at specified time
|
||||
time_sec = max(0, min(time_sec, self.video_duration))
|
||||
frame = self.current_clip.get_frame(time_sec)
|
||||
|
||||
# Convert to PIL Image
|
||||
if frame.dtype != np.uint8:
|
||||
frame = (frame * 255).astype(np.uint8)
|
||||
|
||||
pil_image = Image.fromarray(frame)
|
||||
|
||||
# Resize to fit canvas while maintaining aspect ratio
|
||||
canvas_width = self.video_canvas.winfo_width()
|
||||
canvas_height = self.video_canvas.winfo_height()
|
||||
|
||||
if canvas_width > 1 and canvas_height > 1:
|
||||
# Calculate scaling to fit canvas
|
||||
scale_w = canvas_width / pil_image.width
|
||||
scale_h = canvas_height / pil_image.height
|
||||
scale = min(scale_w, scale_h)
|
||||
|
||||
new_width = int(pil_image.width * scale)
|
||||
new_height = int(pil_image.height * scale)
|
||||
|
||||
pil_image = pil_image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
|
||||
# Convert to PhotoImage
|
||||
photo = ImageTk.PhotoImage(pil_image)
|
||||
|
||||
# Clear canvas and display image
|
||||
self.video_canvas.delete("all")
|
||||
x = canvas_width // 2
|
||||
y = canvas_height // 2
|
||||
self.video_canvas.create_image(x, y, image=photo)
|
||||
|
||||
# Keep reference to prevent garbage collection
|
||||
self.video_canvas.image = photo
|
||||
|
||||
except Exception as e:
|
||||
print(f"Frame display error: {e}")
|
||||
|
||||
def update_timeline(self):
|
||||
"""Update the timeline display"""
|
||||
if not self.timeline_canvas.winfo_exists():
|
||||
return
|
||||
|
||||
self.timeline_canvas.delete("all")
|
||||
|
||||
if not self.current_clip:
|
||||
return
|
||||
|
||||
canvas_width = self.timeline_canvas.winfo_width()
|
||||
canvas_height = self.timeline_canvas.winfo_height()
|
||||
|
||||
if canvas_width <= 1:
|
||||
return
|
||||
|
||||
# Calculate timeline scale
|
||||
self.timeline_scale = (canvas_width - 40) / max(self.video_duration, 1)
|
||||
|
||||
# Draw timeline background
|
||||
self.timeline_canvas.create_rectangle(20, 20, canvas_width - 20, canvas_height - 20,
|
||||
fill=self.colors['bg_primary'], outline=self.colors['border'])
|
||||
|
||||
# Draw time markers
|
||||
for i in range(0, int(self.video_duration) + 1):
|
||||
x = 20 + i * self.timeline_scale
|
||||
if x < canvas_width - 20:
|
||||
self.timeline_canvas.create_line(x, 20, x, canvas_height - 20,
|
||||
fill=self.colors['border'], width=1)
|
||||
|
||||
# Time labels
|
||||
if i % 2 == 0: # Every 2 seconds
|
||||
self.timeline_canvas.create_text(x, canvas_height - 35,
|
||||
text=f"{i}s", fill=self.colors['text_secondary'],
|
||||
font=self.fonts['caption'])
|
||||
|
||||
# Draw playhead
|
||||
playhead_x = 20 + self.current_time * self.timeline_scale
|
||||
self.timeline_canvas.create_line(playhead_x, 20, playhead_x, canvas_height - 20,
|
||||
fill=self.colors['accent_blue'], width=3)
|
||||
|
||||
# Draw playhead handle
|
||||
self.timeline_canvas.create_oval(playhead_x - 5, 15, playhead_x + 5, 25,
|
||||
fill=self.colors['accent_blue'], outline='white')
|
||||
|
||||
def timeline_click(self, event):
|
||||
"""Handle timeline click"""
|
||||
if not self.current_clip:
|
||||
return
|
||||
|
||||
canvas_width = self.timeline_canvas.winfo_width()
|
||||
click_x = event.x
|
||||
|
||||
# Convert click position to time
|
||||
relative_x = click_x - 20
|
||||
if relative_x >= 0 and relative_x <= canvas_width - 40:
|
||||
clicked_time = relative_x / self.timeline_scale
|
||||
clicked_time = max(0, min(clicked_time, self.video_duration))
|
||||
|
||||
# Update current time and display
|
||||
self.current_time = clicked_time
|
||||
self.display_frame_at_time(self.current_time)
|
||||
self.update_timeline()
|
||||
self.update_time_display()
|
||||
|
||||
def timeline_drag(self, event):
|
||||
"""Handle timeline dragging"""
|
||||
self.timeline_click(event) # Same behavior as click for now
|
||||
|
||||
def update_time_display(self):
|
||||
"""Update the time display"""
|
||||
current_min = int(self.current_time // 60)
|
||||
current_sec = int(self.current_time % 60)
|
||||
total_min = int(self.video_duration // 60)
|
||||
total_sec = int(self.video_duration % 60)
|
||||
|
||||
time_text = f"{current_min:02d}:{current_sec:02d} / {total_min:02d}:{total_sec:02d}"
|
||||
self.time_display.config(text=time_text)
|
||||
|
||||
def timeline_play(self):
|
||||
"""Start timeline playback"""
|
||||
if not self.current_clip:
|
||||
return
|
||||
|
||||
self.timeline_is_playing = True
|
||||
self.play_video() # Start actual video playback
|
||||
self._start_timeline_playback()
|
||||
|
||||
def timeline_pause(self):
|
||||
"""Pause timeline playback"""
|
||||
self.timeline_is_playing = False
|
||||
self.pause_video() # Pause actual video
|
||||
|
||||
def timeline_stop(self):
|
||||
"""Stop timeline playback"""
|
||||
self.timeline_is_playing = False
|
||||
self.stop_video() # Stop actual video
|
||||
self.current_time = 0.0
|
||||
self.display_frame_at_time(0.0)
|
||||
self.update_timeline()
|
||||
self.update_time_display()
|
||||
|
||||
def _start_timeline_playback(self):
|
||||
"""Start the timeline playback loop"""
|
||||
def playback_loop():
|
||||
while self.timeline_is_playing and self.current_time < self.video_duration:
|
||||
if not self.is_playing: # Sync with video player state
|
||||
break
|
||||
|
||||
# Update timeline display
|
||||
self.editor_window.after(0, self.update_timeline)
|
||||
self.editor_window.after(0, self.update_time_display)
|
||||
|
||||
time.sleep(1/30) # 30 FPS update rate
|
||||
|
||||
# Playback finished
|
||||
self.timeline_is_playing = False
|
||||
|
||||
if not hasattr(self, 'timeline_thread') or not self.timeline_thread.is_alive():
|
||||
self.timeline_thread = threading.Thread(target=playback_loop, daemon=True)
|
||||
self.timeline_thread.start()
|
||||
|
||||
def play_video(self):
|
||||
"""Start video playback"""
|
||||
if not self.current_clip or self.is_playing:
|
||||
return
|
||||
|
||||
self.is_playing = True
|
||||
|
||||
def play_thread():
|
||||
start_time = time.time()
|
||||
start_video_time = self.current_time
|
||||
|
||||
while self.is_playing and self.current_time < self.video_duration:
|
||||
try:
|
||||
# Calculate current video time
|
||||
elapsed = time.time() - start_time
|
||||
self.current_time = start_video_time + elapsed
|
||||
|
||||
if self.current_time >= self.video_duration:
|
||||
self.current_time = self.video_duration
|
||||
self.is_playing = False
|
||||
break
|
||||
|
||||
# Update display
|
||||
self.display_frame_at_time(self.current_time)
|
||||
self.update_time_display()
|
||||
|
||||
# Frame rate control (approximately 30 FPS)
|
||||
time.sleep(1/30)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Playback error: {e}")
|
||||
break
|
||||
|
||||
# Playback finished
|
||||
self.is_playing = False
|
||||
|
||||
self.play_thread = threading.Thread(target=play_thread, daemon=True)
|
||||
self.play_thread.start()
|
||||
|
||||
def pause_video(self):
|
||||
"""Pause video playback"""
|
||||
self.is_playing = False
|
||||
|
||||
def stop_video(self):
|
||||
"""Stop video and return to beginning"""
|
||||
self.is_playing = False
|
||||
self.current_time = 0.0
|
||||
self.display_frame_at_time(0.0)
|
||||
|
||||
def apply_trim(self):
|
||||
"""Apply trim to the video"""
|
||||
if not MOVIEPY_AVAILABLE:
|
||||
messagebox.showwarning("Feature Unavailable",
|
||||
"Trim feature requires MoviePy.\nInstall with: pip install moviepy")
|
||||
return
|
||||
|
||||
if not self.current_clip:
|
||||
messagebox.showwarning("No Video", "Please load a video first.")
|
||||
return
|
||||
|
||||
start_time = self.trim_start_var.get()
|
||||
end_time = self.trim_end_var.get()
|
||||
|
||||
if start_time >= end_time:
|
||||
messagebox.showerror("Invalid Range", "Start time must be less than end time.")
|
||||
return
|
||||
|
||||
if end_time > self.video_duration:
|
||||
messagebox.showerror("Invalid Range", f"End time cannot exceed video duration ({self.video_duration:.1f}s).")
|
||||
return
|
||||
|
||||
try:
|
||||
# Apply trim
|
||||
self.current_clip = self.current_clip.subclipped(start_time, end_time)
|
||||
self.video_duration = self.current_clip.duration
|
||||
self.current_time = 0.0
|
||||
|
||||
# Update UI
|
||||
self.trim_end_var.set(self.video_duration)
|
||||
self.display_frame_at_time(0.0)
|
||||
self.update_timeline()
|
||||
self.update_time_display()
|
||||
|
||||
messagebox.showinfo("Success", f"Video trimmed to {start_time:.1f}s - {end_time:.1f}s")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Trim Error", f"Could not trim video: {e}")
|
||||
|
||||
def apply_speed(self):
|
||||
"""Apply speed change to the video"""
|
||||
if not MOVIEPY_AVAILABLE:
|
||||
messagebox.showwarning("Feature Unavailable",
|
||||
"Speed change requires MoviePy.\nInstall with: pip install moviepy")
|
||||
return
|
||||
|
||||
if not self.current_clip:
|
||||
messagebox.showwarning("No Video", "Please load a video first.")
|
||||
return
|
||||
|
||||
speed_factor = self.speed_var.get()
|
||||
|
||||
try:
|
||||
if speed_factor > 1:
|
||||
# Speed up
|
||||
self.current_clip = self.current_clip.with_fps(self.current_clip.fps * speed_factor)
|
||||
self.current_clip = self.current_clip.subclipped(0, self.current_clip.duration / speed_factor)
|
||||
else:
|
||||
# Slow down
|
||||
self.current_clip = self.current_clip.with_fps(self.current_clip.fps * speed_factor)
|
||||
|
||||
self.video_duration = self.current_clip.duration
|
||||
self.current_time = 0.0
|
||||
|
||||
# Update UI
|
||||
self.trim_end_var.set(self.video_duration)
|
||||
self.display_frame_at_time(0.0)
|
||||
self.update_timeline()
|
||||
self.update_time_display()
|
||||
|
||||
messagebox.showinfo("Success", f"Speed changed to {speed_factor:.1f}x")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Speed Error", f"Could not change speed: {e}")
|
||||
|
||||
def apply_volume(self):
|
||||
"""Apply volume adjustment"""
|
||||
if not MOVIEPY_AVAILABLE:
|
||||
messagebox.showwarning("Feature Unavailable",
|
||||
"Volume adjustment requires MoviePy.\nInstall with: pip install moviepy")
|
||||
return
|
||||
|
||||
if not self.current_clip:
|
||||
messagebox.showwarning("No Video", "Please load a video first.")
|
||||
return
|
||||
|
||||
if not self.current_clip.audio:
|
||||
messagebox.showwarning("No Audio", "This video has no audio track.")
|
||||
return
|
||||
|
||||
volume_factor = self.volume_var.get()
|
||||
|
||||
try:
|
||||
self.current_clip = self.current_clip.with_effects([MultiplyVolume(volume_factor)])
|
||||
messagebox.showinfo("Success", f"Volume adjusted to {volume_factor:.1f}x")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Volume Error", f"Could not adjust volume: {e}")
|
||||
|
||||
def apply_fade(self):
|
||||
"""Apply fade in/out effects"""
|
||||
if not MOVIEPY_AVAILABLE:
|
||||
messagebox.showwarning("Feature Unavailable",
|
||||
"Fade effects require MoviePy.\nInstall with: pip install moviepy")
|
||||
return
|
||||
|
||||
if not self.current_clip:
|
||||
messagebox.showwarning("No Video", "Please load a video first.")
|
||||
return
|
||||
|
||||
try:
|
||||
fade_duration = min(1.0, self.video_duration / 4) # Max 1 second or 1/4 of video
|
||||
|
||||
self.current_clip = self.current_clip.with_effects([
|
||||
FadeIn(fade_duration),
|
||||
FadeOut(fade_duration)
|
||||
])
|
||||
|
||||
messagebox.showinfo("Success", f"Fade effects applied ({fade_duration:.1f}s)")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Fade Error", f"Could not apply fade effects: {e}")
|
||||
|
||||
def apply_text(self):
|
||||
"""Apply text overlay"""
|
||||
if not MOVIEPY_AVAILABLE:
|
||||
messagebox.showwarning("Feature Unavailable",
|
||||
"Text overlay requires MoviePy.\nInstall with: pip install moviepy")
|
||||
return
|
||||
|
||||
if not self.current_clip:
|
||||
messagebox.showwarning("No Video", "Please load a video first.")
|
||||
return
|
||||
|
||||
text = self.text_var.get().strip()
|
||||
if not text:
|
||||
messagebox.showwarning("No Text", "Please enter text to overlay.")
|
||||
return
|
||||
|
||||
try:
|
||||
# Create text clip
|
||||
text_clip = TextClip(text, fontsize=50, color='white', font='Arial-Bold')
|
||||
text_clip = text_clip.with_duration(self.current_clip.duration)
|
||||
text_clip = text_clip.with_position(('center', 'bottom'))
|
||||
|
||||
# Composite with video
|
||||
self.current_clip = CompositeVideoClip([self.current_clip, text_clip])
|
||||
|
||||
messagebox.showinfo("Success", f"Text overlay added: '{text}'")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Text Error", f"Could not add text overlay: {e}")
|
||||
|
||||
def reset_video(self):
|
||||
"""Reset video to original state"""
|
||||
if not self.current_video:
|
||||
messagebox.showwarning("No Video", "No video loaded.")
|
||||
return
|
||||
|
||||
if messagebox.askyesno("Reset Video", "Reset all changes and reload original video?"):
|
||||
self.load_video(self.current_video)
|
||||
|
||||
def export_video(self):
|
||||
"""Export the edited video"""
|
||||
if not MOVIEPY_AVAILABLE:
|
||||
messagebox.showwarning("Feature Unavailable",
|
||||
"Video export requires MoviePy.\nInstall with: pip install moviepy")
|
||||
return
|
||||
|
||||
if not self.current_clip:
|
||||
messagebox.showwarning("No Video", "Please load a video first.")
|
||||
return
|
||||
|
||||
# Get output filename
|
||||
timestamp = datetime.now().strftime("%H%M%S")
|
||||
default_name = f"edited_video_{timestamp}.mp4"
|
||||
|
||||
output_path = filedialog.asksaveasfilename(
|
||||
title="Save Edited Video",
|
||||
defaultextension=".mp4",
|
||||
filetypes=[("MP4 files", "*.mp4"), ("All files", "*.*")],
|
||||
initialname=default_name
|
||||
)
|
||||
|
||||
if not output_path:
|
||||
return
|
||||
|
||||
# Create progress window
|
||||
progress_window = tk.Toplevel(self.editor_window)
|
||||
progress_window.title("Exporting Video")
|
||||
progress_window.geometry("400x150")
|
||||
progress_window.configure(bg=self.colors['bg_primary'])
|
||||
progress_window.transient(self.editor_window)
|
||||
progress_window.grab_set()
|
||||
|
||||
progress_label = tk.Label(progress_window, text="Exporting video...",
|
||||
font=self.fonts['body'], bg=self.colors['bg_primary'],
|
||||
fg=self.colors['text_primary'])
|
||||
progress_label.pack(pady=20)
|
||||
|
||||
progress_bar = ttk.Progressbar(progress_window, mode='indeterminate')
|
||||
progress_bar.pack(pady=10, padx=20, fill="x")
|
||||
progress_bar.start()
|
||||
|
||||
def export_thread():
|
||||
try:
|
||||
# Export video
|
||||
self.current_clip.write_videofile(
|
||||
output_path,
|
||||
codec="libx264",
|
||||
audio_codec="aac",
|
||||
verbose=False,
|
||||
logger=None
|
||||
)
|
||||
|
||||
def show_success():
|
||||
progress_window.destroy()
|
||||
messagebox.showinfo("Export Complete", f"Video exported successfully!\n\nSaved to: {output_path}")
|
||||
|
||||
self.editor_window.after(0, show_success)
|
||||
|
||||
except Exception as e:
|
||||
def show_error():
|
||||
progress_window.destroy()
|
||||
messagebox.showerror("Export Error", f"Could not export video: {e}")
|
||||
|
||||
self.editor_window.after(0, show_error)
|
||||
|
||||
# Start export in background thread
|
||||
threading.Thread(target=export_thread, daemon=True).start()
|
||||
|
||||
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()
|
||||
Loading…
Reference in New Issue
Block a user