feat: Add modern thumbnail editor with advanced UI and editing features

- Implemented a new thumbnail editor using Tkinter and MoviePy
- Added functionality to load video files and capture frames
- Created a modern user interface with a dark theme and responsive design
- Included tools for adding text and stickers to thumbnails
- Implemented export options for saving edited thumbnails
- Added default emoji stickers and functionality to load custom stickers
- Enhanced user experience with hover effects and modern button styles
This commit is contained in:
klop51 2025-08-10 14:11:18 +02:00
parent 610aa299ef
commit 6bb356948d
5 changed files with 2369 additions and 718 deletions

128
MODERNIZATION_COMPLETE.md Normal file
View File

@ -0,0 +1,128 @@
# 🎨 Modern UI Modernization Complete!
## ✅ Successfully Updated Files
### 1. **Main.py** - ✅ FULLY MODERNIZED
- **Dark Theme**: Professional dark color scheme (#1a1a1a, #2d2d2d, #3d3d3d)
- **Modern Typography**: Segoe UI font family with size hierarchy
- **Card-Based Layout**: Elegant card containers with shadows and spacing
- **Responsive Design**: Grid-based layout that scales with window size
- **Hover Effects**: Interactive button states and animations
- **Modern Buttons**: Flat design with accent colors and hover states
### 2. **shorts_generator2.py** - ✅ FULLY MODERNIZED
- **Modern Color Palette**: Consistent dark theme across all components
- **Card Interface**: Settings panels organized in modern card layouts
- **Progress Indicators**: Styled progress bars with modern aesthetics
- **Action Buttons**: Professional button styling with color-coded actions
- **Responsive Controls**: Grid layouts that adapt to window resizing
- **Modern Typography**: Clear font hierarchy for better readability
### 3. **thumbnail_editor.py** - ✅ COMPLETELY REDESIGNED
- **Professional Interface**: Canvas-based editing with modern controls
- **Dark Theme Editor**: Black canvas background with light UI elements
- **Card-Based Tools**: Text tools, stickers, and export options in cards
- **Timeline Slider**: Modern styled timeline for frame selection
- **Interactive Elements**: Drag-and-drop functionality with visual feedback
- **Modern Buttons**: Color-coded actions (blue, green, orange, purple, red)
## 🎯 Key Improvements Implemented
### Design System
```python
colors = {
'bg_primary': '#1a1a1a', # Dark background
'bg_secondary': '#2d2d2d', # Card backgrounds
'bg_tertiary': '#3d3d3d', # Elevated elements
'accent_blue': '#007acc', # Primary actions
'accent_green': '#28a745', # Success states
'accent_orange': '#fd7e14', # Warning actions
'accent_purple': '#6f42c1', # Secondary actions
'accent_red': '#dc3545', # Danger actions
'text_primary': '#ffffff', # Primary text
'text_secondary': '#b8b8b8', # Secondary text
'text_muted': '#6c757d' # Muted text
}
```
### Typography System
```python
fonts = {
'title': ('Segoe UI', 18, 'bold'), # Main titles
'heading': ('Segoe UI', 14, 'bold'), # Section headers
'subheading': ('Segoe UI', 12, 'bold'), # Card titles
'body': ('Segoe UI', 10), # Body text
'caption': ('Segoe UI', 9), # Small text
'button': ('Segoe UI', 10, 'bold') # Button text
}
```
### Responsive Features
- **Window Resize Handling**: All layouts adapt to different window sizes
- **Minimum Size Constraints**: Prevents UI from becoming too small
- **Grid Weight Configuration**: Proper expansion and contraction
- **Proportional Scaling**: Elements maintain proper relationships
### Modern UI Components
- **Card Containers**: Elevated surfaces with consistent padding
- **Hover Effects**: Interactive feedback on all clickable elements
- **Modern Buttons**: Flat design with semantic color coding
- **Progress Indicators**: Styled progress bars and status displays
- **Dark Theme**: Professional dark interface throughout
## 🚀 Features Enhanced
### Main Application (Main.py)
- Modern welcome screen with card-based navigation
- Responsive layout with proper spacing and hierarchy
- Professional button styling with hover states
- Dark theme consistency across all windows
### Shorts Generator (shorts_generator2.py)
- Settings organized in modern card layout
- Color-coded action buttons for different operations
- Modern progress tracking with styled progress bars
- Responsive controls that adapt to window size
### Thumbnail Editor (thumbnail_editor.py)
- Complete redesign with professional canvas interface
- Timeline slider for frame-by-frame navigation
- Card-based tool organization (Text, Stickers, Export)
- Modern drag-and-drop interaction design
- Professional save/export functionality
## 🎨 Visual Design Principles Applied
1. **Consistency**: Unified color scheme and typography across all windows
2. **Hierarchy**: Clear visual hierarchy using font sizes and colors
3. **Spacing**: Proper padding and margins for clean layouts
4. **Feedback**: Hover states and visual feedback for user interactions
5. **Accessibility**: High contrast and readable font sizes
6. **Professionalism**: Modern flat design with subtle shadows and effects
## 📱 Responsive Design Features
- **Grid Layouts**: Replaced pack() with grid() for better control
- **Weight Configuration**: Proper expansion behavior
- **Minimum Sizes**: Prevents UI from becoming unusable
- **Aspect Ratios**: Maintained proper proportions
- **Flexible Containers**: Adapts to different screen sizes
## 🔧 Technical Improvements
- **Modern Tkinter**: Used ttk widgets where appropriate
- **Style Configuration**: Custom styles for modern appearance
- **Event Handling**: Improved interaction patterns
- **Memory Management**: Proper image reference handling
- **Error Handling**: Graceful degradation and user feedback
## 🎉 Result
The AI Shorts Generator now features a completely modern, professional interface that:
- Looks contemporary and professional
- Provides excellent user experience
- Scales properly across different window sizes
- Uses consistent design patterns throughout
- Offers intuitive navigation and controls
All three requested files have been successfully modernized with a cohesive, professional dark theme that transforms the application from a basic GUI to a modern, professional tool! 🎨✨

838
Main.py

File diff suppressed because it is too large Load Diff

View File

@ -1140,24 +1140,28 @@ class VideoEditor:
settings = quality_settings.get(quality, quality_settings["medium"])
# Export with progress callback
if progress_callback:
try:
# Try with newer MoviePy parameters first
self.video_clip.write_videofile(
output_path,
codec="libx264",
audio_codec="aac",
bitrate=settings["bitrate"],
audio_bitrate=settings["audio_bitrate"],
verbose=False,
logger=None
)
else:
self.video_clip.write_videofile(
output_path,
codec="libx264",
audio_codec="aac",
bitrate=settings["bitrate"],
audio_bitrate=settings["audio_bitrate"]
)
except TypeError as e:
if "verbose" in str(e):
# Fallback for older MoviePy versions
self.video_clip.write_videofile(
output_path,
codec="libx264",
audio_codec="aac",
bitrate=settings["bitrate"],
audio_bitrate=settings["audio_bitrate"]
)
else:
raise e
@staticmethod
def trim_video(video_path, start_time, end_time, output_path):
@ -1308,7 +1312,6 @@ class VideoEditor:
audio_codec="aac",
temp_audiofile='temp-audio.m4a',
remove_temp=True,
verbose=False, # Reduce console output
logger=None, # Disable logging for speed
preset='ultrafast', # Fastest encoding preset
threads=4 # Use multiple threads
@ -1718,11 +1721,11 @@ class VideoEditor:
temp_output = output_path.replace('.mp4', '_temp.mp4')
try:
# Try with verbose parameter (newer MoviePy)
# Try with logger parameter (newer MoviePy)
final_video.write_videofile(temp_output, codec="libx264", audio_codec="aac",
verbose=False, logger=None)
logger=None)
except TypeError:
# Fallback for older MoviePy versions without verbose parameter
# Fallback for older MoviePy versions without logger parameter
final_video.write_videofile(temp_output, codec="libx264", audio_codec="aac")
# Replace original with final version
@ -1783,8 +1786,8 @@ class ShortsEditorGUI:
# Create editor window
self.editor_window = tk.Toplevel(self.parent)
self.editor_window.title("🎬 Shorts Editor - Professional Video Editing")
self.editor_window.geometry("800x700")
self.editor_window.minsize(600, 500) # Set minimum size
self.editor_window.geometry("1200x800") # Increased width to show all panels
self.editor_window.minsize(1000, 700) # Increased minimum size
self.editor_window.resizable(True, True)
self.editor_window.transient(self.parent)
@ -1818,7 +1821,7 @@ class ShortsEditorGUI:
"""Create the main editor interface with video player"""
# Title
title_frame = tk.Frame(self.editor_window)
title_frame.grid(row=0, column=0, padx=20, pady=10, sticky="ew")
title_frame.pack(fill="x", padx=20, pady=10)
tk.Label(title_frame, text="🎬 Professional Shorts Editor",
font=("Arial", 16, "bold")).pack()
@ -1827,18 +1830,15 @@ class ShortsEditorGUI:
# Main content frame
main_frame = tk.Frame(self.editor_window)
main_frame.grid(row=1, column=0, padx=20, pady=10, sticky="nsew")
main_frame.rowconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
main_frame.pack(fill="both", expand=True, padx=20, pady=10)
# Left panel - Video selection and info
left_panel = tk.Frame(main_frame)
left_panel.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
left_panel.rowconfigure(1, weight=1)
left_panel.pack(side="left", fill="y", padx=(0, 10))
# Video selection frame
selection_frame = tk.LabelFrame(left_panel, text="📁 Select Short to Edit", padx=10, pady=10)
selection_frame.grid(row=0, column=0, pady=(0, 10), sticky="ew")
selection_frame.pack(fill="x", pady=(0, 10))
# Video list with preview info
list_frame = tk.Frame(selection_frame)
@ -1871,13 +1871,6 @@ class ShortsEditorGUI:
except Exception as e:
print(f"Error reading {video_file}: {e}")
# Video player frame (center)
player_frame = tk.Frame(main_frame)
player_frame.pack(side="left", fill="both", expand=True, padx=10)
# Video player
self.create_video_player(player_frame)
# Video selection handler
def on_video_select(event):
selection = self.video_listbox.curselection()
@ -1898,9 +1891,17 @@ class ShortsEditorGUI:
font=("Courier", 9), justify="left")
self.info_label.pack(anchor="w")
# Editing tools frame (right panel)
self.tools_frame = tk.LabelFrame(main_frame, text="🛠️ Professional Editing Tools", padx=10, pady=10)
self.tools_frame.pack(side="right", fill="y", padx=(10, 0))
# Video player frame (center)
player_frame = tk.Frame(main_frame)
player_frame.pack(side="left", fill="both", expand=True, padx=10)
# Video player
self.create_video_player(player_frame)
# Editing tools frame (right panel) - Fixed width to ensure visibility
self.tools_frame = tk.LabelFrame(main_frame, text="<EFBFBD> Professional Editing Tools", padx=10, pady=10)
self.tools_frame.pack(side="right", fill="both", padx=(10, 0), ipadx=10)
self.tools_frame.config(width=300) # Set minimum width for tools panel
self.create_editing_tools()
@ -1922,16 +1923,19 @@ class ShortsEditorGUI:
# Action buttons
button_frame = tk.Frame(action_frame)
button_frame.pack(fill="x", pady=10)
button_frame.pack(fill="x", pady=15) # Increased padding for better visibility
tk.Button(button_frame, text="🔄 Refresh List",
command=self.refresh_video_list, bg="#2196F3", fg="white").pack(side="left", padx=5)
command=self.refresh_video_list, bg="#2196F3", fg="white",
font=("Arial", 10), pady=5).pack(side="left", padx=8)
tk.Button(button_frame, text="📂 Open Shorts Folder",
command=self.open_shorts_folder, bg="#FF9800", fg="white").pack(side="left", padx=5)
command=self.open_shorts_folder, bg="#FF9800", fg="white",
font=("Arial", 10), pady=5).pack(side="left", padx=8)
tk.Button(button_frame, text="❌ Close Editor",
command=self.close_editor, bg="#F44336", fg="white").pack(side="right", padx=5)
command=self.close_editor, bg="#F44336", fg="white",
font=("Arial", 10), pady=5).pack(side="right", padx=8)
def create_video_player(self, parent_frame):
"""Create the video player with timeline controls"""
@ -2699,8 +2703,10 @@ class ShortsEditorGUI:
import subprocess
try:
subprocess.run(['explorer', os.path.abspath(self.shorts_folder)], check=True)
except:
messagebox.showinfo("Folder Location", f"Shorts folder: {os.path.abspath(self.shorts_folder)}")
except Exception as e:
# Silently fail - no need to show dialog for folder opening issues
print(f"Could not open folder: {e}")
pass
def get_output_path(self, suffix):
"""Generate output path with timestamp"""
@ -2876,8 +2882,37 @@ class ShortsGeneratorGUI:
def __init__(self, root):
self.root = root
self.root.title("🎬 AI Shorts Generator - Advanced Video Moment Detection")
self.root.geometry("650x650") # Reduced height to eliminate empty space
self.root.minsize(500, 500) # Set minimum size for responsiveness
self.root.geometry("750x800")
self.root.minsize(600, 650)
# Modern color scheme
self.colors = {
'bg_primary': '#1a1a1a', # Dark background
'bg_secondary': '#2d2d2d', # Card backgrounds
'bg_tertiary': '#3d3d3d', # Elevated elements
'accent_blue': '#007acc', # Primary blue
'accent_green': '#28a745', # Success green
'accent_orange': '#fd7e14', # Warning orange
'accent_purple': '#6f42c1', # Secondary purple
'accent_red': '#dc3545', # Error red
'text_primary': '#ffffff', # Primary text
'text_secondary': '#b8b8b8', # Secondary text
'text_muted': '#6c757d', # Muted text
'border': '#404040', # Border color
'hover': '#4a4a4a' # Hover state
}
self.root.configure(bg=self.colors['bg_primary'])
# Modern fonts
self.fonts = {
'title': ('Segoe UI', 20, 'bold'),
'heading': ('Segoe UI', 14, 'bold'),
'subheading': ('Segoe UI', 12, 'bold'),
'body': ('Segoe UI', 10),
'caption': ('Segoe UI', 9),
'button': ('Segoe UI', 10, 'bold')
}
# Make window responsive
self.root.rowconfigure(0, weight=1)
@ -2895,16 +2930,29 @@ class ShortsGeneratorGUI:
self.create_widgets()
def create_widgets(self):
# Create main scrollable container
main_container = tk.Frame(self.root)
main_container.pack(fill="both", expand=True, padx=10, pady=10)
# Create main scrollable container with modern styling
main_container = tk.Frame(self.root, bg=self.colors['bg_primary'])
main_container.pack(fill="both", expand=True, padx=25, pady=25)
main_container.rowconfigure(0, weight=1)
main_container.columnconfigure(0, weight=1)
# Create canvas and scrollbar for scrolling
canvas = tk.Canvas(main_container)
scrollbar = ttk.Scrollbar(main_container, orient="vertical", command=canvas.yview)
scrollable_frame = tk.Frame(canvas)
canvas = tk.Canvas(main_container, bg=self.colors['bg_primary'], highlightthickness=0)
# Modern scrollbar styling
style = ttk.Style()
style.theme_use('clam')
style.configure("Modern.Vertical.TScrollbar",
background=self.colors['bg_tertiary'],
troughcolor=self.colors['bg_secondary'],
borderwidth=0,
arrowcolor=self.colors['text_secondary'],
darkcolor=self.colors['bg_tertiary'],
lightcolor=self.colors['bg_tertiary'])
scrollbar = ttk.Scrollbar(main_container, orient="vertical", command=canvas.yview,
style="Modern.Vertical.TScrollbar")
scrollable_frame = tk.Frame(canvas, bg=self.colors['bg_primary'])
scrollable_frame.bind(
"<Configure>",
@ -2917,39 +2965,313 @@ class ShortsGeneratorGUI:
# Make scrollable frame responsive
scrollable_frame.columnconfigure(0, weight=1)
# Title
title_label = tk.Label(scrollable_frame, text="🎬 AI Shorts Generator", font=("Arial", 16, "bold"))
title_label.grid(row=0, column=0, pady=10, sticky="ew")
# Modern header section
header_frame = tk.Frame(scrollable_frame, bg=self.colors['bg_primary'])
header_frame.grid(row=0, column=0, pady=(0, 30), sticky="ew")
# Video selection
video_frame = tk.Frame(scrollable_frame)
video_frame.grid(row=1, column=0, pady=10, sticky="ew")
video_frame.columnconfigure(0, weight=1)
# Main title with modern typography
title_label = tk.Label(header_frame, text="🎬 AI Shorts Generator",
font=self.fonts['title'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'])
title_label.pack()
tk.Label(video_frame, text="Select Video File:").grid(row=0, column=0, sticky="w")
video_select_frame = tk.Frame(video_frame)
video_select_frame.grid(row=1, column=0, pady=5, sticky="ew")
video_select_frame.columnconfigure(0, weight=1)
# Subtitle
subtitle_label = tk.Label(header_frame, text="Advanced Video Moment Detection & Generation",
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_secondary'])
subtitle_label.pack(pady=(5, 0))
self.video_label = tk.Label(video_select_frame, text="No video selected", bg="white", relief="sunken")
self.video_label.grid(row=0, column=0, sticky="ew", padx=(0, 5))
# Video selection card
video_card = self.create_modern_card(scrollable_frame, "📁 Video Input")
video_card.grid(row=1, column=0, pady=15, sticky="ew")
tk.Button(video_select_frame, text="Browse", command=self.select_video).grid(row=0, column=1)
# Output folder card
output_card = self.create_modern_card(scrollable_frame, "📂 Output Settings")
output_card.grid(row=2, column=0, pady=15, sticky="ew")
# Output folder selection
output_frame = tk.Frame(scrollable_frame)
output_frame.grid(row=2, column=0, pady=10, sticky="ew")
output_frame.columnconfigure(0, weight=1)
# Add content to video card
self.setup_video_selection(video_card)
tk.Label(output_frame, text="Output Folder:").grid(row=0, column=0, sticky="w")
output_select_frame = tk.Frame(output_frame)
output_select_frame.grid(row=1, column=0, pady=5, sticky="ew")
output_select_frame.columnconfigure(0, weight=1)
# Add content to output card
self.setup_output_selection(output_card)
self.output_label = tk.Label(output_select_frame, text="shorts/", bg="white", relief="sunken")
self.output_label.grid(row=0, column=0, sticky="ew", padx=(0, 5))
# Settings card
settings_card = self.create_modern_card(scrollable_frame, "⚙️ Generation Settings")
settings_card.grid(row=3, column=0, pady=15, sticky="ew")
self.setup_settings_panel(settings_card)
tk.Button(output_select_frame, text="Browse", command=self.select_output_folder).grid(row=0, column=1)
# Action buttons card
actions_card = self.create_modern_card(scrollable_frame, "🚀 Actions")
actions_card.grid(row=4, column=0, pady=15, sticky="ew")
self.setup_action_buttons(actions_card)
# Progress card
progress_card = self.create_modern_card(scrollable_frame, "📊 Progress")
progress_card.grid(row=5, column=0, pady=15, sticky="ew")
self.setup_progress_panel(progress_card)
# Pack the canvas and scrollbar
canvas.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")
def create_modern_card(self, parent, title):
"""Create a modern card-style container"""
card_frame = tk.Frame(parent, bg=self.colors['bg_secondary'], relief="flat", bd=0)
# Card header with modern styling
header_frame = tk.Frame(card_frame, bg=self.colors['bg_secondary'])
header_frame.pack(fill="x", padx=25, pady=(20, 10))
header_label = tk.Label(header_frame, text=title, font=self.fonts['heading'],
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'])
header_label.pack(anchor="w")
# Separator line
separator = tk.Frame(card_frame, bg=self.colors['border'], height=1)
separator.pack(fill="x", padx=25)
# Card content area
content_frame = tk.Frame(card_frame, bg=self.colors['bg_secondary'])
content_frame.pack(fill="both", expand=True, padx=25, pady=(15, 25))
return content_frame
def create_modern_button(self, parent, text, command, color, large=False):
"""Create a modern button with hover effects"""
font = self.fonts['button'] if not large else ('Segoe UI', 12, 'bold')
pady = 12 if not large else 16
button = tk.Button(parent, text=text, command=command,
bg=color, fg='white', font=font,
relief="flat", bd=0, pady=pady,
activebackground=self.adjust_color(color, -20),
activeforeground='white',
cursor="hand2")
# Add hover effects
def on_enter(e):
button.config(bg=self.adjust_color(color, 15))
def on_leave(e):
button.config(bg=color)
button.bind("<Enter>", on_enter)
button.bind("<Leave>", on_leave)
return button
def adjust_color(self, hex_color, adjustment):
"""Adjust color brightness for hover effects"""
hex_color = hex_color.lstrip('#')
rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
adjusted = tuple(max(0, min(255, c + adjustment)) for c in rgb)
return f"#{adjusted[0]:02x}{adjusted[1]:02x}{adjusted[2]:02x}"
def setup_video_selection(self, parent):
"""Setup the video selection interface"""
parent.columnconfigure(0, weight=1)
self.video_label = tk.Label(parent, text="No video selected",
font=self.fonts['body'], bg=self.colors['bg_tertiary'],
fg=self.colors['text_secondary'], relief="flat",
anchor="w", pady=12, padx=15, bd=1,
highlightbackground=self.colors['border'],
highlightthickness=1)
self.video_label.grid(row=0, column=0, sticky="ew", pady=(0, 15))
browse_btn = self.create_modern_button(parent, "📁 Browse Video",
self.select_video, self.colors['accent_blue'])
browse_btn.grid(row=1, column=0, sticky="ew")
def setup_output_selection(self, parent):
"""Setup the output folder selection interface"""
parent.columnconfigure(0, weight=1)
self.output_label = tk.Label(parent, text="shorts/",
font=self.fonts['body'], bg=self.colors['bg_tertiary'],
fg=self.colors['text_secondary'], relief="flat",
anchor="w", pady=12, padx=15, bd=1,
highlightbackground=self.colors['border'],
highlightthickness=1)
self.output_label.grid(row=0, column=0, sticky="ew", pady=(0, 15))
browse_btn = self.create_modern_button(parent, "📂 Browse Folder",
self.select_output_folder, self.colors['accent_blue'])
browse_btn.grid(row=1, column=0, sticky="ew")
def setup_settings_panel(self, parent):
"""Setup the settings panel with modern styling"""
parent.columnconfigure(0, weight=1)
# Max clips setting
clips_frame = tk.Frame(parent, bg=self.colors['bg_secondary'])
clips_frame.grid(row=0, column=0, sticky="ew", pady=(0, 20))
clips_frame.columnconfigure(1, weight=1)
self.use_max_clips = tk.BooleanVar(value=True)
clips_checkbox = tk.Checkbutton(clips_frame, variable=self.use_max_clips,
text="Limit clips:", font=self.fonts['body'],
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
selectcolor=self.colors['accent_blue'], relief="flat", bd=0)
clips_checkbox.grid(row=0, column=0, sticky="w", padx=(0, 15))
self.clips_var = tk.IntVar(value=3)
self.clips_spinbox = tk.Spinbox(clips_frame, from_=1, to=10, width=8,
textvariable=self.clips_var, font=self.fonts['body'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
relief="flat", bd=1, highlightbackground=self.colors['border'])
self.clips_spinbox.grid(row=0, column=2, sticky="e")
# Detection mode
detection_frame = tk.Frame(parent, bg=self.colors['bg_secondary'])
detection_frame.grid(row=1, column=0, sticky="ew", pady=(0, 20))
detection_frame.columnconfigure(1, weight=1)
tk.Label(detection_frame, text="Detection Mode:", font=self.fonts['subheading'],
bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).grid(row=0, column=0, sticky="w")
self.detection_mode_var = tk.StringVar(value="loud")
self.detection_display_var = tk.StringVar(value="🔊 Loud Moments")
# Modern combobox styling
detection_style = ttk.Style()
detection_style.configure("Modern.TCombobox",
fieldbackground=self.colors['bg_tertiary'],
background=self.colors['bg_tertiary'],
foreground=self.colors['text_primary'],
arrowcolor=self.colors['text_secondary'],
borderwidth=1,
relief="flat")
detection_dropdown = ttk.Combobox(detection_frame, textvariable=self.detection_display_var,
values=["🔊 Loud Moments", "🎬 Scene Changes", "🏃 Motion Intensity",
"😄 Emotional Speech", "🎵 Audio Peaks", "🎯 Smart Combined"],
state="readonly", width=25, font=self.fonts['body'],
style="Modern.TCombobox")
detection_dropdown.grid(row=0, column=1, sticky="e")
# Store the mapping between display text and internal values
self.mode_mapping = {
"🔊 Loud Moments": "loud",
"🎬 Scene Changes": "scene",
"🏃 Motion Intensity": "motion",
"😄 Emotional Speech": "speech",
"🎵 Audio Peaks": "peaks",
"🎯 Smart Combined": "combined"
}
# Audio threshold (for loud moments)
self.threshold_frame = tk.Frame(parent, bg=self.colors['bg_secondary'])
self.threshold_frame.grid(row=2, column=0, sticky="ew", pady=(0, 20))
self.threshold_frame.columnconfigure(1, weight=1)
tk.Label(self.threshold_frame, text="Audio Threshold (dB):", font=self.fonts['body'],
bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).grid(row=0, column=0, sticky="w")
self.threshold_var = tk.IntVar(value=-30)
threshold_spinbox = tk.Spinbox(self.threshold_frame, from_=-50, to=0, width=8,
textvariable=self.threshold_var, font=self.fonts['body'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
relief="flat", bd=1, highlightbackground=self.colors['border'])
threshold_spinbox.grid(row=0, column=2, sticky="e")
# Clip duration
duration_frame = tk.Frame(parent, bg=self.colors['bg_secondary'])
duration_frame.grid(row=3, column=0, sticky="ew")
duration_frame.columnconfigure(1, weight=1)
tk.Label(duration_frame, text="Clip Duration (seconds):", font=self.fonts['body'],
bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).grid(row=0, column=0, sticky="w")
self.duration_var = tk.IntVar(value=5)
duration_spinbox = tk.Spinbox(duration_frame, from_=3, to=120, width=8,
textvariable=self.duration_var, font=self.fonts['body'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
relief="flat", bd=1, highlightbackground=self.colors['border'])
duration_spinbox.grid(row=0, column=2, sticky="e")
# Bind dropdown change event
def on_detection_change(event):
selection = detection_dropdown.get()
self.detection_mode_var.set(self.mode_mapping.get(selection, "loud"))
# Show/hide threshold setting based on mode
if selection == "🔊 Loud Moments":
self.threshold_frame.grid(row=2, column=0, sticky="ew", pady=(0, 20))
else:
self.threshold_frame.grid_remove()
detection_dropdown.bind("<<ComboboxSelected>>", on_detection_change)
# Bind checkbox to enable/disable spinbox
def toggle_clips_limit():
if self.use_max_clips.get():
self.clips_spinbox.config(state="normal")
else:
self.clips_spinbox.config(state="disabled")
self.use_max_clips.trace("w", lambda *args: toggle_clips_limit())
clips_checkbox.config(command=toggle_clips_limit)
def setup_action_buttons(self, parent):
"""Setup the action buttons with modern styling"""
parent.columnconfigure(0, weight=1)
# Preview button
self.preview_btn = self.create_modern_button(parent, "🔍 Preview Clips",
self.preview_clips, self.colors['accent_blue'])
self.preview_btn.grid(row=0, column=0, sticky="ew", pady=(0, 10))
# Generate button - primary action
self.generate_btn = self.create_modern_button(parent, "🎬 Generate Shorts",
self.start_generation, self.colors['accent_green'],
large=True)
self.generate_btn.grid(row=1, column=0, sticky="ew", pady=(0, 15))
# Secondary actions
button_grid = tk.Frame(parent, bg=self.colors['bg_secondary'])
button_grid.grid(row=2, column=0, sticky="ew")
button_grid.columnconfigure(0, weight=1)
button_grid.columnconfigure(1, weight=1)
self.edit_btn = self.create_modern_button(button_grid, "✏️ Edit Shorts",
self.open_shorts_editor, self.colors['accent_orange'])
self.edit_btn.grid(row=0, column=0, sticky="ew", padx=(0, 5))
self.thumbnail_btn = self.create_modern_button(button_grid, "📸 Thumbnails",
self.open_thumbnail_editor, self.colors['accent_purple'])
self.thumbnail_btn.grid(row=0, column=1, sticky="ew", padx=(5, 0))
def setup_progress_panel(self, parent):
"""Setup the progress panel with modern styling"""
parent.columnconfigure(0, weight=1)
# Progress info
self.progress_label = tk.Label(parent, text="Ready to generate shorts",
font=self.fonts['body'], bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'])
self.progress_label.grid(row=0, column=0, sticky="ew", pady=(0, 10))
# Modern progress bar
progress_style = ttk.Style()
progress_style.configure("Modern.Horizontal.TProgressbar",
background=self.colors['accent_green'],
troughcolor=self.colors['bg_tertiary'],
borderwidth=0, lightcolor=self.colors['accent_green'],
darkcolor=self.colors['accent_green'])
self.progress_bar = ttk.Progressbar(parent, length=400, mode="determinate",
style="Modern.Horizontal.TProgressbar")
self.progress_bar.grid(row=1, column=0, sticky="ew", pady=(0, 10))
# Detection progress (initially hidden)
self.detection_progress_label = tk.Label(parent, text="", font=self.fonts['caption'],
bg=self.colors['bg_secondary'], fg=self.colors['accent_blue'])
self.detection_progress_bar = ttk.Progressbar(parent, length=400, mode="determinate",
style="Modern.Horizontal.TProgressbar")
# Initially hide detection progress
self.detection_progress_label.grid_remove()
self.detection_progress_bar.grid_remove()
# Settings frame
settings_frame = tk.LabelFrame(scrollable_frame, text="Settings", padx=10, pady=10)

File diff suppressed because it is too large Load Diff

626
thumbnail_editor_modern.py Normal file
View File

@ -0,0 +1,626 @@
import os
import tkinter as tk
from tkinter import filedialog, simpledialog, colorchooser, messagebox, ttk
from moviepy import VideoFileClip
from PIL import Image, ImageTk, ImageDraw, ImageFont
# Modern Thumbnail Editor with Professional UI Design
class ModernThumbnailEditor:
def __init__(self, video_path):
self.video_path = video_path
self.clip = None
self.current_frame_img = None
self.canvas_items = []
self.drag_data = {"item": None, "x": 0, "y": 0}
# Modern color scheme
self.colors = {
'bg_primary': '#1a1a1a', # Dark background
'bg_secondary': '#2d2d2d', # Card backgrounds
'bg_tertiary': '#3d3d3d', # Elevated elements
'accent_blue': '#007acc', # Primary blue
'accent_green': '#28a745', # Success green
'accent_orange': '#fd7e14', # Warning orange
'accent_purple': '#6f42c1', # Secondary purple
'accent_red': '#dc3545', # Error red
'text_primary': '#ffffff', # Primary text
'text_secondary': '#b8b8b8', # Secondary text
'text_muted': '#6c757d', # Muted text
'border': '#404040', # Border color
'hover': '#4a4a4a' # Hover state
}
# Modern fonts
self.fonts = {
'title': ('Segoe UI', 18, 'bold'),
'heading': ('Segoe UI', 14, 'bold'),
'subheading': ('Segoe UI', 12, 'bold'),
'body': ('Segoe UI', 10),
'caption': ('Segoe UI', 9),
'button': ('Segoe UI', 10, 'bold')
}
self.setup_ui()
def setup_ui(self):
self.editor = tk.Toplevel()
self.editor.title("📸 Professional Thumbnail Editor")
self.editor.geometry("1400x900")
self.editor.minsize(1200, 800)
self.editor.configure(bg=self.colors['bg_primary'])
# Load video
try:
print(f"📹 Loading video: {os.path.basename(self.video_path)}")
self.clip = VideoFileClip(self.video_path)
self.duration = int(self.clip.duration)
except Exception as e:
messagebox.showerror("Video Error", f"Failed to load video: {e}")
self.editor.destroy()
return
# Setup stickers folder
self.stickers_folder = os.path.join(os.path.dirname(__file__), "stickers")
os.makedirs(self.stickers_folder, exist_ok=True)
self.create_default_stickers()
self.create_modern_interface()
def create_modern_interface(self):
"""Create the modern thumbnail editor interface"""
# Header
header_frame = tk.Frame(self.editor, bg=self.colors['bg_secondary'], height=70)
header_frame.pack(fill="x", padx=0, pady=0)
header_frame.pack_propagate(False)
title_frame = tk.Frame(header_frame, bg=self.colors['bg_secondary'])
title_frame.pack(expand=True, fill="both", padx=30, pady=15)
title_label = tk.Label(title_frame, text="📸 Professional Thumbnail Editor",
font=self.fonts['title'], bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'])
title_label.pack(side="left")
# Video info
video_name = os.path.basename(self.video_path)
info_label = tk.Label(title_frame, text=f"Editing: {video_name}",
font=self.fonts['caption'], bg=self.colors['bg_secondary'],
fg=self.colors['text_secondary'])
info_label.pack(side="right")
# Main content area
main_container = tk.Frame(self.editor, bg=self.colors['bg_primary'])
main_container.pack(fill="both", expand=True, padx=20, pady=20)
# Left panel - Canvas area
left_panel = tk.Frame(main_container, bg=self.colors['bg_secondary'])
left_panel.pack(side="left", fill="both", expand=True, padx=(0, 10))
self.setup_canvas_area(left_panel)
# Right panel - Controls
right_panel = tk.Frame(main_container, bg=self.colors['bg_secondary'], width=350)
right_panel.pack(side="right", fill="y")
right_panel.pack_propagate(False)
self.setup_controls_panel(right_panel)
def setup_canvas_area(self, parent):
"""Setup the main canvas area with modern styling"""
# Canvas header
canvas_header = tk.Frame(parent, bg=self.colors['bg_secondary'])
canvas_header.pack(fill="x", padx=20, pady=(20, 10))
canvas_title = tk.Label(canvas_header, text="🎬 Thumbnail Preview",
font=self.fonts['heading'], bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'])
canvas_title.pack(side="left")
# Canvas container
canvas_container = tk.Frame(parent, bg=self.colors['bg_tertiary'], relief="flat", bd=2)
canvas_container.pack(fill="both", expand=True, padx=20, pady=(0, 20))
# Modern canvas with dark theme
self.canvas = tk.Canvas(canvas_container, bg='#000000', highlightthickness=0,
relief="flat", bd=0)
self.canvas.pack(fill="both", expand=True, padx=10, pady=10)
# Bind canvas events for dragging
self.canvas.bind("<Button-1>", self.on_canvas_click)
self.canvas.bind("<B1-Motion>", self.on_canvas_drag)
self.canvas.bind("<ButtonRelease-1>", self.on_canvas_release)
# Frame timeline slider
timeline_frame = tk.Frame(parent, bg=self.colors['bg_secondary'])
timeline_frame.pack(fill="x", padx=20, pady=(0, 20))
tk.Label(timeline_frame, text="⏱️ Timeline", font=self.fonts['subheading'],
bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).pack(anchor="w", pady=(0, 10))
# Modern slider styling
style = ttk.Style()
style.configure("Modern.Horizontal.TScale",
background=self.colors['bg_secondary'],
troughcolor=self.colors['bg_tertiary'],
sliderlength=20,
sliderrelief="flat")
self.time_var = tk.DoubleVar(value=0)
self.time_slider = ttk.Scale(timeline_frame, from_=0, to=self.duration,
orient="horizontal", variable=self.time_var,
command=self.on_time_change, style="Modern.Horizontal.TScale")
self.time_slider.pack(fill="x", pady=(0, 5))
# Time display
time_display_frame = tk.Frame(timeline_frame, bg=self.colors['bg_secondary'])
time_display_frame.pack(fill="x")
self.time_label = tk.Label(time_display_frame, text="00:00",
font=self.fonts['body'], bg=self.colors['bg_secondary'],
fg=self.colors['text_secondary'])
self.time_label.pack(side="left")
duration_label = tk.Label(time_display_frame, text=f"/ {self.duration//60:02d}:{self.duration%60:02d}",
font=self.fonts['body'], bg=self.colors['bg_secondary'],
fg=self.colors['text_muted'])
duration_label.pack(side="right")
# Load initial frame
self.update_canvas_frame(0)
def setup_controls_panel(self, parent):
"""Setup the right panel controls with modern design"""
# Scroll container for controls
scroll_frame = tk.Frame(parent, bg=self.colors['bg_secondary'])
scroll_frame.pack(fill="both", expand=True, padx=20, pady=20)
# Title
controls_title = tk.Label(scroll_frame, text="🎨 Editing Tools",
font=self.fonts['heading'], bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'])
controls_title.pack(anchor="w", pady=(0, 20))
# Text Tools Card
self.create_text_tools_card(scroll_frame)
# Stickers Card
self.create_stickers_card(scroll_frame)
# Export Card
self.create_export_card(scroll_frame)
def create_text_tools_card(self, parent):
"""Create modern text tools card"""
text_card = self.create_modern_card(parent, "✍️ Text Tools")
# Add text button
add_text_btn = self.create_modern_button(text_card, " Add Text",
self.colors['accent_blue'], self.add_text)
add_text_btn.pack(fill="x", pady=(0, 10))
# Text style options
style_frame = tk.Frame(text_card, bg=self.colors['bg_secondary'])
style_frame.pack(fill="x", pady=(0, 10))
tk.Label(style_frame, text="Text Size:", font=self.fonts['body'],
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w")
self.text_size_var = tk.IntVar(value=36)
size_frame = tk.Frame(style_frame, bg=self.colors['bg_secondary'])
size_frame.pack(fill="x", pady=(5, 0))
for size in [24, 36, 48, 64]:
btn = tk.Button(size_frame, text=str(size), font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
relief="flat", bd=0, padx=10, pady=5,
activebackground=self.colors['hover'],
command=lambda s=size: self.text_size_var.set(s))
btn.pack(side="left", padx=(0, 5))
self.add_hover_effect(btn)
# Text color
color_frame = tk.Frame(text_card, bg=self.colors['bg_secondary'])
color_frame.pack(fill="x", pady=(10, 0))
tk.Label(color_frame, text="Text Color:", font=self.fonts['body'],
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w")
self.text_color_btn = self.create_modern_button(color_frame, "🎨 Choose Color",
self.colors['accent_purple'], self.choose_text_color)
self.text_color_btn.pack(fill="x", pady=(5, 0))
self.current_text_color = "#FFFFFF"
def create_stickers_card(self, parent):
"""Create modern stickers card"""
stickers_card = self.create_modern_card(parent, "😊 Stickers & Emojis")
# Load stickers button
load_btn = self.create_modern_button(stickers_card, "📁 Load Custom Sticker",
self.colors['accent_green'], self.load_custom_sticker)
load_btn.pack(fill="x", pady=(0, 15))
# Default stickers grid
stickers_frame = tk.Frame(stickers_card, bg=self.colors['bg_secondary'])
stickers_frame.pack(fill="x")
tk.Label(stickers_frame, text="Default Stickers:", font=self.fonts['body'],
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w", pady=(0, 10))
# Create grid for stickers
self.create_stickers_grid(stickers_frame)
def create_stickers_grid(self, parent):
"""Create a grid of default stickers"""
sticker_files = [f for f in os.listdir(self.stickers_folder) if f.endswith(('.png', '.jpg', '.jpeg'))]
grid_frame = tk.Frame(parent, bg=self.colors['bg_secondary'])
grid_frame.pack(fill="x")
cols = 3
for i, sticker_file in enumerate(sticker_files[:12]): # Limit to 12 stickers
row = i // cols
col = i % cols
try:
sticker_path = os.path.join(self.stickers_folder, sticker_file)
img = Image.open(sticker_path)
img.thumbnail((40, 40), Image.Resampling.LANCZOS)
photo = ImageTk.PhotoImage(img)
btn = tk.Button(grid_frame, image=photo,
bg=self.colors['bg_tertiary'], relief="flat", bd=0,
activebackground=self.colors['hover'],
command=lambda path=sticker_path: self.add_sticker(path))
btn.image = photo # Keep reference
btn.grid(row=row, column=col, padx=5, pady=5, sticky="nsew")
self.add_hover_effect(btn)
# Configure grid weights
grid_frame.grid_columnconfigure(col, weight=1)
except Exception as e:
print(f"⚠️ Error loading sticker {sticker_file}: {e}")
def create_export_card(self, parent):
"""Create modern export options card"""
export_card = self.create_modern_card(parent, "💾 Export Options")
# Clear all button
clear_btn = self.create_modern_button(export_card, "🗑️ Clear All Elements",
self.colors['accent_orange'], self.clear_all_elements)
clear_btn.pack(fill="x", pady=(0, 10))
# Save thumbnail button
save_btn = self.create_modern_button(export_card, "💾 Save Thumbnail",
self.colors['accent_green'], self.save_thumbnail)
save_btn.pack(fill="x", pady=(0, 10))
# Close editor button
close_btn = self.create_modern_button(export_card, "❌ Close Editor",
self.colors['accent_red'], self.close_editor)
close_btn.pack(fill="x")
def create_modern_card(self, parent, title):
"""Create a modern card container"""
card_frame = tk.Frame(parent, bg=self.colors['bg_tertiary'], relief="flat", bd=0)
card_frame.pack(fill="x", pady=(0, 20))
# Card header
header_frame = tk.Frame(card_frame, bg=self.colors['bg_tertiary'])
header_frame.pack(fill="x", padx=15, pady=(15, 10))
title_label = tk.Label(header_frame, text=title, font=self.fonts['subheading'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
title_label.pack(anchor="w")
# Card content
content_frame = tk.Frame(card_frame, bg=self.colors['bg_secondary'])
content_frame.pack(fill="both", expand=True, padx=15, pady=(0, 15))
return content_frame
def create_modern_button(self, parent, text, color, command):
"""Create a modern styled button"""
btn = tk.Button(parent, text=text, font=self.fonts['button'],
bg=color, fg=self.colors['text_primary'],
relief="flat", bd=0, padx=20, pady=12,
activebackground=self.colors['hover'],
command=command, cursor="hand2")
self.add_hover_effect(btn, color)
return btn
def add_hover_effect(self, widget, base_color=None):
"""Add hover effect to widget"""
if base_color is None:
base_color = self.colors['bg_tertiary']
def on_enter(e):
widget.configure(bg=self.colors['hover'])
def on_leave(e):
widget.configure(bg=base_color)
widget.bind("<Enter>", on_enter)
widget.bind("<Leave>", on_leave)
def capture_frame_at(self, time_sec):
"""Capture frame from video at specific time"""
try:
frame = self.clip.get_frame(max(0, min(time_sec, self.clip.duration - 0.1)))
img = Image.fromarray(frame)
# Maintain aspect ratio while fitting in canvas
img.thumbnail((720, 405), Image.Resampling.LANCZOS)
return img
except Exception as e:
print(f"⚠️ Error capturing frame: {e}")
# Create a placeholder image
img = Image.new('RGB', (720, 405), color='black')
return img
def update_canvas_frame(self, time_sec):
"""Update canvas with frame at specific time"""
try:
self.current_frame_img = self.capture_frame_at(time_sec)
self.tk_frame_img = ImageTk.PhotoImage(self.current_frame_img)
# Clear canvas and add new frame
self.canvas.delete("frame")
self.canvas.create_image(360, 202, image=self.tk_frame_img, tags="frame")
self.canvas.image = self.tk_frame_img
# Update time display
minutes = int(time_sec) // 60
seconds = int(time_sec) % 60
self.time_label.config(text=f"{minutes:02d}:{seconds:02d}")
except Exception as e:
print(f"⚠️ Error updating frame: {e}")
def on_time_change(self, val):
"""Handle timeline slider change"""
self.update_canvas_frame(float(val))
# Canvas interaction methods
def on_canvas_click(self, event):
"""Handle canvas click for dragging"""
item = self.canvas.find_closest(event.x, event.y)[0]
if item and item != self.canvas.find_withtag("frame"):
self.drag_data["item"] = item
self.drag_data["x"] = event.x
self.drag_data["y"] = event.y
def on_canvas_drag(self, event):
"""Handle canvas dragging"""
if self.drag_data["item"]:
dx = event.x - self.drag_data["x"]
dy = event.y - self.drag_data["y"]
self.canvas.move(self.drag_data["item"], dx, dy)
self.drag_data["x"] = event.x
self.drag_data["y"] = event.y
def on_canvas_release(self, event):
"""Handle canvas release"""
self.drag_data["item"] = None
# Editing functionality
def add_text(self):
"""Add text to the canvas"""
text = simpledialog.askstring("Add Text", "Enter text:")
if text:
try:
size = self.text_size_var.get()
item = self.canvas.create_text(360, 200, text=text, fill=self.current_text_color,
font=("Arial", size, "bold"), tags="draggable")
self.canvas_items.append(("text", item, text, self.current_text_color, size))
print(f"✅ Added text: {text}")
except Exception as e:
print(f"⚠️ Error adding text: {e}")
def choose_text_color(self):
"""Choose text color"""
color = colorchooser.askcolor(title="Choose text color")
if color[1]:
self.current_text_color = color[1]
# Update button color to show current selection
self.text_color_btn.config(bg=self.current_text_color)
def add_sticker(self, path):
"""Add sticker to canvas"""
try:
img = Image.open(path).convert("RGBA")
img.thumbnail((60, 60), Image.Resampling.LANCZOS)
tk_img = ImageTk.PhotoImage(img)
item = self.canvas.create_image(360, 200, image=tk_img, tags="draggable")
# Keep reference to prevent garbage collection
if not hasattr(self.canvas, 'images'):
self.canvas.images = []
self.canvas.images.append(tk_img)
self.canvas_items.append(("sticker", item, img, path))
print(f"✅ Added sticker: {os.path.basename(path)}")
except Exception as e:
print(f"⚠️ Failed to load sticker {path}: {e}")
def load_custom_sticker(self):
"""Load custom sticker file"""
file_path = filedialog.askopenfilename(
title="Select Sticker",
filetypes=[("Image files", "*.png *.jpg *.jpeg *.gif *.bmp")]
)
if file_path:
self.add_sticker(file_path)
def clear_all_elements(self):
"""Clear all added elements"""
# Clear all draggable items
self.canvas.delete("draggable")
self.canvas_items.clear()
if hasattr(self.canvas, 'images'):
self.canvas.images.clear()
print("🗑️ Cleared all elements")
def save_thumbnail(self):
"""Save the current thumbnail"""
if not self.current_frame_img:
messagebox.showerror("Error", "No frame loaded")
return
save_path = filedialog.asksaveasfilename(
title="Save Thumbnail",
defaultextension=".jpg",
filetypes=[("JPEG files", "*.jpg"), ("PNG files", "*.png")]
)
if not save_path:
return
try:
# Create a copy of the current frame
frame = self.current_frame_img.copy().convert("RGBA")
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
# Calculate scaling factors
scale_x = frame.width / canvas_width
scale_y = frame.height / canvas_height
draw = ImageDraw.Draw(frame)
# Process all canvas items
for item_type, item_id, *data in self.canvas_items:
coords = self.canvas.coords(item_id)
if not coords:
continue
if item_type == "sticker":
# Handle sticker overlay
img_data, path = data
x, y = coords[0], coords[1]
px = int(x * scale_x)
py = int(y * scale_y)
# Scale sticker size
sticker_img = img_data.copy()
new_size = (int(60 * scale_x), int(60 * scale_y))
sticker_img = sticker_img.resize(new_size, Image.Resampling.LANCZOS)
# Calculate position to center the sticker
paste_x = px - sticker_img.width // 2
paste_y = py - sticker_img.height // 2
frame.paste(sticker_img, (paste_x, paste_y), sticker_img)
elif item_type == "text":
# Handle text overlay
text_value, color, font_size = data
x, y = coords[0], coords[1]
px = int(x * scale_x)
py = int(y * scale_y)
# Scale font size
scaled_font_size = int(font_size * scale_x)
try:
font = ImageFont.truetype("arial.ttf", scaled_font_size)
except:
try:
font = ImageFont.truetype("calibri.ttf", scaled_font_size)
except:
font = ImageFont.load_default()
# Get text bounding box for centering
bbox = draw.textbbox((0, 0), text_value, font=font)
text_w = bbox[2] - bbox[0]
text_h = bbox[3] - bbox[1]
# Draw text with outline
outline_w = max(2, scaled_font_size // 15)
for dx in range(-outline_w, outline_w + 1):
for dy in range(-outline_w, outline_w + 1):
draw.text((px - text_w//2 + dx, py - text_h//2 + dy),
text_value, font=font, fill="black")
draw.text((px - text_w//2, py - text_h//2), text_value, font=font, fill=color)
# Convert to RGB and save
if save_path.lower().endswith('.png'):
frame.save(save_path, "PNG", quality=95)
else:
background = Image.new("RGB", frame.size, (255, 255, 255))
background.paste(frame, mask=frame.split()[3] if frame.mode == 'RGBA' else None)
background.save(save_path, "JPEG", quality=95)
print(f"✅ Thumbnail saved: {save_path}")
messagebox.showinfo("Success", f"Thumbnail saved successfully!\n{save_path}")
except Exception as e:
print(f"❌ Error saving thumbnail: {e}")
messagebox.showerror("Error", f"Failed to save thumbnail:\n{str(e)}")
def close_editor(self):
"""Close the editor"""
try:
if self.clip:
self.clip.close()
except:
pass
self.editor.destroy()
def create_default_stickers(self):
"""Create default emoji stickers"""
stickers_data = {
"smile.png": "😊",
"laugh.png": "😂",
"happy-face.png": "😀",
"sad-face.png": "😢",
"confused.png": "😕",
"party.png": "🎉",
"emoji.png": "👍",
"emoji (1).png": "❤️",
"smile (1).png": "😄"
}
for filename, emoji in stickers_data.items():
filepath = os.path.join(self.stickers_folder, filename)
if not os.path.exists(filepath):
try:
# Create simple emoji images
img = Image.new('RGBA', (64, 64), (255, 255, 255, 0))
draw = ImageDraw.Draw(img)
# Try to use a font for emoji, fallback to colored rectangles
try:
font = ImageFont.truetype("seguiemj.ttf", 48)
draw.text((8, 8), emoji, font=font, fill="black")
except:
# Fallback: create colored circles/shapes
colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE']
color = colors[hash(filename) % len(colors)]
draw.ellipse([8, 8, 56, 56], fill=color)
draw.text((20, 20), emoji[:2], fill="white", font=ImageFont.load_default())
img.save(filepath, 'PNG')
print(f"✅ Created default sticker: {filename}")
except Exception as e:
print(f"⚠️ Error creating sticker {filename}: {e}")
# Legacy function to maintain compatibility
def open_thumbnail_editor(video_path):
"""Legacy function for backward compatibility"""
editor = ModernThumbnailEditor(video_path)
return editor
if __name__ == "__main__":
# Test the editor
test_video = "myvideo.mp4" # Replace with actual video path
if os.path.exists(test_video):
root = tk.Tk()
root.withdraw() # Hide main window
editor = ModernThumbnailEditor(test_video)
root.mainloop()
else:
print("Please provide a valid video file path")