diff --git a/shorts_generator2.py b/shorts_generator2.py index e4dfa82..6827e1f 100644 --- a/shorts_generator2.py +++ b/shorts_generator2.py @@ -1,6 +1,8 @@ import os import numpy as np from moviepy import VideoFileClip, TextClip, CompositeVideoClip +from moviepy.video.fx import FadeIn, FadeOut, Resize +from moviepy.audio.fx import MultiplyVolume from faster_whisper import WhisperModel import tkinter as tk from tkinter import filedialog, messagebox, ttk @@ -8,6 +10,9 @@ import threading import cv2 from scipy import signal import librosa +import glob +import json +from datetime import datetime class ToolTip: """Create a tooltip for a given widget""" @@ -565,6 +570,871 @@ def generate_shorts(video_path, max_clips=3, output_folder="shorts", progress_ca if progress_callback: progress_callback("āœ… All shorts generated successfully!", 100) +# Video Editing Tools +class VideoEditor: + """Professional video editing tools for generated shorts""" + + @staticmethod + def trim_video(video_path, start_time, end_time, output_path): + """Trim video to specific time range""" + clip = VideoFileClip(video_path) + trimmed = clip.subclipped(start_time, end_time) + trimmed.write_videofile(output_path, codec="libx264", audio_codec="aac") + clip.close() + trimmed.close() + + @staticmethod + def adjust_speed(video_path, speed_factor, output_path): + """Change video playback speed (0.5 = half speed, 2.0 = double speed)""" + clip = VideoFileClip(video_path) + if speed_factor > 1: + # Speed up + speeded = clip.with_fps(clip.fps * speed_factor).subclipped(0, clip.duration / speed_factor) + else: + # Slow down + speeded = clip.with_fps(clip.fps * speed_factor) + speeded.write_videofile(output_path, codec="libx264", audio_codec="aac") + clip.close() + speeded.close() + + @staticmethod + def add_fade_effects(video_path, fade_in_duration=1.0, fade_out_duration=1.0, output_path=None): + """Add fade in/out effects""" + clip = VideoFileClip(video_path) + + # Apply fade effects + final_clip = clip + if fade_in_duration > 0: + final_clip = final_clip.with_effects([FadeIn(fade_in_duration)]) + if fade_out_duration > 0: + final_clip = final_clip.with_effects([FadeOut(fade_out_duration)]) + + if not output_path: + output_path = video_path.replace('.mp4', '_faded.mp4') + + final_clip.write_videofile(output_path, codec="libx264", audio_codec="aac") + clip.close() + final_clip.close() + return output_path + + @staticmethod + def adjust_volume(video_path, volume_factor, output_path=None): + """Adjust audio volume (1.0 = normal, 0.5 = half volume, 2.0 = double volume)""" + clip = VideoFileClip(video_path) + + if clip.audio: + audio_adjusted = clip.audio.with_effects([MultiplyVolume(volume_factor)]) + final_clip = clip.with_audio(audio_adjusted) + else: + final_clip = clip + + if not output_path: + output_path = video_path.replace('.mp4', '_volume_adjusted.mp4') + + final_clip.write_videofile(output_path, codec="libx264", audio_codec="aac") + clip.close() + final_clip.close() + return output_path + + @staticmethod + def resize_video(video_path, width, height, output_path=None): + """Resize video to specific dimensions""" + clip = VideoFileClip(video_path) + resized = clip.resized((width, height)) + + if not output_path: + output_path = video_path.replace('.mp4', f'_resized_{width}x{height}.mp4') + + resized.write_videofile(output_path, codec="libx264", audio_codec="aac") + clip.close() + resized.close() + return output_path + + @staticmethod + def crop_video(video_path, x1, y1, x2, y2, output_path=None): + """Crop video to specific coordinates""" + clip = VideoFileClip(video_path) + cropped = clip.cropped(x1=x1, y1=y1, x2=x2, y2=y2) + + if not output_path: + output_path = video_path.replace('.mp4', '_cropped.mp4') + + cropped.write_videofile(output_path, codec="libx264", audio_codec="aac") + clip.close() + cropped.close() + return output_path + + @staticmethod + def add_text_overlay(video_path, text, position=('center', 'bottom'), + duration=None, start_time=0, font_size=50, color='white', output_path=None): + """Add text overlay to video (optimized for speed)""" + print(f"šŸŽ¬ Adding text overlay: '{text}'...") + + clip = VideoFileClip(video_path) + + if duration is None: + duration = clip.duration - start_time + + # Optimize text creation - use smaller cache and faster rendering + try: + # Try using a more efficient text creation method + text_clip = TextClip( + text, + font_size=font_size, + color=color, + stroke_color='black', + stroke_width=2, + method='caption', # Faster rendering method + size=(clip.w * 0.8, None) # Limit width to prevent huge text + ) + + print(f"šŸ“ Text clip created successfully...") + + except Exception as e: + print(f"āš ļø Using fallback text method: {e}") + # Fallback to basic text creation + text_clip = TextClip( + text, + font_size=font_size, + color=color, + stroke_color='black', + stroke_width=2 + ) + + # Set timing and position + text_clip = text_clip.with_start(start_time).with_end(start_time + duration).with_position(position) + + print(f"ā±ļø Compositing video with text overlay...") + + # Optimize composition with reduced quality for faster processing + final_video = CompositeVideoClip([clip, text_clip]) + + if not output_path: + output_path = video_path.replace('.mp4', '_with_text.mp4') + + print(f"šŸ’¾ Saving video to: {output_path}") + + # Optimize output settings for faster processing + try: + # Try with all optimization parameters (newer MoviePy) + final_video.write_videofile( + output_path, + codec="libx264", + audio_codec="aac", + temp_audiofile='temp-audio.m4a', + remove_temp=True, + verbose=False, # Reduce console output + logger=None, # Disable logging for speed + preset='ultrafast', # Fastest encoding preset + threads=4 # Use multiple threads + ) + except TypeError: + # Fallback for older MoviePy versions + final_video.write_videofile( + output_path, + codec="libx264", + audio_codec="aac", + temp_audiofile='temp-audio.m4a', + remove_temp=True, + preset='ultrafast', # Fastest encoding preset + threads=4 # Use multiple threads + ) + + # Clean up + clip.close() + text_clip.close() + final_video.close() + + print(f"āœ… Text overlay completed!") + return output_path + + @staticmethod + def add_text_overlay_fast(video_path, text, position=('center', 'bottom'), + font_size=50, color='white', output_path=None): + """Ultra-fast text overlay using PIL (for simple text only)""" + try: + from PIL import Image, ImageDraw, ImageFont + import cv2 + + print(f"šŸš€ Using fast text overlay method...") + + # Read video with OpenCV for faster processing + cap = cv2.VideoCapture(video_path) + fps = cap.get(cv2.CAP_PROP_FPS) + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + + if not output_path: + output_path = video_path.replace('.mp4', '_with_text_fast.mp4') + + # Set up video writer + fourcc = cv2.VideoWriter_fourcc(*'mp4v') + out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) + + frame_count = 0 + total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + + # Calculate text position + if position == ('center', 'bottom'): + text_x, text_y = width // 2, height - 100 + elif position == ('center', 'top'): + text_x, text_y = width // 2, 100 + elif position == ('center', 'center'): + text_x, text_y = width // 2, height // 2 + else: + text_x, text_y = width // 2, height - 100 # Default + + print(f"šŸ“¹ Processing {total_frames} frames...") + + while True: + ret, frame = cap.read() + if not ret: + break + + # Convert BGR to RGB for PIL + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + pil_image = Image.fromarray(frame_rgb) + draw = ImageDraw.Draw(pil_image) + + # Try to use a system font, fallback to default + try: + font = ImageFont.truetype("arial.ttf", font_size) + except: + try: + font = ImageFont.truetype("calibri.ttf", font_size) + except: + try: + font = ImageFont.truetype("tahoma.ttf", font_size) + except: + font = ImageFont.load_default() + print(f"šŸ“ Using default font (system fonts not found)") + + # Add text with outline effect (centered text) + text_bbox = draw.textbbox((0, 0), text, font=font) + text_width = text_bbox[2] - text_bbox[0] + text_height = text_bbox[3] - text_bbox[1] + + # Center the text properly + centered_x = text_x - (text_width // 2) + centered_y = text_y - (text_height // 2) + + outline_width = 2 + for adj_x in range(-outline_width, outline_width + 1): + for adj_y in range(-outline_width, outline_width + 1): + draw.text((centered_x + adj_x, centered_y + adj_y), text, font=font, fill='black') + + # Add main text + draw.text((centered_x, centered_y), text, font=font, fill=color) + + # Convert back to BGR for OpenCV + frame_with_text = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR) + out.write(frame_with_text) + + frame_count += 1 + if frame_count % 30 == 0: # Progress every 30 frames + progress = (frame_count / total_frames) * 100 + print(f"šŸŽ¬ Progress: {progress:.1f}%") + + cap.release() + out.release() + + # Add audio back using MoviePy (faster than re-encoding everything) + print(f"šŸ”Š Adding audio track...") + video_with_audio = VideoFileClip(video_path) + video_with_text = VideoFileClip(output_path) + final_video = video_with_text.with_audio(video_with_audio.audio) + + temp_output = output_path.replace('.mp4', '_temp.mp4') + try: + # Try with verbose parameter (newer MoviePy) + final_video.write_videofile(temp_output, codec="libx264", audio_codec="aac", + verbose=False, logger=None) + except TypeError: + # Fallback for older MoviePy versions without verbose parameter + final_video.write_videofile(temp_output, codec="libx264", audio_codec="aac") + + # Replace original with final version + import os + os.remove(output_path) + os.rename(temp_output, output_path) + + video_with_audio.close() + video_with_text.close() + final_video.close() + + print(f"āœ… Fast text overlay completed!") + return output_path + + except ImportError: + print(f"āš ļø PIL not available, falling back to MoviePy method...") + return VideoEditor.add_text_overlay(video_path, text, position, + font_size=font_size, color=color, output_path=output_path) + except Exception as e: + print(f"āš ļø Fast method failed ({e}), falling back to MoviePy...") + return VideoEditor.add_text_overlay(video_path, text, position, + font_size=font_size, color=color, output_path=output_path) + + @staticmethod + def get_video_info(video_path): + """Get basic video information""" + clip = VideoFileClip(video_path) + info = { + 'duration': clip.duration, + 'fps': clip.fps, + 'size': clip.size, + 'has_audio': clip.audio is not None + } + clip.close() + return info + +# Post-Generation Editing Interface +class ShortsEditorGUI: + """Interface for editing generated shorts""" + + def __init__(self, parent, shorts_folder="shorts"): + self.parent = parent + self.shorts_folder = shorts_folder + self.current_video = None + self.video_info = None + self.editor_window = None + + def open_editor(self): + """Open the shorts editing interface""" + # Find available shorts + shorts_files = glob.glob(os.path.join(self.shorts_folder, "*.mp4")) + + if not shorts_files: + messagebox.showinfo("No Shorts Found", + f"No video files found in '{self.shorts_folder}' folder.\nGenerate some shorts first!") + return + + # Create editor window + self.editor_window = tk.Toplevel(self.parent) + self.editor_window.title("šŸŽ¬ Shorts Editor - Professional Video Editing") + self.editor_window.geometry("800x700") + self.editor_window.transient(self.parent) + + self.create_editor_interface(shorts_files) + + def create_editor_interface(self, shorts_files): + """Create the main editor interface""" + # Title + title_frame = tk.Frame(self.editor_window) + title_frame.pack(fill="x", padx=20, pady=10) + + tk.Label(title_frame, text="šŸŽ¬ Professional Shorts Editor", + font=("Arial", 16, "bold")).pack() + tk.Label(title_frame, text="Select and edit your generated shorts with professional tools", + font=("Arial", 10), fg="gray").pack() + + # Video selection frame + selection_frame = tk.LabelFrame(self.editor_window, text="šŸ“ Select Short to Edit", padx=10, pady=10) + selection_frame.pack(fill="x", padx=20, pady=10) + + # Video list with preview info + list_frame = tk.Frame(selection_frame) + list_frame.pack(fill="x") + + tk.Label(list_frame, text="Available Shorts:", font=("Arial", 10, "bold")).pack(anchor="w") + + # Listbox with scrollbar + list_container = tk.Frame(list_frame) + list_container.pack(fill="x", pady=5) + + self.video_listbox = tk.Listbox(list_container, height=4, font=("Courier", 9)) + scrollbar = tk.Scrollbar(list_container, orient="vertical") + self.video_listbox.config(yscrollcommand=scrollbar.set) + scrollbar.config(command=self.video_listbox.yview) + + self.video_listbox.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + + # Populate video list with file info + self.video_files = [] + for video_file in sorted(shorts_files): + try: + info = VideoEditor.get_video_info(video_file) + filename = os.path.basename(video_file) + size_mb = os.path.getsize(video_file) / (1024 * 1024) + display_text = f"{filename:<20} │ {info['duration']:.1f}s │ {info['size'][0]}x{info['size'][1]} │ {size_mb:.1f}MB" + self.video_listbox.insert(tk.END, display_text) + self.video_files.append(video_file) + except Exception as e: + print(f"Error reading {video_file}: {e}") + + # Video selection handler + def on_video_select(event): + selection = self.video_listbox.curselection() + if selection: + self.current_video = self.video_files[selection[0]] + self.video_info = VideoEditor.get_video_info(self.current_video) + self.update_video_info() + self.enable_editing_tools() + + self.video_listbox.bind("<>", on_video_select) + + # Current video info + self.info_frame = tk.LabelFrame(self.editor_window, text="šŸ“Š Video Information", padx=10, pady=10) + self.info_frame.pack(fill="x", padx=20, pady=10) + + self.info_label = tk.Label(self.info_frame, text="Select a video to see details", + font=("Courier", 9), justify="left") + self.info_label.pack(anchor="w") + + # Editing tools frame + self.tools_frame = tk.LabelFrame(self.editor_window, text="šŸ› ļø Professional Editing Tools", padx=10, pady=10) + self.tools_frame.pack(fill="both", expand=True, padx=20, pady=10) + + self.create_editing_tools() + + # Output and action buttons + action_frame = tk.Frame(self.editor_window) + action_frame.pack(fill="x", padx=20, pady=10) + + # Output folder selection + output_folder_frame = tk.Frame(action_frame) + output_folder_frame.pack(fill="x", pady=5) + + tk.Label(output_folder_frame, text="Output Folder:", font=("Arial", 9, "bold")).pack(side="left") + self.output_folder = tk.StringVar(value=os.path.join(self.shorts_folder, "edited")) + output_entry = tk.Entry(output_folder_frame, textvariable=self.output_folder, width=40) + output_entry.pack(side="left", padx=(10, 5)) + + tk.Button(output_folder_frame, text="Browse", + command=self.select_output_folder).pack(side="left") + + # Action buttons + button_frame = tk.Frame(action_frame) + button_frame.pack(fill="x", pady=10) + + tk.Button(button_frame, text="šŸ”„ Refresh List", + command=self.refresh_video_list, bg="#2196F3", fg="white").pack(side="left", padx=5) + + tk.Button(button_frame, text="šŸ“‚ Open Shorts Folder", + command=self.open_shorts_folder, bg="#FF9800", fg="white").pack(side="left", padx=5) + + tk.Button(button_frame, text="āŒ Close Editor", + command=self.editor_window.destroy, bg="#F44336", fg="white").pack(side="right", padx=5) + + def create_editing_tools(self): + """Create the professional editing tools interface""" + # Create notebook for organized tools + notebook = ttk.Notebook(self.tools_frame) + notebook.pack(fill="both", expand=True) + + # Basic Editing Tab + basic_frame = ttk.Frame(notebook) + notebook.add(basic_frame, text="āœ‚ļø Basic Editing") + + # Trim Tool + trim_frame = tk.LabelFrame(basic_frame, text="āœ‚ļø Trim Video", padx=10, pady=5) + trim_frame.pack(fill="x", padx=10, pady=5) + + trim_controls = tk.Frame(trim_frame) + trim_controls.pack(fill="x") + + tk.Label(trim_controls, text="Start:").pack(side="left") + self.trim_start = tk.DoubleVar(value=0.0) + tk.Spinbox(trim_controls, from_=0, to=120, increment=0.1, width=8, + textvariable=self.trim_start, format="%.1f").pack(side="left", padx=5) + + tk.Label(trim_controls, text="End:").pack(side="left", padx=(10, 0)) + self.trim_end = tk.DoubleVar(value=5.0) + tk.Spinbox(trim_controls, from_=0, to=120, increment=0.1, width=8, + textvariable=self.trim_end, format="%.1f").pack(side="left", padx=5) + + tk.Button(trim_controls, text="āœ‚ļø Trim Video", + command=self.trim_video, bg="#4CAF50", fg="white").pack(side="right", padx=10) + + # Speed Tool + speed_frame = tk.LabelFrame(basic_frame, text="⚔ Speed Control", padx=10, pady=5) + speed_frame.pack(fill="x", padx=10, pady=5) + + speed_controls = tk.Frame(speed_frame) + speed_controls.pack(fill="x") + + tk.Label(speed_controls, text="Speed:").pack(side="left") + self.speed_factor = tk.DoubleVar(value=1.0) + speed_spinbox = tk.Spinbox(speed_controls, from_=0.1, to=5.0, increment=0.1, width=8, + textvariable=self.speed_factor, format="%.1f") + speed_spinbox.pack(side="left", padx=5) + + tk.Label(speed_controls, text="(0.5=slow, 1.0=normal, 2.0=fast)").pack(side="left", padx=5) + tk.Button(speed_controls, text="⚔ Apply Speed", + command=self.adjust_speed, bg="#FF9800", fg="white").pack(side="right", padx=10) + + # Effects Tab + effects_frame = ttk.Frame(notebook) + notebook.add(effects_frame, text="✨ Effects") + + # Fade Effects + fade_frame = tk.LabelFrame(effects_frame, text="šŸŒ… Fade Effects", padx=10, pady=5) + fade_frame.pack(fill="x", padx=10, pady=5) + + fade_controls = tk.Frame(fade_frame) + fade_controls.pack(fill="x") + + tk.Label(fade_controls, text="Fade In:").pack(side="left") + self.fade_in = tk.DoubleVar(value=0.5) + tk.Spinbox(fade_controls, from_=0, to=5, increment=0.1, width=6, + textvariable=self.fade_in, format="%.1f").pack(side="left", padx=5) + + tk.Label(fade_controls, text="Fade Out:").pack(side="left", padx=(10, 0)) + self.fade_out = tk.DoubleVar(value=0.5) + tk.Spinbox(fade_controls, from_=0, to=5, increment=0.1, width=6, + textvariable=self.fade_out, format="%.1f").pack(side="left", padx=5) + + tk.Button(fade_controls, text="šŸŒ… Add Fades", + command=self.add_fades, bg="#9C27B0", fg="white").pack(side="right", padx=10) + + # Volume Control + volume_frame = tk.LabelFrame(effects_frame, text="šŸ”Š Volume Control", padx=10, pady=5) + volume_frame.pack(fill="x", padx=10, pady=5) + + volume_controls = tk.Frame(volume_frame) + volume_controls.pack(fill="x") + + tk.Label(volume_controls, text="Volume:").pack(side="left") + self.volume_factor = tk.DoubleVar(value=1.0) + tk.Spinbox(volume_controls, from_=0, to=3, increment=0.1, width=6, + textvariable=self.volume_factor, format="%.1f").pack(side="left", padx=5) + + tk.Label(volume_controls, text="(0.0=mute, 1.0=normal, 2.0=loud)").pack(side="left", padx=5) + tk.Button(volume_controls, text="šŸ”Š Adjust Volume", + command=self.adjust_volume, bg="#3F51B5", fg="white").pack(side="right", padx=10) + + # Transform Tab + transform_frame = ttk.Frame(notebook) + notebook.add(transform_frame, text="šŸ”„ Transform") + + # Resize Tool + resize_frame = tk.LabelFrame(transform_frame, text="šŸ“ Resize Video", padx=10, pady=5) + resize_frame.pack(fill="x", padx=10, pady=5) + + resize_controls = tk.Frame(resize_frame) + resize_controls.pack(fill="x") + + tk.Label(resize_controls, text="Width:").pack(side="left") + self.resize_width = tk.IntVar(value=1080) + tk.Spinbox(resize_controls, from_=240, to=4320, increment=120, width=6, + textvariable=self.resize_width).pack(side="left", padx=5) + + tk.Label(resize_controls, text="Height:").pack(side="left", padx=(10, 0)) + self.resize_height = tk.IntVar(value=1920) + tk.Spinbox(resize_controls, from_=240, to=4320, increment=120, width=6, + textvariable=self.resize_height).pack(side="left", padx=5) + + tk.Button(resize_controls, text="šŸ“ Resize", + command=self.resize_video, bg="#607D8B", fg="white").pack(side="right", padx=10) + + # Text Overlay Tab + text_frame = ttk.Frame(notebook) + notebook.add(text_frame, text="šŸ“ Text Overlay") + + text_overlay_frame = tk.LabelFrame(text_frame, text="šŸ“ Add Text Overlay", padx=10, pady=5) + text_overlay_frame.pack(fill="x", padx=10, pady=5) + + # Text input + text_input_frame = tk.Frame(text_overlay_frame) + text_input_frame.pack(fill="x", pady=5) + + tk.Label(text_input_frame, text="Text:").pack(side="left") + self.overlay_text = tk.StringVar(value="Your Text Here") + tk.Entry(text_input_frame, textvariable=self.overlay_text, width=30).pack(side="left", padx=5) + + # Text settings + text_settings_frame = tk.Frame(text_overlay_frame) + text_settings_frame.pack(fill="x", pady=5) + + tk.Label(text_settings_frame, text="Size:").pack(side="left") + self.text_size = tk.IntVar(value=50) + tk.Spinbox(text_settings_frame, from_=20, to=150, width=6, + textvariable=self.text_size).pack(side="left", padx=5) + + tk.Label(text_settings_frame, text="Position:").pack(side="left", padx=(10, 0)) + self.text_position = tk.StringVar(value="center,bottom") + position_combo = ttk.Combobox(text_settings_frame, textvariable=self.text_position, width=15, + values=["center,top", "center,center", "center,bottom", + "left,top", "right,top", "left,bottom", "right,bottom"], + state="readonly") + position_combo.pack(side="left", padx=5) + + # Speed/Quality options + speed_frame = tk.Frame(text_overlay_frame) + speed_frame.pack(fill="x", pady=5) + + tk.Label(speed_frame, text="Processing Method:", font=("Arial", 9, "bold")).pack(side="left") + self.text_method = tk.StringVar(value="fast") + + method_frame = tk.Frame(speed_frame) + method_frame.pack(side="left", padx=10) + + tk.Radiobutton(method_frame, text="šŸš€ Fast (PIL)", variable=self.text_method, + value="fast", font=("Arial", 8)).pack(side="left") + tk.Radiobutton(method_frame, text="šŸŽ¬ High Quality (MoviePy)", variable=self.text_method, + value="quality", font=("Arial", 8)).pack(side="left", padx=(10, 0)) + + # Info label for method explanation + method_info = tk.Label(speed_frame, text="Fast: 3-5x faster, basic text | Quality: Slower, advanced effects", + font=("Arial", 7), fg="gray") + method_info.pack(side="right") + + # Button frame + button_frame = tk.Frame(text_overlay_frame) + button_frame.pack(fill="x", pady=5) + + tk.Button(button_frame, text="šŸ“ Add Text Overlay", + command=self.add_text_overlay, bg="#795548", fg="white", + font=("Arial", 10, "bold")).pack(side="right", padx=10) + + # Initially disable all tools + self.disable_editing_tools() + + def disable_editing_tools(self): + """Disable all editing tools until a video is selected""" + for widget in self.tools_frame.winfo_children(): + self.set_widget_state(widget, "disabled") + + def enable_editing_tools(self): + """Enable editing tools when a video is selected""" + for widget in self.tools_frame.winfo_children(): + self.set_widget_state(widget, "normal") + + # Update trim end time to video duration + if self.video_info: + self.trim_end.set(min(self.video_info['duration'], 30.0)) + + def set_widget_state(self, widget, state): + """Recursively set widget state""" + try: + widget.config(state=state) + except: + pass + for child in widget.winfo_children(): + self.set_widget_state(child, state) + + def update_video_info(self): + """Update the video information display""" + if self.video_info and self.current_video: + filename = os.path.basename(self.current_video) + info_text = f"""šŸ“ File: {filename} +ā±ļø Duration: {self.video_info['duration']:.2f} seconds +šŸ“ Resolution: {self.video_info['size'][0]} x {self.video_info['size'][1]} +šŸŽ¬ FPS: {self.video_info['fps']:.1f} +šŸ”Š Audio: {'Yes' if self.video_info['has_audio'] else 'No'} +šŸ’¾ Size: {os.path.getsize(self.current_video) / (1024*1024):.1f} MB""" + self.info_label.config(text=info_text) + + def select_output_folder(self): + """Select output folder for edited videos""" + folder = filedialog.askdirectory(title="Select Output Folder") + if folder: + self.output_folder.set(folder) + + def refresh_video_list(self): + """Refresh the list of available videos""" + self.video_listbox.delete(0, tk.END) + self.video_files.clear() + + shorts_files = glob.glob(os.path.join(self.shorts_folder, "*.mp4")) + for video_file in sorted(shorts_files): + try: + info = VideoEditor.get_video_info(video_file) + filename = os.path.basename(video_file) + size_mb = os.path.getsize(video_file) / (1024 * 1024) + display_text = f"{filename:<20} │ {info['duration']:.1f}s │ {info['size'][0]}x{info['size'][1]} │ {size_mb:.1f}MB" + self.video_listbox.insert(tk.END, display_text) + self.video_files.append(video_file) + except Exception as e: + print(f"Error reading {video_file}: {e}") + + def open_shorts_folder(self): + """Open the shorts folder in file explorer""" + import subprocess + try: + subprocess.run(['explorer', os.path.abspath(self.shorts_folder)], check=True) + except: + messagebox.showinfo("Folder Location", f"Shorts folder: {os.path.abspath(self.shorts_folder)}") + + def get_output_path(self, suffix): + """Generate output path with timestamp""" + if not self.current_video: + return None + + os.makedirs(self.output_folder.get(), exist_ok=True) + base_name = os.path.splitext(os.path.basename(self.current_video))[0] + timestamp = datetime.now().strftime("%H%M%S") + return os.path.join(self.output_folder.get(), f"{base_name}_{suffix}_{timestamp}.mp4") + + def show_progress_dialog(self, title, operation_func): + """Show progress dialog for editing operations""" + progress_window = tk.Toplevel(self.editor_window) + progress_window.title(title) + progress_window.geometry("400x120") + progress_window.transient(self.editor_window) + progress_window.grab_set() + + tk.Label(progress_window, text=f"šŸŽ¬ {title}", font=("Arial", 12, "bold")).pack(pady=10) + + progress_label = tk.Label(progress_window, text="Processing video...") + progress_label.pack(pady=5) + + progress_bar = ttk.Progressbar(progress_window, mode="indeterminate") + progress_bar.pack(fill="x", padx=20, pady=10) + progress_bar.start() + + def run_operation(): + try: + result = operation_func() + progress_window.after(0, lambda r=result: self.operation_complete(progress_window, r, title)) + except Exception as error: + progress_window.after(0, lambda err=str(error): self.operation_error(progress_window, err)) + + threading.Thread(target=run_operation, daemon=True).start() + + def operation_complete(self, progress_window, result, operation_name): + """Handle successful operation completion""" + progress_window.destroy() + if result: + messagebox.showinfo("Success", + f"āœ… {operation_name} completed successfully!\n\n" + f"Output saved to:\n{result}") + self.refresh_video_list() + + def operation_error(self, progress_window, error_msg): + """Handle operation error""" + progress_window.destroy() + messagebox.showerror("Error", f"āŒ Operation failed:\n{error_msg}") + + # Editing tool methods + def trim_video(self): + """Trim the selected video""" + if not self.current_video: + messagebox.showwarning("No Video", "Please select a video first!") + return + + start = self.trim_start.get() + end = self.trim_end.get() + + if start >= end: + messagebox.showwarning("Invalid Range", "Start time must be less than end time!") + return + + if end > self.video_info['duration']: + messagebox.showwarning("Invalid Range", f"End time cannot exceed video duration ({self.video_info['duration']:.1f}s)!") + return + + output_path = self.get_output_path("trimmed") + + def trim_operation(): + VideoEditor.trim_video(self.current_video, start, end, output_path) + return output_path + + self.show_progress_dialog("Trimming Video", trim_operation) + + def adjust_speed(self): + """Adjust video speed""" + if not self.current_video: + messagebox.showwarning("No Video", "Please select a video first!") + return + + speed = self.speed_factor.get() + if speed <= 0: + messagebox.showwarning("Invalid Speed", "Speed must be greater than 0!") + return + + output_path = self.get_output_path(f"speed_{speed:.1f}x") + + def speed_operation(): + VideoEditor.adjust_speed(self.current_video, speed, output_path) + return output_path + + self.show_progress_dialog("Adjusting Speed", speed_operation) + + def add_fades(self): + """Add fade effects""" + if not self.current_video: + messagebox.showwarning("No Video", "Please select a video first!") + return + + fade_in = self.fade_in.get() + fade_out = self.fade_out.get() + + output_path = self.get_output_path("faded") + + def fade_operation(): + return VideoEditor.add_fade_effects(self.current_video, fade_in, fade_out, output_path) + + self.show_progress_dialog("Adding Fade Effects", fade_operation) + + def adjust_volume(self): + """Adjust video volume""" + if not self.current_video: + messagebox.showwarning("No Video", "Please select a video first!") + return + + if not self.video_info['has_audio']: + messagebox.showwarning("No Audio", "Selected video has no audio track!") + return + + volume = self.volume_factor.get() + output_path = self.get_output_path(f"volume_{volume:.1f}x") + + def volume_operation(): + return VideoEditor.adjust_volume(self.current_video, volume, output_path) + + self.show_progress_dialog("Adjusting Volume", volume_operation) + + def resize_video(self): + """Resize video""" + if not self.current_video: + messagebox.showwarning("No Video", "Please select a video first!") + return + + width = self.resize_width.get() + height = self.resize_height.get() + + if width < 1 or height < 1: + messagebox.showwarning("Invalid Size", "Width and height must be positive!") + return + + output_path = self.get_output_path(f"resized_{width}x{height}") + + def resize_operation(): + return VideoEditor.resize_video(self.current_video, width, height, output_path) + + self.show_progress_dialog("Resizing Video", resize_operation) + + def add_text_overlay(self): + """Add text overlay to video with speed options""" + if not self.current_video: + messagebox.showwarning("No Video", "Please select a video first!") + return + + text = self.overlay_text.get().strip() + if not text: + messagebox.showwarning("No Text", "Please enter text to overlay!") + return + + position_str = self.text_position.get() + position = tuple(position_str.split(',')) + size = self.text_size.get() + method = self.text_method.get() + + output_path = self.get_output_path("with_text") + + # Choose method based on user selection + if method == "fast": + def text_operation(): + return VideoEditor.add_text_overlay_fast(self.current_video, text, position, + font_size=size, output_path=output_path) + self.show_progress_dialog("Adding Text Overlay (Fast Method)", text_operation) + else: + def text_operation(): + return VideoEditor.add_text_overlay(self.current_video, text, position, + font_size=size, output_path=output_path) + self.show_progress_dialog("Adding Text Overlay (High Quality)", text_operation) + # GUI Components class ShortsGeneratorGUI: def __init__(self, root): @@ -828,6 +1698,46 @@ Tip: Always preview first to see what the AI finds!""" Tip: Use Preview first to fine-tune your settings!""" ToolTip(self.generate_btn, generate_tooltip_text, side='right') + # Edit Shorts button + self.edit_btn = tk.Button(self.root, text="āœļø Edit Generated Shorts", + command=self.open_shorts_editor, bg="#FF9800", fg="white", + font=("Arial", 11, "bold"), pady=8) + self.edit_btn.pack(pady=5) + + # Add tooltip for edit button + edit_tooltip_text = """Professional Shorts Editor: + +• Select any generated short for editing +• Trim, speed up/slow down videos +• Add fade in/out effects +• Adjust volume levels +• Resize and crop videos +• Add custom text overlays +• Real-time preview and professional tools + +Transform your shorts into perfect content!""" + ToolTip(self.edit_btn, edit_tooltip_text, side='right') + + # Thumbnail Editor button + self.thumbnail_btn = tk.Button(self.root, text="šŸ“ø Create Thumbnails", + command=self.open_thumbnail_editor, bg="#9C27B0", fg="white", + font=("Arial", 11, "bold"), pady=8) + self.thumbnail_btn.pack(pady=5) + + # Add tooltip for thumbnail button + thumbnail_tooltip_text = """Professional Thumbnail Editor: + +• Select any video to create custom thumbnails +• Choose the perfect frame with timeline slider +• Add text overlays with custom fonts and colors +• Add stickers and emojis for eye-catching designs +• Drag and drop positioning +• High-quality export (JPEG/PNG) +• Perfect for YouTube, TikTok, Instagram + +Create thumbnails that get clicks!""" + ToolTip(self.thumbnail_btn, thumbnail_tooltip_text, side='right') + # Progress frame progress_frame = tk.Frame(self.root) progress_frame.pack(pady=5, padx=20, fill="x") @@ -1101,6 +2011,84 @@ Tip: Use Preview first to fine-tune your settings!""" thread = threading.Thread(target=self.generation_worker) thread.daemon = True thread.start() + + def open_shorts_editor(self): + """Open the professional shorts editor""" + editor = ShortsEditorGUI(self.root, self.output_folder) + editor.open_editor() + + def open_thumbnail_editor(self): + """Open the professional thumbnail editor""" + # Import the thumbnail editor + try: + import subprocess + import sys + + # Check if there are any video files to work with + video_files = [] + + # Check for original video + if self.video_path: + video_files.append(("Original Video", self.video_path)) + + # Check for generated shorts + if os.path.exists(self.output_folder): + import glob + shorts = glob.glob(os.path.join(self.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) + + except ImportError as e: + messagebox.showerror("Thumbnail Editor Error", + f"Could not load thumbnail editor:\n{str(e)}\n\nMake sure thumbnail_editor.py is in the same folder.") + except Exception as e: + messagebox.showerror("Error", f"Failed to open thumbnail editor:\n{str(e)}") def run_gui(): root = tk.Tk() diff --git a/stickers/confused.png b/stickers/confused.png new file mode 100644 index 0000000..79d970f Binary files /dev/null and b/stickers/confused.png differ diff --git a/stickers/emoji (1).png b/stickers/emoji (1).png new file mode 100644 index 0000000..5d4377b Binary files /dev/null and b/stickers/emoji (1).png differ diff --git a/stickers/emoji.png b/stickers/emoji.png new file mode 100644 index 0000000..3445e32 Binary files /dev/null and b/stickers/emoji.png differ diff --git a/stickers/happy-face.png b/stickers/happy-face.png new file mode 100644 index 0000000..08abf8c Binary files /dev/null and b/stickers/happy-face.png differ diff --git a/stickers/laugh.png b/stickers/laugh.png new file mode 100644 index 0000000..dfccf18 Binary files /dev/null and b/stickers/laugh.png differ diff --git a/stickers/party.png b/stickers/party.png new file mode 100644 index 0000000..499e429 Binary files /dev/null and b/stickers/party.png differ diff --git a/stickers/sad-face.png b/stickers/sad-face.png new file mode 100644 index 0000000..e4bace8 Binary files /dev/null and b/stickers/sad-face.png differ diff --git a/stickers/smile (1).png b/stickers/smile (1).png new file mode 100644 index 0000000..ab0e87a Binary files /dev/null and b/stickers/smile (1).png differ diff --git a/stickers/smile.png b/stickers/smile.png new file mode 100644 index 0000000..a403a8e Binary files /dev/null and b/stickers/smile.png differ diff --git a/thumbnail_editor.py b/thumbnail_editor.py new file mode 100644 index 0000000..52a619e --- /dev/null +++ b/thumbnail_editor.py @@ -0,0 +1,394 @@ +import os +import tkinter as tk +from tkinter import filedialog, simpledialog, colorchooser, messagebox +from moviepy import VideoFileClip +from PIL import Image, ImageTk, ImageDraw, ImageFont + +# Enhanced Thumbnail Editor with Frame Slider + Default Emoji Pack + Text Adding + +def open_thumbnail_editor(video_path): + try: + editor = tk.Toplevel() + editor.title("šŸ“ø Professional Thumbnail Editor") + editor.geometry("1200x800") + + # Load video + print(f"šŸ“¹ Loading video: {os.path.basename(video_path)}") + clip = VideoFileClip(video_path) + duration = int(clip.duration) + + # Default emoji pack folder + stickers_folder = os.path.join(os.path.dirname(__file__), "stickers") + os.makedirs(stickers_folder, exist_ok=True) + + # Create default stickers if folder is empty + create_default_stickers(stickers_folder) + + # Main layout + main_frame = tk.Frame(editor) + main_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # Canvas setup (left side) + canvas_frame = tk.Frame(main_frame) + canvas_frame.pack(side="left", fill="both", expand=True) + + tk.Label(canvas_frame, text="šŸŽ¬ Thumbnail Preview", font=("Arial", 12, "bold")).pack() + + canvas = tk.Canvas(canvas_frame, width=720, height=405, bg="black", relief="sunken", bd=2) + canvas.pack(pady=10) + + # Track items for dragging + drag_data = {"item": None, "x": 0, "y": 0} + + def capture_frame_at(time_sec): + try: + frame = clip.get_frame(max(0, min(time_sec, clip.duration - 0.1))) + img = Image.fromarray(frame) + # Maintain aspect ratio while fitting in canvas + img.thumbnail((720, 405), Image.Resampling.LANCZOS) + return img + except Exception as e: + print(f"āš ļø Error capturing frame: {e}") + # Create a placeholder image + img = Image.new('RGB', (720, 405), color='black') + return img + + # Displayed image + current_frame = capture_frame_at(duration // 2) + tk_frame_img = ImageTk.PhotoImage(current_frame) + image_item = canvas.create_image(360, 202, image=tk_frame_img) + canvas.image = tk_frame_img + + # Items data + sticker_items = [] + text_items = [] + + def update_canvas_frame(val): + nonlocal current_frame, tk_frame_img + try: + sec = float(val) + current_frame = capture_frame_at(sec) + tk_frame_img = ImageTk.PhotoImage(current_frame) + canvas.itemconfig(image_item, image=tk_frame_img) + canvas.image = tk_frame_img + except Exception as e: + print(f"āš ļø Error updating frame: {e}") + + # Frame controls + controls_frame = tk.Frame(canvas_frame) + controls_frame.pack(fill="x", pady=5) + + tk.Label(controls_frame, text="ā±ļø Frame Time (seconds):").pack() + frame_slider = tk.Scale(controls_frame, from_=0, to=duration, orient="horizontal", + command=update_canvas_frame, length=600, resolution=0.1) + frame_slider.set(duration // 2) + frame_slider.pack(fill="x", pady=5) + + # Tools panel (right side) + tools_frame = tk.Frame(main_frame, width=300, relief="groove", bd=2) + tools_frame.pack(side="right", fill="y", padx=(10, 0)) + tools_frame.pack_propagate(False) + + tk.Label(tools_frame, text="šŸ› ļø Editing Tools", font=("Arial", 14, "bold")).pack(pady=10) + + # Stickers section + stickers_label_frame = tk.LabelFrame(tools_frame, text="šŸŽ­ Stickers & Emojis", padx=10, pady=5) + stickers_label_frame.pack(fill="x", padx=10, pady=5) + + # Create scrollable frame for stickers + stickers_canvas = tk.Canvas(stickers_label_frame, height=200) + stickers_scrollbar = tk.Scrollbar(stickers_label_frame, orient="vertical", command=stickers_canvas.yview) + stickers_scrollable_frame = tk.Frame(stickers_canvas) + + stickers_scrollable_frame.bind( + "", + lambda e: stickers_canvas.configure(scrollregion=stickers_canvas.bbox("all")) + ) + + stickers_canvas.create_window((0, 0), window=stickers_scrollable_frame, anchor="nw") + stickers_canvas.configure(yscrollcommand=stickers_scrollbar.set) + + stickers_canvas.pack(side="left", fill="both", expand=True) + stickers_scrollbar.pack(side="right", fill="y") + + def add_sticker(path): + try: + img = Image.open(path).convert("RGBA") + img.thumbnail((60, 60), Image.Resampling.LANCZOS) + tk_img = ImageTk.PhotoImage(img) + item = canvas.create_image(360, 200, image=tk_img) + + # Keep reference to prevent garbage collection + if not hasattr(canvas, 'images'): + canvas.images = [] + canvas.images.append(tk_img) + sticker_items.append((item, img)) + print(f"āœ… Added sticker: {os.path.basename(path)}") + except Exception as e: + print(f"āš ļø Failed to load sticker {path}: {e}") + + # Load default stickers + sticker_count = 0 + stickers_row_frame = None + + for file in os.listdir(stickers_folder): + if file.lower().endswith(('.png', '.jpg', '.jpeg', '.gif')): + try: + if sticker_count % 4 == 0: # 4 stickers per row + stickers_row_frame = tk.Frame(stickers_scrollable_frame) + stickers_row_frame.pack(fill="x", pady=2) + + btn_img = Image.open(os.path.join(stickers_folder, file)).convert("RGBA") + btn_img.thumbnail((40, 40), Image.Resampling.LANCZOS) + tk_btn_img = ImageTk.PhotoImage(btn_img) + + b = tk.Button(stickers_row_frame, image=tk_btn_img, + command=lambda f=file: add_sticker(os.path.join(stickers_folder, f))) + b.image = tk_btn_img + b.pack(side="left", padx=2) + sticker_count += 1 + except Exception as e: + print(f"āš ļø Failed to load sticker {file}: {e}") + + # Add custom sticker button + tk.Button(stickers_label_frame, text="šŸ“ Add Custom Sticker", + command=lambda: add_custom_sticker()).pack(pady=5) + + def add_custom_sticker(): + file_path = filedialog.askopenfilename( + title="Select Sticker Image", + filetypes=[("Image files", "*.png *.jpg *.jpeg *.gif"), ("All files", "*.*")] + ) + if file_path: + add_sticker(file_path) + + # Text section + text_label_frame = tk.LabelFrame(tools_frame, text="šŸ“ Text Tools", padx=10, pady=5) + text_label_frame.pack(fill="x", padx=10, pady=5) + + def add_text(): + text_value = simpledialog.askstring("Add Text", "Enter text:") + if not text_value: + return + + color_result = colorchooser.askcolor(title="Choose text color") + color = color_result[1] if color_result[1] else "white" + + size = simpledialog.askinteger("Font size", "Enter font size:", + initialvalue=48, minvalue=8, maxvalue=200) + if not size: + size = 48 + + try: + item = canvas.create_text(360, 200, text=text_value, fill=color, + font=("Arial", size, "bold"), anchor="center") + text_items.append((item, text_value, color, size)) + print(f"āœ… Added text: '{text_value}'") + except Exception as e: + print(f"āš ļø Error adding text: {e}") + + tk.Button(text_label_frame, text="āž• Add Text", command=add_text, + bg="#4CAF50", fg="white", font=("Arial", 10, "bold")).pack(pady=5, fill="x") + + # Clear all button + def clear_all(): + if messagebox.askyesno("Clear All", "Remove all stickers and text?"): + for item_id, _ in sticker_items + text_items: + canvas.delete(item_id) + sticker_items.clear() + text_items.clear() + print("šŸ—‘ļø Cleared all items") + + tk.Button(text_label_frame, text="šŸ—‘ļø Clear All", command=clear_all, + bg="#F44336", fg="white").pack(pady=5, fill="x") + + # Drag handling + def on_drag_start(event): + items = canvas.find_overlapping(event.x, event.y, event.x, event.y) + items = [i for i in items if i != image_item] + if not items: + return + item = items[-1] # topmost + drag_data["item"] = item + drag_data["x"] = event.x + drag_data["y"] = event.y + + def on_drag_motion(event): + if drag_data["item"] is None: + return + dx = event.x - drag_data["x"] + dy = event.y - drag_data["y"] + canvas.move(drag_data["item"], dx, dy) + drag_data["x"] = event.x + drag_data["y"] = event.y + + def on_drag_release(event): + drag_data["item"] = None + + canvas.bind("", on_drag_start) + canvas.bind("", on_drag_motion) + canvas.bind("", on_drag_release) + + # Save section + save_frame = tk.LabelFrame(tools_frame, text="šŸ’¾ Export Options", padx=10, pady=5) + save_frame.pack(fill="x", padx=10, pady=5) + + def save_thumbnail(): + try: + save_path = filedialog.asksaveasfilename( + defaultextension=".jpg", + filetypes=[("JPEG", "*.jpg"), ("PNG", "*.png"), ("All files", "*.*")], + title="Save Thumbnail As" + ) + if not save_path: + return + + print("šŸ’¾ Generating high-quality thumbnail...") + + # Get the current frame at full resolution + sec = float(frame_slider.get()) + frame = Image.fromarray(clip.get_frame(sec)).convert("RGBA") + + # Calculate scaling factors + canvas_w, canvas_h = 720, 405 + scale_x = frame.width / canvas_w + scale_y = frame.height / canvas_h + + # Add stickers + for item_id, sticker_img in sticker_items: + coords = canvas.coords(item_id) + if not coords: + continue + + x, y = coords[0], coords[1] + # Convert canvas coordinates to frame coordinates + px = int(x * scale_x) + py = int(y * scale_y) + + # Scale sticker size + target_w = int(sticker_img.width * scale_x) + target_h = int(sticker_img.height * scale_y) + + if target_w > 0 and target_h > 0: + sticker_resized = sticker_img.resize((target_w, target_h), Image.Resampling.LANCZOS) + # Paste with alpha blending + frame.paste(sticker_resized, (px - target_w//2, py - target_h//2), sticker_resized) + + # Add text + draw = ImageDraw.Draw(frame) + for item_id, text_value, color, font_size in text_items: + coords = canvas.coords(item_id) + if not coords: + continue + + x, y = coords[0], coords[1] + px = int(x * scale_x) + py = int(y * scale_y) + + # Scale font size + scaled_font_size = int(font_size * scale_x) + + try: + font = ImageFont.truetype("arial.ttf", scaled_font_size) + except: + try: + font = ImageFont.truetype("calibri.ttf", scaled_font_size) + except: + font = ImageFont.load_default() + + # Get text bounding box for centering + bbox = draw.textbbox((0, 0), text_value, font=font) + text_w = bbox[2] - bbox[0] + text_h = bbox[3] - bbox[1] + + # Draw text with outline + outline_w = max(2, scaled_font_size // 15) + for dx in range(-outline_w, outline_w + 1): + for dy in range(-outline_w, outline_w + 1): + draw.text((px - text_w//2 + dx, py - text_h//2 + dy), + text_value, font=font, fill="black") + + draw.text((px - text_w//2, py - text_h//2), text_value, font=font, fill=color) + + # Convert to RGB and save + if save_path.lower().endswith('.png'): + frame.save(save_path, "PNG", quality=95) + else: + background = Image.new("RGB", frame.size, (255, 255, 255)) + background.paste(frame, mask=frame.split()[3] if frame.mode == 'RGBA' else None) + background.save(save_path, "JPEG", quality=95) + + print(f"āœ… Thumbnail saved: {save_path}") + messagebox.showinfo("Success", f"Thumbnail saved successfully!\n{save_path}") + + except Exception as e: + print(f"āŒ Error saving thumbnail: {e}") + messagebox.showerror("Error", f"Failed to save thumbnail:\n{str(e)}") + + tk.Button(save_frame, text="šŸ’¾ Save Thumbnail", command=save_thumbnail, + bg="#2196F3", fg="white", font=("Arial", 12, "bold")).pack(pady=5, fill="x") + + # Info label + info_text = f"šŸ“¹ Video: {os.path.basename(video_path)}\nā±ļø Duration: {duration}s\nšŸ“ Size: {clip.size[0]}x{clip.size[1]}" + tk.Label(save_frame, text=info_text, font=("Arial", 8), justify="left").pack(pady=5) + + print(f"āœ… Thumbnail editor loaded successfully!") + + except Exception as e: + print(f"āŒ Error opening thumbnail editor: {e}") + messagebox.showerror("Error", f"Failed to open thumbnail editor:\n{str(e)}") + +def create_default_stickers(stickers_folder): + """Create some default emoji stickers if folder is empty""" + if os.listdir(stickers_folder): + return # Already has stickers + + try: + from PIL import Image, ImageDraw + + # Create simple emoji stickers + emojis = [ + ("šŸ˜€", (255, 255, 0)), # Happy face + ("ā¤ļø", (255, 0, 0)), # Heart + ("šŸ‘", (255, 220, 177)), # Thumbs up + ("šŸ”„", (255, 100, 0)), # Fire + ("⭐", (255, 215, 0)), # Star + ("šŸ’Æ", (0, 255, 0)), # 100 + ] + + for i, (emoji, color) in enumerate(emojis): + # Create a simple colored circle as placeholder + img = Image.new('RGBA', (80, 80), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + draw.ellipse([10, 10, 70, 70], fill=color) + + # Save as PNG + img.save(os.path.join(stickers_folder, f"emoji_{i+1}.png")) + + print("āœ… Created default sticker pack") + + except Exception as e: + print(f"āš ļø Could not create default stickers: {e}") + +# Main execution +if __name__ == '__main__': + root = tk.Tk() + root.withdraw() + + video_path = filedialog.askopenfilename( + title='Select a video file', + filetypes=[('Video files', '*.mp4 *.mov *.avi *.mkv'), ('All files', '*.*')] + ) + + if video_path: + try: + root.deiconify() # Show root window + root.title("Thumbnail Editor") + open_thumbnail_editor(video_path) + root.mainloop() + except Exception as e: + print(f"āŒ Error: {e}") + messagebox.showerror("Error", f"Failed to start thumbnail editor:\n{str(e)}") + else: + print('No video selected.') + root.destroy()