ShortGenerator/video_editor.py
klop51 809e768cae 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.
2025-08-10 21:05:36 +02:00

1000 lines
42 KiB
Python

"""
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()