ShortGenerator/Main.py
klop51 a6a528396b Implement responsive design improvements across all GUI windows
- Converted layout from pack() to grid() for better control and responsiveness.
- Set minimum window sizes for MainApplication, ClipSelectionWindow, and ProgressWindow.
- Added dynamic font sizing and text wrapping based on window width.
- Implemented window resize handlers to adjust layouts and element sizes dynamically.
- Enhanced button and frame arrangements for better usability and touch-friendliness.
- Improved scrolling behavior in ShortsGeneratorGUI with a scrollable container.
- Ensured all elements adapt to various screen sizes for a consistent user experience.
2025-08-10 10:14:03 +02:00

942 lines
43 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("400x160")
self.window.minsize(350, 140) # Set minimum size
self.window.resizable(True, False) # Allow horizontal resize only
self.window.transient(parent)
self.window.grab_set()
# Make window responsive
self.window.columnconfigure(0, weight=1)
# Center the window
self.window.update_idletasks()
x = (self.window.winfo_screenwidth() // 2) - (400 // 2)
y = (self.window.winfo_screenheight() // 2) - (160 // 2)
self.window.geometry(f"400x160+{x}+{y}")
# Bind resize event
self.window.bind('<Configure>', self.on_window_resize)
# Create progress widgets with responsive layout
main_frame = tk.Frame(self.window)
main_frame.pack(fill="both", expand=True, padx=15, pady=15)
main_frame.columnconfigure(0, weight=1)
self.status_label = tk.Label(main_frame, text="Initializing...", anchor="w", font=("Arial", 10))
self.status_label.grid(row=0, column=0, sticky="ew", pady=(0, 5))
self.time_label = tk.Label(main_frame, text="Elapsed: 0.0s | Remaining: --s",
anchor="w", font=("Arial", 9), fg="gray")
self.time_label.grid(row=1, column=0, sticky="ew", pady=(0, 5))
# Main progress bar
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var, maximum=100)
self.progress_bar.grid(row=2, column=0, sticky="ew", pady=(5, 3))
# Detection progress bar (hidden by default)
self.detection_label = tk.Label(main_frame, text="", anchor="w", font=("Arial", 9), fg="blue")
self.detection_progress_var = tk.DoubleVar()
self.detection_progress_bar = ttk.Progressbar(main_frame, variable=self.detection_progress_var, maximum=100)
# Cancel button
self.cancel_btn = tk.Button(main_frame, text="Cancel", command=self.cancel)
self.cancel_btn.grid(row=5, column=0, pady=(5, 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 = []
# Create window with parent's root
self.window = tk.Toplevel(parent.root)
self.window.title("Select Clips to Generate")
self.window.geometry("600x500")
self.window.minsize(400, 350) # Set minimum size
self.window.resizable(True, True)
self.window.transient(parent.root)
self.window.grab_set()
# Make window responsive
self.window.rowconfigure(2, weight=1) # Clips list expandable
self.window.columnconfigure(0, weight=1)
# Center the window
self.window.update_idletasks()
x = (self.window.winfo_screenwidth() // 2) - (600 // 2)
y = (self.window.winfo_screenheight() // 2) - (500 // 2)
self.window.geometry(f"600x500+{x}+{y}")
# Bind resize event
self.window.bind('<Configure>', self.on_window_resize)
self.setup_gui()
def setup_gui(self):
# Create main container
main_container = tk.Frame(self.window)
main_container.pack(fill="both", expand=True, padx=20, pady=10)
# Make container responsive
main_container.rowconfigure(2, weight=1) # List area expandable
main_container.columnconfigure(0, weight=1)
# Title
title_label = tk.Label(main_container, text=f"Found {len(self.clips)} clips using {self.detection_mode} detection",
font=("Arial", 12, "bold"))
title_label.grid(row=0, column=0, pady=(0, 10), sticky="ew")
# Instructions
self.instruction_label = tk.Label(main_container,
text="Select the clips you want to generate (check the boxes):",
font=("Arial", 10), wraplength=400)
self.instruction_label.grid(row=1, column=0, pady=(0, 10), sticky="ew")
# Clips list frame with scrollbar
list_frame = tk.Frame(main_container)
list_frame.grid(row=2, column=0, sticky="nsew")
list_frame.rowconfigure(0, weight=1)
list_frame.columnconfigure(0, weight=1)
# Scrollable frame
canvas = tk.Canvas(list_frame)
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
scrollable_frame = tk.Frame(canvas)
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)
# Clip checkboxes
self.clip_vars = []
for i, (start, end) in enumerate(self.clips):
var = tk.BooleanVar(value=True) # All selected by default
self.clip_vars.append(var)
duration = end - start
clip_frame = tk.Frame(scrollable_frame, relief="ridge", bd=1)
clip_frame.pack(fill="x", pady=2, padx=5)
clip_frame.columnconfigure(1, weight=1)
checkbox = tk.Checkbutton(clip_frame, variable=var, text="", width=2)
checkbox.grid(row=0, column=0, padx=5, sticky="w")
info_label = tk.Label(clip_frame,
text=f"Clip {i+1}: {start:.1f}s - {end:.1f}s (Duration: {duration:.1f}s)",
font=("Arial", 10), anchor="w")
info_label.grid(row=0, column=1, padx=5, sticky="ew")
canvas.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")
# Selection buttons
button_frame = tk.Frame(main_container)
button_frame.grid(row=3, column=0, pady=10, sticky="ew")
button_frame.columnconfigure(0, weight=1)
button_frame.columnconfigure(1, weight=1)
select_all_btn = tk.Button(button_frame, text="Select All", command=self.select_all)
select_all_btn.grid(row=0, column=0, padx=(0, 5), sticky="ew")
select_none_btn = tk.Button(button_frame, text="Select None", command=self.select_none)
select_none_btn.grid(row=0, column=1, padx=(5, 0), sticky="ew")
# Generate button
action_frame = tk.Frame(main_container)
action_frame.grid(row=4, column=0, pady=10, sticky="ew")
action_frame.columnconfigure(0, weight=1)
action_frame.columnconfigure(1, weight=1)
cancel_btn = tk.Button(action_frame, text="Cancel", command=self.cancel, bg="#f44336", fg="white")
cancel_btn.grid(row=0, column=1, padx=(5, 0), sticky="ew")
generate_selected_btn = tk.Button(action_frame, text="Generate Selected Clips",
command=self.generate_selected, bg="#4CAF50", fg="white",
font=("Arial", 10, "bold"))
generate_selected_btn.grid(row=0, column=0, padx=(0, 5), sticky="ew")
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("500x600")
self.root.minsize(400, 500) # Set minimum size
self.root.configure(bg="#f0f0f0")
# Make window responsive
self.root.rowconfigure(0, weight=1)
self.root.columnconfigure(0, weight=1)
# Initialize the ShortsGeneratorGUI (but don't show its window)
self.shorts_generator = None
self.init_shorts_generator()
self.setup_gui()
# Bind resize event for responsive updates
self.root.bind('<Configure>', self.on_window_resize)
def init_shorts_generator(self):
"""Initialize the ShortsGeneratorGUI without showing its window"""
try:
# Create a hidden root for ShortsGeneratorGUI
hidden_root = tk.Tk()
hidden_root.withdraw() # Hide the window
# Create ShortsGeneratorGUI instance
self.shorts_generator = ShortsGeneratorGUI(hidden_root)
# Don't show the original window
hidden_root.withdraw()
except Exception as e:
messagebox.showerror("Initialization Error", f"Failed to initialize ShortsGeneratorGUI: {e}")
self.shorts_generator = None
def setup_gui(self):
"""Setup the main GUI with responsive design"""
# Create main container that fills the window
main_container = tk.Frame(self.root, bg="#f0f0f0")
main_container.pack(fill="both", expand=True, padx=10, pady=10)
# Make main container responsive
main_container.rowconfigure(1, weight=1) # File frame expandable
main_container.rowconfigure(2, weight=1) # Settings frame expandable
main_container.rowconfigure(3, weight=2) # Button frame gets more space
main_container.columnconfigure(0, weight=1)
# Title
title_label = tk.Label(main_container, text="🎬 AI Shorts Generator",
font=("Arial", 16, "bold"), bg="#f0f0f0", fg="#2c3e50")
title_label.grid(row=0, column=0, pady=(0, 20), sticky="ew")
# File selection frame
file_frame = tk.Frame(main_container, bg="#f0f0f0")
file_frame.grid(row=1, column=0, pady=10, sticky="ew")
file_frame.columnconfigure(0, weight=1) # Make expandable
tk.Label(file_frame, text="Selected Video:", font=("Arial", 10, "bold"),
bg="#f0f0f0").grid(row=0, column=0, sticky="w")
self.file_label = tk.Label(file_frame, text="No video selected",
font=("Arial", 9), bg="white", relief="sunken",
anchor="w", pady=5, padx=10)
self.file_label.grid(row=1, column=0, pady=(5,10), sticky="ew")
# File selection button
select_btn = tk.Button(file_frame, text="📁 Select Video File",
command=self.select_video_file, bg="#3498db", fg="white",
font=("Arial", 10, "bold"), pady=5)
select_btn.grid(row=2, column=0, pady=5, sticky="ew")
# Settings frame (simplified)
settings_frame = tk.LabelFrame(main_container, text="Quick Settings", font=("Arial", 10, "bold"),
bg="#f0f0f0", padx=10, pady=10)
settings_frame.grid(row=2, column=0, pady=10, sticky="ew")
settings_frame.columnconfigure(0, weight=1) # Make expandable
# Detection mode
tk.Label(settings_frame, text="Detection Mode:", bg="#f0f0f0").grid(row=0, column=0, sticky="w")
self.detection_var = tk.StringVar(value="loud")
detection_container = tk.Frame(settings_frame, bg="#f0f0f0")
detection_container.grid(row=1, column=0, pady=5, sticky="ew")
detection_container.columnconfigure(0, weight=1)
detection_container.columnconfigure(1, weight=1)
detection_container.columnconfigure(2, weight=1)
modes = [("Loud Moments", "loud"), ("Scene Changes", "scene"), ("Motion", "motion"),
("Speech", "speech"), ("Audio Peaks", "peaks"), ("Combined", "combined")]
# Create responsive grid for radio buttons
for i, (text, value) in enumerate(modes):
row = i // 3
col = i % 3
if row >= detection_container.grid_size()[1]:
detection_container.rowconfigure(row, weight=1)
tk.Radiobutton(detection_container, text=text, variable=self.detection_var, value=value,
bg="#f0f0f0").grid(row=row, column=col, padx=5, pady=2, sticky="w")
# Main action buttons with responsive design
button_frame = tk.Frame(main_container, bg="#f0f0f0")
button_frame.grid(row=3, column=0, pady=20, sticky="ew")
button_frame.columnconfigure(0, weight=1) # Make expandable
# Preview Clips Button
self.preview_btn = tk.Button(button_frame, text="🔍 Preview Clips",
command=self.preview_clips_threaded, bg="#2196F3", fg="white",
font=("Arial", 11, "bold"), pady=8)
self.preview_btn.grid(row=0, column=0, pady=5, sticky="ew")
# Generate Shorts Button
self.generate_btn = tk.Button(button_frame, text="🎬 Generate All Detected Clips",
command=self.generate_shorts_threaded, bg="#4CAF50", fg="white",
font=("Arial", 12, "bold"), pady=10)
self.generate_btn.grid(row=1, column=0, pady=5, sticky="ew")
# Info label
info_label = tk.Label(button_frame, text="💡 Tip: Use 'Preview Clips' to select specific clips for faster processing",
font=("Arial", 9), fg="gray", bg="#f0f0f0", wraplength=350)
info_label.grid(row=2, column=0, pady=(5,10), sticky="ew")
# Edit Generated Shorts Button
self.edit_btn = tk.Button(button_frame, text="✏️ Edit Generated Shorts",
command=self.open_editor, bg="#FF9800", fg="white",
font=("Arial", 11, "bold"), pady=8)
self.edit_btn.grid(row=3, column=0, pady=5, sticky="ew")
# Create Thumbnails Button
self.thumbnail_btn = tk.Button(button_frame, text="📸 Create Thumbnails",
command=self.open_thumbnails, bg="#9C27B0", fg="white",
font=("Arial", 11, "bold"), pady=8)
self.thumbnail_btn.grid(row=4, column=0, pady=5, sticky="ew")
# Status label
self.status_label = tk.Label(main_container, text="Ready - Select a video to begin",
font=("Arial", 9), fg="gray", bg="#f0f0f0", wraplength=400)
self.status_label.grid(row=4, column=0, pady=(20,0), sticky="ew")
# Store detected clips for selection
self.detected_clips = []
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
if self.shorts_generator:
self.shorts_generator.video_path = file_path
self.shorts_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"""
if not self.shorts_generator or not self.shorts_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)
video_path = self.shorts_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"""
if not self.shorts_generator or not self.shorts_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
video_path = self.shorts_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"""
if not self.shorts_generator or not self.shorts_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:
video_path = self.shorts_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"""
if self.shorts_generator:
self.shorts_generator.detection_mode_var.set(self.detection_var.get())
def open_editor(self):
"""Open the shorts editor"""
print("DEBUG: open_editor called")
if self.shorts_generator:
print("DEBUG: shorts_generator exists")
try:
print("DEBUG: Attempting to call open_shorts_editor()")
if hasattr(self.shorts_generator, 'open_shorts_editor'):
print("DEBUG: open_shorts_editor method exists")
print(f"DEBUG: shorts_generator root: {self.shorts_generator.root}")
print(f"DEBUG: shorts_generator output_folder: {getattr(self.shorts_generator, 'output_folder', 'NOT SET')}")
# Create the editor with the main window as parent instead of hidden root
from shorts_generator2 import ShortsEditorGUI
editor = ShortsEditorGUI(self.root, self.shorts_generator.output_folder)
editor.open_editor()
print("DEBUG: Editor opened successfully")
else:
print("DEBUG: open_shorts_editor method does NOT exist")
messagebox.showerror("Editor Error", "The open_shorts_editor method is not available in ShortsGeneratorGUI")
except Exception as e:
print(f"DEBUG: Exception in open_editor: {e}")
import traceback
traceback.print_exc()
messagebox.showerror("Editor Error", f"Could not open editor: {e}")
else:
print("DEBUG: shorts_generator is None")
messagebox.showerror("Editor Error", "ShortsGeneratorGUI is not initialized")
def open_thumbnails(self):
"""Open the thumbnail editor"""
print("DEBUG: open_thumbnails called")
if self.shorts_generator:
print("DEBUG: shorts_generator exists")
try:
print("DEBUG: Attempting to call open_thumbnail_editor()")
if hasattr(self.shorts_generator, 'open_thumbnail_editor'):
print("DEBUG: open_thumbnail_editor method exists")
# Call the method directly but handle the parent window issue
# Let's import and call the thumbnail editor function directly
import os
import glob
# Check if there are any video files to work with
video_files = []
# Check for original video
if self.shorts_generator.video_path:
video_files.append(("Original Video", self.shorts_generator.video_path))
# Check for generated shorts
if os.path.exists(self.shorts_generator.output_folder):
shorts = glob.glob(os.path.join(self.shorts_generator.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()
tk.Label(choice_window, text="📸 Select Video for Thumbnail Creation",
font=("Arial", 12, "bold")).pack(pady=10)
selected_video = None
def on_video_select(video_path):
nonlocal selected_video
selected_video = video_path
choice_window.destroy()
# Create list of videos
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=("Arial", 10), pady=5, width=40)
btn.pack(pady=2, padx=20, fill="x")
tk.Button(choice_window, text="Cancel",
command=choice_window.destroy).pack(pady=10)
# 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)
print("DEBUG: Thumbnail editor opened successfully")
else:
print("DEBUG: open_thumbnail_editor method does NOT exist")
messagebox.showerror("Thumbnail Error", "The open_thumbnail_editor method is not available in ShortsGeneratorGUI")
except Exception as e:
print(f"DEBUG: Exception in open_thumbnails: {e}")
import traceback
traceback.print_exc()
messagebox.showerror("Thumbnail Error", f"Could not open thumbnail editor: {e}")
else:
print("DEBUG: shorts_generator is None")
messagebox.showerror("Thumbnail Error", "ShortsGeneratorGUI is not initialized")
def run(self):
"""Start the main application"""
self.root.mainloop()
if __name__ == "__main__":
app = MainApplication()
app.run()