feat: Implement professional timeline features with multi-track support and enhanced editing controls

This commit is contained in:
klop51 2025-08-11 00:14:06 +02:00
parent 809e768cae
commit 291cefc44f
2 changed files with 512 additions and 26 deletions

View File

@ -55,6 +55,41 @@ class ShortsEditorGUI:
self.timeline_scale = 1.0 # Pixels per second
self.timeline_width = 800
# Professional timeline features
self.timeline_clips = []
self.selected_clip = None
self.markers = []
# Multi-track system
self.tracks = {
'video_1': {'y_offset': 40, 'height': 60, 'color': '#3498db', 'name': 'Video 1', '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},
'audio_1': {'y_offset': 170, 'height': 40, 'color': '#e74c3c', 'name': 'Audio 1', 'muted': False, 'locked': False, 'solo': False, 'visible': True},
'audio_2': {'y_offset': 215, 'height': 40, 'color': '#f39c12', 'name': 'Audio 2', 'muted': False, 'locked': False, 'solo': False, 'visible': True},
'text_1': {'y_offset': 260, 'height': 35, 'color': '#9b59b6', 'name': 'Text/Graphics', 'muted': False, 'locked': False, 'solo': False, 'visible': True}
}
# Timeline interaction state
self.dragging_clip = None
self.drag_start_x = None
self.drag_start_time = None
self.drag_offset = 0
self.snap_enabled = True
self.magnetic_timeline = True
self.grid_size = 1.0 # Snap grid in seconds
# Timeline editing modes
self.edit_mode = 'select' # 'select', 'cut', 'trim', 'ripple'
# Visual enhancements
self.show_thumbnails = True
self.show_waveforms = True
self.clip_thumbnails = {}
self.audio_waveforms = {}
# Track widgets for UI
self.track_widgets = {}
# Modern color scheme
self.colors = {
'bg_primary': '#1a1a1a',
@ -150,58 +185,128 @@ class ShortsEditorGUI:
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)
# Professional Timeline Workspace
timeline_workspace = tk.Frame(player_frame, bg=self.colors['bg_secondary'], height=350)
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 header with editing tools
header_frame = tk.Frame(timeline_workspace, bg=self.colors['bg_secondary'])
header_frame.pack(fill="x", pady=(10, 5))
# Timeline control buttons
btn_frame = tk.Frame(controls_frame, bg=self.colors['bg_secondary'])
btn_frame.pack(side="left")
# Left side - Timeline controls
left_controls = tk.Frame(header_frame, bg=self.colors['bg_secondary'])
left_controls.pack(side="left")
self.timeline_play_btn = tk.Button(btn_frame, text="▶️ Play",
# Editing mode selector
tk.Label(left_controls, text="Mode:", font=self.fonts['caption'],
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(side="left")
self.mode_var = tk.StringVar(value="select")
mode_combo = ttk.Combobox(left_controls, textvariable=self.mode_var, width=8,
values=["select", "cut", "trim", "ripple"], state="readonly")
mode_combo.pack(side="left", padx=(5, 10))
mode_combo.bind('<<ComboboxSelected>>', self.on_mode_change)
# Snap and magnetic timeline toggles
self.snap_var = tk.BooleanVar(value=True)
snap_check = tk.Checkbutton(left_controls, text="Snap", variable=self.snap_var,
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
selectcolor=self.colors['accent_blue'], command=self.toggle_snap)
snap_check.pack(side="left", padx=5)
self.magnetic_var = tk.BooleanVar(value=True)
magnetic_check = tk.Checkbutton(left_controls, text="Magnetic", variable=self.magnetic_var,
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
selectcolor=self.colors['accent_blue'], command=self.toggle_magnetic)
magnetic_check.pack(side="left", padx=5)
# Center - Playback controls
center_controls = tk.Frame(header_frame, bg=self.colors['bg_secondary'])
center_controls.pack(side="left", padx=20)
self.timeline_play_btn = tk.Button(center_controls, text="▶️",
command=self.timeline_play,
bg=self.colors['accent_green'], fg='white',
font=self.fonts['button'], padx=15, pady=5,
font=('Arial', 12, 'bold'), width=3, height=1,
relief="flat", bd=0, cursor="hand2")
self.timeline_play_btn.pack(side="left", padx=(0, 5))
self.timeline_play_btn.pack(side="left", padx=2)
self.timeline_pause_btn = tk.Button(btn_frame, text="⏸️ Pause",
self.timeline_pause_btn = tk.Button(center_controls, text="⏸️",
command=self.timeline_pause,
bg=self.colors['accent_orange'], fg='white',
font=self.fonts['button'], padx=15, pady=5,
font=('Arial', 12, 'bold'), width=3, height=1,
relief="flat", bd=0, cursor="hand2")
self.timeline_pause_btn.pack(side="left", padx=5)
self.timeline_pause_btn.pack(side="left", padx=2)
self.timeline_stop_btn = tk.Button(btn_frame, text="⏹️ Stop",
self.timeline_stop_btn = tk.Button(center_controls, text="⏹️",
command=self.timeline_stop,
bg=self.colors['accent_red'], fg='white',
font=self.fonts['button'], padx=15, pady=5,
font=('Arial', 12, 'bold'), width=3, height=1,
relief="flat", bd=0, cursor="hand2")
self.timeline_stop_btn.pack(side="left", padx=5)
self.timeline_stop_btn.pack(side="left", padx=2)
# Right side - Zoom and time display
right_controls = tk.Frame(header_frame, bg=self.colors['bg_secondary'])
right_controls.pack(side="right")
# Zoom control
tk.Label(right_controls, text="Zoom:", font=self.fonts['caption'],
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(side="left")
self.zoom_var = tk.DoubleVar(value=1.0)
zoom_scale = tk.Scale(right_controls, from_=0.1, to=5.0, resolution=0.1,
orient="horizontal", variable=self.zoom_var,
command=self.on_zoom_change, length=150,
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
highlightthickness=0, troughcolor=self.colors['bg_tertiary'])
zoom_scale.pack(side="left", padx=10)
# Time display
self.time_display = tk.Label(controls_frame, text="00:00 / 00:00",
self.time_display = tk.Label(right_controls, text="00:00 / 00:00",
font=self.fonts['body'], bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'])
self.time_display.pack(side="right", padx=20)
self.time_display.pack(side="left", padx=20)
# Timeline canvas
timeline_frame = tk.Frame(timeline_workspace, bg=self.colors['bg_tertiary'])
timeline_frame.pack(fill="both", expand=True, pady=10)
# Main timeline container
timeline_container = tk.Frame(timeline_workspace, bg=self.colors['bg_tertiary'])
timeline_container.pack(fill="both", expand=True, pady=5)
self.timeline_canvas = tk.Canvas(timeline_frame, bg=self.colors['bg_tertiary'],
height=120, highlightthickness=1,
highlightbackground=self.colors['border'])
# Track labels panel (left side)
self.track_panel = tk.Frame(timeline_container, bg=self.colors['bg_secondary'], width=120)
self.track_panel.pack(side="left", fill="y")
self.track_panel.pack_propagate(False)
# Timeline canvas with scrollbars
canvas_frame = tk.Frame(timeline_container, bg=self.colors['bg_tertiary'])
canvas_frame.pack(side="right", fill="both", expand=True)
# Create canvas with scrollbars
self.timeline_canvas = tk.Canvas(canvas_frame, bg='#1a1a1a',
highlightthickness=0, scrollregion=(0, 0, 2000, 300))
# Scrollbars
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)
self.timeline_canvas.configure(xscrollcommand=h_scrollbar.set, yscrollcommand=v_scrollbar.set)
# Pack scrollbars and canvas
h_scrollbar.pack(side="bottom", fill="x")
v_scrollbar.pack(side="right", fill="y")
self.timeline_canvas.pack(side="left", fill="both", expand=True)
# Bind timeline events
# Bind professional timeline events
self.timeline_canvas.bind("<Button-1>", self.timeline_click)
self.timeline_canvas.bind("<B1-Motion>", self.timeline_drag)
self.timeline_canvas.bind("<ButtonRelease-1>", self.on_timeline_drag_end)
self.timeline_canvas.bind("<Button-3>", self.on_timeline_right_click)
self.timeline_canvas.bind("<Double-Button-1>", self.on_timeline_double_click)
# Create track controls
self.create_track_controls()
# Initialize sample clips for demonstration
self.create_sample_timeline_content()
# Right panel - Tools and effects
tools_frame = tk.Frame(main_frame, bg=self.colors['bg_secondary'])
@ -221,6 +326,168 @@ class ShortsEditorGUI:
# Initialize timeline
self.update_timeline()
def create_track_controls(self):
"""Create professional track control panel"""
# Clear existing track controls
for widget in self.track_panel.winfo_children():
widget.destroy()
# Header
header = tk.Label(self.track_panel, text="TRACKS", font=('Arial', 9, 'bold'),
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary'])
header.pack(fill="x", pady=(5, 10))
# Create controls for each track
for track_id, track_info in self.tracks.items():
self.create_track_control(track_id, track_info)
def create_track_control(self, track_id, track_info):
"""Create control panel for a single track"""
# Track frame
track_frame = tk.Frame(self.track_panel, bg=self.colors['bg_secondary'],
height=track_info['height'], relief="raised", bd=1)
track_frame.pack(fill="x", pady=1)
track_frame.pack_propagate(False)
# Track name
name_label = tk.Label(track_frame, text=track_info['name'],
font=('Arial', 8, 'bold'), bg=self.colors['bg_secondary'],
fg=track_info['color'])
name_label.pack(anchor="w", padx=5, pady=2)
# Control buttons frame
controls = tk.Frame(track_frame, bg=self.colors['bg_secondary'])
controls.pack(fill="x", padx=5)
# Mute button
mute_text = "🔇" if track_info['muted'] else "🔊"
mute_btn = tk.Button(controls, text=mute_text, width=2, height=1,
bg=self.colors['accent_red'] if track_info['muted'] else self.colors['bg_tertiary'],
fg='white', font=('Arial', 8), relief="flat", bd=0,
command=lambda: self.toggle_track_mute(track_id))
mute_btn.pack(side="left", padx=1)
# Solo button
solo_text = "S"
solo_btn = tk.Button(controls, text=solo_text, width=2, height=1,
bg=self.colors['accent_orange'] if track_info['solo'] else self.colors['bg_tertiary'],
fg='white', font=('Arial', 8, 'bold'), relief="flat", bd=0,
command=lambda: self.toggle_track_solo(track_id))
solo_btn.pack(side="left", padx=1)
# Lock button
lock_text = "🔒" if track_info['locked'] else "🔓"
lock_btn = tk.Button(controls, text=lock_text, width=2, height=1,
bg=self.colors['accent_blue'] if track_info['locked'] else self.colors['bg_tertiary'],
fg='white', font=('Arial', 8), relief="flat", bd=0,
command=lambda: self.toggle_track_lock(track_id))
lock_btn.pack(side="left", padx=1)
# Store track widgets for updates
self.track_widgets[track_id] = {
'frame': track_frame,
'mute_btn': mute_btn,
'solo_btn': solo_btn,
'lock_btn': lock_btn
}
def create_sample_timeline_content(self):
"""Create sample timeline content for demonstration"""
if self.current_video and self.video_duration > 0:
# Create a sample clip representing the loaded video
sample_clip = {
'id': 1,
'name': os.path.basename(self.current_video) if self.current_video else 'Sample Video',
'start_time': 0,
'end_time': min(self.video_duration, 10), # Cap at 10 seconds for demo
'track': 'video_1',
'color': self.tracks['video_1']['color'],
'file_path': self.current_video,
'type': 'video'
}
self.timeline_clips = [sample_clip]
# Add sample markers
self.markers = [
{'time': 2.0, 'name': 'Intro End', 'color': '#ffeb3b'},
{'time': 5.0, 'name': 'Mid Point', 'color': '#4caf50'},
{'time': 8.0, 'name': 'Outro Start', 'color': '#f44336'}
]
self.update_timeline()
# Professional timeline interaction methods
def on_mode_change(self, event=None):
"""Handle editing mode change"""
self.edit_mode = self.mode_var.get()
print(f"🎬 Editing mode changed to: {self.edit_mode}")
# Update cursor based on mode
cursor_map = {
'select': 'hand2',
'cut': 'crosshair',
'trim': 'sb_h_double_arrow',
'ripple': 'fleur'
}
self.timeline_canvas.configure(cursor=cursor_map.get(self.edit_mode, 'hand2'))
def toggle_snap(self):
"""Toggle snap to grid"""
self.snap_enabled = self.snap_var.get()
print(f"🧲 Snap enabled: {self.snap_enabled}")
def toggle_magnetic(self):
"""Toggle magnetic timeline"""
self.magnetic_timeline = self.magnetic_var.get()
print(f"🧲 Magnetic timeline: {self.magnetic_timeline}")
def toggle_track_mute(self, track_id):
"""Toggle track mute"""
self.tracks[track_id]['muted'] = not self.tracks[track_id]['muted']
self.update_track_controls()
print(f"🔇 Track {track_id} muted: {self.tracks[track_id]['muted']}")
def toggle_track_solo(self, track_id):
"""Toggle track solo"""
self.tracks[track_id]['solo'] = not self.tracks[track_id]['solo']
self.update_track_controls()
print(f"🎵 Track {track_id} solo: {self.tracks[track_id]['solo']}")
def toggle_track_lock(self, track_id):
"""Toggle track lock"""
self.tracks[track_id]['locked'] = not self.tracks[track_id]['locked']
self.update_track_controls()
print(f"🔒 Track {track_id} locked: {self.tracks[track_id]['locked']}")
def update_track_controls(self):
"""Update track control button states"""
for track_id, widgets in self.track_widgets.items():
track_info = self.tracks[track_id]
# Update mute button
mute_text = "🔇" if track_info['muted'] else "🔊"
mute_color = self.colors['accent_red'] if track_info['muted'] else self.colors['bg_tertiary']
widgets['mute_btn'].configure(text=mute_text, bg=mute_color)
# Update solo button
solo_color = self.colors['accent_orange'] if track_info['solo'] else self.colors['bg_tertiary']
widgets['solo_btn'].configure(bg=solo_color)
# Update lock button
lock_text = "🔒" if track_info['locked'] else "🔓"
lock_color = self.colors['accent_blue'] if track_info['locked'] else self.colors['bg_tertiary']
widgets['lock_btn'].configure(text=lock_text, bg=lock_color)
def on_zoom_change(self, value):
"""Handle timeline zoom change"""
zoom_level = float(value)
self.timeline_scale = 50 * zoom_level # Base scale of 50 pixels per second
self.update_timeline()
print(f"🔍 Timeline zoom: {zoom_level:.1f}x")
# Initialize timeline
self.update_timeline()
def create_basic_tools(self, parent):
"""Create basic editing tools"""
basic_frame = tk.LabelFrame(parent, text="Basic Editing", font=self.fonts['heading'],
@ -656,6 +923,74 @@ class ShortsEditorGUI:
"""Handle timeline dragging"""
self.timeline_click(event) # Same behavior as click for now
def on_timeline_drag_end(self, event):
"""End timeline drag operation"""
if hasattr(self, 'dragging_clip') and self.dragging_clip:
print(f"🎬 Moved clip '{self.dragging_clip['name']}' to {self.dragging_clip['start_time']:.2f}s")
# Clear drag state
if hasattr(self, 'dragging_clip'):
self.dragging_clip = None
if hasattr(self, 'drag_start_x'):
self.drag_start_x = None
if hasattr(self, 'drag_start_time'):
self.drag_start_time = None
if hasattr(self, 'drag_offset'):
self.drag_offset = 0
def on_timeline_right_click(self, event):
"""Handle right-click context menu"""
try:
canvas_x = self.timeline_canvas.canvasx(event.x)
canvas_y = self.timeline_canvas.canvasy(event.y)
clicked_clip = self.get_clip_at_position(canvas_x, canvas_y) if hasattr(self, 'get_clip_at_position') else None
# Create context menu
context_menu = tk.Menu(self.root, tearoff=0, bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'])
if clicked_clip:
# Clip context menu
self.selected_clip = clicked_clip
context_menu.add_command(label=f"Cut '{clicked_clip['name']}'",
command=lambda: self.cut_clip_at_playhead())
context_menu.add_command(label=f"Delete '{clicked_clip['name']}'",
command=lambda: self.delete_clip(clicked_clip))
context_menu.add_separator()
context_menu.add_command(label="Duplicate Clip",
command=lambda: self.duplicate_clip(clicked_clip))
context_menu.add_command(label="Properties",
command=lambda: self.show_clip_properties(clicked_clip))
else:
# Timeline context menu
click_time = canvas_x / self.timeline_scale if hasattr(self, 'timeline_scale') else 0
context_menu.add_command(label="Add Marker",
command=lambda: self.add_marker_at_time(click_time))
context_menu.add_command(label="Zoom to Fit", command=self.zoom_to_fit)
try:
context_menu.tk_popup(event.x_root, event.y_root)
finally:
context_menu.grab_release()
except Exception as e:
print(f"Context menu error: {e}")
def on_timeline_double_click(self, event):
"""Handle timeline double-click"""
try:
canvas_x = self.timeline_canvas.canvasx(event.x)
canvas_y = self.timeline_canvas.canvasy(event.y)
clicked_clip = self.get_clip_at_position(canvas_x, canvas_y) if hasattr(self, 'get_clip_at_position') else None
if clicked_clip:
self.show_clip_properties(clicked_clip)
else:
# Add marker on double-click
click_time = canvas_x / self.timeline_scale if hasattr(self, 'timeline_scale') else 0
self.add_marker_at_time(click_time)
except Exception as e:
print(f"Double-click error: {e}")
def update_time_display(self):
"""Update the time display"""
current_min = int(self.current_time // 60)
@ -988,6 +1323,157 @@ class ShortsEditorGUI:
# Start export in background thread
threading.Thread(target=export_thread, daemon=True).start()
# Professional timeline helper methods
def get_clip_at_position(self, x, y):
"""Get the clip at the given canvas position"""
time_pos = x / self.timeline_scale
for clip in self.timeline_clips:
if clip['start_time'] <= time_pos <= clip['end_time']:
# Check if Y position is within the clip's track
track_info = self.tracks[clip['track']]
if track_info['y_offset'] <= y <= track_info['y_offset'] + track_info['height']:
return clip
return None
def snap_to_grid(self, time_value):
"""Snap time value to grid"""
if self.snap_enabled and self.grid_size > 0:
return round(time_value / self.grid_size) * self.grid_size
return time_value
def magnetic_snap(self, new_time, dragging_clip):
"""Apply magnetic timeline snapping to other clips"""
if not self.magnetic_timeline:
return new_time
snap_distance = 0.2 # 200ms snap distance
clip_duration = dragging_clip['end_time'] - dragging_clip['start_time']
for clip in self.timeline_clips:
if clip == dragging_clip or clip['track'] != dragging_clip['track']:
continue
# Snap to start of other clips
if abs(new_time - clip['start_time']) < snap_distance:
return clip['start_time']
# Snap to end of other clips
if abs(new_time - clip['end_time']) < snap_distance:
return clip['end_time']
# Snap end of dragging clip to start of other clips
if abs((new_time + clip_duration) - clip['start_time']) < snap_distance:
return clip['start_time'] - clip_duration
return new_time
def cut_clip_at_position(self, clip, cut_time):
"""Cut a clip at the specified time"""
if cut_time <= clip['start_time'] or cut_time >= clip['end_time']:
return
# Create two new clips
first_clip = clip.copy()
first_clip['id'] = len(self.timeline_clips) + 1
first_clip['end_time'] = cut_time
first_clip['name'] = f"{clip['name']} (1)"
second_clip = clip.copy()
second_clip['id'] = len(self.timeline_clips) + 2
second_clip['start_time'] = cut_time
second_clip['name'] = f"{clip['name']} (2)"
# Remove original clip and add new ones
self.timeline_clips.remove(clip)
self.timeline_clips.extend([first_clip, second_clip])
self.selected_clip = first_clip
self.update_timeline()
print(f"✂️ Cut clip at {cut_time:.2f}s")
def cut_clip_at_playhead(self):
"""Cut selected clip at current playhead position"""
if self.selected_clip:
self.cut_clip_at_position(self.selected_clip, self.current_time)
def delete_clip(self, clip):
"""Delete a clip from timeline"""
if clip in self.timeline_clips:
self.timeline_clips.remove(clip)
if self.selected_clip == clip:
self.selected_clip = None
self.update_timeline()
print(f"🗑️ Deleted clip: {clip['name']}")
def duplicate_clip(self, clip):
"""Duplicate a clip"""
new_clip = clip.copy()
new_clip['id'] = len(self.timeline_clips) + 1
new_clip['name'] = f"{clip['name']} (Copy)"
# Place after original clip
duration = clip['end_time'] - clip['start_time']
new_clip['start_time'] = clip['end_time']
new_clip['end_time'] = clip['end_time'] + duration
self.timeline_clips.append(new_clip)
self.selected_clip = new_clip
self.update_timeline()
print(f"📄 Duplicated clip: {new_clip['name']}")
def add_marker_at_time(self, time):
"""Add a marker at specified time"""
marker = {
'time': time,
'name': f"Marker {len(self.markers) + 1}",
'color': '#ffeb3b'
}
self.markers.append(marker)
self.update_timeline()
print(f"📍 Added marker at {time:.2f}s")
def show_clip_properties(self, clip):
"""Show clip properties dialog"""
props_window = tk.Toplevel(self.root)
props_window.title(f"Clip Properties - {clip['name']}")
props_window.configure(bg=self.colors['bg_primary'])
props_window.geometry("400x300")
# Clip info
tk.Label(props_window, text=f"Clip: {clip['name']}",
font=self.fonts['heading'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary']).pack(pady=10)
info_frame = tk.Frame(props_window, bg=self.colors['bg_primary'])
info_frame.pack(fill='x', padx=20)
# Duration, start time, etc.
duration = clip['end_time'] - clip['start_time']
tk.Label(info_frame, text=f"Duration: {duration:.2f}s",
bg=self.colors['bg_primary'], fg=self.colors['text_secondary']).pack(anchor='w')
tk.Label(info_frame, text=f"Start: {clip['start_time']:.2f}s",
bg=self.colors['bg_primary'], fg=self.colors['text_secondary']).pack(anchor='w')
tk.Label(info_frame, text=f"End: {clip['end_time']:.2f}s",
bg=self.colors['bg_primary'], fg=self.colors['text_secondary']).pack(anchor='w')
tk.Label(info_frame, text=f"Track: {clip['track']}",
bg=self.colors['bg_primary'], fg=self.colors['text_secondary']).pack(anchor='w')
def zoom_to_fit(self):
"""Zoom timeline to fit all content"""
if not self.timeline_clips:
return
# Find the last clip end time
max_time = max(clip['end_time'] for clip in self.timeline_clips)
canvas_width = self.timeline_canvas.winfo_width()
if max_time > 0 and canvas_width > 100:
zoom_level = (canvas_width - 100) / (max_time * 50) # 50 is base scale
self.zoom_var.set(max(0.1, min(5.0, zoom_level)))
self.on_zoom_change(zoom_level)
print(f"🔍 Zoomed to fit content ({zoom_level:.1f}x)")
def open_shorts_editor(shorts_folder="shorts"):
"""Open the shorts editor as a standalone application"""

0
video_editor_clean.py Normal file
View File