Enhance ShortsEditorGUI with tabbed tool interface and OpenCV support
- Introduced a tabbed interface for editing tools, including Basic Editing, Video Effects, Audio Effects, and Export tabs. - Added support for additional video tracks and adjusted layout for better usability. - Implemented OpenCV-based alternatives for trimming, speed adjustment, volume control, fade effects, text overlay, and resizing when MoviePy is unavailable. - Improved track visualization with road lines and synchronized scrolling between track panel and timeline. - Enhanced user feedback with success and error messages for various operations.
This commit is contained in:
parent
291cefc44f
commit
a029a670a4
731
video_editor.py
731
video_editor.py
@ -62,11 +62,11 @@ class ShortsEditorGUI:
|
|||||||
|
|
||||||
# Multi-track system
|
# Multi-track system
|
||||||
self.tracks = {
|
self.tracks = {
|
||||||
'video_1': {'y_offset': 40, 'height': 60, 'color': '#3498db', 'name': 'Video 1', 'muted': False, 'locked': False, 'solo': False, 'visible': True},
|
'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},
|
'video_2': {'y_offset': 105, 'height': 60, 'color': '#2ecc71', 'name': 'Video 2', 'muted': False, 'locked': False, 'solo': False, 'visible': True, 'type': 'video'},
|
||||||
'audio_1': {'y_offset': 170, 'height': 40, 'color': '#e74c3c', 'name': 'Audio 1', 'muted': False, 'locked': False, 'solo': False, 'visible': True},
|
'video_3': {'y_offset': 170, 'height': 60, 'color': '#9b59b6', 'name': 'Video 3', 'muted': False, 'locked': False, 'solo': False, 'visible': True, 'type': 'video'},
|
||||||
'audio_2': {'y_offset': 215, 'height': 40, 'color': '#f39c12', 'name': 'Audio 2', 'muted': False, 'locked': False, 'solo': False, 'visible': True},
|
'audio_1': {'y_offset': 235, 'height': 40, 'color': '#e74c3c', 'name': 'Audio 1', 'muted': False, 'locked': False, 'solo': False, 'visible': True, 'type': 'audio'},
|
||||||
'text_1': {'y_offset': 260, 'height': 35, 'color': '#9b59b6', 'name': 'Text/Graphics', 'muted': False, 'locked': False, 'solo': False, 'visible': True}
|
'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
|
# Timeline interaction state
|
||||||
@ -186,7 +186,7 @@ class ShortsEditorGUI:
|
|||||||
self.video_canvas.grid(row=0, column=0, sticky="nsew")
|
self.video_canvas.grid(row=0, column=0, sticky="nsew")
|
||||||
|
|
||||||
# Professional Timeline Workspace
|
# Professional Timeline Workspace
|
||||||
timeline_workspace = tk.Frame(player_frame, bg=self.colors['bg_secondary'], height=350)
|
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.grid(row=1, column=0, sticky="ew", padx=15, pady=(0, 15))
|
||||||
timeline_workspace.pack_propagate(False)
|
timeline_workspace.pack_propagate(False)
|
||||||
|
|
||||||
@ -283,11 +283,11 @@ class ShortsEditorGUI:
|
|||||||
|
|
||||||
# Create canvas with scrollbars
|
# Create canvas with scrollbars
|
||||||
self.timeline_canvas = tk.Canvas(canvas_frame, bg='#1a1a1a',
|
self.timeline_canvas = tk.Canvas(canvas_frame, bg='#1a1a1a',
|
||||||
highlightthickness=0, scrollregion=(0, 0, 2000, 300))
|
highlightthickness=0, scrollregion=(0, 0, 2000, 400))
|
||||||
|
|
||||||
# Scrollbars
|
# Scrollbars
|
||||||
h_scrollbar = ttk.Scrollbar(canvas_frame, orient="horizontal", command=self.timeline_canvas.xview)
|
h_scrollbar = ttk.Scrollbar(canvas_frame, orient="horizontal", command=self.timeline_canvas.xview)
|
||||||
v_scrollbar = ttk.Scrollbar(canvas_frame, orient="vertical", command=self.timeline_canvas.yview)
|
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)
|
self.timeline_canvas.configure(xscrollcommand=h_scrollbar.set, yscrollcommand=v_scrollbar.set)
|
||||||
|
|
||||||
# Pack scrollbars and canvas
|
# Pack scrollbars and canvas
|
||||||
@ -305,6 +305,9 @@ class ShortsEditorGUI:
|
|||||||
# Create track controls
|
# Create track controls
|
||||||
self.create_track_controls()
|
self.create_track_controls()
|
||||||
|
|
||||||
|
# Bind canvas resize to redraw road lines
|
||||||
|
self.track_panel.bind("<Configure>", self.on_track_panel_resize)
|
||||||
|
|
||||||
# Initialize sample clips for demonstration
|
# Initialize sample clips for demonstration
|
||||||
self.create_sample_timeline_content()
|
self.create_sample_timeline_content()
|
||||||
|
|
||||||
@ -318,74 +321,215 @@ class ShortsEditorGUI:
|
|||||||
fg=self.colors['text_primary'])
|
fg=self.colors['text_primary'])
|
||||||
tools_header.pack(pady=(15, 10))
|
tools_header.pack(pady=(15, 10))
|
||||||
|
|
||||||
# Create tool sections
|
# Create tabbed interface for tools
|
||||||
self.create_basic_tools(tools_frame)
|
self.create_tabbed_tools(tools_frame)
|
||||||
self.create_effects_tools(tools_frame)
|
|
||||||
self.create_export_tools(tools_frame)
|
|
||||||
|
|
||||||
# Initialize timeline
|
# Initialize timeline
|
||||||
self.update_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):
|
def create_track_controls(self):
|
||||||
"""Create professional track control panel"""
|
"""Create professional track control panel with road lines"""
|
||||||
# Clear existing track controls
|
# Clear existing track controls
|
||||||
for widget in self.track_panel.winfo_children():
|
for widget in self.track_panel.winfo_children():
|
||||||
widget.destroy()
|
widget.destroy()
|
||||||
|
|
||||||
# Header
|
# Create road line background canvas
|
||||||
header = tk.Label(self.track_panel, text="TRACKS", font=('Arial', 9, 'bold'),
|
self.track_road_canvas = tk.Canvas(self.track_panel,
|
||||||
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary'])
|
bg=self.colors['bg_secondary'],
|
||||||
header.pack(fill="x", pady=(5, 10))
|
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
|
# Create controls for each track
|
||||||
for track_id, track_info in self.tracks.items():
|
for track_id, track_info in self.tracks.items():
|
||||||
self.create_track_control(track_id, track_info)
|
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):
|
def create_track_control(self, track_id, track_info):
|
||||||
"""Create control panel for a single track"""
|
"""Create control panel for a single track positioned on road lines"""
|
||||||
# Track frame
|
# Calculate precise positioning based on track offset
|
||||||
track_frame = tk.Frame(self.track_panel, bg=self.colors['bg_secondary'],
|
y_position = track_info['y_offset']
|
||||||
height=track_info['height'], relief="raised", bd=1)
|
track_height = track_info['height']
|
||||||
track_frame.pack(fill="x", pady=1)
|
|
||||||
track_frame.pack_propagate(False)
|
|
||||||
|
|
||||||
# Track name
|
# Create control buttons frame positioned on canvas
|
||||||
name_label = tk.Label(track_frame, text=track_info['name'],
|
controls_frame = tk.Frame(self.track_road_canvas, bg=self.colors['bg_secondary'])
|
||||||
font=('Arial', 8, 'bold'), 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'])
|
fg=track_info['color'])
|
||||||
name_label.pack(anchor="w", padx=5, pady=2)
|
name_label.pack(anchor="center")
|
||||||
|
|
||||||
# Control buttons frame
|
# Button container
|
||||||
controls = tk.Frame(track_frame, bg=self.colors['bg_secondary'])
|
button_container = tk.Frame(controls, bg=self.colors['bg_secondary'])
|
||||||
controls.pack(fill="x", padx=5)
|
button_container.pack()
|
||||||
|
|
||||||
# Mute button
|
# Mute button
|
||||||
mute_text = "🔇" if track_info['muted'] else "🔊"
|
mute_text = "🔇" if track_info['muted'] else "🔊"
|
||||||
mute_btn = tk.Button(controls, text=mute_text, width=2, height=1,
|
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'],
|
bg=self.colors['accent_red'] if track_info['muted'] else self.colors['bg_tertiary'],
|
||||||
fg='white', font=('Arial', 8), relief="flat", bd=0,
|
fg='white', font=('Arial', 7), relief="flat", bd=0,
|
||||||
command=lambda: self.toggle_track_mute(track_id))
|
command=lambda: self.toggle_track_mute(track_id))
|
||||||
mute_btn.pack(side="left", padx=1)
|
mute_btn.pack(side="left", padx=1)
|
||||||
|
|
||||||
# Solo button
|
# Solo button
|
||||||
solo_text = "S"
|
solo_text = "S"
|
||||||
solo_btn = tk.Button(controls, text=solo_text, width=2, height=1,
|
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'],
|
bg=self.colors['accent_orange'] if track_info['solo'] else self.colors['bg_tertiary'],
|
||||||
fg='white', font=('Arial', 8, 'bold'), relief="flat", bd=0,
|
fg='white', font=('Arial', 7, 'bold'), relief="flat", bd=0,
|
||||||
command=lambda: self.toggle_track_solo(track_id))
|
command=lambda: self.toggle_track_solo(track_id))
|
||||||
solo_btn.pack(side="left", padx=1)
|
solo_btn.pack(side="left", padx=1)
|
||||||
|
|
||||||
# Lock button
|
# Lock button
|
||||||
lock_text = "🔒" if track_info['locked'] else "🔓"
|
lock_text = "🔒" if track_info['locked'] else "🔓"
|
||||||
lock_btn = tk.Button(controls, text=lock_text, width=2, height=1,
|
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'],
|
bg=self.colors['accent_blue'] if track_info['locked'] else self.colors['bg_tertiary'],
|
||||||
fg='white', font=('Arial', 8), relief="flat", bd=0,
|
fg='white', font=('Arial', 7), relief="flat", bd=0,
|
||||||
command=lambda: self.toggle_track_lock(track_id))
|
command=lambda: self.toggle_track_lock(track_id))
|
||||||
lock_btn.pack(side="left", padx=1)
|
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
|
# Store track widgets for updates
|
||||||
self.track_widgets[track_id] = {
|
self.track_widgets[track_id] = {
|
||||||
'frame': track_frame,
|
'frame': controls_frame,
|
||||||
'mute_btn': mute_btn,
|
'mute_btn': mute_btn,
|
||||||
'solo_btn': solo_btn,
|
'solo_btn': solo_btn,
|
||||||
'lock_btn': lock_btn
|
'lock_btn': lock_btn
|
||||||
@ -490,18 +634,28 @@ class ShortsEditorGUI:
|
|||||||
|
|
||||||
def create_basic_tools(self, parent):
|
def create_basic_tools(self, parent):
|
||||||
"""Create basic editing tools"""
|
"""Create basic editing tools"""
|
||||||
basic_frame = tk.LabelFrame(parent, text="Basic Editing", font=self.fonts['heading'],
|
# Create scrollable frame for tools
|
||||||
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
|
tools_canvas = tk.Canvas(parent, bg=self.colors['bg_secondary'], highlightthickness=0)
|
||||||
relief="flat", bd=1)
|
scrollbar = tk.Scrollbar(parent, orient="vertical", command=tools_canvas.yview)
|
||||||
basic_frame.pack(fill="x", padx=15, pady=5)
|
scrollable_frame = tk.Frame(tools_canvas, bg=self.colors['bg_secondary'])
|
||||||
|
|
||||||
|
scrollable_frame.bind(
|
||||||
|
"<Configure>",
|
||||||
|
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 controls
|
||||||
trim_frame = tk.Frame(basic_frame, bg=self.colors['bg_secondary'])
|
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_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 = tk.Frame(trim_frame, bg=self.colors['bg_secondary'])
|
||||||
trim_controls.pack(fill="x", pady=5)
|
trim_controls.pack(fill="x", pady=5)
|
||||||
|
|
||||||
@ -523,78 +677,168 @@ class ShortsEditorGUI:
|
|||||||
font=self.fonts['caption'])
|
font=self.fonts['caption'])
|
||||||
trim_end_spin.pack(side="left", padx=5)
|
trim_end_spin.pack(side="left", padx=5)
|
||||||
|
|
||||||
trim_btn = tk.Button(basic_frame, text="✂️ Apply Trim", command=self.apply_trim,
|
trim_btn = tk.Button(trim_frame, text="✂️ Apply Trim", command=self.apply_trim,
|
||||||
bg=self.colors['accent_blue'], fg='white', font=self.fonts['button'],
|
bg=self.colors['accent_blue'], fg='white', font=self.fonts['button'],
|
||||||
relief="flat", bd=0, cursor="hand2")
|
relief="flat", bd=0, cursor="hand2")
|
||||||
trim_btn.pack(fill="x", padx=10, pady=5)
|
trim_btn.pack(fill="x", padx=10, pady=5)
|
||||||
|
|
||||||
# Speed controls
|
# Speed controls
|
||||||
speed_frame = tk.Frame(basic_frame, bg=self.colors['bg_secondary'])
|
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)
|
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)
|
self.speed_var = tk.DoubleVar(value=1.0)
|
||||||
speed_scale = tk.Scale(speed_frame, from_=0.25, to=3.0, resolution=0.25,
|
speed_scale = tk.Scale(speed_frame, from_=0.25, to=3.0, resolution=0.25,
|
||||||
orient="horizontal", variable=self.speed_var,
|
orient="horizontal", variable=self.speed_var,
|
||||||
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
|
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
|
||||||
highlightthickness=0, troughcolor=self.colors['bg_tertiary'])
|
highlightthickness=0, troughcolor=self.colors['bg_tertiary'])
|
||||||
speed_scale.pack(fill="x", pady=5)
|
speed_scale.pack(fill="x", pady=5, padx=10)
|
||||||
|
|
||||||
speed_btn = tk.Button(basic_frame, text="⚡ Apply Speed", command=self.apply_speed,
|
speed_btn = tk.Button(speed_frame, text="⚡ Apply Speed", command=self.apply_speed,
|
||||||
bg=self.colors['accent_green'], fg='white', font=self.fonts['button'],
|
bg=self.colors['accent_green'], fg='white', font=self.fonts['button'],
|
||||||
relief="flat", bd=0, cursor="hand2")
|
relief="flat", bd=0, cursor="hand2")
|
||||||
speed_btn.pack(fill="x", padx=10, pady=5)
|
speed_btn.pack(fill="x", padx=10, pady=5)
|
||||||
|
|
||||||
# Volume controls
|
# Volume controls
|
||||||
volume_frame = tk.Frame(basic_frame, bg=self.colors['bg_secondary'])
|
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)
|
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)
|
self.volume_var = tk.DoubleVar(value=1.0)
|
||||||
volume_scale = tk.Scale(volume_frame, from_=0.0, to=2.0, resolution=0.1,
|
volume_scale = tk.Scale(volume_frame, from_=0.0, to=2.0, resolution=0.1,
|
||||||
orient="horizontal", variable=self.volume_var,
|
orient="horizontal", variable=self.volume_var,
|
||||||
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
|
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
|
||||||
highlightthickness=0, troughcolor=self.colors['bg_tertiary'])
|
highlightthickness=0, troughcolor=self.colors['bg_tertiary'])
|
||||||
volume_scale.pack(fill="x", pady=5)
|
volume_scale.pack(fill="x", pady=5, padx=10)
|
||||||
|
|
||||||
volume_btn = tk.Button(basic_frame, text="🔊 Apply Volume", command=self.apply_volume,
|
volume_btn = tk.Button(volume_frame, text="🔊 Apply Volume", command=self.apply_volume,
|
||||||
bg=self.colors['accent_orange'], fg='white', font=self.fonts['button'],
|
bg=self.colors['accent_orange'], fg='white', font=self.fonts['button'],
|
||||||
relief="flat", bd=0, cursor="hand2")
|
relief="flat", bd=0, cursor="hand2")
|
||||||
volume_btn.pack(fill="x", padx=10, pady=5)
|
volume_btn.pack(fill="x", padx=10, pady=5)
|
||||||
|
|
||||||
def create_effects_tools(self, parent):
|
# Resize controls
|
||||||
"""Create effects tools"""
|
resize_frame = tk.LabelFrame(scrollable_frame, text="📐 Resize Video", font=self.fonts['body'],
|
||||||
effects_frame = tk.LabelFrame(parent, text="Effects", font=self.fonts['heading'],
|
|
||||||
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
|
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
|
||||||
relief="flat", bd=1)
|
relief="flat", bd=1)
|
||||||
effects_frame.pack(fill="x", padx=15, pady=5)
|
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(
|
||||||
|
"<Configure>",
|
||||||
|
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 effects
|
||||||
fade_btn = tk.Button(effects_frame, text="🌅 Add Fade In/Out", command=self.apply_fade,
|
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'],
|
bg=self.colors['accent_blue'], fg='white', font=self.fonts['button'],
|
||||||
relief="flat", bd=0, cursor="hand2")
|
relief="flat", bd=0, cursor="hand2")
|
||||||
fade_btn.pack(fill="x", padx=10, pady=5)
|
fade_btn.pack(fill="x", padx=10, pady=5)
|
||||||
|
|
||||||
# Text overlay
|
# Text overlay
|
||||||
text_frame = tk.Frame(effects_frame, bg=self.colors['bg_secondary'])
|
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_frame.pack(fill="x", padx=10, pady=5)
|
||||||
|
|
||||||
tk.Label(text_frame, text="Text Overlay:", font=self.fonts['body'],
|
text_controls = tk.Frame(text_frame, bg=self.colors['bg_secondary'])
|
||||||
bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).pack(anchor="w")
|
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")
|
self.text_var = tk.StringVar(value="Sample Text")
|
||||||
text_entry = tk.Entry(text_frame, textvariable=self.text_var, font=self.fonts['body'])
|
text_entry = tk.Entry(text_controls, textvariable=self.text_var, font=self.fonts['body'],
|
||||||
|
width=25)
|
||||||
text_entry.pack(fill="x", pady=5)
|
text_entry.pack(fill="x", pady=5)
|
||||||
|
|
||||||
text_btn = tk.Button(effects_frame, text="📝 Add Text", command=self.apply_text,
|
text_btn = tk.Button(text_frame, text="📝 Add Text", command=self.apply_text,
|
||||||
bg=self.colors['accent_green'], fg='white', font=self.fonts['button'],
|
bg=self.colors['accent_green'], fg='white', font=self.fonts['button'],
|
||||||
relief="flat", bd=0, cursor="hand2")
|
relief="flat", bd=0, cursor="hand2")
|
||||||
text_btn.pack(fill="x", padx=10, pady=5)
|
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(
|
||||||
|
"<Configure>",
|
||||||
|
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):
|
def create_export_tools(self, parent):
|
||||||
"""Create export tools"""
|
"""Create export tools"""
|
||||||
export_frame = tk.LabelFrame(parent, text="Export", font=self.fonts['heading'],
|
export_frame = tk.LabelFrame(parent, text="Export", font=self.fonts['heading'],
|
||||||
@ -877,6 +1121,9 @@ class ShortsEditorGUI:
|
|||||||
self.timeline_canvas.create_rectangle(20, 20, canvas_width - 20, canvas_height - 20,
|
self.timeline_canvas.create_rectangle(20, 20, canvas_width - 20, canvas_height - 20,
|
||||||
fill=self.colors['bg_primary'], outline=self.colors['border'])
|
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
|
# Draw time markers
|
||||||
for i in range(0, int(self.video_duration) + 1):
|
for i in range(0, int(self.video_duration) + 1):
|
||||||
x = 20 + i * self.timeline_scale
|
x = 20 + i * self.timeline_scale
|
||||||
@ -899,6 +1146,41 @@ class ShortsEditorGUI:
|
|||||||
self.timeline_canvas.create_oval(playhead_x - 5, 15, playhead_x + 5, 25,
|
self.timeline_canvas.create_oval(playhead_x - 5, 15, playhead_x + 5, 25,
|
||||||
fill=self.colors['accent_blue'], outline='white')
|
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):
|
def timeline_click(self, event):
|
||||||
"""Handle timeline click"""
|
"""Handle timeline click"""
|
||||||
if not self.current_clip:
|
if not self.current_clip:
|
||||||
@ -1095,11 +1377,6 @@ class ShortsEditorGUI:
|
|||||||
|
|
||||||
def apply_trim(self):
|
def apply_trim(self):
|
||||||
"""Apply trim to the video"""
|
"""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:
|
if not self.current_clip:
|
||||||
messagebox.showwarning("No Video", "Please load a video first.")
|
messagebox.showwarning("No Video", "Please load a video first.")
|
||||||
return
|
return
|
||||||
@ -1116,7 +1393,8 @@ class ShortsEditorGUI:
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Apply trim
|
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.current_clip = self.current_clip.subclipped(start_time, end_time)
|
||||||
self.video_duration = self.current_clip.duration
|
self.video_duration = self.current_clip.duration
|
||||||
self.current_time = 0.0
|
self.current_time = 0.0
|
||||||
@ -1126,19 +1404,55 @@ class ShortsEditorGUI:
|
|||||||
self.display_frame_at_time(0.0)
|
self.display_frame_at_time(0.0)
|
||||||
self.update_timeline()
|
self.update_timeline()
|
||||||
self.update_time_display()
|
self.update_time_display()
|
||||||
|
|
||||||
messagebox.showinfo("Success", f"Video trimmed to {start_time:.1f}s - {end_time:.1f}s")
|
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:
|
except Exception as e:
|
||||||
messagebox.showerror("Trim Error", f"Could not trim video: {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):
|
def apply_speed(self):
|
||||||
"""Apply speed change to the video"""
|
"""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:
|
if not self.current_clip:
|
||||||
messagebox.showwarning("No Video", "Please load a video first.")
|
messagebox.showwarning("No Video", "Please load a video first.")
|
||||||
return
|
return
|
||||||
@ -1146,6 +1460,8 @@ class ShortsEditorGUI:
|
|||||||
speed_factor = self.speed_var.get()
|
speed_factor = self.speed_var.get()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'with_fps'):
|
||||||
|
# Apply speed change using MoviePy
|
||||||
if speed_factor > 1:
|
if speed_factor > 1:
|
||||||
# Speed up
|
# Speed up
|
||||||
self.current_clip = self.current_clip.with_fps(self.current_clip.fps * speed_factor)
|
self.current_clip = self.current_clip.with_fps(self.current_clip.fps * speed_factor)
|
||||||
@ -1162,90 +1478,285 @@ class ShortsEditorGUI:
|
|||||||
self.display_frame_at_time(0.0)
|
self.display_frame_at_time(0.0)
|
||||||
self.update_timeline()
|
self.update_timeline()
|
||||||
self.update_time_display()
|
self.update_time_display()
|
||||||
|
|
||||||
messagebox.showinfo("Success", f"Speed changed to {speed_factor:.1f}x")
|
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:
|
except Exception as e:
|
||||||
messagebox.showerror("Speed Error", f"Could not change speed: {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):
|
def apply_volume(self):
|
||||||
"""Apply volume adjustment"""
|
"""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:
|
if not self.current_clip:
|
||||||
messagebox.showwarning("No Video", "Please load a video first.")
|
messagebox.showwarning("No Video", "Please load a video first.")
|
||||||
return
|
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:
|
if not self.current_clip.audio:
|
||||||
messagebox.showwarning("No Audio", "This video has no audio track.")
|
messagebox.showwarning("No Audio", "This video has no audio track.")
|
||||||
return
|
return
|
||||||
|
|
||||||
volume_factor = self.volume_var.get()
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.current_clip = self.current_clip.with_effects([MultiplyVolume(volume_factor)])
|
self.current_clip = self.current_clip.with_effects([MultiplyVolume(volume_factor)])
|
||||||
messagebox.showinfo("Success", f"Volume adjusted to {volume_factor:.1f}x")
|
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:
|
except Exception as e:
|
||||||
messagebox.showerror("Volume Error", f"Could not adjust volume: {e}")
|
messagebox.showerror("Volume Error", f"Could not adjust volume: {e}")
|
||||||
|
|
||||||
def apply_fade(self):
|
def apply_fade(self):
|
||||||
"""Apply fade in/out effects"""
|
"""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:
|
if not self.current_clip:
|
||||||
messagebox.showwarning("No Video", "Please load a video first.")
|
messagebox.showwarning("No Video", "Please load a video first.")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fade_duration = min(1.0, self.video_duration / 4) # Max 1 second or 1/4 of video
|
if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'fadein'):
|
||||||
|
# Use MoviePy for advanced fade effects
|
||||||
self.current_clip = self.current_clip.with_effects([
|
fade_duration = min(1.0, self.video_duration / 4)
|
||||||
FadeIn(fade_duration),
|
|
||||||
FadeOut(fade_duration)
|
|
||||||
])
|
|
||||||
|
|
||||||
|
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)")
|
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:
|
except Exception as e:
|
||||||
messagebox.showerror("Fade Error", f"Could not apply fade effects: {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):
|
def apply_text(self):
|
||||||
"""Apply text overlay"""
|
"""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:
|
if not self.current_clip:
|
||||||
messagebox.showwarning("No Video", "Please load a video first.")
|
messagebox.showwarning("No Video", "Please load a video first.")
|
||||||
return
|
return
|
||||||
|
|
||||||
text = self.text_var.get().strip()
|
# Get text from the text variable
|
||||||
if not text:
|
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.")
|
messagebox.showwarning("No Text", "Please enter text to overlay.")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Create text clip
|
if MOVIEPY_AVAILABLE and hasattr(self.current_clip, 'duration'):
|
||||||
text_clip = TextClip(text, fontsize=50, color='white', font='Arial-Bold')
|
# 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_duration(self.current_clip.duration)
|
||||||
text_clip = text_clip.with_position(('center', 'bottom'))
|
text_clip = text_clip.with_position(('center', 'bottom'))
|
||||||
|
|
||||||
# Composite with video
|
# Composite with video
|
||||||
self.current_clip = CompositeVideoClip([self.current_clip, text_clip])
|
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
|
||||||
|
|
||||||
messagebox.showinfo("Success", f"Text overlay added: '{text}'")
|
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:
|
except Exception as e:
|
||||||
messagebox.showerror("Text Error", f"Could not add text overlay: {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):
|
def reset_video(self):
|
||||||
"""Reset video to original state"""
|
"""Reset video to original state"""
|
||||||
if not self.current_video:
|
if not self.current_video:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user