feat: Enhance progress tracking with a dedicated ProgressWindow for clip processing and previews

This commit is contained in:
klop51 2025-08-09 22:49:02 +02:00
parent ae5a287aeb
commit 0b25f6fef5

455
Main.py
View File

@ -1,52 +1,431 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk, filedialog from tkinter import ttk, filedialog, messagebox
import threading import threading
import time import time
import os
import sys
# Example function that simulates clip processing with time tracking # Import the ShortsGeneratorGUI from shorts_generator2.py
def process_clips(progress_var, progress_bar, status_label, time_label, total_steps=10): try:
start_time = time.time() from shorts_generator2 import ShortsGeneratorGUI
for i in range(total_steps): except ImportError:
time.sleep(0.5) # Simulate processing time messagebox.showerror("Error", "Could not import ShortsGeneratorGUI from shorts_generator2.py")
elapsed = time.time() - start_time sys.exit(1)
remaining = (elapsed / (i+1)) * (total_steps - (i+1))
progress_var.set((i+1) * 100 / total_steps)
status_label.config(text=f"Processing... {i+1}/{total_steps}")
time_label.config(text=f"Elapsed: {elapsed:.1f}s | Remaining: {remaining:.1f}s")
status_label.config(text="Done!")
time_label.config(text=f"Total Time: {time.time() - start_time:.1f}s")
def start_preview_with_progress(): class ProgressWindow:
progress_window = tk.Toplevel(root) def __init__(self, parent, title="Processing"):
progress_window.title("Processing Preview") self.parent = parent
progress_window.geometry("320x130") self.window = tk.Toplevel(parent)
progress_window.resizable(False, False) self.window.title(title)
self.window.geometry("400x160")
self.window.resizable(False, False)
self.window.transient(parent)
self.window.grab_set()
status_label = tk.Label(progress_window, text="Initializing...", anchor="w") # Center the window
status_label.pack(fill="x", padx=10, pady=(10,0)) 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}")
time_label = tk.Label(progress_window, text="Elapsed: 0.0s | Remaining: --s", anchor="w") # Create progress widgets
time_label.pack(fill="x", padx=10) self.status_label = tk.Label(self.window, text="Initializing...", anchor="w", font=("Arial", 10))
self.status_label.pack(fill="x", padx=15, pady=(15,5))
progress_var = tk.DoubleVar() self.time_label = tk.Label(self.window, text="Elapsed: 0.0s | Remaining: --s",
progress_bar = ttk.Progressbar(progress_window, variable=progress_var, maximum=100) anchor="w", font=("Arial", 9), fg="gray")
progress_bar.pack(fill="x", padx=10, pady=10) self.time_label.pack(fill="x", padx=15, pady=(0,5))
threading.Thread(target=process_clips, args=(progress_var, progress_bar, status_label, time_label), daemon=True).start() # Main progress bar
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(self.window, variable=self.progress_var, maximum=100, length=370)
self.progress_bar.pack(fill="x", padx=15, pady=(5,3))
# Main Tkinter app remains responsive with all buttons available # Detection progress bar (hidden by default)
root = tk.Tk() self.detection_label = tk.Label(self.window, text="", anchor="w", font=("Arial", 9), fg="blue")
root.title("Shorts Generator 2") self.detection_progress_var = tk.DoubleVar()
root.geometry("400x200") self.detection_progress_bar = ttk.Progressbar(self.window, variable=self.detection_progress_var,
maximum=100, length=370)
preview_button = tk.Button(root, text="Preview Clips", command=start_preview_with_progress) # Cancel button
preview_button.pack(pady=10) self.cancel_btn = tk.Button(self.window, text="Cancel", command=self.cancel_operation)
self.cancel_btn.pack(pady=(5,15))
# Example of other functional buttons remaining available self.start_time = time.time()
open_button = tk.Button(root, text="Open Video", command=lambda: filedialog.askopenfilename()) self.cancelled = False
open_button.pack(pady=10)
exit_button = tk.Button(root, text="Exit", command=root.quit) def show_detection_progress(self):
exit_button.pack(pady=10) """Show the detection progress bar - thread safe"""
def _show():
self.detection_label.pack(fill="x", padx=15, pady=(3,0))
self.detection_progress_bar.pack(fill="x", padx=15, pady=(3,5))
root.mainloop() self.window.after(0, _show)
def hide_detection_progress(self):
"""Hide the detection progress bar - thread safe"""
def _hide():
self.detection_label.pack_forget()
self.detection_progress_bar.pack_forget()
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_operation(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 MainApplication:
def __init__(self):
self.root = tk.Tk()
self.root.title("AI Shorts Generator - Main Controller")
self.root.geometry("500x600")
self.root.configure(bg="#f0f0f0")
# Initialize the ShortsGeneratorGUI (but don't show its window)
self.shorts_generator = None
self.init_shorts_generator()
self.setup_gui()
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"""
# Title
title_label = tk.Label(self.root, text="🎬 AI Shorts Generator",
font=("Arial", 16, "bold"), bg="#f0f0f0", fg="#2c3e50")
title_label.pack(pady=20)
# File selection frame
file_frame = tk.Frame(self.root, bg="#f0f0f0")
file_frame.pack(pady=10, padx=20, fill="x")
tk.Label(file_frame, text="Selected Video:", font=("Arial", 10, "bold"),
bg="#f0f0f0").pack(anchor="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.pack(fill="x", pady=(5,10))
# 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.pack(pady=5)
# Settings frame (simplified)
settings_frame = tk.LabelFrame(self.root, text="Quick Settings", font=("Arial", 10, "bold"),
bg="#f0f0f0", padx=10, pady=10)
settings_frame.pack(pady=10, padx=20, fill="x")
# Detection mode
tk.Label(settings_frame, text="Detection Mode:", bg="#f0f0f0").pack(anchor="w")
self.detection_var = tk.StringVar(value="loud")
detection_frame = tk.Frame(settings_frame, bg="#f0f0f0")
detection_frame.pack(fill="x", pady=5)
modes = [("Loud Moments", "loud"), ("Scene Changes", "scene"), ("Motion", "motion"),
("Speech", "speech"), ("Audio Peaks", "peaks"), ("Combined", "combined")]
for i, (text, value) in enumerate(modes):
if i % 3 == 0:
row_frame = tk.Frame(detection_frame, bg="#f0f0f0")
row_frame.pack(fill="x")
tk.Radiobutton(row_frame, text=text, variable=self.detection_var, value=value,
bg="#f0f0f0").pack(side="left", padx=10)
# Number of clips
clips_frame = tk.Frame(settings_frame, bg="#f0f0f0")
clips_frame.pack(fill="x", pady=5)
tk.Label(clips_frame, text="Max Clips:", bg="#f0f0f0").pack(side="left")
self.clips_var = tk.IntVar(value=3)
clips_spinbox = tk.Spinbox(clips_frame, from_=1, to=10, textvariable=self.clips_var, width=5)
clips_spinbox.pack(side="left", padx=10)
# Main action buttons
button_frame = tk.Frame(self.root, bg="#f0f0f0")
button_frame.pack(pady=20, padx=20, fill="x")
# 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.pack(fill="x", pady=5)
# Generate Shorts Button
self.generate_btn = tk.Button(button_frame, text="🎬 Generate Shorts",
command=self.generate_shorts_threaded, bg="#4CAF50", fg="white",
font=("Arial", 12, "bold"), pady=10)
self.generate_btn.pack(fill="x", pady=5)
# 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.pack(fill="x", pady=5)
# 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.pack(fill="x", pady=5)
# Status label
self.status_label = tk.Label(self.root, text="Ready - Select a video to begin",
font=("Arial", 9), fg="gray", bg="#f0f0f0")
self.status_label.pack(pady=(20,10))
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 = self.clips_var.get() # Use our own duration setting
# Thread-safe progress callbacks
def progress_callback(message, percent):
self.root.after(0, lambda: progress_window.update_progress(message, percent))
def detection_callback(message, percent):
self.root.after(0, lambda: progress_window.update_detection_progress(message, percent))
# 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)
self.root.after(0, lambda: progress_callback("Analysis complete!", 90))
# Show results
def show_results():
if moments:
result_msg = f"Found {len(moments)} interesting moments:\n\n"
for i, (start, end) in enumerate(moments[:10], 1): # Show first 10
result_msg += f"{i}. {start:.1f}s - {end:.1f}s ({end-start:.1f}s)\n"
if len(moments) > 10:
result_msg += f"\n... and {len(moments)-10} more moments"
messagebox.showinfo("Preview Results", result_msg)
else:
messagebox.showwarning("No Results", "No interesting moments found with current settings.")
progress_window.close()
self.root.after(0, lambda: progress_callback("Preparing results...", 100))
self.root.after(500, show_results)
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
max_clips = self.clips_var.get()
clip_duration = self.clips_var.get() # Using clips_var as duration for simplicity
# Thread-safe progress callbacks
def progress_callback(message, percent):
if not progress_window.cancelled:
self.root.after(0, lambda: progress_window.update_progress(message, percent))
def detection_callback(message, percent):
if not progress_window.cancelled:
self.root.after(0, lambda: progress_window.update_detection_progress(message, percent))
# Run the actual generation
generate_shorts(
video_path,
max_clips=max_clips,
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", f"Successfully generated {max_clips} shorts!\n\nCheck the 'shorts' folder for your videos.")
self.root.after(0, show_success)
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 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())
self.shorts_generator.clips_var.set(self.clips_var.get())
def open_editor(self):
"""Open the shorts editor"""
if self.shorts_generator:
try:
self.shorts_generator.open_shorts_editor()
except Exception as e:
messagebox.showerror("Editor Error", f"Could not open editor: {e}")
def open_thumbnails(self):
"""Open the thumbnail editor"""
if self.shorts_generator:
try:
self.shorts_generator.open_thumbnail_editor()
except Exception as e:
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()