ShortGenerator/Main.py
klop51 6bb356948d 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
2025-08-10 14:11:18 +02:00

1174 lines
53 KiB
Python

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import threading
import time
import os
import sys
# Import the ShortsGeneratorGUI from shorts_generator2.py
try:
from shorts_generator2 import ShortsGeneratorGUI
except ImportError:
messagebox.showerror("Error", "Could not import ShortsGeneratorGUI from shorts_generator2.py")
sys.exit(1)
class ProgressWindow:
def __init__(self, parent, title="Processing"):
self.parent = parent
self.window = tk.Toplevel(parent)
self.window.title(title)
self.window.geometry("450x180")
self.window.minsize(400, 160)
self.window.resizable(True, False)
self.window.transient(parent)
self.window.grab_set()
# Modern colors
self.colors = {
'bg_primary': '#1a1a1a',
'bg_secondary': '#2d2d2d',
'text_primary': '#ffffff',
'text_secondary': '#b8b8b8',
'accent_blue': '#007acc',
'accent_red': '#dc3545'
}
self.window.configure(bg=self.colors['bg_primary'])
# Make window responsive
self.window.columnconfigure(0, weight=1)
# Center the window
self.window.update_idletasks()
x = (self.window.winfo_screenwidth() // 2) - (450 // 2)
y = (self.window.winfo_screenheight() // 2) - (180 // 2)
self.window.geometry(f"450x180+{x}+{y}")
# Bind resize event
self.window.bind('<Configure>', self.on_window_resize)
# Create modern progress interface
main_frame = tk.Frame(self.window, bg=self.colors['bg_secondary'], relief="flat", bd=0)
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
main_frame.columnconfigure(0, weight=1)
# Title
title_label = tk.Label(main_frame, text=title,
font=('Segoe UI', 14, 'bold'),
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'])
title_label.grid(row=0, column=0, sticky="ew", pady=(0, 15))
# Status
self.status_label = tk.Label(main_frame, text="Initializing...",
font=('Segoe UI', 10), bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'], anchor="w")
self.status_label.grid(row=1, column=0, sticky="ew", pady=(0, 5))
# Time info
self.time_label = tk.Label(main_frame, text="Elapsed: 0.0s | Remaining: --s",
font=('Segoe UI', 9), bg=self.colors['bg_secondary'],
fg=self.colors['text_secondary'], anchor="w")
self.time_label.grid(row=2, column=0, sticky="ew", pady=(0, 10))
# Modern progress bar styling
style = ttk.Style()
style.theme_use('clam')
style.configure("Modern.Horizontal.TProgressbar",
background=self.colors['accent_blue'],
troughcolor=self.colors['bg_primary'],
borderwidth=0, lightcolor=self.colors['accent_blue'],
darkcolor=self.colors['accent_blue'])
# Main progress bar
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var,
maximum=100, style="Modern.Horizontal.TProgressbar")
self.progress_bar.grid(row=3, column=0, sticky="ew", pady=(0, 8))
# Detection progress bar (hidden by default)
self.detection_label = tk.Label(main_frame, text="", font=('Segoe UI', 9),
bg=self.colors['bg_secondary'], fg=self.colors['accent_blue'],
anchor="w")
self.detection_progress_var = tk.DoubleVar()
self.detection_progress_bar = ttk.Progressbar(main_frame, variable=self.detection_progress_var,
maximum=100, style="Modern.Horizontal.TProgressbar")
# Modern cancel button
self.cancel_btn = tk.Button(main_frame, text="Cancel", command=self.cancel,
bg=self.colors['accent_red'], fg='white',
font=('Segoe UI', 10, 'bold'), relief="flat", bd=0,
pady=8, cursor="hand2")
self.cancel_btn.grid(row=6, column=0, pady=(10, 0))
self.start_time = time.time()
self.cancelled = False
def on_window_resize(self, event):
"""Handle window resize events"""
if event.widget == self.window:
# Update progress bar length based on window width
width = self.window.winfo_width()
progress_length = max(250, width - 80) # Minimum 250px, with padding
try:
self.progress_bar.config(length=progress_length)
self.detection_progress_bar.config(length=progress_length)
except:
pass
def show_detection_progress(self):
"""Show the detection progress bar - thread safe"""
def _show():
self.detection_label.grid(row=3, column=0, sticky="ew", pady=(3, 0))
self.detection_progress_bar.grid(row=4, column=0, sticky="ew", pady=(3, 5))
self.window.after(0, _show)
def hide_detection_progress(self):
"""Hide the detection progress bar - thread safe"""
def _hide():
self.detection_label.grid_remove()
self.detection_progress_bar.grid_remove()
self.window.after(0, _hide)
def update_progress(self, message, percent):
"""Update main progress bar - thread safe"""
def _update():
if self.cancelled:
return
self.status_label.config(text=message)
self.progress_var.set(percent)
elapsed = time.time() - self.start_time
if percent > 0:
remaining = (elapsed / percent) * (100 - percent)
self.time_label.config(text=f"Elapsed: {elapsed:.1f}s | Remaining: {remaining:.1f}s")
# Schedule the update on the main thread
self.window.after(0, _update)
def update_detection_progress(self, message, percent):
"""Update detection progress bar - thread safe"""
def _update():
if self.cancelled:
return
self.detection_label.config(text=message)
self.detection_progress_var.set(percent)
# Schedule the update on the main thread
self.window.after(0, _update)
def cancel(self):
"""Cancel the current operation"""
self.cancelled = True
self.window.destroy()
def close(self):
"""Close the progress window"""
if not self.cancelled:
self.window.destroy()
class ClipSelectionWindow:
def __init__(self, parent, clips, video_path, detection_mode):
self.parent = parent
self.clips = clips
self.video_path = video_path
self.detection_mode = detection_mode
self.selected_clips = []
# Modern colors
self.colors = {
'bg_primary': '#1a1a1a',
'bg_secondary': '#2d2d2d',
'bg_tertiary': '#3d3d3d',
'text_primary': '#ffffff',
'text_secondary': '#b8b8b8',
'accent_green': '#28a745',
'accent_red': '#dc3545',
'accent_blue': '#007acc',
'border': '#404040'
}
# Create modern window
self.window = tk.Toplevel(parent.root)
self.window.title("Select Clips to Generate")
self.window.geometry("700x600")
self.window.minsize(500, 400)
self.window.resizable(True, True)
self.window.transient(parent.root)
self.window.grab_set()
self.window.configure(bg=self.colors['bg_primary'])
# Make window responsive
self.window.rowconfigure(1, weight=1)
self.window.columnconfigure(0, weight=1)
# Center the window
self.window.update_idletasks()
x = (self.window.winfo_screenwidth() // 2) - (700 // 2)
y = (self.window.winfo_screenheight() // 2) - (600 // 2)
self.window.geometry(f"700x600+{x}+{y}")
# Bind resize event
self.window.bind('<Configure>', self.on_window_resize)
self.setup_gui()
def setup_gui(self):
# Header section
header_frame = tk.Frame(self.window, bg=self.colors['bg_secondary'], relief="flat", bd=0)
header_frame.grid(row=0, column=0, sticky="ew", padx=20, pady=(20, 0))
header_frame.columnconfigure(0, weight=1)
# Modern title
title_label = tk.Label(header_frame,
text=f"🎯 Found {len(self.clips)} clips using {self.detection_mode} detection",
font=('Segoe UI', 16, 'bold'), bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'])
title_label.pack(pady=20)
# Instructions with modern styling
self.instruction_label = tk.Label(header_frame,
text="Select the clips you want to generate by checking the boxes below:",
font=('Segoe UI', 11), bg=self.colors['bg_secondary'],
fg=self.colors['text_secondary'], wraplength=600)
self.instruction_label.pack(pady=(0, 20))
# Main content area
content_frame = tk.Frame(self.window, bg=self.colors['bg_primary'])
content_frame.grid(row=1, column=0, sticky="nsew", padx=20, pady=10)
content_frame.rowconfigure(0, weight=1)
content_frame.columnconfigure(0, weight=1)
# Modern clips list with card design
list_frame = tk.Frame(content_frame, bg=self.colors['bg_secondary'], relief="flat", bd=0)
list_frame.grid(row=0, column=0, sticky="nsew", padx=0, pady=0)
list_frame.rowconfigure(0, weight=1)
list_frame.columnconfigure(0, weight=1)
# Scrollable canvas with modern scrollbar
canvas = tk.Canvas(list_frame, bg=self.colors['bg_secondary'], highlightthickness=0)
# Modern scrollbar styling
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
scrollable_frame = tk.Frame(canvas, bg=self.colors['bg_secondary'])
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# Modern clip cards
self.clip_vars = []
for i, (start, end) in enumerate(self.clips):
var = tk.BooleanVar(value=True)
self.clip_vars.append(var)
duration = end - start
# Modern clip card
clip_card = tk.Frame(scrollable_frame, bg=self.colors['bg_tertiary'],
relief="flat", bd=1, highlightbackground=self.colors['border'],
highlightthickness=1)
clip_card.pack(fill="x", pady=8, padx=15)
clip_card.columnconfigure(1, weight=1)
# Modern checkbox
checkbox = tk.Checkbutton(clip_card, variable=var, text="", width=2,
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
selectcolor=self.colors['accent_blue'],
activebackground=self.colors['bg_tertiary'],
relief="flat", bd=0)
checkbox.grid(row=0, column=0, padx=15, pady=15, sticky="w")
# Clip info with modern typography
info_frame = tk.Frame(clip_card, bg=self.colors['bg_tertiary'])
info_frame.grid(row=0, column=1, sticky="ew", padx=(0, 15), pady=15)
clip_title = tk.Label(info_frame, text=f"Clip {i+1}",
font=('Segoe UI', 11, 'bold'), bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'], anchor="w")
clip_title.pack(anchor="w")
clip_details = tk.Label(info_frame,
text=f"⏱️ {start:.1f}s - {end:.1f}s • Duration: {duration:.1f}s",
font=('Segoe UI', 10), bg=self.colors['bg_tertiary'],
fg=self.colors['text_secondary'], anchor="w")
clip_details.pack(anchor="w", pady=(2, 0))
canvas.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")
# Modern action buttons
action_frame = tk.Frame(self.window, bg=self.colors['bg_primary'])
action_frame.grid(row=2, column=0, sticky="ew", padx=20, pady=(10, 20))
action_frame.columnconfigure(0, weight=1)
action_frame.columnconfigure(1, weight=1)
action_frame.columnconfigure(2, weight=1)
action_frame.columnconfigure(3, weight=1)
# Selection buttons
select_all_btn = self.create_modern_button(action_frame, "✅ Select All",
self.select_all, self.colors['accent_blue'])
select_all_btn.grid(row=0, column=0, padx=(0, 5), sticky="ew")
select_none_btn = self.create_modern_button(action_frame, "❌ Select None",
self.select_none, self.colors['bg_tertiary'])
select_none_btn.grid(row=0, column=1, padx=5, sticky="ew")
# Action buttons
cancel_btn = self.create_modern_button(action_frame, "Cancel",
self.cancel, self.colors['accent_red'])
cancel_btn.grid(row=0, column=2, padx=5, sticky="ew")
generate_btn = self.create_modern_button(action_frame, "🎬 Generate Selected",
self.generate_selected, self.colors['accent_green'])
generate_btn.grid(row=0, column=3, padx=(5, 0), sticky="ew")
def create_modern_button(self, parent, text, command, color):
"""Create a modern button for the clip selection window"""
button = tk.Button(parent, text=text, command=command,
bg=color, fg='white', font=('Segoe UI', 10, 'bold'),
relief="flat", bd=0, pady=10, cursor="hand2")
# Add hover effect
original_color = color
def on_enter(e):
# Lighten color on hover
button.config(bg=self.lighten_color(original_color, 0.2))
def on_leave(e):
button.config(bg=original_color)
button.bind("<Enter>", on_enter)
button.bind("<Leave>", on_leave)
return button
def lighten_color(self, hex_color, factor):
"""Lighten a hex color by a factor (0.0 to 1.0)"""
hex_color = hex_color.lstrip('#')
rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
rgb = tuple(min(255, int(c + (255 - c) * factor)) for c in rgb)
return f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}"
def on_window_resize(self, event):
"""Handle window resize events"""
if event.widget == self.window:
# Get current window size
width = self.window.winfo_width()
# Adjust wrap length for instruction text
wrap_length = max(300, width - 100)
try:
self.instruction_label.config(wraplength=wrap_length)
except:
pass
def select_all(self):
"""Select all clips"""
for var in self.clip_vars:
var.set(True)
def select_none(self):
"""Deselect all clips"""
for var in self.clip_vars:
var.set(False)
def cancel(self):
"""Cancel selection"""
self.window.destroy()
def generate_selected(self):
"""Generate the selected clips"""
# Get selected clips
self.selected_clips = []
for i, var in enumerate(self.clip_vars):
if var.get():
self.selected_clips.append(self.clips[i])
if not self.selected_clips:
messagebox.showwarning("No Selection", "Please select at least one clip to generate.")
return
# Close selection window
self.window.destroy()
# Trigger generation with selected clips
if hasattr(self.parent, 'generate_selected_clips'):
self.parent.generate_selected_clips(self.selected_clips)
else:
messagebox.showerror("Error", "Generation method not available.")
class MainApplication:
def __init__(self):
self.root = tk.Tk()
self.root.title("AI Shorts Generator - Main Controller")
self.root.geometry("800x600") # Wider window for horizontal layout
self.root.minsize(700, 500) # Increased minimum width for horizontal layout
# 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', 18, 'bold'),
'heading': ('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)
self.root.columnconfigure(0, weight=1)
# Initialize the ShortsGeneratorGUI will be created when needed
self.shorts_generator = None
self.setup_gui()
# Bind resize event for responsive updates
self.root.bind('<Configure>', self.on_window_resize)
def get_shorts_generator(self):
"""Get or create a minimal ShortsGenerator instance when needed"""
if self.shorts_generator is None:
try:
# Create a simple container class with just the attributes we need
class MinimalShortsGenerator:
def __init__(self):
self.video_path = None
self.output_folder = "shorts"
self.max_clips = 3
self.threshold_db = -30
self.clip_duration = 5
self.detection_mode_var = tk.StringVar(value="loud")
self.shorts_generator = MinimalShortsGenerator()
print("✅ Minimal ShortsGenerator initialized successfully")
except Exception as e:
print(f"❌ Failed to initialize ShortsGenerator: {e}")
messagebox.showerror("Initialization Error", f"Failed to initialize ShortsGenerator: {e}")
return None
return self.shorts_generator
def setup_gui(self):
"""Setup the main GUI with modern horizontal design"""
# Create main container that fills the window
main_container = tk.Frame(self.root, bg=self.colors['bg_primary'])
main_container.pack(fill="both", expand=True, padx=20, pady=20)
# Modern title with gradient effect simulation
title_frame = tk.Frame(main_container, bg=self.colors['bg_primary'])
title_frame.pack(fill="x", pady=(0, 20))
title_label = tk.Label(title_frame, text="🎬 AI Shorts Generator",
font=self.fonts['title'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'])
title_label.pack()
subtitle_label = tk.Label(title_frame, text="Create viral content with AI-powered video analysis",
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_secondary'])
subtitle_label.pack(pady=(5, 0))
# Create horizontal layout with left and right panels
content_frame = tk.Frame(main_container, bg=self.colors['bg_primary'])
content_frame.pack(fill="both", expand=True)
# Left panel - Video Selection and Settings
left_panel = tk.Frame(content_frame, bg=self.colors['bg_primary'])
left_panel.pack(side="left", fill="both", expand=True, padx=(0, 15))
# Modern card-style file selection frame
file_card = self.create_modern_card(left_panel, "📁 Video Selection")
self.file_label = tk.Label(file_card, 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.file_label.pack(fill="x", pady=(0, 10))
# Modern button with hover effect
select_btn = self.create_modern_button(file_card, "📁 Select Video File",
self.select_video_file, self.colors['accent_blue'])
select_btn.pack(fill="x", pady=5)
# Modern settings card
settings_card = self.create_modern_card(left_panel, "⚙️ Detection Settings")
# Detection mode with modern styling
mode_label = tk.Label(settings_card, text="Detection Mode:",
font=self.fonts['heading'], bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'])
mode_label.pack(anchor="w", pady=(0, 10))
self.detection_var = tk.StringVar(value="loud")
detection_container = tk.Frame(settings_card, bg=self.colors['bg_secondary'])
detection_container.pack(fill="x", pady=5)
modes = [("🔊 Loud Moments", "loud"), ("🎬 Scene Changes", "scene"), ("🏃 Motion", "motion"),
("💬 Speech", "speech"), ("🎵 Audio Peaks", "peaks"), ("🎯 Combined", "combined")]
# Create modern radio buttons in rows
for i in range(0, len(modes), 3): # 3 per row
row_frame = tk.Frame(detection_container, bg=self.colors['bg_secondary'])
row_frame.pack(fill="x", pady=3)
for j in range(3):
if i + j < len(modes):
text, value = modes[i + j]
radio_frame = tk.Frame(row_frame, bg=self.colors['bg_tertiary'],
relief="flat", bd=1)
radio_frame.pack(side="left", fill="x", expand=True, padx=5)
radio = tk.Radiobutton(radio_frame, text=text, variable=self.detection_var,
value=value, bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'], font=self.fonts['body'],
selectcolor=self.colors['accent_blue'],
activebackground=self.colors['hover'],
activeforeground=self.colors['text_primary'],
relief="flat", bd=0)
radio.pack(pady=8, padx=10)
# Right panel - Actions and Controls
right_panel = tk.Frame(content_frame, bg=self.colors['bg_primary'])
right_panel.pack(side="right", fill="y", padx=(15, 0))
right_panel.config(width=300) # Fixed width for actions panel
# Modern action buttons card
button_card = self.create_modern_card(right_panel, "🚀 Actions")
# Preview button
self.preview_btn = self.create_modern_button(button_card, "🔍 Preview Clips",
self.preview_clips_threaded,
self.colors['accent_blue'])
self.preview_btn.pack(fill="x", pady=5)
# Generate button - primary action
self.generate_btn = self.create_modern_button(button_card, "🎬 Generate All Clips",
self.generate_shorts_threaded,
self.colors['accent_green'], large=True)
self.generate_btn.pack(fill="x", pady=5)
# Secondary action buttons
self.edit_btn = self.create_modern_button(button_card, "✏️ Edit Generated Shorts",
self.open_editor, self.colors['accent_orange'])
self.edit_btn.pack(fill="x", pady=5)
self.thumbnail_btn = self.create_modern_button(button_card, "📸 Create Thumbnails",
self.open_thumbnails, self.colors['accent_purple'])
self.thumbnail_btn.pack(fill="x", pady=5)
# Info tip with modern styling - moved to bottom
tip_frame = tk.Frame(button_card, bg=self.colors['bg_secondary'])
tip_frame.pack(fill="x", pady=(20, 0))
tip_icon = tk.Label(tip_frame, text="💡", font=self.fonts['body'],
bg=self.colors['bg_secondary'], fg=self.colors['accent_orange'])
tip_icon.pack(side="left", padx=(0, 8))
info_label = tk.Label(tip_frame, text="Tip: Use 'Preview Clips' to select specific clips for faster processing",
font=self.fonts['caption'], fg=self.colors['text_muted'],
bg=self.colors['bg_secondary'], wraplength=250, anchor="w")
info_label.pack(side="left", fill="x", expand=True)
# Modern status bar at the bottom
status_frame = tk.Frame(main_container, bg=self.colors['bg_tertiary'],
relief="flat", bd=1)
status_frame.pack(fill="x", pady=(20, 0))
status_icon = tk.Label(status_frame, text="", font=self.fonts['body'],
bg=self.colors['bg_tertiary'], fg=self.colors['accent_green'])
status_icon.pack(side="left", padx=15, pady=8)
self.status_label = tk.Label(status_frame, text="Ready - Select a video to begin",
font=self.fonts['caption'], fg=self.colors['text_secondary'],
bg=self.colors['bg_tertiary'], wraplength=400, anchor="w")
self.status_label.pack(side="left", fill="x", expand=True, pady=8, padx=(0, 15))
# Store detected clips for selection
self.detected_clips = []
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_frame.pack(fill="x", pady=10)
# Card header
header_frame = tk.Frame(card_frame, bg=self.colors['bg_secondary'])
header_frame.pack(fill="x", padx=20, pady=(15, 5))
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")
# Card content area
content_frame = tk.Frame(card_frame, bg=self.colors['bg_secondary'])
content_frame.pack(fill="both", expand=True, padx=20, pady=(5, 20))
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 15
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"""
# Remove # if present
hex_color = hex_color.lstrip('#')
# Convert to RGB
rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
# Adjust brightness
adjusted = tuple(max(0, min(255, c + adjustment)) for c in rgb)
# Convert back to hex
return f"#{adjusted[0]:02x}{adjusted[1]:02x}{adjusted[2]:02x}"
def on_window_resize(self, event):
"""Handle window resize events for responsive layout"""
if event.widget == self.root:
# Get current window size
width = self.root.winfo_width()
height = self.root.winfo_height()
# Adjust font sizes based on window width
if width < 450:
title_font_size = 14
button_font_size = 10
info_wrap_length = 300
elif width < 550:
title_font_size = 15
button_font_size = 11
info_wrap_length = 350
else:
title_font_size = 16
button_font_size = 12
info_wrap_length = 400
# Update wraplength for text elements
try:
# Find and update info label
for widget in self.root.winfo_children():
if isinstance(widget, tk.Frame):
for subwidget in widget.winfo_children():
if isinstance(subwidget, tk.Frame):
for subsubwidget in subwidget.winfo_children():
if isinstance(subsubwidget, tk.Label) and "Tip:" in str(subsubwidget.cget("text")):
subsubwidget.config(wraplength=info_wrap_length)
elif isinstance(subsubwidget, tk.Label) and "Ready" in str(subsubwidget.cget("text")):
subsubwidget.config(wraplength=info_wrap_length)
except:
pass # Ignore any errors during dynamic updates
def select_video_file(self):
"""Select video file"""
filetypes = [
("Video files", "*.mp4 *.avi *.mov *.mkv *.wmv *.flv *.webm"),
("All files", "*.*")
]
file_path = filedialog.askopenfilename(
title="Select Video File",
filetypes=filetypes
)
if file_path:
# Update display
filename = os.path.basename(file_path)
self.file_label.config(text=filename)
# Update shorts generator
generator = self.get_shorts_generator()
if generator:
generator.video_path = file_path
if hasattr(generator, 'video_label'):
generator.video_label.config(text=os.path.basename(file_path))
self.status_label.config(text=f"Video loaded: {filename}")
def preview_clips_threaded(self):
"""Run preview clips with progress window"""
generator = self.get_shorts_generator()
if not generator or not hasattr(generator, 'video_path') or not generator.video_path:
messagebox.showwarning("No Video", "Please select a video file first.")
return
# Update settings in shorts generator
self.update_shorts_generator_settings()
# Create progress window
progress_window = ProgressWindow(self.root, "Previewing Clips")
# Show detection progress for heavy modes
detection_mode = self.detection_var.get()
if detection_mode in ["scene", "motion", "speech", "peaks", "combined"]:
progress_window.show_detection_progress()
def run_preview():
try:
from shorts_generator2 import (detect_loud_moments, detect_scene_changes_with_progress,
detect_motion_intensity_with_progress, detect_speech_emotion_with_progress,
detect_audio_peaks_with_progress, detect_combined_intensity_with_progress,
validate_video)
generator = self.get_shorts_generator()
if not generator or not hasattr(generator, 'video_path'):
raise Exception("ShortsGeneratorGUI not properly initialized")
video_path = generator.video_path
clip_duration = 5 # Fixed clip duration since we removed the setting
detection_mode = self.detection_var.get()
# Thread-safe progress callbacks with cancellation checks
def progress_callback(message, percent):
if progress_window.cancelled:
raise InterruptedError("Preview cancelled by user")
self.root.after(0, lambda: progress_window.update_progress(message, percent))
def detection_callback(message, percent):
if progress_window.cancelled:
raise InterruptedError("Preview cancelled by user")
self.root.after(0, lambda: progress_window.update_detection_progress(message, percent))
# Check for cancellation before starting
if progress_window.cancelled:
return
# Validate video first
self.root.after(0, lambda: progress_callback("Validating video...", 5))
validate_video(video_path, min_duration=clip_duration * 2)
# Run detection based on mode
self.root.after(0, lambda: progress_callback(f"Analyzing {detection_mode} moments...", 10))
if detection_mode == "loud":
moments = detect_loud_moments(video_path, chunk_duration=clip_duration, threshold_db=-30)
elif detection_mode == "scene":
moments = detect_scene_changes_with_progress(video_path, chunk_duration=clip_duration,
progress_callback=detection_callback)
elif detection_mode == "motion":
moments = detect_motion_intensity_with_progress(video_path, chunk_duration=clip_duration,
progress_callback=detection_callback)
elif detection_mode == "speech":
moments = detect_speech_emotion_with_progress(video_path, chunk_duration=clip_duration,
progress_callback=detection_callback)
elif detection_mode == "peaks":
moments = detect_audio_peaks_with_progress(video_path, chunk_duration=clip_duration,
progress_callback=detection_callback)
elif detection_mode == "combined":
moments = detect_combined_intensity_with_progress(video_path, chunk_duration=clip_duration,
progress_callback=detection_callback)
else:
moments = detect_loud_moments(video_path, chunk_duration=clip_duration, threshold_db=-30)
# Final progress updates without cancellation checks (process is nearly complete)
if not progress_window.cancelled:
self.root.after(0, lambda: progress_window.update_progress("Analysis complete!", 90))
# Show results with selection window
def show_results():
if progress_window.cancelled:
return
if moments:
self.detected_clips = moments # Store for potential generation
progress_window.close()
# Show clip selection window
ClipSelectionWindow(self, moments, video_path, detection_mode)
else:
progress_window.close()
messagebox.showwarning("No Results", "No interesting moments found with current settings.")
if not progress_window.cancelled:
self.root.after(0, lambda: progress_window.update_progress("Preparing results...", 100))
self.root.after(500, show_results)
except InterruptedError:
# Preview was cancelled by user
def show_cancelled():
progress_window.close()
# Don't show an error message for user cancellation
self.root.after(0, show_cancelled)
except Exception as e:
error_msg = str(e)
def show_error():
progress_window.close()
messagebox.showerror("Preview Error", f"An error occurred during preview:\n{error_msg}")
self.root.after(0, show_error)
# Start the preview in a background thread
threading.Thread(target=run_preview, daemon=True).start()
def generate_shorts_threaded(self):
"""Run generate shorts with progress window"""
generator = self.get_shorts_generator()
if not generator or not hasattr(generator, 'video_path') or not generator.video_path:
messagebox.showwarning("No Video", "Please select a video file first.")
return
# Update settings
self.update_shorts_generator_settings()
# Create progress window
progress_window = ProgressWindow(self.root, "Generating Shorts")
# Show detection progress for heavy modes
detection_mode = self.detection_var.get()
if detection_mode in ["scene", "motion", "speech", "peaks", "combined"]:
progress_window.show_detection_progress()
def run_generation():
try:
from shorts_generator2 import generate_shorts
generator = self.get_shorts_generator()
if not generator or not hasattr(generator, 'video_path'):
raise Exception("ShortsGeneratorGUI not properly initialized")
video_path = generator.video_path
detection_mode = self.detection_var.get()
clip_duration = 5 # Default duration
# Thread-safe progress callbacks with cancellation checks
def progress_callback(message, percent):
if progress_window.cancelled:
raise InterruptedError("Generation cancelled by user")
self.root.after(0, lambda: progress_window.update_progress(message, percent))
def detection_callback(message, percent):
if progress_window.cancelled:
raise InterruptedError("Generation cancelled by user")
self.root.after(0, lambda: progress_window.update_detection_progress(message, percent))
# Check for cancellation before starting
if progress_window.cancelled:
return
# Run the actual generation (no max clips limit - user will select from preview)
generate_shorts(
video_path,
max_clips=50, # Generate a reasonable number for preview/selection
output_folder="shorts",
progress_callback=progress_callback,
detection_progress_callback=detection_callback,
threshold_db=-30,
clip_duration=5,
detection_mode=detection_mode
)
def show_success():
progress_window.close()
messagebox.showinfo("Success", "Successfully generated shorts!\n\nCheck the 'shorts' folder for your videos.")
self.root.after(0, show_success)
except InterruptedError:
# Generation was cancelled by user
def show_cancelled():
progress_window.close()
# Don't show an error message for user cancellation
self.root.after(0, show_cancelled)
except Exception as e:
error_msg = str(e)
def show_error():
progress_window.close()
messagebox.showerror("Generation Error", f"An error occurred during generation:\n{error_msg}")
self.root.after(0, show_error)
# Start the generation in a background thread
threading.Thread(target=run_generation, daemon=True).start()
def generate_selected_clips(self, selected_clips):
"""Generate only the selected clips"""
generator = self.get_shorts_generator()
if not generator or not hasattr(generator, 'video_path') or not generator.video_path:
messagebox.showerror("Error", "No video selected.")
return
# Create progress window
progress_window = ProgressWindow(self.root, "Generating Selected Clips")
def run_selected_generation():
try:
generator = self.get_shorts_generator()
if not generator or not hasattr(generator, 'video_path'):
raise Exception("ShortsGeneratorGUI not properly initialized")
video_path = generator.video_path
# Thread-safe progress callback with cancellation checks
def progress_callback(message, percent):
if progress_window.cancelled:
raise InterruptedError("Generation cancelled by user")
self.root.after(0, lambda: progress_window.update_progress(message, percent))
# Check for cancellation before starting
if progress_window.cancelled:
return
# Use a custom generation function for selected clips
self.generate_clips_from_moments(
video_path,
selected_clips,
progress_callback,
progress_window
)
def show_success():
progress_window.close()
messagebox.showinfo("Success", f"Successfully generated {len(selected_clips)} selected clips!\n\nCheck the 'shorts' folder for your videos.")
self.root.after(0, show_success)
except InterruptedError:
# Generation was cancelled by user
def show_cancelled():
progress_window.close()
# Don't show an error message for user cancellation
self.root.after(0, show_cancelled)
except Exception as e:
error_msg = str(e)
def show_error():
progress_window.close()
messagebox.showerror("Generation Error", f"An error occurred during generation:\n{error_msg}")
self.root.after(0, show_error)
# Start the generation in a background thread
threading.Thread(target=run_selected_generation, daemon=True).start()
def generate_clips_from_moments(self, video_path, moments, progress_callback, progress_window):
"""Generate video clips from specific moments"""
try:
import os
from moviepy.editor import VideoFileClip
os.makedirs("shorts", exist_ok=True)
total_clips = len(moments)
for i, (start, end) in enumerate(moments):
if progress_window.cancelled:
break
progress_callback(f"Processing clip {i+1}/{total_clips}...", (i/total_clips) * 90)
# Create clip
with VideoFileClip(video_path) as video:
clip = video.subclipped(start, end)
output_path = os.path.join("shorts", f"short_{i+1}.mp4")
clip.write_videofile(output_path, verbose=False, logger=None)
clip.close()
progress_callback("All clips generated successfully!", 100)
except ImportError as e:
# MoviePy not available, use alternative approach
progress_callback("Using alternative generation method...", 10)
# Check for cancellation before starting
if progress_window.cancelled:
return
# Import and use the existing generate_shorts function
from shorts_generator2 import generate_shorts
# Create a temporary callback that updates our progress window and checks for cancellation
def alt_progress_callback(message, percent):
if progress_window.cancelled:
# Try to indicate cancellation to the calling function
raise InterruptedError("Generation cancelled by user")
progress_callback(message, percent)
def alt_detection_callback(message, percent):
if progress_window.cancelled:
raise InterruptedError("Generation cancelled by user")
progress_callback(f"Detection: {message}", percent)
try:
# Use generate_shorts with the exact number of selected clips
# This ensures we generate exactly what the user selected
generate_shorts(
video_path,
max_clips=len(moments), # Generate exactly the number of selected clips
output_folder="shorts",
progress_callback=alt_progress_callback,
detection_progress_callback=alt_detection_callback,
threshold_db=-30,
clip_duration=5,
detection_mode="loud" # Default detection mode
)
except InterruptedError:
# Generation was cancelled
progress_callback("Generation cancelled by user", 0)
return
except Exception as e:
progress_callback(f"Error: {str(e)}", 100)
raise
def update_shorts_generator_settings(self):
"""Update the shorts generator with current settings"""
generator = self.get_shorts_generator()
if generator and hasattr(generator, 'detection_mode_var'):
generator.detection_mode_var.set(self.detection_var.get())
def open_editor(self):
"""Open the shorts editor"""
try:
# Import and create the editor directly
from shorts_generator2 import ShortsEditorGUI
# Get the output folder from generator if available, otherwise use default
generator = self.get_shorts_generator()
output_folder = getattr(generator, 'output_folder', 'shorts') if generator else 'shorts'
# Create and open the editor
editor = ShortsEditorGUI(self.root, output_folder)
editor.open_editor()
except Exception as e:
print(f"Editor Error: {e}")
import traceback
traceback.print_exc()
messagebox.showerror("Editor Error", f"Could not open editor: {e}")
def open_thumbnails(self):
"""Open the thumbnail editor"""
try:
import os
import glob
# Check if there are any video files to work with
video_files = []
# Check for original video
generator = self.get_shorts_generator()
if generator and hasattr(generator, 'video_path') and generator.video_path:
video_files.append(("Original Video", generator.video_path))
# Check for generated shorts
output_folder = getattr(generator, 'output_folder', 'shorts') if generator else 'shorts'
if os.path.exists(output_folder):
shorts = glob.glob(os.path.join(output_folder, "*.mp4"))
for short in shorts:
video_files.append((os.path.basename(short), short))
if not video_files:
messagebox.showinfo("No Videos Found",
"Please select a video or generate some shorts first!")
return
# If only one video, open it directly
if len(video_files) == 1:
selected_video = video_files[0][1]
else:
# Let user choose which video to edit
choice_window = tk.Toplevel(self.root)
choice_window.title("Select Video for Thumbnail")
choice_window.geometry("400x300")
choice_window.transient(self.root)
choice_window.grab_set()
choice_window.configure(bg=self.colors['bg_primary'])
tk.Label(choice_window, text="📸 Select Video for Thumbnail Creation",
font=("Segoe UI", 12, "bold"), bg=self.colors['bg_primary'],
fg=self.colors['text_primary']).pack(pady=20)
selected_video = None
def on_video_select(video_path):
nonlocal selected_video
selected_video = video_path
choice_window.destroy()
# Create list of videos with modern styling
for display_name, video_path in video_files:
btn = tk.Button(choice_window, text=f"📹 {display_name}",
command=lambda vp=video_path: on_video_select(vp),
font=("Segoe UI", 10), pady=8, width=40,
bg=self.colors['accent_blue'], fg='white',
relief="flat", bd=0, cursor="hand2")
btn.pack(pady=3, padx=20, fill="x")
cancel_btn = tk.Button(choice_window, text="Cancel",
command=choice_window.destroy,
font=("Segoe UI", 10), pady=8,
bg=self.colors['accent_red'], fg='white',
relief="flat", bd=0, cursor="hand2")
cancel_btn.pack(pady=15)
# Wait for selection
choice_window.wait_window()
if not selected_video:
return
# Import and open thumbnail editor
from thumbnail_editor import open_thumbnail_editor
open_thumbnail_editor(selected_video)
except Exception as e:
print(f"Thumbnail Error: {e}")
import traceback
traceback.print_exc()
messagebox.showerror("Thumbnail Error", f"Could not open thumbnail editor: {e}")
def run(self):
"""Start the main application"""
self.root.mainloop()
if __name__ == "__main__":
app = MainApplication()
app.run()