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

469
Main.py
View File

@ -1,52 +1,431 @@
import tkinter as tk
from tkinter import ttk, filedialog
from tkinter import ttk, filedialog, messagebox
import threading
import time
import os
import sys
# Example function that simulates clip processing with time tracking
def process_clips(progress_var, progress_bar, status_label, time_label, total_steps=10):
start_time = time.time()
for i in range(total_steps):
time.sleep(0.5) # Simulate processing time
elapsed = time.time() - start_time
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")
# 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)
def start_preview_with_progress():
progress_window = tk.Toplevel(root)
progress_window.title("Processing Preview")
progress_window.geometry("320x130")
progress_window.resizable(False, False)
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.resizable(False, False)
self.window.transient(parent)
self.window.grab_set()
# 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}")
# Create progress widgets
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))
self.time_label = tk.Label(self.window, text="Elapsed: 0.0s | Remaining: --s",
anchor="w", font=("Arial", 9), fg="gray")
self.time_label.pack(fill="x", padx=15, pady=(0,5))
# 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))
# Detection progress bar (hidden by default)
self.detection_label = tk.Label(self.window, text="", anchor="w", font=("Arial", 9), fg="blue")
self.detection_progress_var = tk.DoubleVar()
self.detection_progress_bar = ttk.Progressbar(self.window, variable=self.detection_progress_var,
maximum=100, length=370)
# Cancel button
self.cancel_btn = tk.Button(self.window, text="Cancel", command=self.cancel_operation)
self.cancel_btn.pack(pady=(5,15))
self.start_time = time.time()
self.cancelled = False
def show_detection_progress(self):
"""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))
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()
status_label = tk.Label(progress_window, text="Initializing...", anchor="w")
status_label.pack(fill="x", padx=10, pady=(10,0))
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()
time_label = tk.Label(progress_window, text="Elapsed: 0.0s | Remaining: --s", anchor="w")
time_label.pack(fill="x", padx=10)
progress_var = tk.DoubleVar()
progress_bar = ttk.Progressbar(progress_window, variable=progress_var, maximum=100)
progress_bar.pack(fill="x", padx=10, pady=10)
threading.Thread(target=process_clips, args=(progress_var, progress_bar, status_label, time_label), daemon=True).start()
# Main Tkinter app remains responsive with all buttons available
root = tk.Tk()
root.title("Shorts Generator 2")
root.geometry("400x200")
preview_button = tk.Button(root, text="Preview Clips", command=start_preview_with_progress)
preview_button.pack(pady=10)
# Example of other functional buttons remaining available
open_button = tk.Button(root, text="Open Video", command=lambda: filedialog.askopenfilename())
open_button.pack(pady=10)
exit_button = tk.Button(root, text="Exit", command=root.quit)
exit_button.pack(pady=10)
root.mainloop()
if __name__ == "__main__":
app = MainApplication()
app.run()