From 0b25f6fef5f27d48e921602a1ec7e9469f021a46 Mon Sep 17 00:00:00 2001 From: klop51 Date: Sat, 9 Aug 2025 22:49:02 +0200 Subject: [PATCH] feat: Enhance progress tracking with a dedicated ProgressWindow for clip processing and previews --- Main.py | 469 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 424 insertions(+), 45 deletions(-) diff --git a/Main.py b/Main.py index 0348d65..3e89c99 100644 --- a/Main.py +++ b/Main.py @@ -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()