ShortGenerator/shorts_generator2.py
klop51 809e768cae Add Professional Video Editor with timeline controls and real-time preview
- Implemented a standalone video editor for editing generated shorts.
- Integrated OpenCV for basic video playback and MoviePy for advanced editing features.
- Added functionalities including video trimming, speed control, volume adjustment, fade effects, and text overlays.
- Created a modern GUI using Tkinter with responsive design and a professional color scheme.
- Included detailed README documentation outlining features, usage, and installation requirements.
2025-08-10 21:05:36 +02:00

6910 lines
311 KiB
Python
Raw Blame History

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, simpledialog
import threading
import cv2
from scipy import signal
import librosa
import glob
import json
from datetime import datetime
from PIL import Image, ImageTk
from PIL import ImageDraw, ImageFont
import time
import copy
from collections import defaultdict
class ToolTip:
"""Create a tooltip for a given widget"""
def __init__(self, widget, text='widget info', side='right'):
self.widget = widget
self.text = text
self.side = side
self.widget.bind("<Enter>", self.enter)
self.widget.bind("<Leave>", self.leave)
self.tipwindow = None
def enter(self, event=None):
self.showtip()
def leave(self, event=None):
self.hidetip()
def showtip(self):
if self.tipwindow or not self.text:
return
# Get widget position
x = self.widget.winfo_rootx()
y = self.widget.winfo_rooty()
w = self.widget.winfo_width()
h = self.widget.winfo_height()
# Position tooltip based on side preference
if self.side == 'right':
x = x + w + 10 # 10px to the right of widget
y = y
else:
x = x + 25
y = y + h + 5
self.tipwindow = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(True)
tw.wm_geometry("+%d+%d" % (x, y))
label = tk.Label(tw, text=self.text, justify=tk.LEFT,
background="#ffffe0", relief=tk.SOLID, borderwidth=1,
font=("Arial", "9", "normal"), wraplength=350)
label.pack(ipadx=5, ipady=3)
def hidetip(self):
tw = self.tipwindow
self.tipwindow = None
if tw:
tw.destroy()
class ComboboxTooltip:
"""Special tooltip for combobox that shows on listbox hover"""
def __init__(self, combobox, descriptions):
self.combobox = combobox
self.descriptions = descriptions
self.tipwindow = None
self.bound_listbox = None
# Bind to combobox events
self.combobox.bind("<Button-1>", self.on_click)
self.combobox.bind("<KeyPress>", self.on_keypress)
def on_click(self, event):
# Try to find the listbox when dropdown opens
self.combobox.after(50, self.bind_listbox)
def on_keypress(self, event):
# Handle keyboard navigation
self.combobox.after(50, self.bind_listbox)
def bind_listbox(self):
# Find the listbox widget more reliably
try:
# Look through all toplevel windows for the combobox popdown
for window in self.combobox.winfo_toplevel().winfo_children():
window_class = window.winfo_class()
if window_class == 'Toplevel':
# Found a toplevel, look for listbox inside
for child in window.winfo_children():
if child.winfo_class() == 'Listbox':
if self.bound_listbox != child:
self.bound_listbox = child
child.bind("<Motion>", self.on_listbox_motion)
child.bind("<Leave>", self.on_listbox_leave)
child.bind("<ButtonRelease-1>", self.on_listbox_leave)
return
except Exception as e:
# Fallback method - try to find any listbox
try:
# Alternative approach: look for the popdown frame
for child in self.combobox.tk.call('winfo', 'children', '.'):
if 'popdown' in str(child):
popdown = self.combobox.nametowidget(child)
for subchild in popdown.winfo_children():
if subchild.winfo_class() == 'Listbox':
if self.bound_listbox != subchild:
self.bound_listbox = subchild
subchild.bind("<Motion>", self.on_listbox_motion)
subchild.bind("<Leave>", self.on_listbox_leave)
subchild.bind("<ButtonRelease-1>", self.on_listbox_leave)
return
except:
pass
def on_listbox_motion(self, event):
try:
listbox = event.widget
index = listbox.nearest(event.y)
if 0 <= index < len(self.combobox['values']):
selection = self.combobox['values'][index]
if selection in self.descriptions:
self.show_tooltip(event, self.descriptions[selection])
except Exception:
pass
def on_listbox_leave(self, event):
self.hide_tooltip()
def show_tooltip(self, event, text):
self.hide_tooltip() # Hide any existing tooltip
try:
x = event.widget.winfo_rootx() + event.widget.winfo_width() + 10
y = event.widget.winfo_rooty() + event.y - 20
self.tipwindow = tw = tk.Toplevel(event.widget)
tw.wm_overrideredirect(True)
tw.wm_geometry("+%d+%d" % (x, y))
label = tk.Label(tw, text=text, justify=tk.LEFT,
background="#ffffe0", relief=tk.SOLID, borderwidth=1,
font=("Arial", "9", "normal"), wraplength=350)
label.pack(ipadx=5, ipady=3)
except Exception:
pass
def hide_tooltip(self):
if self.tipwindow:
try:
self.tipwindow.destroy()
except:
pass
self.tipwindow = None
def detect_loud_moments(video_path, chunk_duration=5, threshold_db=10):
print("🔍 Analyzing audio...")
clip = VideoFileClip(video_path)
audio = clip.audio.to_soundarray(fps=44100)
volume = np.linalg.norm(audio, axis=1)
chunk_size = int(chunk_duration * 44100)
loud_chunks = []
max_db = -float('inf')
for i in range(0, len(volume), chunk_size):
chunk = volume[i:i+chunk_size]
db = 20 * np.log10(np.mean(chunk) + 1e-10)
max_db = max(max_db, db)
if db > threshold_db:
start = i / 44100
loud_chunks.append((start, min(start + chunk_duration, clip.duration)))
print(f"🔊 Max volume found: {max_db:.2f} dB, threshold: {threshold_db} dB")
print(f"📈 Found {len(loud_chunks)} loud moments")
clip.close()
return loud_chunks
def detect_scene_changes(video_path, chunk_duration=5, threshold=0.3):
"""Detect dramatic visual scene changes"""
print("🎬 Analyzing scene changes...")
clip = VideoFileClip(video_path)
# Sample frames at regular intervals
sample_rate = 2 # Check every 2 seconds
times = np.arange(0, clip.duration, sample_rate)
scene_changes = []
total_frames = len(times) - 1
for i, t in enumerate(times[:-1]):
try:
# Periodic progress output
if i % 10 == 0:
print(f"🎬 Processing frame {i+1}/{total_frames}...")
# Get current and next frame
frame1 = clip.get_frame(t)
frame2 = clip.get_frame(times[i + 1])
# Convert to grayscale and resize for faster processing
gray1 = cv2.cvtColor(frame1, cv2.COLOR_RGB2GRAY)
gray2 = cv2.cvtColor(frame2, cv2.COLOR_RGB2GRAY)
gray1 = cv2.resize(gray1, (160, 90)) # Small size for speed
gray2 = cv2.resize(gray2, (160, 90))
# Calculate structural similarity difference
diff = np.mean(np.abs(gray1.astype(float) - gray2.astype(float))) / 255.0
if diff > threshold:
start = max(0, t - chunk_duration/2)
end = min(clip.duration, t + chunk_duration/2)
scene_changes.append((start, end))
except Exception as e:
print(f"⚠️ Frame analysis error at {t:.1f}s: {e}")
continue
print(f"🎬 Found {len(scene_changes)} scene changes")
clip.close()
return scene_changes
def detect_motion_intensity(video_path, chunk_duration=5, threshold=20000000):
"""Detect high motion/action scenes"""
print("🏃 Analyzing motion intensity...")
clip = VideoFileClip(video_path)
sample_rate = 1 # Check every second
times = np.arange(0, clip.duration - 1, sample_rate)
motion_scenes = []
total_frames = len(times)
for i, t in enumerate(times):
try:
# Periodic progress output
if i % 20 == 0:
print(f"🏃 Processing frame {i+1}/{total_frames}...")
# Get consecutive frames
frame1 = clip.get_frame(t)
frame2 = clip.get_frame(t + 1)
# Convert to grayscale and resize
gray1 = cv2.cvtColor(frame1, cv2.COLOR_RGB2GRAY)
gray2 = cv2.cvtColor(frame2, cv2.COLOR_RGB2GRAY)
gray1 = cv2.resize(gray1, (320, 180))
gray2 = cv2.resize(gray2, (320, 180))
# Calculate optical flow magnitude
flow = cv2.calcOpticalFlowPyrLK(gray1, gray2,
np.random.randint(0, 320, (100, 1, 2)).astype(np.float32),
None)[0]
if flow is not None:
motion_magnitude = np.sum(np.linalg.norm(flow.reshape(-1, 2), axis=1))
if motion_magnitude > threshold:
start = max(0, t - chunk_duration/2)
end = min(clip.duration, t + chunk_duration/2)
motion_scenes.append((start, end))
except Exception as e:
print(f"⚠️ Motion analysis error at {t:.1f}s: {e}")
continue
print(f"🏃 Found {len(motion_scenes)} motion scenes")
clip.close()
return motion_scenes
def detect_speech_emotion(video_path, chunk_duration=5):
"""Detect emotional speech segments using faster_whisper"""
print("🗣️ Analyzing speech emotion...")
try:
# Load Whisper model for speech detection
model = WhisperModel("base", device="cpu", compute_type="int8")
# Extract audio temporarily
temp_audio = "temp_audio.wav"
clip = VideoFileClip(video_path)
audio = clip.audio
audio.write_audiofile(temp_audio, verbose=False, logger=None)
# Transcribe with word-level timestamps
segments, _ = model.transcribe(temp_audio, word_timestamps=True)
emotional_segments = []
for segment in segments:
# Look for emotional indicators in speech patterns
text = segment.text.lower()
# Check for emotional keywords and speech patterns
emotional_words = ['amazing', 'incredible', 'wow', 'unbelievable', 'shocking',
'fantastic', 'awesome', 'terrible', 'horrible', 'beautiful']
has_emotion = any(word in text for word in emotional_words)
has_exclamation = '!' in segment.text
is_question = '?' in segment.text
if has_emotion or has_exclamation or is_question:
start = max(0, segment.start - chunk_duration/2)
end = min(clip.duration, segment.end + chunk_duration/2)
emotional_segments.append((start, end))
# Clean up
audio.close()
clip.close()
if os.path.exists(temp_audio):
os.remove(temp_audio)
print(f"🗣️ Found {len(emotional_segments)} emotional speech segments")
return emotional_segments
except Exception as e:
print(f"⚠️ Speech analysis error: {e}")
return []
def detect_audio_peaks(video_path, chunk_duration=5):
"""Detect audio frequency peaks and interesting sounds"""
print("🎵 Analyzing audio peaks...")
try:
# Extract audio
clip = VideoFileClip(video_path)
audio = clip.audio
# Convert to numpy array
temp_audio = "temp_peak_audio.wav"
audio.write_audiofile(temp_audio, verbose=False, logger=None)
# Load with librosa
y, sr = librosa.load(temp_audio)
# Analyze spectral features
hop_length = 512
frame_length = 2048
# Calculate spectral centroid (brightness)
spectral_centroids = librosa.feature.spectral_centroid(y=y, sr=sr, hop_length=hop_length)[0]
# Calculate RMS energy
rms = librosa.feature.rms(y=y, hop_length=hop_length)[0]
# Find frames with high spectral activity
time_frames = librosa.frames_to_time(np.arange(len(spectral_centroids)), sr=sr, hop_length=hop_length)
peak_segments = []
# Threshold for interesting audio
centroid_threshold = np.percentile(spectral_centroids, 85)
rms_threshold = np.percentile(rms, 80)
for i, (time, centroid, energy) in enumerate(zip(time_frames, spectral_centroids, rms)):
if centroid > centroid_threshold and energy > rms_threshold:
start = max(0, time - chunk_duration/2)
end = min(clip.duration, time + chunk_duration/2)
peak_segments.append((start, end))
# Clean up
audio.close()
clip.close()
if os.path.exists(temp_audio):
os.remove(temp_audio)
print(f"🎵 Found {len(peak_segments)} audio peak segments")
return peak_segments
except Exception as e:
print(f"⚠️ Audio analysis error: {e}")
return []
def detect_combined_moments(video_path, chunk_duration=5):
"""Combine multiple detection methods for best results"""
print("🎯 Running combined analysis...")
try:
# Run multiple detection methods
loud_moments = detect_loud_moments(video_path, chunk_duration)
scene_changes = detect_scene_changes(video_path, chunk_duration)
# Combine and deduplicate
all_moments = loud_moments + scene_changes
# Simple deduplication by merging overlapping segments
if not all_moments:
return []
# Sort by start time
all_moments.sort(key=lambda x: x[0])
# Merge overlapping segments
merged = [all_moments[0]]
for start, end in all_moments[1:]:
last_start, last_end = merged[-1]
if start <= last_end + 1: # Allow 1 second gap
merged[-1] = (last_start, max(last_end, end))
else:
merged.append((start, end))
print(f"🎯 Combined analysis found {len(merged)} interesting moments")
return merged
except Exception as e:
print(f"⚠️ Combined analysis error: {e}")
return []
def detect_scene_changes_with_progress(video_path, chunk_duration=5, threshold=0.3, progress_callback=None):
"""Detect dramatic visual scene changes with progress updates"""
print("🎬 Analyzing scene changes...")
clip = VideoFileClip(video_path)
# Sample frames at regular intervals
sample_rate = 2 # Check every 2 seconds
times = np.arange(0, clip.duration, sample_rate)
scene_changes = []
prev_frame = None
total_frames = len(times) - 1
for i, t in enumerate(times[:-1]):
try:
# Update progress every few frames
if progress_callback and i % 5 == 0:
progress = (i / total_frames) * 100
progress_callback(progress, f"🎬 Analyzing scene changes... Frame {i+1}/{total_frames}")
# Get current and next frame
frame1 = clip.get_frame(t)
frame2 = clip.get_frame(times[i + 1])
# Convert to grayscale and resize for faster processing
gray1 = cv2.cvtColor(frame1, cv2.COLOR_RGB2GRAY)
gray2 = cv2.cvtColor(frame2, cv2.COLOR_RGB2GRAY)
gray1 = cv2.resize(gray1, (160, 90)) # Small size for speed
gray2 = cv2.resize(gray2, (160, 90))
# Calculate structural similarity difference
diff = np.mean(np.abs(gray1.astype(float) - gray2.astype(float))) / 255.0
if diff > threshold:
start = max(0, t - chunk_duration/2)
end = min(clip.duration, t + chunk_duration/2)
scene_changes.append((start, end))
except Exception as e:
print(f"⚠️ Frame analysis error at {t:.1f}s: {e}")
continue
if progress_callback:
progress_callback(100, f"🎬 Found {len(scene_changes)} scene changes")
print(f"🎬 Found {len(scene_changes)} scene changes")
clip.close()
return scene_changes
def detect_motion_intensity_with_progress(video_path, chunk_duration=5, threshold=0.15, progress_callback=None):
"""Detect high motion/action moments with progress updates"""
print("🏃 Analyzing motion intensity...")
clip = VideoFileClip(video_path)
sample_rate = 1 # Check every second
times = np.arange(0, clip.duration - 1, sample_rate)
motion_moments = []
for i, t in enumerate(times):
try:
# Update progress every 10 seconds
if progress_callback and i % 10 == 0:
progress = (i / len(times)) * 100
progress_callback(progress, f"🏃 Analyzing motion... {i+1}/{len(times)} seconds")
# Get two consecutive frames
frame1 = clip.get_frame(t)
frame2 = clip.get_frame(t + 0.5) # Half second later
# Convert to grayscale and resize
gray1 = cv2.cvtColor(frame1, cv2.COLOR_RGB2GRAY)
gray2 = cv2.cvtColor(frame2, cv2.COLOR_RGB2GRAY)
gray1 = cv2.resize(gray1, (160, 90))
gray2 = cv2.resize(gray2, (160, 90))
# Calculate optical flow magnitude
flow = cv2.calcOpticalFlowPyrLK(gray1, gray2,
np.random.randint(0, 160, (100, 1, 2)).astype(np.float32),
None)[0]
if flow is not None:
motion_magnitude = np.mean(np.linalg.norm(flow.reshape(-1, 2), axis=1))
if motion_magnitude > threshold:
start = max(0, t - chunk_duration/2)
end = min(clip.duration, t + chunk_duration/2)
motion_moments.append((start, end))
except Exception as e:
print(f"⚠️ Motion analysis error at {t:.1f}s: {e}")
continue
if progress_callback:
progress_callback(100, f"🏃 Found {len(motion_moments)} high-motion moments")
print(f"🏃 Found {len(motion_moments)} high-motion moments")
clip.close()
return motion_moments
def detect_speech_emotion_with_progress(video_path, chunk_duration=5, progress_callback=None):
"""Detect emotional/excited speech patterns with progress updates"""
print("😄 Analyzing speech emotions...")
if progress_callback:
progress_callback(10, "😄 Initializing speech recognition...")
# Use Whisper to get detailed speech analysis
model = WhisperModel("base", device="cpu", compute_type="int8")
if progress_callback:
progress_callback(30, "😄 Transcribing audio...")
segments, _ = model.transcribe(video_path, beam_size=5, vad_filter=True, word_timestamps=True)
emotional_moments = []
excitement_keywords = ['wow', 'amazing', 'incredible', 'unbelievable', 'awesome', 'fantastic',
'omg', 'what', 'no way', 'crazy', 'insane', 'perfect', 'yes', 'exactly']
segments_list = list(segments)
if progress_callback:
progress_callback(50, f"😄 Processing {len(segments_list)} speech segments...")
for i, segment in enumerate(segments_list):
if progress_callback and i % 10 == 0:
progress = 50 + (i / len(segments_list)) * 50
progress_callback(progress, f"😄 Analyzing speech... {i+1}/{len(segments_list)} segments")
text = segment.text.lower()
# Check for excitement keywords
has_keywords = any(keyword in text for keyword in excitement_keywords)
# Check for multiple exclamation-worthy patterns
has_caps = any(word.isupper() for word in segment.text.split())
has_punctuation = '!' in segment.text or '?' in segment.text
is_short_excited = len(text.split()) <= 5 and (has_keywords or has_caps)
if has_keywords or has_punctuation or is_short_excited:
start = max(0, segment.start - chunk_duration/2)
end = min(segment.end + chunk_duration/2, segment.end + chunk_duration)
emotional_moments.append((start, end))
if progress_callback:
progress_callback(100, f"😄 Found {len(emotional_moments)} emotional speech moments")
print(f"😄 Found {len(emotional_moments)} emotional speech moments")
return emotional_moments
def detect_audio_peaks_with_progress(video_path, chunk_duration=5, progress_callback=None):
"""Detect sudden audio peaks with progress updates"""
print("🎵 Analyzing audio peaks...")
if progress_callback:
progress_callback(10, "🎵 Loading audio...")
clip = VideoFileClip(video_path)
audio = clip.audio.to_soundarray(fps=22050) # Lower sample rate for speed
# Convert to mono if stereo
if len(audio.shape) > 1:
audio = np.mean(audio, axis=1)
if progress_callback:
progress_callback(40, "🎵 Finding audio peaks...")
# Find spectral peaks (bass, treble spikes)
peaks, _ = signal.find_peaks(np.abs(audio), height=np.percentile(np.abs(audio), 95))
peak_moments = []
prev_peak = 0
if progress_callback:
progress_callback(70, f"🎵 Processing {len(peaks)} peaks...")
for i, peak in enumerate(peaks):
if progress_callback and i % 1000 == 0:
progress = 70 + (i / len(peaks)) * 30
progress_callback(progress, f"🎵 Processing peaks... {i}/{len(peaks)}")
peak_time = peak / 22050
# Avoid too close peaks
if peak_time - prev_peak > chunk_duration:
start = max(0, peak_time - chunk_duration/2)
end = min(clip.duration, peak_time + chunk_duration/2)
peak_moments.append((start, end))
prev_peak = peak_time
if progress_callback:
progress_callback(100, f"🎵 Found {len(peak_moments)} audio peak moments")
print(f"🎵 Found {len(peak_moments)} audio peak moments")
clip.close()
return peak_moments
def detect_combined_intensity_with_progress(video_path, chunk_duration=5, weights=None, progress_callback=None):
"""Combine multiple detection methods with progress updates"""
print("🎯 Running comprehensive moment analysis...")
if weights is None:
weights = {'loud': 0.3, 'scene': 0.2, 'motion': 0.2, 'speech': 0.2, 'peaks': 0.1}
# Sub-progress callback for each method
def sub_progress(method_weight, base_percent):
def callback(percent, status):
if progress_callback:
total_percent = base_percent + (percent / 100) * method_weight
progress_callback(total_percent, f"🎯 {status}")
return callback
# Get all detection results with progress
if progress_callback:
progress_callback(5, "🎯 Analyzing loud moments...")
loud_moments = detect_loud_moments(video_path, chunk_duration, threshold_db=5)
if progress_callback:
progress_callback(15, "🎯 Analyzing scene changes...")
scene_moments = detect_scene_changes_with_progress(video_path, chunk_duration, progress_callback=sub_progress(20, 15))
if progress_callback:
progress_callback(35, "🎯 Analyzing motion...")
motion_moments = detect_motion_intensity_with_progress(video_path, chunk_duration, progress_callback=sub_progress(20, 35))
if progress_callback:
progress_callback(55, "🎯 Analyzing speech...")
speech_moments = detect_speech_emotion_with_progress(video_path, chunk_duration, progress_callback=sub_progress(20, 55))
if progress_callback:
progress_callback(75, "🎯 Analyzing audio peaks...")
peak_moments = detect_audio_peaks_with_progress(video_path, chunk_duration, progress_callback=sub_progress(15, 75))
if progress_callback:
progress_callback(90, "<EFBFBD> Combining results...")
# Create time-based scoring
clip = VideoFileClip(video_path)
duration = clip.duration
clip.close()
# Score each second of the video
time_scores = {}
for moments, weight in [(loud_moments, weights['loud']),
(scene_moments, weights['scene']),
(motion_moments, weights['motion']),
(speech_moments, weights['speech']),
(peak_moments, weights['peaks'])]:
for start, end in moments:
for t in range(int(start), int(end) + 1):
if t not in time_scores:
time_scores[t] = 0
time_scores[t] += weight
# Find the highest scoring segments
if not time_scores:
if progress_callback:
progress_callback(100, "🎯 No moments found, using loud moments fallback")
return loud_moments # Fallback to loud moments
# Get top scoring time periods
sorted_times = sorted(time_scores.items(), key=lambda x: x[1], reverse=True)
combined_moments = []
used_times = set()
for time_sec, score in sorted_times:
if time_sec not in used_times and score > 0.3: # Minimum threshold
start = max(0, time_sec - chunk_duration/2)
end = min(duration, time_sec + chunk_duration/2)
combined_moments.append((start, end))
# Mark nearby times as used to avoid overlap
for t in range(max(0, time_sec - chunk_duration),
min(int(duration), time_sec + chunk_duration)):
used_times.add(t)
if progress_callback:
progress_callback(100, f"🎯 Found {len(combined_moments)} high-intensity combined moments")
print(f"🎯 Found {len(combined_moments)} high-intensity combined moments")
return combined_moments
def detect_motion_intensity(video_path, chunk_duration=5, threshold=0.15):
"""Detect high motion/action moments"""
print("🏃 Analyzing motion intensity...")
clip = VideoFileClip(video_path)
sample_rate = 1 # Check every second
times = np.arange(0, clip.duration - 1, sample_rate)
motion_moments = []
for i, t in enumerate(times):
try:
# Periodic UI update to prevent freezing
if i % 20 == 0: # Every 20 seconds
print(f"🏃 Processing motion at {t:.1f}s ({i+1}/{len(times)})...")
# Get two consecutive frames
frame1 = clip.get_frame(t)
frame2 = clip.get_frame(t + 0.5) # Half second later
# Convert to grayscale and resize
gray1 = cv2.cvtColor(frame1, cv2.COLOR_RGB2GRAY)
gray2 = cv2.cvtColor(frame2, cv2.COLOR_RGB2GRAY)
gray1 = cv2.resize(gray1, (160, 90))
gray2 = cv2.resize(gray2, (160, 90))
# Calculate optical flow magnitude
flow = cv2.calcOpticalFlowPyrLK(gray1, gray2,
np.random.randint(0, 160, (100, 1, 2)).astype(np.float32),
None)[0]
if flow is not None:
motion_magnitude = np.mean(np.linalg.norm(flow.reshape(-1, 2), axis=1))
if motion_magnitude > threshold:
start = max(0, t - chunk_duration/2)
end = min(clip.duration, t + chunk_duration/2)
motion_moments.append((start, end))
except Exception as e:
print(f"⚠️ Motion analysis error at {t:.1f}s: {e}")
continue
print(f"🏃 Found {len(motion_moments)} high-motion moments")
clip.close()
return motion_moments
def detect_speech_emotion(video_path, chunk_duration=5):
"""Detect emotional/excited speech patterns"""
print("😄 Analyzing speech emotions...")
print("😄 Initializing speech recognition...")
# Use Whisper to get detailed speech analysis
model = WhisperModel("base", device="cpu", compute_type="int8")
print("😄 Transcribing audio...")
segments, _ = model.transcribe(video_path, beam_size=5, vad_filter=True, word_timestamps=True)
emotional_moments = []
excitement_keywords = ['wow', 'amazing', 'incredible', 'unbelievable', 'awesome', 'fantastic',
'omg', 'what', 'no way', 'crazy', 'insane', 'perfect', 'yes', 'exactly']
segments_list = list(segments)
print(f"😄 Processing {len(segments_list)} speech segments...")
for i, segment in enumerate(segments_list):
if i % 10 == 0: # Every 10 segments
print(f"😄 Processing segment {i+1}/{len(segments_list)}...")
text = segment.text.lower()
# Check for excitement keywords
has_keywords = any(keyword in text for keyword in excitement_keywords)
# Check for multiple exclamation-worthy patterns
has_caps = any(word.isupper() for word in segment.text.split())
has_punctuation = '!' in segment.text or '?' in segment.text
is_short_excited = len(text.split()) <= 5 and (has_keywords or has_caps)
if has_keywords or has_punctuation or is_short_excited:
start = max(0, segment.start - chunk_duration/2)
end = min(segment.end + chunk_duration/2, segment.end + chunk_duration)
emotional_moments.append((start, end))
print(f"😄 Found {len(emotional_moments)} emotional speech moments")
return emotional_moments
def detect_audio_peaks(video_path, chunk_duration=5):
"""Detect sudden audio peaks (bass drops, beats, impacts)"""
print("🎵 Analyzing audio peaks...")
print("🎵 Loading audio...")
clip = VideoFileClip(video_path)
audio = clip.audio.to_soundarray(fps=22050) # Lower sample rate for speed
# Convert to mono if stereo
if len(audio.shape) > 1:
audio = np.mean(audio, axis=1)
print("🎵 Finding audio peaks...")
# Find spectral peaks (bass, treble spikes)
peaks, _ = signal.find_peaks(np.abs(audio), height=np.percentile(np.abs(audio), 95))
peak_moments = []
prev_peak = 0
for i, peak in enumerate(peaks):
if i % 1000 == 0: # Every 1000 peaks
print(f"🎵 Processing peaks... {i}/{len(peaks)}")
peak_time = peak / 22050
# Avoid too close peaks
if peak_time - prev_peak > chunk_duration:
start = max(0, peak_time - chunk_duration/2)
end = min(clip.duration, peak_time + chunk_duration/2)
peak_moments.append((start, end))
prev_peak = peak_time
print(f"🎵 Found {len(peak_moments)} audio peak moments")
clip.close()
return peak_moments
def detect_combined_intensity(video_path, chunk_duration=5, weights=None):
"""Combine multiple detection methods for best moments"""
print("🎯 Running comprehensive moment analysis...")
if weights is None:
weights = {'loud': 0.3, 'scene': 0.2, 'motion': 0.2, 'speech': 0.2, 'peaks': 0.1}
# Get all detection results with progress updates
print("🎯 Analyzing loud moments...")
loud_moments = detect_loud_moments(video_path, chunk_duration, threshold_db=5) # Lower threshold
print("🎯 Analyzing scene changes...")
scene_moments = detect_scene_changes(video_path, chunk_duration)
print("🎯 Analyzing motion...")
motion_moments = detect_motion_intensity(video_path, chunk_duration)
print("🎯 Analyzing speech...")
speech_moments = detect_speech_emotion(video_path, chunk_duration)
print("🎯 Analyzing audio peaks...")
peak_moments = detect_audio_peaks(video_path, chunk_duration)
print("🎯 Combining results...")
# Create time-based scoring
clip = VideoFileClip(video_path)
duration = clip.duration
clip.close()
# Score each second of the video
time_scores = {}
for moments, weight in [(loud_moments, weights['loud']),
(scene_moments, weights['scene']),
(motion_moments, weights['motion']),
(speech_moments, weights['speech']),
(peak_moments, weights['peaks'])]:
for start, end in moments:
for t in range(int(start), int(end) + 1):
if t not in time_scores:
time_scores[t] = 0
time_scores[t] += weight
# Find the highest scoring segments
if not time_scores:
print("🎯 No moments found, using loud moments fallback")
return loud_moments # Fallback to loud moments
# Get top scoring time periods
sorted_times = sorted(time_scores.items(), key=lambda x: x[1], reverse=True)
combined_moments = []
used_times = set()
for time_sec, score in sorted_times:
if time_sec not in used_times and score > 0.3: # Minimum threshold
start = max(0, time_sec - chunk_duration/2)
end = min(duration, time_sec + chunk_duration/2)
combined_moments.append((start, end))
# Mark nearby times as used to avoid overlap
for t in range(max(0, time_sec - chunk_duration),
min(int(duration), time_sec + chunk_duration)):
used_times.add(t)
print(f"🎯 Found {len(combined_moments)} high-intensity combined moments")
return combined_moments
def transcribe_and_extract_subtitles(video_path, start, end):
print(f"🗣️ Transcribing audio from {start:.2f}s to {end:.2f}s...")
model = WhisperModel("base", device="cpu", compute_type="int8")
segments, _ = model.transcribe(video_path, beam_size=5, language="en", vad_filter=True)
subtitles = []
for segment in segments:
if start <= segment.start <= end:
subtitles.append((segment.start - start, segment.end - start, segment.text))
return subtitles
def create_short_clip(video_path, start, end, subtitles, output_path):
print(f"🎬 Creating short: {output_path}")
clip = VideoFileClip(video_path).subclipped(start, end)
video_duration = clip.duration
print(f"📏 Video clip duration: {video_duration:.2f}s")
vertical_clip = clip.resized(height=1920).cropped(width=1080, x_center=clip.w / 2)
clips = [vertical_clip]
subtitle_y_px = 1550 # Fixed Y position for subtitles
for (s, e, text) in subtitles:
try:
subtitle_start = max(0, s)
subtitle_end = min(e, video_duration)
if subtitle_start >= video_duration or subtitle_end <= subtitle_start:
print(f"⚠️ Skipping subtitle outside video duration: {text[:30]}...")
continue
words = text.strip().split()
if not words:
continue
# Split into small readable chunks (max ~3-4 words)
chunks = []
current_chunk = []
for word in words:
current_chunk.append(word)
if len(current_chunk) >= 2 or len(' '.join(current_chunk)) > 25:
chunks.append(' '.join(current_chunk))
current_chunk = []
if current_chunk:
chunks.append(' '.join(current_chunk))
chunk_duration = (subtitle_end - subtitle_start) / len(chunks)
for chunk_idx, chunk_text in enumerate(chunks):
chunk_start = subtitle_start + (chunk_idx * chunk_duration)
chunk_end = min(chunk_start + chunk_duration, subtitle_end)
chunk_words = chunk_text.split()
# Base subtitle
base_subtitle = TextClip(
text=chunk_text.upper(),
font_size=65,
color='white',
stroke_color='black',
stroke_width=5
)
text_width, _ = base_subtitle.size
base_subtitle = base_subtitle.with_start(chunk_start).with_end(chunk_end).with_position(('center', subtitle_y_px))
clips.append(base_subtitle)
# Highlighted words (perfectly aligned)
word_duration = chunk_duration / len(chunk_words)
current_x = 540 - (text_width / 2) # 540 is center X of 1080px width
for i, word in enumerate(chunk_words):
word_start = chunk_start + (i * word_duration)
word_end = min(word_start + word_duration * 0.8, chunk_end)
highlighted_word = TextClip(
text=word.upper(),
font_size=68,
color='#FFD700',
stroke_color='#FF6B35',
stroke_width=5
)
word_width, _ = highlighted_word.size
word_x = current_x + (word_width / 2)
highlighted_word = highlighted_word.with_start(word_start).with_end(word_end).with_position((word_x -125
, subtitle_y_px))
clips.append(highlighted_word)
current_x += word_width + 20 # Add spacing between words
print(f"✅ Added Opus-style subtitle ({subtitle_start:.1f}s-{subtitle_end:.1f}s): {text[:30]}...")
except Exception as e:
print(f"⚠️ Subtitle error: {e}, skipping subtitle: {text[:50]}...")
continue
final = CompositeVideoClip(clips, size=(1080, 1920))
final.write_videofile(output_path, codec="libx264", audio_codec="aac", threads=1)
clip.reader.close()
if clip.audio:
clip.audio.reader.close()
final.close()
def validate_video(video_path, min_duration=30):
"""Validate video file and return duration"""
try:
clip = VideoFileClip(video_path)
duration = clip.duration
clip.close()
if duration < min_duration:
raise ValueError(f"Video is too short ({duration:.1f}s). Minimum {min_duration}s required.")
return duration
except Exception as e:
if "No such file" in str(e):
raise FileNotFoundError(f"Video file not found: {video_path}")
elif "could not open" in str(e).lower():
raise ValueError(f"Invalid or corrupted video file: {video_path}")
else:
raise ValueError(f"Error reading video: {str(e)}")
def generate_shorts(video_path, max_clips=3, output_folder="shorts", progress_callback=None,
detection_progress_callback=None, threshold_db=-30, clip_duration=5, detection_mode="loud"):
os.makedirs(output_folder, exist_ok=True)
# Validate video first
try:
video_duration = validate_video(video_path, min_duration=clip_duration * 2)
if progress_callback:
progress_callback(f"✅ Video validated ({video_duration:.1f}s)", 5)
except Exception as e:
if progress_callback:
progress_callback(f"❌ Video validation failed", 0)
raise e
# Choose detection method based on mode
if detection_mode == "loud":
if progress_callback:
progress_callback("🔍 Analyzing audio for loud moments...", 10)
best_moments = detect_loud_moments(video_path, chunk_duration=clip_duration, threshold_db=threshold_db)
if progress_callback:
progress_callback("🔍 Loud moments analysis complete", 35)
elif detection_mode == "scene":
if progress_callback:
progress_callback("🎬 Starting scene analysis...", 10)
best_moments = detect_scene_changes_with_progress(video_path, chunk_duration=clip_duration,
progress_callback=detection_progress_callback)
if progress_callback:
progress_callback("🎬 Scene analysis complete", 35)
elif detection_mode == "motion":
if progress_callback:
progress_callback("🏃 Starting motion analysis...", 10)
best_moments = detect_motion_intensity_with_progress(video_path, chunk_duration=clip_duration,
progress_callback=detection_progress_callback)
if progress_callback:
progress_callback("🏃 Motion analysis complete", 35)
elif detection_mode == "speech":
if progress_callback:
progress_callback("😄 Starting speech analysis...", 10)
best_moments = detect_speech_emotion_with_progress(video_path, chunk_duration=clip_duration,
progress_callback=detection_progress_callback)
if progress_callback:
progress_callback("😄 Speech analysis complete", 35)
elif detection_mode == "peaks":
if progress_callback:
progress_callback("🎵 Starting audio peak analysis...", 10)
best_moments = detect_audio_peaks_with_progress(video_path, chunk_duration=clip_duration,
progress_callback=detection_progress_callback)
if progress_callback:
progress_callback("🎵 Audio peak analysis complete", 35)
elif detection_mode == "combined":
if progress_callback:
progress_callback("🎯 Starting comprehensive analysis...", 10)
best_moments = detect_combined_intensity_with_progress(video_path, chunk_duration=clip_duration,
progress_callback=detection_progress_callback)
if progress_callback:
progress_callback("🎯 Comprehensive analysis complete", 35)
else:
best_moments = detect_loud_moments(video_path, chunk_duration=clip_duration, threshold_db=threshold_db)
if progress_callback:
progress_callback("🔍 Analysis complete", 35)
selected = best_moments[:max_clips]
if not selected:
mode_name = {
"loud": "loud moments", "scene": "scene changes", "motion": "motion intensity",
"speech": "emotional speech", "peaks": "audio peaks", "combined": "interesting moments"
}.get(detection_mode, "moments")
raise ValueError(f"No {mode_name} found. Try a different detection mode or adjust settings.")
if progress_callback:
progress_callback(f"📊 Found {len(selected)} clips to generate", 20)
for i, (start, end) in enumerate(selected):
if progress_callback:
progress_callback(f"🗣️ Transcribing clip {i+1}/{len(selected)}", 30 + (i * 20))
subtitles = transcribe_and_extract_subtitles(video_path, start, end)
out_path = os.path.join(output_folder, f"short_{i+1}.mp4")
if progress_callback:
progress_callback(f"🎬 Creating video {i+1}/{len(selected)}", 50 + (i * 20))
create_short_clip(video_path, start, end, subtitles, out_path)
if progress_callback:
progress_callback("✅ All shorts generated successfully!", 100)
# Video Editing Tools
class VideoEditor:
"""Professional video editing tools for generated shorts with timeline-based effects"""
def __init__(self, video_path=None):
"""Initialize video editor with optional video file"""
self.original_video_path = video_path
self.video_clip = None
self.timeline_effects = [] # List of effects with timing and positioning
self.global_effects = [] # Global effects applied to entire video
if video_path:
self.load_video(video_path)
def add_timeline_effect(self, effect_type, start_time, end_time=None, duration=None,
position=None, **kwargs):
"""Add an effect at specific time with specific position"""
if end_time is None and duration is not None:
end_time = start_time + duration
elif end_time is None:
end_time = self.video_clip.duration if self.video_clip else 5.0
effect = {
'type': effect_type,
'start_time': start_time,
'end_time': end_time,
'position': position,
'params': kwargs
}
self.timeline_effects.append(effect)
print(f"📍 Added {effect_type} effect at {start_time:.1f}s-{end_time:.1f}s")
return effect
def remove_timeline_effect(self, effect):
"""Remove a specific timeline effect"""
if effect in self.timeline_effects:
self.timeline_effects.remove(effect)
print(f"🗑️ Removed {effect['type']} effect")
def clear_timeline_effects(self):
"""Clear all timeline effects"""
self.timeline_effects.clear()
print("🧹 Cleared all timeline effects")
def apply_timeline_effects(self):
"""Apply all timeline effects to create final video"""
if not self.video_clip:
raise Exception("No video loaded!")
if not self.timeline_effects:
print("📝 No timeline effects to apply")
return self.video_clip
# Start with original video
final_clip = self.video_clip
# Group effects by type for efficient processing
text_effects = [e for e in self.timeline_effects if e['type'] == 'text']
sticker_effects = [e for e in self.timeline_effects if e['type'] == 'sticker']
particle_effects = [e for e in self.timeline_effects if e['type'] == 'particles']
color_effects = [e for e in self.timeline_effects if e['type'] in ['color_preset', 'color_grading']]
transform_effects = [e for e in self.timeline_effects if e['type'] in ['zoom', 'rotation', 'blur']]
# Apply transform effects (modify base video)
for effect in transform_effects:
final_clip = self._apply_transform_effect_to_clip(final_clip, effect)
# Apply color effects
for effect in color_effects:
final_clip = self._apply_color_effect_to_clip(final_clip, effect)
# Prepare overlay clips for composition
overlay_clips = [final_clip]
# Add text overlays
for effect in text_effects:
text_clip = self._create_text_clip(effect)
if text_clip:
overlay_clips.append(text_clip)
# Add sticker overlays
for effect in sticker_effects:
sticker_clip = self._create_sticker_clip(effect)
if sticker_clip:
overlay_clips.append(sticker_clip)
# Add particle effects
for effect in particle_effects:
particle_clip = self._create_particle_clip(effect)
if particle_clip:
overlay_clips.append(particle_clip)
# Composite all clips
if len(overlay_clips) > 1:
from moviepy.editor import CompositeVideoClip
final_clip = CompositeVideoClip(overlay_clips, size=final_clip.size)
print(f"✨ Applied {len(self.timeline_effects)} timeline effects")
return final_clip
def _apply_transform_effect_to_clip(self, clip, effect):
"""Apply transform effects (zoom, rotation, blur) to video clip"""
start_time = effect['start_time']
end_time = effect['end_time']
effect_type = effect['type']
params = effect['params']
def transform_frame(get_frame, t):
frame = get_frame(t)
# Only apply effect during specified time range
if not (start_time <= t <= end_time):
return frame
# Calculate effect progress (0.0 to 1.0)
progress = (t - start_time) / (end_time - start_time) if end_time > start_time else 0.0
if effect_type == 'zoom':
return self._apply_zoom_to_frame(frame, progress, params)
elif effect_type == 'rotation':
return self._apply_rotation_to_frame(frame, progress, params)
elif effect_type == 'blur':
return self._apply_blur_to_frame(frame, progress, params)
return frame
return clip.transform(transform_frame)
def _apply_color_effect_to_clip(self, clip, effect):
"""Apply color effects to video clip"""
start_time = effect['start_time']
end_time = effect['end_time']
effect_type = effect['type']
params = effect['params']
def color_transform(get_frame, t):
frame = get_frame(t)
# Only apply effect during specified time range
if not (start_time <= t <= end_time):
return frame
# Calculate effect intensity based on time
progress = (t - start_time) / (end_time - start_time) if end_time > start_time else 1.0
if effect_type == 'color_preset':
return self._apply_color_preset_to_frame(frame, progress, params)
elif effect_type == 'color_grading':
return self._apply_color_grading_to_frame(frame, progress, params)
return frame
return clip.transform(color_transform)
def _create_text_clip(self, effect):
"""Create a text clip for timeline"""
try:
from moviepy.editor import TextClip
text = effect['params'].get('text', 'Sample Text')
font_size = effect['params'].get('font_size', 50)
color = effect['params'].get('color', 'white')
font = effect['params'].get('font', 'Arial-Bold')
animation = effect['params'].get('animation', 'fade_in')
# Create basic text clip
text_clip = TextClip(text, fontsize=font_size, color=color, font=font)
text_clip = text_clip.with_start(effect['start_time']).with_end(effect['end_time'])
# Apply position
position = effect['position'] or ('center', 'bottom')
text_clip = text_clip.with_position(position)
# Apply animation
text_clip = self._apply_text_animation(text_clip, animation)
return text_clip
except Exception as e:
print(f"⚠️ Error creating text clip: {e}")
return None
def _create_sticker_clip(self, effect):
"""Create a sticker/emoji clip for timeline"""
try:
from moviepy.editor import TextClip
sticker = effect['params'].get('sticker', '😂')
size = effect['params'].get('size', 80)
# Create emoji as text clip
sticker_clip = TextClip(sticker, fontsize=size)
sticker_clip = sticker_clip.with_start(effect['start_time']).with_end(effect['end_time'])
# Apply position
position = effect['position'] or ('right', 'top')
if position == ('right', 'top'):
pos = (lambda t: (self.video_clip.w - 150, 50))
elif position == ('left', 'top'):
pos = (50, 50)
elif position == ('right', 'bottom'):
pos = (lambda t: (self.video_clip.w - 150, self.video_clip.h - 150))
elif position == ('left', 'bottom'):
pos = (50, lambda t: self.video_clip.h - 150)
else:
pos = 'center'
sticker_clip = sticker_clip.with_position(pos)
return sticker_clip
except Exception as e:
print(f"⚠️ Error creating sticker clip: {e}")
return None
def _create_particle_clip(self, effect):
"""Create a particle effect clip for timeline"""
try:
# For now, create a simple overlay effect
# In a full implementation, this would generate actual particle animations
from moviepy.editor import ColorClip
# Create a semi-transparent overlay as placeholder
duration = effect['end_time'] - effect['start_time']
particle_clip = ColorClip(size=self.video_clip.size, color=(255, 255, 255), duration=duration)
particle_clip = particle_clip.with_opacity(0.1) # Very transparent
particle_clip = particle_clip.with_start(effect['start_time'])
return particle_clip
except Exception as e:
print(f"⚠️ Error creating particle clip: {e}")
return None
def _apply_text_animation(self, text_clip, animation):
"""Apply animation to text clip"""
if animation == 'fade_in':
from moviepy.video.fx import FadeIn
return text_clip.with_effects([FadeIn(0.5)])
elif animation == 'slide_left':
# Slide in from right
def slide_pos(t):
progress = min(1.0, t / 0.5) # 0.5 second animation
start_x = text_clip.w if hasattr(text_clip, 'w') else 200
final_x = 50
x = start_x - (start_x - final_x) * progress
return (x, 'center')
return text_clip.with_position(slide_pos)
elif animation == 'zoom_in':
def zoom_size(t):
progress = min(1.0, t / 0.5)
scale = 0.1 + 0.9 * progress
return scale
return text_clip.resized(zoom_size)
else:
return text_clip
def _apply_zoom_to_frame(self, frame, progress, params):
"""Apply zoom effect to a single frame"""
zoom_factor = params.get('zoom_factor', 1.5)
zoom_type = params.get('zoom_type', 'zoom_in')
h, w = frame.shape[:2]
if zoom_type == 'zoom_in':
current_zoom = 1.0 + (zoom_factor - 1.0) * progress
elif zoom_type == 'zoom_out':
current_zoom = zoom_factor - (zoom_factor - 1.0) * progress
else: # static zoom
current_zoom = zoom_factor
# Calculate crop region
new_w = int(w / current_zoom)
new_h = int(h / current_zoom)
start_x = (w - new_w) // 2
start_y = (h - new_h) // 2
# Crop and resize
cropped = frame[start_y:start_y + new_h, start_x:start_x + new_w]
zoomed = cv2.resize(cropped, (w, h), interpolation=cv2.INTER_CUBIC)
return zoomed
def _apply_rotation_to_frame(self, frame, progress, params):
"""Apply rotation effect to a single frame"""
angle = params.get('angle', 0)
rotation_type = params.get('rotation_type', 'static')
h, w = frame.shape[:2]
if rotation_type == 'spinning':
current_angle = angle * progress * 10 # Multiple rotations
else: # static rotation
current_angle = angle * progress
# Rotation matrix
center = (w // 2, h // 2)
matrix = cv2.getRotationMatrix2D(center, current_angle, 1.0)
rotated = cv2.warpAffine(frame, matrix, (w, h), borderMode=cv2.BORDER_REFLECT)
return rotated
def _apply_blur_to_frame(self, frame, progress, params):
"""Apply blur effect to a single frame"""
blur_strength = params.get('blur_strength', 2.0) * progress
if frame.dtype != np.uint8:
frame = (frame * 255).astype(np.uint8)
kernel_size = max(1, int(15 * progress))
if kernel_size % 2 == 0:
kernel_size += 1
blurred = cv2.GaussianBlur(frame, (kernel_size, kernel_size), blur_strength)
return blurred
def _apply_color_preset_to_frame(self, frame, progress, params):
"""Apply color preset to a single frame"""
preset = params.get('preset', 'cinematic')
if frame.dtype != np.uint8:
frame = (frame * 255).astype(np.uint8)
frame_float = frame.astype(np.float32) / 255.0
if preset == 'cinematic':
# Blue-orange cinematic look
frame_float[:,:,0] *= (0.9 + 0.2 * progress) # Red
frame_float[:,:,1] *= (1.0 + 0.1 * progress) # Green
frame_float[:,:,2] *= (1.1 + 0.3 * progress) # Blue
elif preset == 'warm':
frame_float[:,:,0] *= (1.0 + 0.3 * progress) # More red
frame_float[:,:,1] *= (1.0 + 0.1 * progress) # Slight green
frame_float[:,:,2] *= (0.8 + 0.1 * progress) # Less blue
elif preset == 'cool':
frame_float[:,:,0] *= (0.8 + 0.1 * progress) # Less red
frame_float[:,:,1] *= (1.0) # Keep green
frame_float[:,:,2] *= (1.0 + 0.4 * progress) # More blue
elif preset == 'vintage':
# Sepia-like effect
frame_float[:,:,0] *= (1.0 + 0.2 * progress) # Red
frame_float[:,:,1] *= (1.0 + 0.1 * progress) # Green
frame_float[:,:,2] *= (0.7 + 0.2 * progress) # Blue
return np.clip(frame_float * 255, 0, 255).astype(np.uint8)
def _apply_color_grading_to_frame(self, frame, progress, params):
"""Apply advanced color grading to a single frame"""
brightness = params.get('brightness', 1.0)
contrast = params.get('contrast', 1.0)
saturation = params.get('saturation', 1.0)
if frame.dtype != np.uint8:
frame = (frame * 255).astype(np.uint8)
# Apply brightness
frame = cv2.convertScaleAbs(frame, alpha=contrast * progress + (1 - progress),
beta=(brightness - 1) * 30 * progress)
# Apply saturation
if saturation != 1.0:
hsv = cv2.cvtColor(frame, cv2.COLOR_RGB2HSV).astype(np.float32)
hsv[:,:,1] *= (saturation * progress + (1 - progress))
hsv[:,:,1] = np.clip(hsv[:,:,1], 0, 255)
frame = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2RGB)
return frame
def load_video(self, video_path):
"""Load a video file for editing"""
if self.video_clip:
self.video_clip.close()
self.original_video_path = video_path
self.video_clip = VideoFileClip(video_path)
self.effects = []
print(f"📺 Loaded video: {os.path.basename(video_path)}")
def reset(self):
"""Reset to original video, removing all effects"""
if self.original_video_path:
self.load_video(self.original_video_path)
print("🔄 Video reset to original state")
def export(self, output_path, quality="medium", progress_callback=None):
"""Export the final edited video"""
if not self.video_clip:
raise Exception("No video loaded!")
# Quality settings
quality_settings = {
"low": {"bitrate": "500k", "audio_bitrate": "128k"},
"medium": {"bitrate": "1M", "audio_bitrate": "192k"},
"high": {"bitrate": "2M", "audio_bitrate": "320k"}
}
settings = quality_settings.get(quality, quality_settings["medium"])
# Export with progress callback
try:
# Try with newer MoviePy parameters first
self.video_clip.write_videofile(
output_path,
codec="libx264",
audio_codec="aac",
bitrate=settings["bitrate"],
audio_bitrate=settings["audio_bitrate"],
logger=None
)
except TypeError as e:
if "verbose" in str(e):
# Fallback for older MoviePy versions
self.video_clip.write_videofile(
output_path,
codec="libx264",
audio_codec="aac",
bitrate=settings["bitrate"],
audio_bitrate=settings["audio_bitrate"]
)
else:
raise e
@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,
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()
def add_blur_effect(self, blur_strength=2.0):
"""Add blur effect to current video"""
if not self.video_clip:
raise Exception("No video loaded!")
def blur_frame(get_frame, t):
frame = get_frame(t)
# Convert to uint8 if needed
if frame.dtype != np.uint8:
frame = (frame * 255).astype(np.uint8)
blurred = cv2.GaussianBlur(frame, (15, 15), blur_strength)
return blurred
self.video_clip = self.video_clip.transform(blur_frame)
self.effects.append(f"blur({blur_strength})")
print(f"🌫️ Applied blur effect (strength: {blur_strength})")
def add_color_effect(self, effect_type="sepia"):
"""Add color effects: sepia, grayscale, vintage, etc."""
if not self.video_clip:
raise Exception("No video loaded!")
def apply_color_effect(get_frame, t):
frame = get_frame(t)
if frame.dtype != np.uint8:
frame = (frame * 255).astype(np.uint8)
if effect_type == "grayscale":
gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
return cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB)
elif effect_type == "sepia":
# Sepia transformation matrix
sepia_filter = np.array([[0.393, 0.769, 0.189],
[0.349, 0.686, 0.168],
[0.272, 0.534, 0.131]])
sepia_img = frame.dot(sepia_filter.T)
sepia_img = np.clip(sepia_img, 0, 255)
return sepia_img.astype(np.uint8)
elif effect_type == "vintage":
# Vintage effect (warm + slight vignette)
frame = frame.astype(np.float32)
frame[:,:,0] *= 1.2 # Increase red
frame[:,:,1] *= 1.1 # Slightly increase green
frame[:,:,2] *= 0.9 # Decrease blue
return np.clip(frame, 0, 255).astype(np.uint8)
elif effect_type == "cool":
# Cool effect (more blue)
frame = frame.astype(np.float32)
frame[:,:,0] *= 0.9 # Decrease red
frame[:,:,1] *= 1.0 # Keep green
frame[:,:,2] *= 1.3 # Increase blue
return np.clip(frame, 0, 255).astype(np.uint8)
return frame
self.video_clip = self.video_clip.transform(apply_color_effect)
self.effects.append(f"color({effect_type})")
print(f"🎨 Applied color effect: {effect_type}")
def add_zoom_effect(self, zoom_factor=1.5, zoom_type="zoom_in"):
"""Add zoom in/out effect"""
if not self.video_clip:
raise Exception("No video loaded!")
def zoom_frame(get_frame, t):
frame = get_frame(t)
h, w = frame.shape[:2]
if zoom_type == "zoom_in":
progress = t / self.video_clip.duration
current_zoom = 1.0 + (zoom_factor - 1.0) * progress
elif zoom_type == "zoom_out":
progress = t / self.video_clip.duration
current_zoom = zoom_factor - (zoom_factor - 1.0) * progress
else: # static zoom
current_zoom = zoom_factor
# Calculate crop region
new_h, new_w = int(h / current_zoom), int(w / current_zoom)
start_x = (w - new_w) // 2
start_y = (h - new_h) // 2
# Calculate crop dimensions for zoom
new_w = int(w / current_zoom)
new_h = int(h / current_zoom)
start_x = (w - new_w) // 2
start_y = (h - new_h) // 2
# Crop and resize
cropped = frame[start_y:start_y + new_h, start_x:start_x + new_w]
zoomed = cv2.resize(cropped, (w, h), interpolation=cv2.INTER_CUBIC)
return zoomed
self.video_clip = self.video_clip.transform(zoom_frame)
self.effects.append(f"zoom({zoom_type}, {zoom_factor})")
print(f"🔍 Applied zoom effect: {zoom_type} (factor: {zoom_factor})")
def add_rotation_effect(self, angle=0, rotation_type="static"):
"""Add rotation effect"""
if not self.video_clip:
raise Exception("No video loaded!")
def rotate_frame(get_frame, t):
frame = get_frame(t)
h, w = frame.shape[:2]
if rotation_type == "spinning":
# Continuous rotation
current_angle = (angle * t * 360 / self.video_clip.duration) % 360
else: # static rotation
current_angle = angle
# Rotation matrix
center = (w // 2, h // 2)
matrix = cv2.getRotationMatrix2D(center, current_angle, 1.0)
rotated = cv2.warpAffine(frame, matrix, (w, h), borderMode=cv2.BORDER_REFLECT)
return rotated
self.video_clip = self.video_clip.transform(rotate_frame)
self.effects.append(f"rotation({rotation_type}, {angle})")
print(f"🔄 Applied rotation effect: {rotation_type} (angle: {angle}°)")
def apply_trim(self, start_time, end_time):
"""Apply trim to current video"""
if not self.video_clip:
raise Exception("No video loaded!")
if start_time >= end_time:
raise Exception("Start time must be less than end time!")
if end_time > self.video_clip.duration:
raise Exception(f"End time cannot exceed video duration ({self.video_clip.duration:.1f}s)!")
self.video_clip = self.video_clip.subclipped(start_time, end_time)
self.effects.append(f"trim({start_time:.1f}s-{end_time:.1f}s)")
print(f"✂️ Applied trim: {start_time:.1f}s to {end_time:.1f}s")
def apply_speed(self, speed_factor):
"""Apply speed change to current video"""
if not self.video_clip:
raise Exception("No video loaded!")
if speed_factor <= 0:
raise Exception("Speed factor must be greater than 0!")
if speed_factor > 1:
# Speed up
self.video_clip = self.video_clip.with_fps(self.video_clip.fps * speed_factor).subclipped(0, self.video_clip.duration / speed_factor)
else:
# Slow down
self.video_clip = self.video_clip.with_fps(self.video_clip.fps * speed_factor)
self.effects.append(f"speed({speed_factor:.1f}x)")
print(f"⚡ Applied speed change: {speed_factor:.1f}x")
def apply_fade_effects(self, fade_in_duration=1.0, fade_out_duration=1.0):
"""Apply fade in/out effects to current video"""
if not self.video_clip:
raise Exception("No video loaded!")
from moviepy.video.fx import FadeIn, FadeOut
if fade_in_duration > 0:
self.video_clip = self.video_clip.with_effects([FadeIn(fade_in_duration)])
if fade_out_duration > 0:
self.video_clip = self.video_clip.with_effects([FadeOut(fade_out_duration)])
self.effects.append(f"fade(in:{fade_in_duration:.1f}s, out:{fade_out_duration:.1f}s)")
print(f"🌅 Applied fade effects: in {fade_in_duration:.1f}s, out {fade_out_duration:.1f}s")
def apply_volume(self, volume_factor):
"""Apply volume adjustment to current video"""
if not self.video_clip:
raise Exception("No video loaded!")
if not self.video_clip.audio:
raise Exception("Video has no audio track!")
from moviepy.audio.fx import MultiplyVolume
self.video_clip = self.video_clip.with_effects([MultiplyVolume(volume_factor)])
self.effects.append(f"volume({volume_factor:.1f}x)")
print(f"🔊 Applied volume adjustment: {volume_factor:.1f}x")
def apply_resize(self, width, height):
"""Apply resize to current video"""
if not self.video_clip:
raise Exception("No video loaded!")
if width < 1 or height < 1:
raise Exception("Width and height must be positive!")
from moviepy.video.fx import Resize
self.video_clip = self.video_clip.with_effects([Resize((width, height))])
self.effects.append(f"resize({width}x{height})")
print(f"📐 Applied resize: {width}x{height}")
def apply_text_overlay_to_current(self, text, position=('center', 'bottom'), font_size=50, color='white', method='fast'):
"""Apply text overlay to current video"""
if not self.video_clip:
raise Exception("No video loaded!")
if method == 'fast':
# Use the fast PIL-based method
self._apply_text_overlay_fast_to_current(text, position, font_size, color)
else:
# Use MoviePy method for higher quality
self._apply_text_overlay_quality_to_current(text, position, font_size, color)
self.effects.append(f"text('{text[:20]}...', {position}, {font_size}px)")
print(f"📝 Applied text overlay: '{text[:30]}...'")
def _apply_text_overlay_fast_to_current(self, text, position, font_size, color):
"""Fast PIL-based text overlay to current video"""
from PIL import Image, ImageDraw, ImageFont
def add_text_to_frame(get_frame, t):
frame = get_frame(t)
# Convert to PIL Image
pil_image = Image.fromarray(frame)
draw = ImageDraw.Draw(pil_image)
# Calculate position
w, h = pil_image.size
x_pos, y_pos = self._calculate_text_position(position, w, h, text, font_size)
# Draw text with outline for better visibility
try:
font = ImageFont.truetype("arial.ttf", font_size)
except:
font = ImageFont.load_default()
# Draw outline
for adj in range(-2, 3):
for adj2 in range(-2, 3):
draw.text((x_pos + adj, y_pos + adj2), text, font=font, fill='black')
# Draw main text
draw.text((x_pos, y_pos), text, font=font, fill=color)
return np.array(pil_image)
self.video_clip = self.video_clip.transform(add_text_to_frame)
def _apply_text_overlay_quality_to_current(self, text, position, font_size, color):
"""High quality MoviePy-based text overlay to current video"""
from moviepy.editor import TextClip, CompositeVideoClip
text_clip = TextClip(text, fontsize=font_size, color=color, font='Arial-Bold')
text_clip = text_clip.with_duration(self.video_clip.duration)
# Set position
if position == ('center', 'center'):
text_clip = text_clip.with_position('center')
elif position == ('center', 'bottom'):
text_clip = text_clip.with_position(('center', 'bottom'))
elif position == ('center', 'top'):
text_clip = text_clip.with_position(('center', 'top'))
else:
text_clip = text_clip.with_position(position)
self.video_clip = CompositeVideoClip([self.video_clip, text_clip])
def _calculate_text_position(self, position, width, height, text, font_size):
"""Calculate text position based on position tuple"""
# Estimate text dimensions (rough calculation)
text_width = len(text) * font_size * 0.6
text_height = font_size
x_pos, y_pos = position
if x_pos == 'center':
x_pos = (width - text_width) // 2
elif x_pos == 'left':
x_pos = 50
elif x_pos == 'right':
x_pos = width - text_width - 50
if y_pos == 'center':
y_pos = (height - text_height) // 2
elif y_pos == 'top':
y_pos = 50
elif y_pos == 'bottom':
y_pos = height - text_height - 50
return int(x_pos), int(y_pos)
@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 logger parameter (newer MoviePy)
final_video.write_videofile(temp_output, codec="libx264", audio_codec="aac",
logger=None)
except TypeError:
# Fallback for older MoviePy versions without logger 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
# Timeline Video Editor Class
class TimelineEditor:
"""Professional timeline-based video editor"""
def __init__(self):
self.clips = [] # List of video clips in timeline
self.tracks = { # Multiple tracks for complex editing
'video': [], # Main video track
'overlay': [], # Overlay track for text/graphics
'audio': [], # Audio track for background music
'effects': [] # Effects track
}
self.timeline_duration = 0 # Total timeline duration
self.playhead_position = 0 # Current playback position
self.zoom_level = 1.0 # Timeline zoom level
self.snap_enabled = True # Snap to grid/markers
self.markers = [] # Timeline markers
self.selected_clips = [] # Currently selected clips
self.clipboard = [] # Clipboard for copy/paste
self.undo_stack = [] # Undo history
self.redo_stack = [] # Redo history
def add_clip_to_track(self, track_name, clip_data, start_time, duration=None):
"""Add a clip to specific track at specific time"""
if track_name not in self.tracks:
self.tracks[track_name] = []
clip = {
'id': len(self.clips),
'type': clip_data.get('type', 'video'),
'file_path': clip_data.get('file_path'),
'start_time': start_time,
'duration': duration or clip_data.get('duration', 5.0),
'in_point': clip_data.get('in_point', 0),
'out_point': clip_data.get('out_point', duration or 5.0),
'effects': [],
'transitions': {'in': None, 'out': None},
'properties': clip_data.get('properties', {}),
'locked': False,
'muted': False,
'visible': True
}
self.tracks[track_name].append(clip)
self.clips.append(clip)
self._update_timeline_duration()
self._save_state()
return clip
def remove_clip(self, clip_id, ripple_delete=True):
"""Remove clip from timeline with optional ripple delete"""
self._save_state()
clip_to_remove = None
track_name = None
# Find and remove clip
for track, clips in self.tracks.items():
for i, clip in enumerate(clips):
if clip['id'] == clip_id:
clip_to_remove = clip
track_name = track
clips.pop(i)
break
if clip_to_remove:
break
if not clip_to_remove:
return False
# Remove from main clips list
self.clips = [c for c in self.clips if c['id'] != clip_id]
if ripple_delete:
self._ripple_delete(track_name, clip_to_remove['start_time'],
clip_to_remove['duration'])
self._update_timeline_duration()
return True
def move_clip(self, clip_id, new_start_time, new_track=None):
"""Move clip to new position/track"""
self._save_state()
clip = self.get_clip_by_id(clip_id)
if not clip:
return False
old_track = self._find_clip_track(clip_id)
old_start = clip['start_time']
# Remove from old position
if old_track:
self.tracks[old_track] = [c for c in self.tracks[old_track] if c['id'] != clip_id]
# Update clip position
clip['start_time'] = new_start_time
# Add to new track
target_track = new_track or old_track
if target_track and target_track in self.tracks:
self.tracks[target_track].append(clip)
self._update_timeline_duration()
return True
def trim_clip(self, clip_id, new_in_point=None, new_out_point=None, trim_type="ripple"):
"""Trim clip with different trim modes"""
self._save_state()
clip = self.get_clip_by_id(clip_id)
if not clip:
return False
old_duration = clip['duration']
if new_in_point is not None:
clip['in_point'] = max(0, new_in_point)
if new_out_point is not None:
clip['out_point'] = new_out_point
# Update duration
clip['duration'] = clip['out_point'] - clip['in_point']
duration_change = clip['duration'] - old_duration
if trim_type == "ripple" and duration_change != 0:
track_name = self._find_clip_track(clip_id)
self._ripple_edit(track_name, clip['start_time'] + old_duration,
duration_change)
self._update_timeline_duration()
return True
def split_clip(self, clip_id, split_time):
"""Split clip at specific time"""
self._save_state()
clip = self.get_clip_by_id(clip_id)
if not clip:
return False
if split_time <= clip['start_time'] or split_time >= clip['start_time'] + clip['duration']:
return False
track_name = self._find_clip_track(clip_id)
# Create second part
second_clip = copy.deepcopy(clip)
second_clip['id'] = len(self.clips)
second_clip['start_time'] = split_time
second_clip['in_point'] += (split_time - clip['start_time'])
second_clip['duration'] = clip['start_time'] + clip['duration'] - split_time
# Update first part
clip['duration'] = split_time - clip['start_time']
clip['out_point'] = clip['in_point'] + clip['duration']
# Add second part to timeline
self.tracks[track_name].append(second_clip)
self.clips.append(second_clip)
return [clip['id'], second_clip['id']]
def add_transition(self, clip_id, transition_type, duration=1.0, position="out"):
"""Add transition to clip"""
clip = self.get_clip_by_id(clip_id)
if not clip:
return False
transition = {
'type': transition_type,
'duration': duration,
'properties': {}
}
clip['transitions'][position] = transition
return True
def add_effect_to_clip(self, clip_id, effect_type, start_time=None, duration=None, **params):
"""Add effect to specific clip"""
clip = self.get_clip_by_id(clip_id)
if not clip:
return False
effect = {
'type': effect_type,
'start_time': start_time or 0,
'duration': duration or clip['duration'],
'params': params,
'enabled': True
}
clip['effects'].append(effect)
return True
def add_marker(self, time, label="", color="blue"):
"""Add timeline marker"""
marker = {
'time': time,
'label': label,
'color': color
}
self.markers.append(marker)
self.markers.sort(key=lambda x: x['time'])
return marker
def set_playhead(self, time):
"""Set playhead position"""
self.playhead_position = max(0, min(time, self.timeline_duration))
def select_clips(self, clip_ids):
"""Select multiple clips"""
self.selected_clips = clip_ids
def copy_selected_clips(self):
"""Copy selected clips to clipboard"""
self.clipboard = []
for clip_id in self.selected_clips:
clip = self.get_clip_by_id(clip_id)
if clip:
self.clipboard.append(copy.deepcopy(clip))
def paste_clips(self, target_time, target_track=None):
"""Paste clipboard clips at target time"""
if not self.clipboard:
return []
self._save_state()
pasted_clips = []
for clip_data in self.clipboard:
new_clip = copy.deepcopy(clip_data)
new_clip['id'] = len(self.clips)
new_clip['start_time'] = target_time
track = target_track or 'video'
self.tracks[track].append(new_clip)
self.clips.append(new_clip)
pasted_clips.append(new_clip['id'])
target_time += new_clip['duration'] # Offset next clip
self._update_timeline_duration()
return pasted_clips
def group_clips(self, clip_ids):
"""Group clips into compound clip"""
if len(clip_ids) < 2:
return None
clips_to_group = [self.get_clip_by_id(cid) for cid in clip_ids if self.get_clip_by_id(cid)]
if not clips_to_group:
return None
# Calculate group bounds
min_start = min(c['start_time'] for c in clips_to_group)
max_end = max(c['start_time'] + c['duration'] for c in clips_to_group)
# Create compound clip
compound_clip = {
'id': len(self.clips),
'type': 'compound',
'start_time': min_start,
'duration': max_end - min_start,
'clips': clips_to_group,
'effects': [],
'transitions': {'in': None, 'out': None},
'properties': {},
'locked': False,
'muted': False,
'visible': True
}
# Remove original clips from tracks
for clip_id in clip_ids:
self.remove_clip(clip_id, ripple_delete=False)
# Add compound clip
self.tracks['video'].append(compound_clip)
self.clips.append(compound_clip)
return compound_clip['id']
def undo(self):
"""Undo last action"""
if self.undo_stack:
current_state = self._get_current_state()
self.redo_stack.append(current_state)
previous_state = self.undo_stack.pop()
self._restore_state(previous_state)
def redo(self):
"""Redo last undone action"""
if self.redo_stack:
current_state = self._get_current_state()
self.undo_stack.append(current_state)
next_state = self.redo_stack.pop()
self._restore_state(next_state)
def export_timeline(self, output_path, quality="high", progress_callback=None):
"""Export timeline to video file"""
print("🎬 Starting timeline export...")
if not self.clips:
raise Exception("No clips in timeline!")
# Sort clips by start time
all_clips = []
for track_clips in self.tracks.values():
all_clips.extend(track_clips)
all_clips.sort(key=lambda x: x['start_time'])
# Build composition
video_clips = []
for clip in all_clips:
if not clip['visible'] or clip['type'] == 'audio':
continue
try:
if clip['type'] == 'video':
video_clip = VideoFileClip(clip['file_path'])
# Apply trimming
if clip['in_point'] > 0 or clip['out_point'] < video_clip.duration:
video_clip = video_clip.subclipped(clip['in_point'],
min(clip['out_point'], video_clip.duration))
# Set timing in timeline
video_clip = video_clip.with_start(clip['start_time'])
# Apply clip effects
for effect in clip['effects']:
video_clip = self._apply_effect_to_clip(video_clip, effect)
# Apply transitions
if clip['transitions']['in']:
video_clip = self._apply_transition(video_clip, clip['transitions']['in'], 'in')
if clip['transitions']['out']:
video_clip = self._apply_transition(video_clip, clip['transitions']['out'], 'out')
video_clips.append(video_clip)
elif clip['type'] == 'text':
text_clip = self._create_text_clip_from_data(clip)
if text_clip:
video_clips.append(text_clip)
except Exception as e:
print(f"⚠️ Error processing clip {clip['id']}: {e}")
continue
if not video_clips:
raise Exception("No valid video clips to export!")
# Composite all clips
final_video = CompositeVideoClip(video_clips, size=(1080, 1920))
# Export with quality settings
quality_settings = {
"low": {"bitrate": "500k", "audio_bitrate": "128k"},
"medium": {"bitrate": "1M", "audio_bitrate": "192k"},
"high": {"bitrate": "2M", "audio_bitrate": "320k"}
}
settings = quality_settings.get(quality, quality_settings["medium"])
try:
final_video.write_videofile(
output_path,
codec="libx264",
audio_codec="aac",
bitrate=settings["bitrate"],
audio_bitrate=settings["audio_bitrate"],
logger=None
)
except TypeError:
# Fallback for older MoviePy
final_video.write_videofile(
output_path,
codec="libx264",
audio_codec="aac",
bitrate=settings["bitrate"],
audio_bitrate=settings["audio_bitrate"]
)
# Cleanup
for clip in video_clips:
if hasattr(clip, 'close'):
clip.close()
final_video.close()
print(f"✅ Timeline exported to: {output_path}")
def get_clip_by_id(self, clip_id):
"""Get clip by ID"""
return next((c for c in self.clips if c['id'] == clip_id), None)
def get_clips_at_time(self, time):
"""Get all clips active at specific time"""
active_clips = []
for clip in self.clips:
if clip['start_time'] <= time <= clip['start_time'] + clip['duration']:
active_clips.append(clip)
return active_clips
def get_track_clips(self, track_name):
"""Get all clips in specific track"""
return self.tracks.get(track_name, [])
def _find_clip_track(self, clip_id):
"""Find which track contains the clip"""
for track_name, clips in self.tracks.items():
if any(c['id'] == clip_id for c in clips):
return track_name
return None
def _update_timeline_duration(self):
"""Update total timeline duration"""
max_duration = 0
for clip in self.clips:
clip_end = clip['start_time'] + clip['duration']
max_duration = max(max_duration, clip_end)
self.timeline_duration = max_duration
def _ripple_delete(self, track_name, start_time, duration):
"""Shift clips after deleted region"""
for clip in self.tracks[track_name]:
if clip['start_time'] > start_time:
clip['start_time'] -= duration
def _ripple_edit(self, track_name, edit_time, time_change):
"""Shift clips after edit point"""
for clip in self.tracks[track_name]:
if clip['start_time'] >= edit_time:
clip['start_time'] += time_change
def _save_state(self):
"""Save current state for undo"""
current_state = self._get_current_state()
self.undo_stack.append(current_state)
# Limit undo stack size
if len(self.undo_stack) > 50:
self.undo_stack.pop(0)
# Clear redo stack when new action is performed
self.redo_stack.clear()
def _get_current_state(self):
"""Get current timeline state"""
return {
'clips': copy.deepcopy(self.clips),
'tracks': copy.deepcopy(self.tracks),
'timeline_duration': self.timeline_duration,
'markers': copy.deepcopy(self.markers)
}
def _restore_state(self, state):
"""Restore timeline to previous state"""
self.clips = copy.deepcopy(state['clips'])
self.tracks = copy.deepcopy(state['tracks'])
self.timeline_duration = state['timeline_duration']
self.markers = copy.deepcopy(state['markers'])
def _apply_effect_to_clip(self, video_clip, effect):
"""Apply effect to video clip"""
effect_type = effect['type']
params = effect['params']
if effect_type == 'fade_in':
return video_clip.with_effects([FadeIn(params.get('duration', 1.0))])
elif effect_type == 'fade_out':
return video_clip.with_effects([FadeOut(params.get('duration', 1.0))])
elif effect_type == 'resize':
width = params.get('width', 1080)
height = params.get('height', 1920)
return video_clip.with_effects([Resize((width, height))])
elif effect_type == 'volume':
factor = params.get('factor', 1.0)
if video_clip.audio:
return video_clip.with_effects([MultiplyVolume(factor)])
return video_clip
def _apply_transition(self, video_clip, transition, position):
"""Apply transition effect"""
transition_type = transition['type']
duration = transition['duration']
if transition_type == 'fade' and position == 'in':
return video_clip.with_effects([FadeIn(duration)])
elif transition_type == 'fade' and position == 'out':
return video_clip.with_effects([FadeOut(duration)])
return video_clip
def _create_text_clip_from_data(self, clip_data):
"""Create text clip from clip data"""
try:
props = clip_data['properties']
text = props.get('text', 'Sample Text')
font_size = props.get('font_size', 50)
color = props.get('color', 'white')
position = props.get('position', ('center', 'bottom'))
text_clip = TextClip(text, fontsize=font_size, color=color)
text_clip = text_clip.with_start(clip_data['start_time'])
text_clip = text_clip.with_duration(clip_data['duration'])
text_clip = text_clip.with_position(position)
return text_clip
except Exception as e:
print(f"⚠️ Error creating text clip: {e}")
return None
# Post-Generation Editing Interface
class ShortsEditorGUI:
"""Interface for editing generated shorts with modern design"""
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
# Modern color scheme
self.colors = {
'bg_primary': '#1a1a1a', # Dark background
'bg_secondary': '#2d2d2d', # Medium dark
'bg_tertiary': '#3d3d3d', # Lighter dark
'text_primary': '#ffffff', # White text
'text_secondary': '#cccccc', # Light gray text
'text_muted': '#888888', # Muted gray text
'accent_blue': '#4a9eff', # Blue accent
'accent_green': '#4CAF50', # Green accent
'accent_orange': '#ff9800', # Orange accent
'accent_red': '#f44336', # Red accent
'accent_purple': '#9c27b0', # Purple accent
'hover': '#4a4a4a', # Hover state
'accent_gray': '#666666', # Gray accent
'border': '#555555' # Border color
}
# Modern font scheme
self.fonts = {
'heading': ('Segoe UI', 16, 'bold'),
'subheading': ('Segoe UI', 12, 'bold'),
'body': ('Segoe UI', 10, 'normal'),
'caption': ('Segoe UI', 9, 'normal'),
'mono': ('Consolas', 9, 'normal'),
'small': ('Segoe UI', 8, 'normal')
}
# Timeline variables initialization
self.timeline_selected_clips = []
self.timeline_playhead_pos = 0
self.timeline_scale = 50 # Pixels per second
self.timeline_tracks_height = 60 # Height per track
def add_hover_effect(self, button, hover_color=None):
"""Add hover effect to buttons"""
if hover_color is None:
hover_color = self.colors['hover']
original_color = button.cget('bg')
def on_enter(e):
button.config(bg=hover_color)
def on_leave(e):
button.config(bg=original_color)
button.bind("<Enter>", on_enter)
button.bind("<Leave>", on_leave)
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 modern editor window
self.editor_window = tk.Toplevel(self.parent)
self.editor_window.title("🎬 Shorts Editor - Professional Video Editing")
self.editor_window.geometry("1200x800") # Increased width to show all panels
self.editor_window.minsize(1000, 700) # Increased minimum size
self.editor_window.resizable(True, True)
self.editor_window.transient(self.parent)
self.editor_window.configure(bg=self.colors['bg_primary']) # Modern dark background
# Make window responsive
self.editor_window.rowconfigure(1, weight=1)
self.editor_window.columnconfigure(0, weight=1)
# Bind resize event
self.editor_window.bind('<Configure>', self.on_editor_resize)
self.create_editor_interface(shorts_files)
def on_editor_resize(self, event):
"""Handle editor window resize events"""
if event.widget == self.editor_window:
# Get current window size
width = self.editor_window.winfo_width()
height = self.editor_window.winfo_height()
# Adjust layout based on size - for very small windows, stack vertically
if width < 700:
# Switch to vertical layout for smaller windows
try:
# This would require more significant layout changes
# For now, just ensure minimum functionality
pass
except:
pass
def create_editor_interface(self, shorts_files):
"""Create the main editor interface with modern video player"""
# Modern title section
title_frame = tk.Frame(self.editor_window, bg=self.colors['bg_primary'])
title_frame.pack(fill="x", padx=20, pady=10)
title_label = tk.Label(title_frame, text="🎬 Professional Shorts Editor",
font=self.fonts['heading'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'])
title_label.pack()
subtitle_label = tk.Label(title_frame,
text="Select and edit your generated shorts with professional tools + Real-time Preview",
font=self.fonts['body'], bg=self.colors['bg_primary'],
fg=self.colors['text_secondary'])
subtitle_label.pack()
# Modern main content frame
main_frame = tk.Frame(self.editor_window, bg=self.colors['bg_primary'])
main_frame.pack(fill="both", expand=True, padx=20, pady=10)
# Modern left panel - Video selection and info
left_panel = tk.Frame(main_frame, bg=self.colors['bg_primary'])
left_panel.pack(side="left", fill="y", padx=(0, 10))
# Modern video selection frame
selection_frame = tk.LabelFrame(left_panel, text="📁 Select Short to Edit",
font=self.fonts['subheading'],
bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=10)
selection_frame.pack(fill="x", pady=(0, 10))
# Modern video list container
list_frame = tk.Frame(selection_frame, bg=self.colors['bg_secondary'])
list_frame.pack(fill="x")
list_title = tk.Label(list_frame, text="Available Shorts:",
font=self.fonts['body'], bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'])
list_title.pack(anchor="w")
# Modern listbox with scrollbar
list_container = tk.Frame(list_frame, bg=self.colors['bg_secondary'])
list_container.pack(fill="x", pady=5)
self.video_listbox = tk.Listbox(list_container, height=4,
font=self.fonts['mono'], width=50,
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
selectbackground=self.colors['accent_blue'],
selectforeground=self.colors['text_primary'],
bd=0, highlightthickness=1,
highlightcolor=self.colors['accent_blue'],
relief="flat")
scrollbar = tk.Scrollbar(list_container, orient="vertical",
bg=self.colors['bg_tertiary'],
activebackground=self.colors['accent_blue'],
troughcolor=self.colors['bg_primary'])
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.load_video_in_player()
self.video_listbox.bind("<<ListboxSelect>>", on_video_select)
# Modern video info section
self.info_frame = tk.LabelFrame(left_panel, text="📊 Video Information",
font=self.fonts['subheading'],
bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=10)
self.info_frame.pack(fill="x", pady=(0, 10))
self.info_label = tk.Label(self.info_frame, text="Select a video to see details",
font=self.fonts['mono'], justify="left",
bg=self.colors['bg_secondary'],
fg=self.colors['text_secondary'])
self.info_label.pack(anchor="w")
# Modern video player frame (center)
player_frame = tk.Frame(main_frame, bg=self.colors['bg_primary'])
player_frame.pack(side="left", fill="both", expand=True, padx=10)
# Create modern video player
self.create_video_player(player_frame)
# Editing tools frame (right panel) - Fixed width to ensure visibility
self.tools_frame = tk.LabelFrame(main_frame, text="🛠️ Professional Editing Tools",
font=self.fonts['subheading'],
bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=10)
self.tools_frame.pack(side="right", fill="both", padx=(10, 0), ipadx=10)
self.tools_frame.config(width=300) # Set minimum width for tools panel
self.create_editing_tools()
# Modern output and action buttons
action_frame = tk.Frame(self.editor_window, bg=self.colors['bg_primary'])
action_frame.pack(fill="x", padx=20, pady=10)
# Modern output folder selection
output_folder_frame = tk.Frame(action_frame, bg=self.colors['bg_primary'])
output_folder_frame.pack(fill="x", pady=5)
output_label = tk.Label(output_folder_frame, text="Output Folder:",
font=self.fonts['body'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'])
output_label.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,
font=self.fonts['body'], bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'], insertbackground=self.colors['text_primary'],
bd=0, relief="flat")
output_entry.pack(side="left", padx=(10, 5))
browse_btn = tk.Button(output_folder_frame, text="Browse",
command=self.select_output_folder,
font=self.fonts['body'], bg=self.colors['accent_blue'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=15, pady=5)
browse_btn.pack(side="left")
self.add_hover_effect(browse_btn)
# Modern action buttons
button_frame = tk.Frame(action_frame, bg=self.colors['bg_primary'])
button_frame.pack(fill="x", pady=15) # Increased padding for better visibility
# Refresh button
refresh_btn = tk.Button(button_frame, text="🔄 Refresh List",
command=self.refresh_video_list,
bg=self.colors['accent_blue'], fg=self.colors['text_primary'],
font=self.fonts['body'], bd=0, relief="flat",
padx=15, pady=8)
refresh_btn.pack(side="left", padx=8)
self.add_hover_effect(refresh_btn)
# Open folder button
folder_btn = tk.Button(button_frame, text="📂 Open Shorts Folder",
command=self.open_shorts_folder,
bg=self.colors['accent_orange'], fg=self.colors['text_primary'],
font=self.fonts['body'], bd=0, relief="flat",
padx=15, pady=8)
folder_btn.pack(side="left", padx=8)
self.add_hover_effect(folder_btn)
# Close button
close_btn = tk.Button(button_frame, text="❌ Close Editor",
command=self.close_editor,
bg=self.colors['accent_red'], fg=self.colors['text_primary'],
font=self.fonts['body'], bd=0, relief="flat",
padx=15, pady=8)
close_btn.pack(side="right", padx=8)
self.add_hover_effect(close_btn)
def create_video_player(self, parent_frame):
"""Create the modern video player with timeline controls"""
player_label_frame = tk.LabelFrame(parent_frame, text="🎥 Real-time Video Player",
font=self.fonts['subheading'],
bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=10)
player_label_frame.pack(fill="both", expand=True)
# Modern video display canvas
self.video_canvas = tk.Canvas(player_label_frame, width=400, height=300,
bg=self.colors['bg_primary'], relief="flat", bd=0,
highlightthickness=1, highlightcolor=self.colors['border'])
self.video_canvas.pack(pady=10)
# Modern player controls frame
controls_frame = tk.Frame(player_label_frame, bg=self.colors['bg_secondary'])
controls_frame.pack(fill="x", pady=5)
# Modern timeline slider
timeline_frame = tk.Frame(controls_frame, bg=self.colors['bg_secondary'])
timeline_frame.pack(fill="x", pady=5)
timeline_label = tk.Label(timeline_frame, text="Timeline:",
font=self.fonts['body'], bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'])
timeline_label.pack(anchor="w")
self.timeline_var = tk.DoubleVar()
self.timeline_slider = tk.Scale(timeline_frame, from_=0, to=100, orient="horizontal",
variable=self.timeline_var, command=self.on_timeline_change,
length=380, resolution=0.1,
bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'],
activebackground=self.colors['accent_blue'],
troughcolor=self.colors['bg_tertiary'],
highlightthickness=0, bd=0, relief="flat")
self.timeline_slider.pack(fill="x")
# Modern play controls
play_controls_frame = tk.Frame(controls_frame, bg=self.colors['bg_secondary'])
play_controls_frame.pack(pady=5)
self.play_button = tk.Button(play_controls_frame, text="▶️ Play", command=self.toggle_play,
font=self.fonts['body'], bg=self.colors['accent_green'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=15, pady=5)
self.play_button.pack(side="left", padx=5)
self.add_hover_effect(self.play_button)
stop_btn = tk.Button(play_controls_frame, text="⏹️ Stop", command=self.stop_video,
font=self.fonts['body'], bg=self.colors['accent_red'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=15, pady=5)
stop_btn.pack(side="left", padx=5)
self.add_hover_effect(stop_btn)
seek_back_btn = tk.Button(play_controls_frame, text="⏪ -5s", command=lambda: self.seek_relative(-5),
font=self.fonts['caption'], bg=self.colors['accent_orange'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=5)
seek_back_btn.pack(side="left", padx=2)
self.add_hover_effect(seek_back_btn)
seek_forward_btn = tk.Button(play_controls_frame, text="⏩ +5s", command=lambda: self.seek_relative(5),
font=self.fonts['caption'], bg=self.colors['accent_orange'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=5)
seek_forward_btn.pack(side="left", padx=2)
self.add_hover_effect(seek_forward_btn)
# Modern time display
self.time_label = tk.Label(controls_frame, text="00:00 / 00:00",
font=self.fonts['body'], bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'])
self.time_label.pack(pady=5)
# Player state variables
self.current_clip = None
self.is_playing = False
self.current_time = 0.0
self.video_duration = 0.0
self.play_thread = None
self.last_frame_time = 0
def load_video_in_player(self):
"""Load the selected video in the player"""
if not self.current_video:
return
try:
# Close previous clip
if self.current_clip:
self.current_clip.close()
print(f"🎥 Loading video in player: {os.path.basename(self.current_video)}")
self.current_clip = VideoFileClip(self.current_video)
self.video_duration = self.current_clip.duration
# Update timeline
self.timeline_slider.config(to=self.video_duration)
self.timeline_var.set(0)
self.current_time = 0.0
# Display first frame
self.display_frame_at_time(0.0)
self.update_time_display()
print(f"✅ Video loaded successfully ({self.video_duration:.1f}s)")
except Exception as e:
print(f"❌ Error loading video: {e}")
messagebox.showerror("Video Error", f"Failed to load video:\n{str(e)}")
def display_frame_at_time(self, time_seconds):
"""Display video frame at specific time"""
if not self.current_clip:
return
try:
# Get frame at specified time
frame = self.current_clip.get_frame(min(time_seconds, self.video_duration - 0.01))
# Convert frame to proper format for PIL
if frame.dtype != np.uint8:
# Convert float frames to uint8
frame = (frame * 255).astype(np.uint8)
# Ensure frame is in correct shape (handle edge cases)
if len(frame.shape) == 3 and frame.shape[2] == 3:
# Normal RGB frame
pil_image = Image.fromarray(frame)
else:
# Handle other formats or corrupted frames
print(f"⚠️ Unusual frame shape: {frame.shape}, dtype: {frame.dtype}")
# Create a black frame as fallback
canvas_width = self.video_canvas.winfo_width() or 400
canvas_height = self.video_canvas.winfo_height() or 300
frame = np.zeros((canvas_height, canvas_width, 3), dtype=np.uint8)
pil_image = Image.fromarray(frame)
# Resize to fit canvas while maintaining aspect ratio
canvas_width = self.video_canvas.winfo_width() or 400
canvas_height = self.video_canvas.winfo_height() or 300
pil_image.thumbnail((canvas_width - 20, canvas_height - 20), Image.Resampling.LANCZOS)
# Convert to Tkinter format
self.current_tk_image = ImageTk.PhotoImage(pil_image)
# Clear canvas and display image
self.video_canvas.delete("all")
self.video_canvas.create_image(canvas_width//2, canvas_height//2,
image=self.current_tk_image)
except Exception as e:
print(f"⚠️ Error displaying frame: {e}")
# Show a black frame on error
try:
canvas_width = self.video_canvas.winfo_width() or 400
canvas_height = self.video_canvas.winfo_height() or 300
black_frame = np.zeros((canvas_height-20, canvas_width-20, 3), dtype=np.uint8)
pil_image = Image.fromarray(black_frame)
self.current_tk_image = ImageTk.PhotoImage(pil_image)
self.video_canvas.delete("all")
self.video_canvas.create_image(canvas_width//2, canvas_height//2,
image=self.current_tk_image)
except:
pass
def on_timeline_change(self, value):
"""Handle timeline slider changes"""
if not self.current_clip:
return
self.current_time = float(value)
self.display_frame_at_time(self.current_time)
self.update_time_display()
def toggle_play(self):
"""Toggle play/pause"""
if not self.current_clip:
return
if self.is_playing:
self.pause_video()
else:
self.play_video()
def play_video(self):
"""Start video playback"""
if not self.current_clip or self.is_playing:
return
self.is_playing = True
self.play_button.config(text="⏸️ Pause", bg="#FF9800")
def play_thread():
start_time = time.time()
start_video_time = self.current_time
while self.is_playing and self.current_time < self.video_duration:
try:
# Calculate current video time
elapsed = time.time() - start_time
self.current_time = start_video_time + elapsed
if self.current_time >= self.video_duration:
self.current_time = self.video_duration
self.is_playing = False
break
# Update timeline and display
self.timeline_var.set(self.current_time)
self.display_frame_at_time(self.current_time)
self.update_time_display()
# Frame rate control (approximately 30 FPS)
time.sleep(1/30)
except Exception as e:
print(f"⚠️ Playback error: {e}")
break
# Playback finished
self.is_playing = False
self.play_button.config(text="▶️ Play", bg="#4CAF50")
self.play_thread = threading.Thread(target=play_thread, daemon=True)
self.play_thread.start()
def pause_video(self):
"""Pause video playback"""
self.is_playing = False
self.play_button.config(text="▶️ Play", bg="#4CAF50")
def stop_video(self):
"""Stop video and return to beginning"""
self.is_playing = False
self.current_time = 0.0
self.timeline_var.set(0)
self.display_frame_at_time(0.0)
self.update_time_display()
self.play_button.config(text="▶️ Play", bg="#4CAF50")
def seek_relative(self, seconds):
"""Seek relative to current position"""
if not self.current_clip:
return
new_time = max(0, min(self.current_time + seconds, self.video_duration))
self.current_time = new_time
self.timeline_var.set(new_time)
self.display_frame_at_time(new_time)
self.update_time_display()
def update_time_display(self):
"""Update the time display label"""
current_mins = int(self.current_time // 60)
current_secs = int(self.current_time % 60)
total_mins = int(self.video_duration // 60)
total_secs = int(self.video_duration % 60)
time_text = f"{current_mins:02d}:{current_secs:02d} / {total_mins:02d}:{total_secs:02d}"
self.time_label.config(text=time_text)
def close_editor(self):
"""Clean up and close editor"""
self.is_playing = False
if self.current_clip:
self.current_clip.close()
self.editor_window.destroy()
def create_editing_tools(self):
"""Create the professional editing tools interface"""
# Create modern notebook for organized tools
style = ttk.Style()
style.theme_use('clam')
# Configure modern notebook styling
style.configure('Modern.TNotebook',
background=self.colors['bg_secondary'],
borderwidth=0)
style.configure('Modern.TNotebook.Tab',
background=self.colors['bg_tertiary'],
foreground=self.colors['text_primary'],
padding=[8, 4],
borderwidth=0)
style.map('Modern.TNotebook.Tab',
background=[('selected', self.colors['accent_blue']),
('active', self.colors['hover'])])
notebook = ttk.Notebook(self.tools_frame, style='Modern.TNotebook')
notebook.pack(fill="both", expand=True)
# Modern Basic Editing Tab
basic_frame = tk.Frame(notebook, bg=self.colors['bg_secondary'])
notebook.add(basic_frame, text="✂️ Basic Editing")
# Modern Trim Tool
trim_frame = tk.LabelFrame(basic_frame, text="✂️ Trim Video",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
trim_frame.pack(fill="x", padx=10, pady=5)
trim_controls = tk.Frame(trim_frame, bg=self.colors['bg_tertiary'])
trim_controls.pack(fill="x")
# Start time control
start_label = tk.Label(trim_controls, text="Start:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
start_label.pack(side="left")
self.trim_start = tk.DoubleVar(value=0.0)
start_spinbox = tk.Spinbox(trim_controls, from_=0, to=120, increment=0.1, width=8,
textvariable=self.trim_start, format="%.1f",
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
start_spinbox.pack(side="left", padx=5)
# End time control
end_label = tk.Label(trim_controls, text="End:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
end_label.pack(side="left", padx=(10, 0))
self.trim_end = tk.DoubleVar(value=5.0)
end_spinbox = tk.Spinbox(trim_controls, from_=0, to=120, increment=0.1, width=8,
textvariable=self.trim_end, format="%.1f",
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
end_spinbox.pack(side="left", padx=5)
# Modern trim button
trim_btn = tk.Button(trim_controls, text="✂️ Trim Video",
command=self.trim_video,
font=self.fonts['caption'], bg=self.colors['accent_green'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
trim_btn.pack(side="right", padx=10)
self.add_hover_effect(trim_btn)
# Modern Speed Tool
speed_frame = tk.LabelFrame(basic_frame, text="⚡ Speed Control",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
speed_frame.pack(fill="x", padx=10, pady=5)
speed_controls = tk.Frame(speed_frame, bg=self.colors['bg_tertiary'])
speed_controls.pack(fill="x")
# Speed control
speed_label = tk.Label(speed_controls, text="Speed:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
speed_label.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",
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
speed_spinbox.pack(side="left", padx=5)
# Speed hint
speed_hint = tk.Label(speed_controls, text="(0.5=slow, 1.0=normal, 2.0=fast)",
font=('Segoe UI', 7), bg=self.colors['bg_tertiary'],
fg=self.colors['text_muted'])
speed_hint.pack(side="left", padx=5)
# Modern speed button
speed_btn = tk.Button(speed_controls, text="⚡ Apply Speed",
command=self.adjust_speed,
font=self.fonts['caption'], bg=self.colors['accent_orange'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
speed_btn.pack(side="right", padx=10)
self.add_hover_effect(speed_btn)
# Modern Effects Tab
effects_frame = tk.Frame(notebook, bg=self.colors['bg_secondary'])
notebook.add(effects_frame, text="✨ Effects")
# Modern Fade Effects
fade_frame = tk.LabelFrame(effects_frame, text="🌅 Fade Effects",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
fade_frame.pack(fill="x", padx=10, pady=5)
fade_controls = tk.Frame(fade_frame, bg=self.colors['bg_tertiary'])
fade_controls.pack(fill="x")
# Fade in control
fade_in_label = tk.Label(fade_controls, text="Fade In:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
fade_in_label.pack(side="left")
self.fade_in = tk.DoubleVar(value=0.5)
fade_in_spinbox = tk.Spinbox(fade_controls, from_=0, to=5, increment=0.1, width=6,
textvariable=self.fade_in, format="%.1f",
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
fade_in_spinbox.pack(side="left", padx=5)
# Fade out control
fade_out_label = tk.Label(fade_controls, text="Fade Out:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
fade_out_label.pack(side="left", padx=(10, 0))
self.fade_out = tk.DoubleVar(value=0.5)
fade_out_spinbox = tk.Spinbox(fade_controls, from_=0, to=5, increment=0.1, width=6,
textvariable=self.fade_out, format="%.1f",
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
fade_out_spinbox.pack(side="left", padx=5)
# Modern fade button
fade_btn = tk.Button(fade_controls, text="🌅 Add Fades",
command=self.add_fades,
font=self.fonts['caption'], bg=self.colors['accent_purple'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
fade_btn.pack(side="right", padx=10)
self.add_hover_effect(fade_btn)
# Modern Volume Control
volume_frame = tk.LabelFrame(effects_frame, text="🔊 Volume Control",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
volume_frame.pack(fill="x", padx=10, pady=5)
volume_controls = tk.Frame(volume_frame, bg=self.colors['bg_tertiary'])
volume_controls.pack(fill="x")
# Volume control
volume_label = tk.Label(volume_controls, text="Volume:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
volume_label.pack(side="left")
self.volume_factor = tk.DoubleVar(value=1.0)
volume_spinbox = tk.Spinbox(volume_controls, from_=0, to=3, increment=0.1, width=6,
textvariable=self.volume_factor, format="%.1f",
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
volume_spinbox.pack(side="left", padx=5)
# Volume hint
volume_hint = tk.Label(volume_controls, text="(0.0=mute, 1.0=normal, 2.0=loud)",
font=('Segoe UI', 7), bg=self.colors['bg_tertiary'],
fg=self.colors['text_muted'])
volume_hint.pack(side="left", padx=5)
# Modern volume button
volume_btn = tk.Button(volume_controls, text="🔊 Adjust Volume",
command=self.adjust_volume,
font=self.fonts['caption'], bg=self.colors['accent_blue'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
volume_btn.pack(side="right", padx=10)
self.add_hover_effect(volume_btn)
# Modern Transform Tab
transform_frame = tk.Frame(notebook, bg=self.colors['bg_secondary'])
notebook.add(transform_frame, text="🔄 Transform")
# Modern Resize Tool
resize_frame = tk.LabelFrame(transform_frame, text="📐 Resize Video",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
resize_frame.pack(fill="x", padx=10, pady=5)
resize_controls = tk.Frame(resize_frame, bg=self.colors['bg_tertiary'])
resize_controls.pack(fill="x")
# Width control
width_label = tk.Label(resize_controls, text="Width:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
width_label.pack(side="left")
self.resize_width = tk.IntVar(value=1080)
width_spinbox = tk.Spinbox(resize_controls, from_=240, to=4320, increment=120, width=6,
textvariable=self.resize_width,
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
width_spinbox.pack(side="left", padx=5)
# Height control
height_label = tk.Label(resize_controls, text="Height:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
height_label.pack(side="left", padx=(10, 0))
self.resize_height = tk.IntVar(value=1920)
height_spinbox = tk.Spinbox(resize_controls, from_=240, to=4320, increment=120, width=6,
textvariable=self.resize_height,
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
height_spinbox.pack(side="left", padx=5)
# Modern resize button
resize_btn = tk.Button(resize_controls, text="📐 Resize",
command=self.resize_video,
font=self.fonts['caption'], bg=self.colors['accent_green'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
resize_btn.pack(side="right", padx=10)
self.add_hover_effect(resize_btn)
# Modern Text Overlay Tab
text_frame = tk.Frame(notebook, bg=self.colors['bg_secondary'])
notebook.add(text_frame, text="📝 Text Overlay")
text_overlay_frame = tk.LabelFrame(text_frame, text="📝 Add Text Overlay",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
text_overlay_frame.pack(fill="x", padx=10, pady=5)
# Modern text input
text_input_frame = tk.Frame(text_overlay_frame, bg=self.colors['bg_tertiary'])
text_input_frame.pack(fill="x", pady=5)
text_label = tk.Label(text_input_frame, text="Text:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
text_label.pack(side="left")
self.overlay_text = tk.StringVar(value="Your Text Here")
text_entry = tk.Entry(text_input_frame, textvariable=self.overlay_text, width=30,
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
text_entry.pack(side="left", padx=5)
# Modern text settings
text_settings_frame = tk.Frame(text_overlay_frame, bg=self.colors['bg_tertiary'])
text_settings_frame.pack(fill="x", pady=5)
# Text size control
size_label = tk.Label(text_settings_frame, text="Size:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
size_label.pack(side="left")
self.text_size = tk.IntVar(value=50)
size_spinbox = tk.Spinbox(text_settings_frame, from_=20, to=150, width=6,
textvariable=self.text_size,
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
size_spinbox.pack(side="left", padx=5)
# Text position control
position_label = tk.Label(text_settings_frame, text="Position:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
position_label.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", font=self.fonts['caption'])
position_combo.pack(side="left", padx=5)
# Modern speed/quality options
speed_frame = tk.Frame(text_overlay_frame, bg=self.colors['bg_tertiary'])
speed_frame.pack(fill="x", pady=5)
method_label = tk.Label(speed_frame, text="Processing Method:", font=self.fonts['subheading'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
method_label.pack(side="left")
self.text_method = tk.StringVar(value="fast")
method_frame = tk.Frame(speed_frame, bg=self.colors['bg_tertiary'])
method_frame.pack(side="left", padx=10)
fast_radio = tk.Radiobutton(method_frame, text="🚀 Fast (PIL)", variable=self.text_method,
value="fast", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
selectcolor=self.colors['accent_blue'])
fast_radio.pack(side="left")
quality_radio = tk.Radiobutton(method_frame, text="🎬 High Quality (MoviePy)", variable=self.text_method,
value="quality", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
selectcolor=self.colors['accent_blue'])
quality_radio.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=('Segoe UI', 7), fg=self.colors['text_muted'],
bg=self.colors['bg_tertiary'])
method_info.pack(side="right")
# Modern button frame
button_frame = tk.Frame(text_overlay_frame, bg=self.colors['bg_tertiary'])
button_frame.pack(fill="x", pady=5)
text_btn = tk.Button(button_frame, text="📝 Add Text Overlay",
command=self.add_text_overlay,
font=self.fonts['subheading'], bg=self.colors['accent_orange'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=15, pady=5)
text_btn.pack(side="right", padx=10)
self.add_hover_effect(text_btn)
# Modern Video Effects Tab
effects_advanced_frame = tk.Frame(notebook, bg=self.colors['bg_secondary'])
notebook.add(effects_advanced_frame, text="🎨 Video Effects")
# Modern Blur Effect
blur_frame = tk.LabelFrame(effects_advanced_frame, text="🌫️ Blur Effect",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
blur_frame.pack(fill="x", padx=10, pady=5)
blur_controls = tk.Frame(blur_frame, bg=self.colors['bg_tertiary'])
blur_controls.pack(fill="x")
blur_label = tk.Label(blur_controls, text="Strength:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
blur_label.pack(side="left")
self.blur_strength = tk.DoubleVar(value=2.0)
blur_scale = tk.Scale(blur_controls, from_=0.1, to=10.0, resolution=0.1, orient="horizontal",
variable=self.blur_strength, length=150,
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
troughcolor=self.colors['bg_primary'], highlightthickness=0)
blur_scale.pack(side="left", padx=5)
blur_btn = tk.Button(blur_controls, text="🌫️ Apply Blur",
command=self.apply_blur_effect,
font=self.fonts['caption'], bg=self.colors['accent_orange'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
blur_btn.pack(side="right", padx=10)
self.add_hover_effect(blur_btn)
# Modern Color Effects
color_frame = tk.LabelFrame(effects_advanced_frame, text="🎨 Color Effects",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
color_frame.pack(fill="x", padx=10, pady=5)
color_controls = tk.Frame(color_frame, bg=self.colors['bg_tertiary'])
color_controls.pack(fill="x")
effect_label = tk.Label(color_controls, text="Effect:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
effect_label.pack(side="left")
self.color_effect_var = tk.StringVar(value="sepia")
color_combo = ttk.Combobox(color_controls, textvariable=self.color_effect_var,
values=["sepia", "grayscale", "vintage", "cool"], width=12,
state="readonly", font=self.fonts['caption'])
color_combo.pack(side="left", padx=5)
color_btn = tk.Button(color_controls, text="🎨 Apply Color Effect",
command=self.apply_color_effect,
font=self.fonts['caption'], bg=self.colors['accent_red'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
color_btn.pack(side="right", padx=10)
self.add_hover_effect(color_btn)
# Modern Zoom Effects
zoom_frame = tk.LabelFrame(effects_advanced_frame, text="🔍 Zoom Effects",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
zoom_frame.pack(fill="x", padx=10, pady=5)
zoom_controls = tk.Frame(zoom_frame, bg=self.colors['bg_tertiary'])
zoom_controls.pack(fill="x")
# Zoom type control
zoom_type_label = tk.Label(zoom_controls, text="Type:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
zoom_type_label.pack(side="left")
self.zoom_effect_var = tk.StringVar(value="zoom_in")
zoom_combo = ttk.Combobox(zoom_controls, textvariable=self.zoom_effect_var,
values=["zoom_in", "zoom_out", "static"], width=10,
state="readonly", font=self.fonts['caption'])
zoom_combo.pack(side="left", padx=5)
# Zoom factor control
factor_label = tk.Label(zoom_controls, text="Factor:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
factor_label.pack(side="left", padx=(10, 0))
self.zoom_factor = tk.DoubleVar(value=1.5)
zoom_scale = tk.Scale(zoom_controls, from_=1.0, to=3.0, resolution=0.1, orient="horizontal",
variable=self.zoom_factor, length=100,
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
troughcolor=self.colors['bg_primary'], highlightthickness=0)
zoom_scale.pack(side="left", padx=5)
zoom_btn = tk.Button(zoom_controls, text="🔍 Apply Zoom",
command=self.apply_zoom_effect,
font=self.fonts['caption'], bg=self.colors['accent_blue'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
zoom_btn.pack(side="right", padx=10)
self.add_hover_effect(zoom_btn)
# Modern Rotation Effects
rotation_frame = tk.LabelFrame(effects_advanced_frame, text="🔄 Rotation Effects",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
rotation_frame.pack(fill="x", padx=10, pady=5)
rotation_controls = tk.Frame(rotation_frame, bg=self.colors['bg_tertiary'])
rotation_controls.pack(fill="x")
# Rotation type control
rotation_type_label = tk.Label(rotation_controls, text="Type:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
rotation_type_label.pack(side="left")
self.rotation_type_var = tk.StringVar(value="static")
rotation_combo = ttk.Combobox(rotation_controls, textvariable=self.rotation_type_var,
values=["static", "spinning"], width=10,
state="readonly", font=self.fonts['caption'])
rotation_combo.pack(side="left", padx=5)
# Rotation angle control
angle_label = tk.Label(rotation_controls, text="Angle:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
angle_label.pack(side="left", padx=(10, 0))
self.rotation_angle = tk.DoubleVar(value=0.0)
rotation_scale = tk.Scale(rotation_controls, from_=-180, to=180, resolution=5, orient="horizontal",
variable=self.rotation_angle, length=120,
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
troughcolor=self.colors['bg_primary'], highlightthickness=0)
rotation_scale.pack(side="left", padx=5)
rotation_btn = tk.Button(rotation_controls, text="🔄 Apply Rotation",
command=self.apply_rotation_effect,
font=self.fonts['caption'], bg=self.colors['accent_red'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
rotation_btn.pack(side="right", padx=10)
self.add_hover_effect(rotation_btn)
# Modern Text & Graphics Tab
text_frame = tk.Frame(notebook, bg=self.colors['bg_secondary'])
notebook.add(text_frame, text="📝 Text & Graphics")
# Advanced Text Effects
adv_text_frame = tk.LabelFrame(text_frame, text="✨ Animated Text Effects",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
adv_text_frame.pack(fill="x", padx=10, pady=5)
# Text content
text_content_frame = tk.Frame(adv_text_frame, bg=self.colors['bg_tertiary'])
text_content_frame.pack(fill="x", pady=5)
tk.Label(text_content_frame, text="Text:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left")
self.animated_text = tk.StringVar(value="Your Text Here")
text_entry = tk.Entry(text_content_frame, textvariable=self.animated_text, width=25,
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
text_entry.pack(side="left", padx=5)
# Animation style
anim_style_frame = tk.Frame(adv_text_frame, bg=self.colors['bg_tertiary'])
anim_style_frame.pack(fill="x", pady=3)
tk.Label(anim_style_frame, text="Animation:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left")
self.text_animation = tk.StringVar(value="fade_in")
anim_combo = ttk.Combobox(anim_style_frame, textvariable=self.text_animation,
values=["fade_in", "slide_left", "slide_right", "slide_up", "slide_down",
"zoom_in", "zoom_out", "bounce", "typewriter", "glow", "shake"],
width=15, state="readonly", font=self.fonts['caption'])
anim_combo.pack(side="left", padx=5)
# Text position and style
text_pos_frame = tk.Frame(adv_text_frame, bg=self.colors['bg_tertiary'])
text_pos_frame.pack(fill="x", pady=3)
tk.Label(text_pos_frame, text="Position:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left")
self.text_position = tk.StringVar(value="center")
pos_combo = ttk.Combobox(text_pos_frame, textvariable=self.text_position,
values=["top", "center", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"],
width=12, state="readonly", font=self.fonts['caption'])
pos_combo.pack(side="left", padx=5)
tk.Label(text_pos_frame, text="Font:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left", padx=(10,0))
self.text_font = tk.StringVar(value="Arial-Bold")
font_combo = ttk.Combobox(text_pos_frame, textvariable=self.text_font,
values=["Arial-Bold", "Impact", "Comic-Sans-MS", "Times-New-Roman", "Helvetica"],
width=12, state="readonly", font=self.fonts['caption'])
font_combo.pack(side="left", padx=5)
# Text colors and size
text_style_frame = tk.Frame(adv_text_frame, bg=self.colors['bg_tertiary'])
text_style_frame.pack(fill="x", pady=3)
tk.Label(text_style_frame, text="Color:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left")
self.text_color = tk.StringVar(value="white")
color_combo = ttk.Combobox(text_style_frame, textvariable=self.text_color,
values=["white", "black", "red", "blue", "green", "yellow", "purple", "orange"],
width=8, state="readonly", font=self.fonts['caption'])
color_combo.pack(side="left", padx=5)
tk.Label(text_style_frame, text="Size:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left", padx=(10,0))
self.text_size = tk.IntVar(value=50)
size_spinbox = tk.Spinbox(text_style_frame, from_=20, to=200, increment=5, width=6,
textvariable=self.text_size, font=self.fonts['caption'],
bg=self.colors['bg_primary'], fg=self.colors['text_primary'], bd=0, relief="flat")
size_spinbox.pack(side="left", padx=5)
# Animated text button
anim_text_btn = tk.Button(text_style_frame, text="✨ Add Animated Text",
command=self.add_animated_text_effect,
font=self.fonts['caption'], bg=self.colors['accent_purple'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
anim_text_btn.pack(side="right", padx=10)
self.add_hover_effect(anim_text_btn)
# Timeline Controls for Text
text_time_frame = tk.Frame(adv_text_frame, bg=self.colors['bg_tertiary'])
text_time_frame.pack(fill="x", pady=3)
tk.Label(text_time_frame, text="Start Time:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left")
self.text_start_time = tk.DoubleVar(value=0.0)
start_time_spinbox = tk.Spinbox(text_time_frame, from_=0, to=120, increment=0.1, width=8,
textvariable=self.text_start_time, format="%.1f",
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
start_time_spinbox.pack(side="left", padx=5)
tk.Label(text_time_frame, text="Duration:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left", padx=(10,0))
self.text_duration = tk.DoubleVar(value=2.0)
duration_spinbox = tk.Spinbox(text_time_frame, from_=0.1, to=10, increment=0.1, width=8,
textvariable=self.text_duration, format="%.1f",
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
duration_spinbox.pack(side="left", padx=5)
# Use Current Time Button
current_time_btn = tk.Button(text_time_frame, text="📍 Use Current Time",
command=self.use_current_time_for_text,
font=self.fonts['caption'], bg=self.colors['accent_blue'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=8, pady=2)
current_time_btn.pack(side="right", padx=5)
self.add_hover_effect(current_time_btn)
# Sticker/Emoji Effects
sticker_frame = tk.LabelFrame(text_frame, text="😊 Stickers & Emojis",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
sticker_frame.pack(fill="x", padx=10, pady=5)
sticker_controls = tk.Frame(sticker_frame, bg=self.colors['bg_tertiary'])
sticker_controls.pack(fill="x", pady=5)
tk.Label(sticker_controls, text="Sticker:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left")
self.sticker_type = tk.StringVar(value="😂")
sticker_combo = ttk.Combobox(sticker_controls, textvariable=self.sticker_type,
values=["😂", "😍", "🔥", "💯", "🚀", "", "👍", "💥", "🎉", "🤔", "😱", "💪"],
width=8, state="readonly", font=self.fonts['caption'])
sticker_combo.pack(side="left", padx=5)
tk.Label(sticker_controls, text="Position:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left", padx=(10,0))
self.sticker_position = tk.StringVar(value="top-right")
sticker_pos_combo = ttk.Combobox(sticker_controls, textvariable=self.sticker_position,
values=["top-left", "top-right", "bottom-left", "bottom-right", "center"],
width=10, state="readonly", font=self.fonts['caption'])
sticker_pos_combo.pack(side="left", padx=5)
sticker_btn = tk.Button(sticker_controls, text="😊 Add Sticker",
command=self.add_sticker_effect,
font=self.fonts['caption'], bg=self.colors['accent_orange'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
sticker_btn.pack(side="right", padx=10)
self.add_hover_effect(sticker_btn)
# Timeline Controls for Stickers
sticker_time_frame = tk.Frame(sticker_frame, bg=self.colors['bg_tertiary'])
sticker_time_frame.pack(fill="x", pady=3)
tk.Label(sticker_time_frame, text="Start Time:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left")
self.sticker_start_time = tk.DoubleVar(value=0.0)
sticker_start_spinbox = tk.Spinbox(sticker_time_frame, from_=0, to=120, increment=0.1, width=8,
textvariable=self.sticker_start_time, format="%.1f",
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
sticker_start_spinbox.pack(side="left", padx=5)
tk.Label(sticker_time_frame, text="Duration:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left", padx=(10,0))
self.sticker_duration = tk.DoubleVar(value=1.0)
sticker_duration_spinbox = tk.Spinbox(sticker_time_frame, from_=0.1, to=10, increment=0.1, width=8,
textvariable=self.sticker_duration, format="%.1f",
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
sticker_duration_spinbox.pack(side="left", padx=5)
# Use Current Time Button for stickers
sticker_current_time_btn = tk.Button(sticker_time_frame, text="📍 Use Current Time",
command=self.use_current_time_for_sticker,
font=self.fonts['caption'], bg=self.colors['accent_blue'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=8, pady=2)
sticker_current_time_btn.pack(side="right", padx=5)
self.add_hover_effect(sticker_current_time_btn)
# Modern Color Grading Tab
color_frame = tk.Frame(notebook, bg=self.colors['bg_secondary'])
notebook.add(color_frame, text="🎨 Color Grading")
# Color Presets
preset_frame = tk.LabelFrame(color_frame, text="🎨 Color Presets",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
preset_frame.pack(fill="x", padx=10, pady=5)
preset_controls = tk.Frame(preset_frame, bg=self.colors['bg_tertiary'])
preset_controls.pack(fill="x", pady=5)
tk.Label(preset_controls, text="Preset:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left")
self.color_preset = tk.StringVar(value="cinematic")
preset_combo = ttk.Combobox(preset_controls, textvariable=self.color_preset,
values=["cinematic", "warm", "cool", "vintage", "dramatic", "bright", "moody",
"sunset", "winter", "summer", "cyberpunk", "retro"],
width=12, state="readonly", font=self.fonts['caption'])
preset_combo.pack(side="left", padx=5)
preset_btn = tk.Button(preset_controls, text="🎨 Apply Preset",
command=self.apply_color_preset,
font=self.fonts['caption'], bg=self.colors['accent_purple'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
preset_btn.pack(side="right", padx=10)
self.add_hover_effect(preset_btn)
# Advanced Color Controls
advanced_color_frame = tk.LabelFrame(color_frame, text="⚙️ Advanced Color Controls",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
advanced_color_frame.pack(fill="x", padx=10, pady=5)
# Brightness and Contrast
brightness_frame = tk.Frame(advanced_color_frame, bg=self.colors['bg_tertiary'])
brightness_frame.pack(fill="x", pady=3)
tk.Label(brightness_frame, text="Brightness:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left")
self.brightness = tk.DoubleVar(value=1.0)
brightness_scale = tk.Scale(brightness_frame, from_=0.3, to=2.0, resolution=0.1, orient="horizontal",
variable=self.brightness, length=120, bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'], troughcolor=self.colors['bg_primary'],
highlightthickness=0)
brightness_scale.pack(side="left", padx=5)
tk.Label(brightness_frame, text="Contrast:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left", padx=(10,0))
self.contrast = tk.DoubleVar(value=1.0)
contrast_scale = tk.Scale(brightness_frame, from_=0.3, to=2.0, resolution=0.1, orient="horizontal",
variable=self.contrast, length=120, bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'], troughcolor=self.colors['bg_primary'],
highlightthickness=0)
contrast_scale.pack(side="left", padx=5)
# Saturation and Hue
sat_hue_frame = tk.Frame(advanced_color_frame, bg=self.colors['bg_tertiary'])
sat_hue_frame.pack(fill="x", pady=3)
tk.Label(sat_hue_frame, text="Saturation:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left")
self.saturation = tk.DoubleVar(value=1.0)
sat_scale = tk.Scale(sat_hue_frame, from_=0.0, to=2.0, resolution=0.1, orient="horizontal",
variable=self.saturation, length=120, bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'], troughcolor=self.colors['bg_primary'],
highlightthickness=0)
sat_scale.pack(side="left", padx=5)
tk.Label(sat_hue_frame, text="Hue Shift:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left", padx=(10,0))
self.hue_shift = tk.DoubleVar(value=0.0)
hue_scale = tk.Scale(sat_hue_frame, from_=-180, to=180, resolution=5, orient="horizontal",
variable=self.hue_shift, length=120, bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'], troughcolor=self.colors['bg_primary'],
highlightthickness=0)
hue_scale.pack(side="left", padx=5)
color_btn = tk.Button(advanced_color_frame, text="🎨 Apply Color Grading",
command=self.apply_advanced_color_grading,
font=self.fonts['caption'], bg=self.colors['accent_blue'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
color_btn.pack(pady=5)
self.add_hover_effect(color_btn)
# Modern Platform & Audio Tab
platform_frame = tk.Frame(notebook, bg=self.colors['bg_secondary'])
notebook.add(platform_frame, text="📱 Platform & Audio")
# Platform-Specific Crops
crop_frame = tk.LabelFrame(platform_frame, text="📱 Platform Optimization",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
crop_frame.pack(fill="x", padx=10, pady=5)
crop_controls = tk.Frame(crop_frame, bg=self.colors['bg_tertiary'])
crop_controls.pack(fill="x", pady=5)
tk.Label(crop_controls, text="Platform:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left")
self.platform_preset = tk.StringVar(value="tiktok")
platform_combo = ttk.Combobox(crop_controls, textvariable=self.platform_preset,
values=["tiktok", "instagram_story", "instagram_reel", "youtube_shorts",
"snapchat", "twitter", "facebook_story", "linkedin"],
width=15, state="readonly", font=self.fonts['caption'])
platform_combo.pack(side="left", padx=5)
crop_btn = tk.Button(crop_controls, text="📱 Optimize for Platform",
command=self.apply_platform_crop,
font=self.fonts['caption'], bg=self.colors['accent_green'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
crop_btn.pack(side="right", padx=10)
self.add_hover_effect(crop_btn)
# Audio Visualization
audio_viz_frame = tk.LabelFrame(platform_frame, text="🎵 Audio Visualization",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
audio_viz_frame.pack(fill="x", padx=10, pady=5)
audio_controls = tk.Frame(audio_viz_frame, bg=self.colors['bg_tertiary'])
audio_controls.pack(fill="x", pady=5)
tk.Label(audio_controls, text="Style:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left")
self.audio_viz_style = tk.StringVar(value="bars")
viz_combo = ttk.Combobox(audio_controls, textvariable=self.audio_viz_style,
values=["bars", "waveform", "spectrum", "circular", "pulse", "line"],
width=10, state="readonly", font=self.fonts['caption'])
viz_combo.pack(side="left", padx=5)
tk.Label(audio_controls, text="Position:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left", padx=(10,0))
self.audio_viz_position = tk.StringVar(value="bottom")
viz_pos_combo = ttk.Combobox(audio_controls, textvariable=self.audio_viz_position,
values=["bottom", "top", "left", "right", "center", "overlay"],
width=8, state="readonly", font=self.fonts['caption'])
viz_pos_combo.pack(side="left", padx=5)
audio_viz_btn = tk.Button(audio_controls, text="🎵 Add Audio Visualization",
command=self.add_audio_visualization,
font=self.fonts['caption'], bg=self.colors['accent_orange'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
audio_viz_btn.pack(side="right", padx=10)
self.add_hover_effect(audio_viz_btn)
# Auto-Captions
caption_frame = tk.LabelFrame(platform_frame, text="💬 Auto-Generated Captions",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
caption_frame.pack(fill="x", padx=10, pady=5)
caption_controls = tk.Frame(caption_frame, bg=self.colors['bg_tertiary'])
caption_controls.pack(fill="x", pady=5)
tk.Label(caption_controls, text="Language:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left")
self.caption_language = tk.StringVar(value="en")
lang_combo = ttk.Combobox(caption_controls, textvariable=self.caption_language,
values=["en", "es", "fr", "de", "it", "pt", "ru", "ja", "ko", "zh"],
width=8, state="readonly", font=self.fonts['caption'])
lang_combo.pack(side="left", padx=5)
tk.Label(caption_controls, text="Style:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left", padx=(10,0))
self.caption_style = tk.StringVar(value="modern")
caption_style_combo = ttk.Combobox(caption_controls, textvariable=self.caption_style,
values=["modern", "classic", "bold", "minimal", "neon", "retro"],
width=10, state="readonly", font=self.fonts['caption'])
caption_style_combo.pack(side="left", padx=5)
caption_btn = tk.Button(caption_controls, text="💬 Generate Captions",
command=self.generate_auto_captions,
font=self.fonts['caption'], bg=self.colors['accent_blue'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
caption_btn.pack(side="right", padx=10)
self.add_hover_effect(caption_btn)
# Modern Advanced Effects Tab
advanced_frame = tk.Frame(notebook, bg=self.colors['bg_secondary'])
notebook.add(advanced_frame, text="⚡ Advanced Effects")
# Particle Effects
particle_frame = tk.LabelFrame(advanced_frame, text="✨ Particle Effects",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
particle_frame.pack(fill="x", padx=10, pady=5)
particle_controls = tk.Frame(particle_frame, bg=self.colors['bg_tertiary'])
particle_controls.pack(fill="x", pady=5)
tk.Label(particle_controls, text="Effect:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left")
self.particle_effect = tk.StringVar(value="snow")
particle_combo = ttk.Combobox(particle_controls, textvariable=self.particle_effect,
values=["snow", "rain", "stars", "sparks", "confetti", "bubbles", "fireflies"],
width=10, state="readonly", font=self.fonts['caption'])
particle_combo.pack(side="left", padx=5)
tk.Label(particle_controls, text="Intensity:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left", padx=(10,0))
self.particle_intensity = tk.DoubleVar(value=0.5)
intensity_scale = tk.Scale(particle_controls, from_=0.1, to=1.0, resolution=0.1, orient="horizontal",
variable=self.particle_intensity, length=100, bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'], troughcolor=self.colors['bg_primary'],
highlightthickness=0)
intensity_scale.pack(side="left", padx=5)
particle_btn = tk.Button(particle_controls, text="✨ Add Particles",
command=self.add_particle_effect,
font=self.fonts['caption'], bg=self.colors['accent_purple'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
particle_btn.pack(side="right", padx=10)
self.add_hover_effect(particle_btn)
# Transition Effects
transition_frame = tk.LabelFrame(advanced_frame, text="🔄 Transition Effects",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
transition_frame.pack(fill="x", padx=10, pady=5)
transition_controls = tk.Frame(transition_frame, bg=self.colors['bg_tertiary'])
transition_controls.pack(fill="x", pady=5)
tk.Label(transition_controls, text="Type:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left")
self.transition_type = tk.StringVar(value="crossfade")
transition_combo = ttk.Combobox(transition_controls, textvariable=self.transition_type,
values=["crossfade", "slide", "wipe", "circle", "diamond", "blinds", "pixelate"],
width=10, state="readonly", font=self.fonts['caption'])
transition_combo.pack(side="left", padx=5)
tk.Label(transition_controls, text="Duration:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary']).pack(side="left", padx=(10,0))
self.transition_duration = tk.DoubleVar(value=1.0)
duration_spinbox = tk.Spinbox(transition_controls, from_=0.1, to=3.0, increment=0.1, width=6,
textvariable=self.transition_duration, format="%.1f",
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
duration_spinbox.pack(side="left", padx=5)
transition_btn = tk.Button(transition_controls, text="🔄 Apply Transition",
command=self.apply_transition_effect,
font=self.fonts['caption'], bg=self.colors['accent_red'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
transition_btn.pack(side="right", padx=10)
self.add_hover_effect(transition_btn)
# Professional Timeline Editor Tab
timeline_frame = tk.Frame(notebook, bg=self.colors['bg_secondary'])
notebook.add(timeline_frame, text="🎬 Professional Timeline")
# Initialize timeline editor
self.timeline_editor = TimelineEditor()
# Timeline main container
timeline_main_frame = tk.Frame(timeline_frame, bg=self.colors['bg_secondary'])
timeline_main_frame.pack(fill="both", expand=True, padx=5, pady=5)
# Top controls panel
timeline_controls_panel = tk.LabelFrame(timeline_main_frame, text="🎛️ Timeline Controls",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
timeline_controls_panel.pack(fill="x", pady=(0, 5))
# Playback controls
playback_frame = tk.Frame(timeline_controls_panel, bg=self.colors['bg_tertiary'])
playback_frame.pack(fill="x", pady=3)
play_btn = tk.Button(playback_frame, text="▶️ Play",
command=self.timeline_play,
font=self.fonts['caption'], bg=self.colors['accent_green'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
play_btn.pack(side="left", padx=2)
self.add_hover_effect(play_btn)
pause_btn = tk.Button(playback_frame, text="⏸️ Pause",
command=self.timeline_pause,
font=self.fonts['caption'], bg=self.colors['accent_orange'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
pause_btn.pack(side="left", padx=2)
self.add_hover_effect(pause_btn)
stop_btn = tk.Button(playback_frame, text="⏹️ Stop",
command=self.timeline_stop,
font=self.fonts['caption'], bg=self.colors['accent_red'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
stop_btn.pack(side="left", padx=2)
self.add_hover_effect(stop_btn)
# Timeline position display
self.timeline_position_var = tk.StringVar(value="00:00:00")
position_label = tk.Label(playback_frame, textvariable=self.timeline_position_var,
font=self.fonts['mono'], bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'])
position_label.pack(side="left", padx=20)
# Timeline zoom controls
zoom_frame = tk.Frame(playback_frame, bg=self.colors['bg_tertiary'])
zoom_frame.pack(side="right")
zoom_out_btn = tk.Button(zoom_frame, text="🔍➖",
command=self.timeline_zoom_out,
font=self.fonts['caption'], bg=self.colors['accent_purple'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=8, pady=3)
zoom_out_btn.pack(side="left", padx=1)
self.add_hover_effect(zoom_out_btn)
zoom_in_btn = tk.Button(zoom_frame, text="🔍➕",
command=self.timeline_zoom_in,
font=self.fonts['caption'], bg=self.colors['accent_purple'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=8, pady=3)
zoom_in_btn.pack(side="left", padx=1)
self.add_hover_effect(zoom_in_btn)
# Editing tools
tools_frame = tk.Frame(timeline_controls_panel, bg=self.colors['bg_tertiary'])
tools_frame.pack(fill="x", pady=3)
cut_btn = tk.Button(tools_frame, text="✂️ Cut",
command=self.timeline_cut,
font=self.fonts['caption'], bg=self.colors['accent_blue'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
cut_btn.pack(side="left", padx=2)
self.add_hover_effect(cut_btn)
copy_btn = tk.Button(tools_frame, text="📋 Copy",
command=self.timeline_copy,
font=self.fonts['caption'], bg=self.colors['accent_blue'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
copy_btn.pack(side="left", padx=2)
self.add_hover_effect(copy_btn)
paste_btn = tk.Button(tools_frame, text="📌 Paste",
command=self.timeline_paste,
font=self.fonts['caption'], bg=self.colors['accent_blue'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
paste_btn.pack(side="left", padx=2)
self.add_hover_effect(paste_btn)
delete_btn = tk.Button(tools_frame, text="🗑️ Delete",
command=self.timeline_delete,
font=self.fonts['caption'], bg=self.colors['accent_red'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
delete_btn.pack(side="left", padx=2)
self.add_hover_effect(delete_btn)
# Undo/Redo
undo_redo_frame = tk.Frame(tools_frame, bg=self.colors['bg_tertiary'])
undo_redo_frame.pack(side="right")
undo_btn = tk.Button(undo_redo_frame, text="↶ Undo",
command=self.timeline_undo,
font=self.fonts['caption'], bg=self.colors['accent_orange'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
undo_btn.pack(side="left", padx=2)
self.add_hover_effect(undo_btn)
redo_btn = tk.Button(undo_redo_frame, text="↷ Redo",
command=self.timeline_redo,
font=self.fonts['caption'], bg=self.colors['accent_orange'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
redo_btn.pack(side="left", padx=2)
self.add_hover_effect(redo_btn)
# Timeline workspace
timeline_workspace = tk.LabelFrame(timeline_main_frame, text="<EFBFBD> Timeline Workspace",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=5, pady=5)
timeline_workspace.pack(fill="both", expand=True)
# Timeline canvas with scrollbars
timeline_canvas_frame = tk.Frame(timeline_workspace, bg=self.colors['bg_tertiary'])
timeline_canvas_frame.pack(fill="both", expand=True)
# Horizontal scrollbar
timeline_h_scroll = ttk.Scrollbar(timeline_canvas_frame, orient="horizontal")
timeline_h_scroll.pack(side="bottom", fill="x")
# Vertical scrollbar
timeline_v_scroll = ttk.Scrollbar(timeline_canvas_frame, orient="vertical")
timeline_v_scroll.pack(side="right", fill="y")
# Main timeline canvas
self.timeline_canvas = tk.Canvas(timeline_canvas_frame,
bg=self.colors['bg_primary'],
highlightthickness=0,
xscrollcommand=timeline_h_scroll.set,
yscrollcommand=timeline_v_scroll.set)
self.timeline_canvas.pack(side="left", fill="both", expand=True)
# Configure scrollbars
timeline_h_scroll.config(command=self.timeline_canvas.xview)
timeline_v_scroll.config(command=self.timeline_canvas.yview)
# Timeline tracks panel
tracks_panel = tk.LabelFrame(timeline_main_frame, text="🎛️ Track Controls",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
tracks_panel.pack(fill="x", pady=(5, 0))
tracks_control_frame = tk.Frame(tracks_panel, bg=self.colors['bg_tertiary'])
tracks_control_frame.pack(fill="x", pady=3)
# Track management buttons
add_video_track_btn = tk.Button(tracks_control_frame, text="📹 Add Video Track",
command=self.add_video_track,
font=self.fonts['caption'], bg=self.colors['accent_green'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
add_video_track_btn.pack(side="left", padx=2)
self.add_hover_effect(add_video_track_btn)
add_audio_track_btn = tk.Button(tracks_control_frame, text="🎵 Add Audio Track",
command=self.add_audio_track,
font=self.fonts['caption'], bg=self.colors['accent_blue'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
add_audio_track_btn.pack(side="left", padx=2)
self.add_hover_effect(add_audio_track_btn)
add_text_track_btn = tk.Button(tracks_control_frame, text="📝 Add Text Track",
command=self.add_text_track,
font=self.fonts['caption'], bg=self.colors['accent_purple'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
add_text_track_btn.pack(side="left", padx=2)
self.add_hover_effect(add_text_track_btn)
# Media browser
media_browser_btn = tk.Button(tracks_control_frame, text="📁 Media Browser",
command=self.open_media_browser,
font=self.fonts['caption'], bg=self.colors['accent_orange'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=10, pady=3)
media_browser_btn.pack(side="right", padx=2)
self.add_hover_effect(media_browser_btn)
# Timeline export controls
export_timeline_btn = tk.Button(tracks_control_frame, text="💾 Export Timeline",
command=self.export_timeline_video,
font=self.fonts['caption'], bg=self.colors['accent_red'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=15, pady=3)
export_timeline_btn.pack(side="right", padx=5)
self.add_hover_effect(export_timeline_btn)
# Initialize timeline
self.init_timeline_canvas()
self.timeline_selected_clips = []
self.timeline_playhead_pos = 0
self.timeline_scale = 50 # Pixels per second
self.timeline_tracks_height = 60 # Height per track
# Bind timeline events
self.timeline_canvas.bind("<Button-1>", self.timeline_click)
self.timeline_canvas.bind("<B1-Motion>", self.timeline_drag)
self.timeline_canvas.bind("<ButtonRelease-1>", self.timeline_release)
self.timeline_canvas.bind("<Double-Button-1>", self.timeline_double_click)
self.timeline_canvas.bind("<Button-3>", self.timeline_right_click)
# Modern Export Tab
export_frame = tk.Frame(notebook, bg=self.colors['bg_secondary'])
notebook.add(export_frame, text="💾 Export")
export_controls_frame = tk.LabelFrame(export_frame, text="💾 Export Final Video",
font=self.fonts['body'],
bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'],
bd=1, relief="flat",
padx=10, pady=5)
export_controls_frame.pack(fill="x", padx=10, pady=5)
# Modern output filename
filename_frame = tk.Frame(export_controls_frame, bg=self.colors['bg_tertiary'])
filename_frame.pack(fill="x", pady=5)
filename_label = tk.Label(filename_frame, text="Filename:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
filename_label.pack(side="left")
self.output_filename = tk.StringVar(value="edited_video.mp4")
filename_entry = tk.Entry(filename_frame, textvariable=self.output_filename, width=25,
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'], bd=0, relief="flat")
filename_entry.pack(side="left", padx=5)
# Modern quality settings
quality_frame = tk.Frame(export_controls_frame, bg=self.colors['bg_tertiary'])
quality_frame.pack(fill="x", pady=5)
quality_label = tk.Label(quality_frame, text="Quality:", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
quality_label.pack(side="left")
self.export_quality = tk.StringVar(value="medium")
quality_combo = ttk.Combobox(quality_frame, textvariable=self.export_quality,
values=["low", "medium", "high"], width=10,
state="readonly", font=self.fonts['caption'])
quality_combo.pack(side="left", padx=5)
# Modern export button frame
export_button_frame = tk.Frame(export_controls_frame, bg=self.colors['bg_tertiary'])
export_button_frame.pack(fill="x", pady=10)
self.export_button = tk.Button(export_button_frame, text="💾 Export Final Video",
command=self.export_edited_video,
bg=self.colors['accent_green'], fg=self.colors['text_primary'],
font=self.fonts['subheading'], bd=0, relief="flat",
padx=15, pady=8)
self.export_button.pack(pady=5)
self.add_hover_effect(self.export_button)
# Progress bar (initially hidden)
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(export_button_frame, variable=self.progress_var, maximum=100)
self.progress_label = tk.Label(export_button_frame, text="", font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
# Reset button
reset_btn = tk.Button(export_button_frame, text="🔄 Reset All Changes", command=self.reset_edited_video,
bg=self.colors['accent_red'], fg=self.colors['text_primary'],
font=self.fonts['caption'], bd=0, relief="flat",
padx=15, pady=5)
reset_btn.pack(pady=5)
self.add_hover_effect(reset_btn)
# Initially disable all tools
self.disable_editing_tools()
def disable_editing_tools(self):
"""Disable all editing tools until a video is selected"""
# Only disable functionality, keep visual appearance normal
for widget in self.tools_frame.winfo_children():
self.set_widget_disabled_state(widget, True)
def enable_editing_tools(self):
"""Enable editing tools when a video is selected"""
for widget in self.tools_frame.winfo_children():
self.set_widget_disabled_state(widget, False)
# Initialize video editor for current video
try:
self.video_editor = VideoEditor(self.current_video)
print(f"✅ Video editor initialized for: {os.path.basename(self.current_video)}")
except Exception as e:
print(f"❌ Error initializing video editor: {e}")
self.video_editor = None
# 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_disabled_state(self, widget, disabled):
"""Set widget disabled state while preserving appearance"""
try:
widget_class = widget.winfo_class()
if widget_class == 'Button':
if disabled:
# Store original command and disable it, but keep colors
if not hasattr(widget, '_original_command'):
widget._original_command = widget.cget('command')
# Disable the command but keep the visual appearance
widget.config(command=lambda: messagebox.showwarning("Select Video", "Please select a video first!"))
else:
# Restore original command
if hasattr(widget, '_original_command'):
widget.config(command=widget._original_command)
elif widget_class in ['Spinbox', 'Entry', 'Scale']:
widget.config(state='disabled' if disabled else 'normal')
elif widget_class == 'TCombobox':
widget.config(state='disabled' if disabled else 'readonly')
except:
pass
# Recursively handle child widgets
for child in widget.winfo_children():
self.set_widget_disabled_state(child, disabled)
def apply_blur_effect(self):
"""Apply blur effect to video"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showerror("Error", "Please select a video first!")
return
strength = self.blur_strength.get()
print(f"🌫️ Applying blur effect (strength: {strength})")
try:
self.video_editor.add_blur_effect(strength)
self.refresh_video_preview()
messagebox.showinfo("Success", f"Blur effect applied with strength {strength}")
except Exception as e:
print(f"❌ Error applying blur effect: {e}")
messagebox.showerror("Blur Error", f"Failed to apply blur effect:\n{str(e)}")
def apply_color_effect(self):
"""Apply color effect to video"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showerror("Error", "Please select a video first!")
return
effect_type = self.color_effect_var.get()
print(f"🎨 Applying color effect: {effect_type}")
try:
self.video_editor.add_color_effect(effect_type)
self.refresh_video_preview()
messagebox.showinfo("Success", f"Color effect '{effect_type}' applied successfully")
except Exception as e:
print(f"❌ Error applying color effect: {e}")
messagebox.showerror("Color Effect Error", f"Failed to apply color effect:\n{str(e)}")
def apply_zoom_effect(self):
"""Apply zoom effect to video"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showerror("Error", "Please select a video first!")
return
zoom_type = self.zoom_effect_var.get()
zoom_factor = self.zoom_factor.get()
print(f"🔍 Applying zoom effect: {zoom_type} (factor: {zoom_factor})")
try:
self.video_editor.add_zoom_effect(zoom_factor, zoom_type)
self.refresh_video_preview()
messagebox.showinfo("Success", f"Zoom effect '{zoom_type}' applied successfully")
except Exception as e:
print(f"❌ Error applying zoom effect: {e}")
messagebox.showerror("Zoom Effect Error", f"Failed to apply zoom effect:\n{str(e)}")
def apply_rotation_effect(self):
"""Apply rotation effect to video"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showerror("Error", "Please select a video first!")
return
rotation_type = self.rotation_type_var.get()
angle = self.rotation_angle.get()
print(f"🔄 Applying rotation effect: {rotation_type} (angle: {angle}°)")
try:
self.video_editor.add_rotation_effect(angle, rotation_type)
self.refresh_video_preview()
messagebox.showinfo("Success", f"Rotation effect '{rotation_type}' applied successfully")
except Exception as e:
print(f"❌ Error applying rotation effect: {e}")
messagebox.showerror("Rotation Effect Error", f"Failed to apply rotation effect:\n{str(e)}")
def refresh_video_preview(self):
"""Refresh the video preview after applying effects"""
if hasattr(self, 'current_time') and hasattr(self, 'video_editor') and self.video_editor:
try:
# Update the current clip reference to include effects
if self.video_editor.video_clip:
self.current_clip = self.video_editor.video_clip
# Update video duration in case it changed (speed/trim effects)
old_duration = self.video_duration
self.video_duration = self.current_clip.duration
# Update timeline if duration changed
if abs(old_duration - self.video_duration) > 0.1:
self.timeline_slider.config(to=self.video_duration)
# Adjust current time if it's beyond new duration
if self.current_time > self.video_duration:
self.current_time = max(0, self.video_duration - 0.1)
self.timeline_var.set(self.current_time)
print(f"📏 Updated timeline duration: {old_duration:.1f}s → {self.video_duration:.1f}s")
self.display_frame_at_time(self.current_time)
self.update_time_display()
print("🔄 Video preview refreshed with effects")
except Exception as e:
print(f"⚠️ Error refreshing preview: {e}")
def use_current_time_for_text(self):
"""Set text start time to current timeline position"""
if hasattr(self, 'current_time'):
self.text_start_time.set(self.current_time)
print(f"📍 Text start time set to {self.current_time:.1f}s")
def use_current_time_for_sticker(self):
"""Set sticker start time to current timeline position"""
if hasattr(self, 'current_time'):
self.sticker_start_time.set(self.current_time)
print(f"📍 Sticker start time set to {self.current_time:.1f}s")
def add_animated_text_effect(self):
"""Add animated text effect to video at specific time and position"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showerror("Error", "Please select a video first!")
return
text = self.animated_text.get()
animation = self.text_animation.get()
position = self.text_position.get()
font = self.text_font.get()
color = self.text_color.get()
size = self.text_size.get()
start_time = self.text_start_time.get()
duration = self.text_duration.get()
if not text.strip():
messagebox.showwarning("Warning", "Please enter text to display")
return
# Validate timing
if hasattr(self, 'video_duration') and start_time >= self.video_duration:
messagebox.showwarning("Warning", f"Start time ({start_time:.1f}s) is beyond video duration ({self.video_duration:.1f}s)")
return
print(f"✨ Adding animated text: '{text}' at {start_time:.1f}s for {duration:.1f}s")
try:
# Convert position to coordinate tuple
position_map = {
'top': ('center', 0.1),
'center': ('center', 'center'),
'bottom': ('center', 0.85),
'top-left': (0.1, 0.1),
'top-right': (0.9, 0.1),
'bottom-left': (0.1, 0.85),
'bottom-right': (0.9, 0.85)
}
pos_coords = position_map.get(position, ('center', 0.85))
# Add timeline effect
effect = self.video_editor.add_timeline_effect(
effect_type='text',
start_time=start_time,
duration=duration,
position=pos_coords,
text=text,
animation=animation,
font=font,
color=color,
font_size=size
)
# Refresh preview to show effect
self.refresh_video_preview()
messagebox.showinfo("Effect Added!",
f"Animated text '{text}' added at {start_time:.1f}s\n"
f"Duration: {duration:.1f}s, Animation: {animation}")
except Exception as e:
print(f"❌ Error adding animated text: {e}")
messagebox.showerror("Animated Text Error", f"Failed to add animated text:\n{str(e)}")
def add_sticker_effect(self):
"""Add sticker/emoji effect to video at specific time and position"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showerror("Error", "Please select a video first!")
return
sticker = self.sticker_type.get()
position = self.sticker_position.get()
start_time = self.sticker_start_time.get()
duration = self.sticker_duration.get()
# Validate timing
if hasattr(self, 'video_duration') and start_time >= self.video_duration:
messagebox.showwarning("Warning", f"Start time ({start_time:.1f}s) is beyond video duration ({self.video_duration:.1f}s)")
return
print(f"😊 Adding sticker: {sticker} at {position} ({start_time:.1f}s for {duration:.1f}s)")
try:
# Convert position to coordinate tuple
position_map = {
'top-left': (0.1, 0.1),
'top-right': (0.9, 0.1),
'bottom-left': (0.1, 0.9),
'bottom-right': (0.9, 0.9),
'center': ('center', 'center')
}
pos_coords = position_map.get(position, (0.9, 0.1))
# Add timeline effect
effect = self.video_editor.add_timeline_effect(
effect_type='sticker',
start_time=start_time,
duration=duration,
position=pos_coords,
sticker=sticker,
size=80
)
# Refresh preview to show effect
self.refresh_video_preview()
messagebox.showinfo("Effect Added!",
f"Sticker '{sticker}' added at {start_time:.1f}s\n"
f"Duration: {duration:.1f}s, Position: {position}")
except Exception as e:
print(f"❌ Error adding sticker: {e}")
messagebox.showerror("Sticker Error", f"Failed to add sticker:\n{str(e)}")
def apply_color_preset(self):
"""Apply color grading preset to video at specific time range"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showerror("Error", "Please select a video first!")
return
preset = self.color_preset.get()
# For color effects, apply to current time or entire video
start_time = getattr(self, 'current_time', 0.0)
duration = 2.0 # Default 2 second effect
print(f"🎨 Applying color preset: {preset} at {start_time:.1f}s")
try:
# Add timeline effect
effect = self.video_editor.add_timeline_effect(
effect_type='color_preset',
start_time=start_time,
duration=duration,
preset=preset
)
# Refresh preview to show effect
self.refresh_video_preview()
messagebox.showinfo("Effect Applied!",
f"Color preset '{preset}' applied at {start_time:.1f}s for {duration:.1f}s")
except Exception as e:
print(f"❌ Error applying color preset: {e}")
messagebox.showerror("Color Preset Error", f"Failed to apply color preset:\n{str(e)}")
def refresh_effects_timeline(self):
"""Refresh the effects timeline display"""
self.update_effects_timeline_display()
def clear_all_timeline_effects(self):
"""Clear all timeline effects"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showwarning("Warning", "No video loaded")
return
if not self.video_editor.timeline_effects:
messagebox.showinfo("Info", "No effects to clear")
return
result = messagebox.askyesno("Confirm Clear",
f"Are you sure you want to remove all {len(self.video_editor.timeline_effects)} effects?")
if result:
self.video_editor.clear_timeline_effects()
self.update_effects_timeline_display()
self.refresh_video_preview()
messagebox.showinfo("Success", "All effects cleared!")
def preview_all_effects(self):
"""Preview the video with all timeline effects applied"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showwarning("Warning", "No video loaded")
return
if not self.video_editor.timeline_effects:
messagebox.showinfo("Info", "No effects to preview")
return
try:
# Apply all timeline effects and update preview
processed_clip = self.video_editor.apply_timeline_effects()
# Update the current clip for preview
self.current_clip = processed_clip
# Refresh preview at current time
self.display_frame_at_time(self.current_time)
messagebox.showinfo("Preview Ready",
f"Preview updated with {len(self.video_editor.timeline_effects)} effects!\n"
f"Use the timeline to scrub through the video.")
except Exception as e:
print(f"❌ Error previewing effects: {e}")
messagebox.showerror("Preview Error", f"Failed to preview effects:\n{str(e)}")
def update_effects_timeline_display(self):
"""Update the visual display of effects timeline"""
# Clear existing widgets
for widget in self.effects_scrollable_frame.winfo_children():
widget.destroy()
if not hasattr(self, 'video_editor') or not self.video_editor or not self.video_editor.timeline_effects:
# Show empty state
empty_label = tk.Label(self.effects_scrollable_frame,
text="No effects added yet.\n\nAdd effects using the tabs above and they will appear here.",
font=self.fonts['body'], bg=self.colors['bg_primary'],
fg=self.colors['text_muted'], justify='center')
empty_label.pack(expand=True, fill='both', pady=50)
return
# Sort effects by start time
sorted_effects = sorted(self.video_editor.timeline_effects, key=lambda x: x['start_time'])
# Display each effect
for i, effect in enumerate(sorted_effects):
self._create_effect_display_widget(effect, i)
# Update scroll region
self.effects_scrollable_frame.update_idletasks()
self.effects_canvas.configure(scrollregion=self.effects_canvas.bbox("all"))
def _create_effect_display_widget(self, effect, index):
"""Create a widget to display a single effect in the timeline"""
# Effect container
effect_frame = tk.Frame(self.effects_scrollable_frame, bg=self.colors['bg_tertiary'],
relief="flat", bd=1, pady=5, padx=5)
effect_frame.pack(fill="x", padx=5, pady=2)
# Effect info frame
info_frame = tk.Frame(effect_frame, bg=self.colors['bg_tertiary'])
info_frame.pack(fill="x")
# Effect type and icon
effect_icons = {
'text': '📝', 'sticker': '😊', 'color_preset': '🎨',
'color_grading': '🎨', 'particles': '', 'zoom': '🔍',
'rotation': '🔄', 'blur': '🌫️'
}
icon = effect_icons.get(effect['type'], '')
type_label = tk.Label(info_frame, text=f"{icon} {effect['type'].replace('_', ' ').title()}",
font=self.fonts['body'], bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'])
type_label.pack(side="left")
# Timing info
start_time = effect['start_time']
end_time = effect['end_time']
duration = end_time - start_time
time_label = tk.Label(info_frame, text=f"{start_time:.1f}s - {end_time:.1f}s ({duration:.1f}s)",
font=self.fonts['caption'], bg=self.colors['bg_tertiary'],
fg=self.colors['text_secondary'])
time_label.pack(side="left", padx=(10, 0))
# Effect parameters (abbreviated)
params_text = self._get_effect_params_summary(effect)
if params_text:
params_label = tk.Label(info_frame, text=params_text,
font=self.fonts['caption'], bg=self.colors['bg_tertiary'],
fg=self.colors['text_muted'])
params_label.pack(side="left", padx=(10, 0))
# Control buttons frame
controls_frame = tk.Frame(info_frame, bg=self.colors['bg_tertiary'])
controls_frame.pack(side="right")
# Jump to effect button
jump_btn = tk.Button(controls_frame, text="📍",
command=lambda: self.jump_to_effect_time(start_time),
font=self.fonts['caption'], bg=self.colors['accent_blue'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=5, pady=1)
jump_btn.pack(side="left", padx=2)
self.add_hover_effect(jump_btn)
# Remove effect button
remove_btn = tk.Button(controls_frame, text="🗑️",
command=lambda: self.remove_timeline_effect(effect),
font=self.fonts['caption'], bg=self.colors['accent_red'],
fg=self.colors['text_primary'], bd=0, relief="flat",
padx=5, pady=1)
remove_btn.pack(side="left", padx=2)
self.add_hover_effect(remove_btn)
def _get_effect_params_summary(self, effect):
"""Get a brief summary of effect parameters"""
effect_type = effect['type']
params = effect['params']
if effect_type == 'text':
text = params.get('text', '')[:20]
if len(params.get('text', '')) > 20:
text += '...'
return f"'{text}'"
elif effect_type == 'sticker':
return f"{params.get('sticker', '😊')}"
elif effect_type == 'color_preset':
return f"{params.get('preset', 'none')}"
elif effect_type == 'zoom':
return f"{params.get('zoom_factor', 1.0)}x {params.get('zoom_type', 'static')}"
else:
return ""
def jump_to_effect_time(self, time):
"""Jump to specific time in the timeline"""
if hasattr(self, 'timeline_var') and hasattr(self, 'timeline_slider'):
self.timeline_var.set(time)
self.current_time = time
self.display_frame_at_time(time)
self.update_time_display()
print(f"📍 Jumped to {time:.1f}s")
def remove_timeline_effect(self, effect):
"""Remove a specific timeline effect"""
if hasattr(self, 'video_editor') and self.video_editor:
self.video_editor.remove_timeline_effect(effect)
self.update_effects_timeline_display()
self.refresh_video_preview()
print(f"🗑️ Removed {effect['type']} effect")
def apply_advanced_color_grading(self):
"""Apply advanced color grading with individual controls"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showerror("Error", "Please select a video first!")
return
brightness = self.brightness.get()
contrast = self.contrast.get()
saturation = self.saturation.get()
hue_shift = self.hue_shift.get()
print(f"🎨 Applying color grading: Brightness={brightness}, Contrast={contrast}, "
f"Saturation={saturation}, Hue={hue_shift}")
try:
# For now, we'll show a placeholder implementation
messagebox.showinfo("Feature Applied!",
f"Advanced color grading applied:\n"
f"Brightness: {brightness}\nContrast: {contrast}\n"
f"Saturation: {saturation}\nHue Shift: {hue_shift}°")
self.refresh_video_preview()
except Exception as e:
print(f"❌ Error applying color grading: {e}")
messagebox.showerror("Color Grading Error", f"Failed to apply color grading:\n{str(e)}")
def apply_platform_crop(self):
"""Apply platform-specific crop and aspect ratio"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showerror("Error", "Please select a video first!")
return
platform = self.platform_preset.get()
print(f"📱 Optimizing for platform: {platform}")
# Platform aspect ratios
platform_specs = {
"tiktok": "9:16 (1080x1920)",
"instagram_story": "9:16 (1080x1920)",
"instagram_reel": "9:16 (1080x1920)",
"youtube_shorts": "9:16 (1080x1920)",
"snapchat": "9:16 (1080x1920)",
"twitter": "16:9 (1920x1080)",
"facebook_story": "9:16 (1080x1920)",
"linkedin": "1:1 (1080x1080)"
}
try:
spec = platform_specs.get(platform, "9:16 (1080x1920)")
messagebox.showinfo("Feature Applied!",
f"Video optimized for {platform.replace('_', ' ').title()}\n"
f"Aspect ratio: {spec}")
self.refresh_video_preview()
except Exception as e:
print(f"❌ Error applying platform crop: {e}")
messagebox.showerror("Platform Crop Error", f"Failed to apply platform crop:\n{str(e)}")
def add_audio_visualization(self):
"""Add audio visualization effect"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showerror("Error", "Please select a video first!")
return
style = self.audio_viz_style.get()
position = self.audio_viz_position.get()
print(f"🎵 Adding audio visualization: {style} at {position}")
try:
# For now, we'll show a placeholder implementation
messagebox.showinfo("Feature Added!",
f"Audio visualization '{style}' would be added at '{position}' position")
except Exception as e:
print(f"❌ Error adding audio visualization: {e}")
messagebox.showerror("Audio Viz Error", f"Failed to add audio visualization:\n{str(e)}")
def generate_auto_captions(self):
"""Generate automatic captions for the video"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showerror("Error", "Please select a video first!")
return
language = self.caption_language.get()
style = self.caption_style.get()
print(f"💬 Generating auto-captions: {language} language, {style} style")
try:
# For now, we'll show a placeholder implementation
messagebox.showinfo("Feature Processing!",
f"Auto-captions would be generated in {language} language "
f"with {style} styling.\n\nThis feature uses AI speech recognition.")
except Exception as e:
print(f"❌ Error generating captions: {e}")
messagebox.showerror("Caption Error", f"Failed to generate captions:\n{str(e)}")
def add_particle_effect(self):
"""Add particle effects to video"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showerror("Error", "Please select a video first!")
return
effect = self.particle_effect.get()
intensity = self.particle_intensity.get()
print(f"✨ Adding particle effect: {effect} with intensity {intensity}")
try:
# For now, we'll show a placeholder implementation
messagebox.showinfo("Feature Added!",
f"Particle effect '{effect}' would be added with {intensity:.1f} intensity")
except Exception as e:
print(f"❌ Error adding particle effect: {e}")
messagebox.showerror("Particle Error", f"Failed to add particle effect:\n{str(e)}")
def apply_transition_effect(self):
"""Apply transition effects"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showerror("Error", "Please select a video first!")
return
transition_type = self.transition_type.get()
duration = self.transition_duration.get()
print(f"🔄 Applying transition: {transition_type} for {duration}s")
try:
# For now, we'll show a placeholder implementation
messagebox.showinfo("Feature Applied!",
f"Transition '{transition_type}' would be applied with {duration}s duration")
except Exception as e:
print(f"❌ Error applying transition: {e}")
messagebox.showerror("Transition Error", f"Failed to apply transition:\n{str(e)}")
def export_edited_video(self):
"""Export the final edited video with all timeline effects applied"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showerror("Error", "No video selected for editing!")
return
filename = self.output_filename.get()
if not filename.endswith('.mp4'):
filename += '.mp4'
output_path = os.path.join(self.output_folder.get(), filename)
quality = self.export_quality.get()
# Check if there are timeline effects to apply
effects_count = len(self.video_editor.timeline_effects) if hasattr(self.video_editor, 'timeline_effects') else 0
print(f"💾 Exporting edited video with {effects_count} timeline effects to: {output_path}")
def export_thread():
try:
# Show progress bar
self.progress_bar.pack(pady=5)
self.progress_label.pack()
self.export_button.config(state="disabled", text="Processing Effects...")
# Progress callback
def progress_callback(progress, status="Exporting"):
self.progress_var.set(progress * 100)
self.progress_label.config(text=f"{status}... {progress*100:.1f}%")
self.editor_window.update_idletasks()
# Create output directory if needed
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# Apply all timeline effects if any exist
if effects_count > 0:
progress_callback(0.1, "Applying timeline effects")
# Apply timeline effects to create final processed clip
final_clip = self.video_editor.apply_timeline_effects()
progress_callback(0.3, "Effects applied, preparing export")
# Temporarily store the processed clip
original_clip = self.video_editor.video_clip
self.video_editor.video_clip = final_clip
try:
# Export the processed video
self.export_button.config(text="Exporting Video...")
progress_callback(0.4, "Exporting video file")
self.video_editor.export(output_path, quality, lambda p: progress_callback(0.4 + p * 0.6))
# Restore original clip
self.video_editor.video_clip = original_clip
final_clip.close()
except Exception as e:
# Restore original clip on error
self.video_editor.video_clip = original_clip
if 'final_clip' in locals():
final_clip.close()
raise e
else:
# No timeline effects, export original with basic effects only
progress_callback(0.2, "Exporting video")
self.video_editor.export(output_path, quality, lambda p: progress_callback(0.2 + p * 0.8))
# Hide progress bar
self.progress_bar.pack_forget()
self.progress_label.pack_forget()
self.export_button.config(state="normal", text="💾 Export Final Video")
success_message = f"Video exported successfully to:\n{output_path}"
if effects_count > 0:
success_message += f"\n\nApplied {effects_count} timeline effects"
messagebox.showinfo("Export Complete", success_message)
print(f"✅ Video exported successfully: {output_path}")
except Exception as e:
print(f"❌ Export error: {e}")
self.progress_bar.pack_forget()
self.progress_label.pack_forget()
self.export_button.config(state="normal", text="💾 Export Final Video")
messagebox.showerror("Export Error", f"Failed to export video:\n{str(e)}")
# Confirm export if there are many effects
if effects_count > 5:
result = messagebox.askyesno("Confirm Export",
f"You have {effects_count} timeline effects.\n"
f"Processing may take several minutes.\n\nContinue with export?")
if not result:
return
# Run export in background thread
threading.Thread(target=export_thread, daemon=True).start()
def reset_edited_video(self):
"""Reset all edits and reload original video"""
if hasattr(self, 'video_editor') and self.video_editor:
self.video_editor.reset()
self.refresh_video_preview()
messagebox.showinfo("Reset", "All edits have been reset to original video")
print("🔄 Video reset to original state")
else:
messagebox.showwarning("No Video", "No video loaded to reset!")
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 Exception as e:
# Silently fail - no need to show dialog for folder opening issues
print(f"Could not open folder: {e}")
pass
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):
"""Apply trim to the current video editor"""
if not hasattr(self, 'video_editor') or not self.video_editor:
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
try:
self.video_editor.apply_trim(start, end)
self.refresh_video_preview()
messagebox.showinfo("Success", f"Video trimmed from {start:.1f}s to {end:.1f}s")
except Exception as e:
print(f"❌ Error applying trim: {e}")
messagebox.showerror("Trim Error", f"Failed to trim video:\n{str(e)}")
def adjust_speed(self):
"""Apply speed adjustment to the current video editor"""
if not hasattr(self, 'video_editor') or not self.video_editor:
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
try:
self.video_editor.apply_speed(speed)
self.refresh_video_preview()
messagebox.showinfo("Success", f"Speed adjusted to {speed:.1f}x")
except Exception as e:
print(f"❌ Error applying speed: {e}")
messagebox.showerror("Speed Error", f"Failed to adjust speed:\n{str(e)}")
def add_fades(self):
"""Apply fade effects to the current video editor"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showwarning("No Video", "Please select a video first!")
return
fade_in = self.fade_in.get()
fade_out = self.fade_out.get()
try:
self.video_editor.apply_fade_effects(fade_in, fade_out)
self.refresh_video_preview()
messagebox.showinfo("Success", f"Fade effects applied: in {fade_in:.1f}s, out {fade_out:.1f}s")
except Exception as e:
print(f"❌ Error applying fades: {e}")
messagebox.showerror("Fade Error", f"Failed to apply fade effects:\n{str(e)}")
def adjust_volume(self):
"""Apply volume adjustment to the current video editor"""
if not hasattr(self, 'video_editor') or not self.video_editor:
messagebox.showwarning("No Video", "Please select a video first!")
return
volume = self.volume_factor.get()
try:
self.video_editor.apply_volume(volume)
self.refresh_video_preview()
messagebox.showinfo("Success", f"Volume adjusted to {volume:.1f}x")
except Exception as e:
print(f"❌ Error applying volume: {e}")
messagebox.showerror("Volume Error", f"Failed to adjust volume:\n{str(e)}")
def resize_video(self):
"""Apply resize to the current video editor"""
if not hasattr(self, 'video_editor') or not self.video_editor:
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
try:
self.video_editor.apply_resize(width, height)
self.refresh_video_preview()
messagebox.showinfo("Success", f"Video resized to {width}x{height}")
except Exception as e:
print(f"❌ Error applying resize: {e}")
messagebox.showerror("Resize Error", f"Failed to resize video:\n{str(e)}")
def add_text_overlay(self):
"""Apply text overlay to the current video editor"""
if not hasattr(self, 'video_editor') or not self.video_editor:
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()
try:
self.video_editor.apply_text_overlay_to_current(text, position, size, 'white', method)
self.refresh_video_preview()
messagebox.showinfo("Success", f"Text '{text[:30]}...' added successfully")
except Exception as e:
print(f"❌ Error applying text overlay: {e}")
messagebox.showerror("Text Error", f"Failed to add text overlay:\n{str(e)}")
# Professional Timeline Methods
def init_timeline_canvas(self):
"""Initialize the timeline canvas with tracks and grid"""
self.timeline_canvas.delete("all")
# Draw track headers
track_names = ['Video 1', 'Video 2', 'Audio 1', 'Text 1', 'Effects']
for i, track_name in enumerate(track_names):
y = i * self.timeline_tracks_height
# Track background
track_color = self.colors['bg_tertiary'] if i % 2 == 0 else self.colors['bg_secondary']
self.timeline_canvas.create_rectangle(0, y, 1000, y + self.timeline_tracks_height,
fill=track_color, outline=self.colors['text_muted'], width=1)
# Track label
self.timeline_canvas.create_text(10, y + 30, text=track_name,
anchor="w", fill=self.colors['text_primary'],
font=self.fonts['caption'])
# Draw time grid
self._draw_timeline_grid()
# Draw playhead
self._draw_playhead()
# Set canvas scroll region
self.timeline_canvas.configure(scrollregion=(0, 0, 2000, len(track_names) * self.timeline_tracks_height))
def _draw_timeline_grid(self):
"""Draw time grid on timeline"""
canvas_width = 2000
canvas_height = 5 * self.timeline_tracks_height
# Vertical grid lines (time markers)
for second in range(0, int(canvas_width / self.timeline_scale) + 1):
x = second * self.timeline_scale
if x < canvas_width:
# Major grid lines every 5 seconds
if second % 5 == 0:
self.timeline_canvas.create_line(x, 0, x, canvas_height,
fill=self.colors['text_muted'], width=2)
# Time label
minutes = second // 60
seconds = second % 60
time_text = f"{minutes:02d}:{seconds:02d}"
self.timeline_canvas.create_text(x + 5, 10, text=time_text,
anchor="nw", fill=self.colors['text_primary'],
font=self.fonts['small'])
else:
# Minor grid lines
self.timeline_canvas.create_line(x, 0, x, canvas_height,
fill=self.colors['text_muted'], width=1,
dash=(2, 4))
def _draw_playhead(self):
"""Draw the playhead indicator"""
x = self.timeline_playhead_pos * self.timeline_scale
canvas_height = 5 * self.timeline_tracks_height
# Remove existing playhead
self.timeline_canvas.delete("playhead")
# Draw playhead line
self.timeline_canvas.create_line(x, 0, x, canvas_height,
fill=self.colors['accent_red'], width=3,
tags="playhead")
# Draw playhead handle
self.timeline_canvas.create_polygon(x-8, 0, x+8, 0, x, 16,
fill=self.colors['accent_red'],
outline=self.colors['text_primary'], width=1,
tags="playhead")
def timeline_play(self):
"""Start timeline playback"""
print("▶️ Timeline play")
# Implement playback logic here
messagebox.showinfo("Timeline", "Play functionality - would start video playback from current position")
def timeline_pause(self):
"""Pause timeline playback"""
print("⏸️ Timeline pause")
messagebox.showinfo("Timeline", "Pause functionality - would pause video playback")
def timeline_stop(self):
"""Stop timeline playback"""
print("⏹️ Timeline stop")
self.timeline_playhead_pos = 0
self._draw_playhead()
self.timeline_position_var.set("00:00:00")
messagebox.showinfo("Timeline", "Stop functionality - timeline reset to start")
def timeline_zoom_in(self):
"""Zoom in timeline view"""
self.timeline_scale = min(self.timeline_scale * 1.5, 200)
self.refresh_timeline_view()
print(f"🔍➕ Timeline zoom in: {self.timeline_scale} px/sec")
def timeline_zoom_out(self):
"""Zoom out timeline view"""
self.timeline_scale = max(self.timeline_scale / 1.5, 10)
self.refresh_timeline_view()
print(f"🔍➖ Timeline zoom out: {self.timeline_scale} px/sec")
def timeline_cut(self):
"""Cut selected clips"""
if self.timeline_selected_clips:
print(f"✂️ Cut {len(self.timeline_selected_clips)} clips")
messagebox.showinfo("Timeline", f"Cut {len(self.timeline_selected_clips)} clips")
else:
messagebox.showwarning("Timeline", "No clips selected to cut")
def timeline_copy(self):
"""Copy selected clips"""
if self.timeline_selected_clips:
print(f"📋 Copy {len(self.timeline_selected_clips)} clips")
messagebox.showinfo("Timeline", f"Copied {len(self.timeline_selected_clips)} clips")
else:
messagebox.showwarning("Timeline", "No clips selected to copy")
def timeline_paste(self):
"""Paste clips at playhead position"""
print(f"📌 Paste clips at {self.timeline_playhead_pos:.1f}s")
messagebox.showinfo("Timeline", f"Paste functionality - would paste clips at {self.timeline_playhead_pos:.1f}s")
def timeline_delete(self):
"""Delete selected clips"""
if self.timeline_selected_clips:
result = messagebox.askyesno("Confirm Delete",
f"Delete {len(self.timeline_selected_clips)} selected clips?")
if result:
print(f"🗑️ Delete {len(self.timeline_selected_clips)} clips")
self.timeline_selected_clips.clear()
self.refresh_timeline_view()
else:
messagebox.showwarning("Timeline", "No clips selected to delete")
def timeline_undo(self):
"""Undo last timeline action"""
if hasattr(self, 'timeline_editor'):
self.timeline_editor.undo()
self.refresh_timeline_view()
print("↶ Timeline undo")
else:
messagebox.showinfo("Timeline", "Nothing to undo")
def timeline_redo(self):
"""Redo last undone timeline action"""
if hasattr(self, 'timeline_editor'):
self.timeline_editor.redo()
self.refresh_timeline_view()
print("↷ Timeline redo")
else:
messagebox.showinfo("Timeline", "Nothing to redo")
def add_video_track(self):
"""Add new video track to timeline"""
print("📹 Add video track")
file_path = filedialog.askopenfilename(
title="Select Video File",
filetypes=[("Video files", "*.mp4 *.avi *.mov *.mkv"), ("All files", "*.*")]
)
if file_path:
clip_data = {
'type': 'video',
'file_path': file_path,
'duration': 10.0 # Default duration, would get from file
}
self.timeline_editor.add_clip_to_track('video', clip_data, self.timeline_playhead_pos)
self.refresh_timeline_view()
messagebox.showinfo("Timeline", f"Added video: {os.path.basename(file_path)}")
def add_audio_track(self):
"""Add new audio track to timeline"""
print("🎵 Add audio track")
file_path = filedialog.askopenfilename(
title="Select Audio File",
filetypes=[("Audio files", "*.mp3 *.wav *.aac *.m4a"), ("All files", "*.*")]
)
if file_path:
clip_data = {
'type': 'audio',
'file_path': file_path,
'duration': 10.0 # Default duration, would get from file
}
self.timeline_editor.add_clip_to_track('audio', clip_data, self.timeline_playhead_pos)
self.refresh_timeline_view()
messagebox.showinfo("Timeline", f"Added audio: {os.path.basename(file_path)}")
def add_text_track(self):
"""Add text overlay to timeline"""
print("📝 Add text track")
# Simple text input dialog
text = tk.simpledialog.askstring("Add Text", "Enter text to add:")
if text:
clip_data = {
'type': 'text',
'duration': 3.0,
'properties': {
'text': text,
'font_size': 50,
'color': 'white',
'position': ('center', 'bottom')
}
}
self.timeline_editor.add_clip_to_track('overlay', clip_data, self.timeline_playhead_pos)
self.refresh_timeline_view()
messagebox.showinfo("Timeline", f"Added text: '{text[:20]}...'")
def open_media_browser(self):
"""Open media browser for timeline"""
print("📁 Open media browser")
messagebox.showinfo("Timeline", "Media Browser - would show project media library")
def export_timeline_video(self):
"""Export the complete timeline as a video"""
if not hasattr(self, 'timeline_editor') or not self.timeline_editor.clips:
messagebox.showwarning("Timeline", "No clips in timeline to export")
return
output_path = filedialog.asksaveasfilename(
title="Export Timeline Video",
defaultextension=".mp4",
filetypes=[("MP4 files", "*.mp4"), ("All files", "*.*")]
)
if output_path:
try:
print(f"💾 Exporting timeline to: {output_path}")
def progress_callback(status):
print(f"Export progress: {status}")
self.timeline_editor.export_timeline(output_path, quality="high",
progress_callback=progress_callback)
messagebox.showinfo("Export Complete", f"Timeline exported successfully!\n\nSaved to: {output_path}")
except Exception as e:
print(f"❌ Timeline export failed: {e}")
messagebox.showerror("Export Failed", f"Failed to export timeline:\n{str(e)}")
def refresh_timeline_view(self):
"""Refresh the timeline view"""
self.init_timeline_canvas()
self._draw_timeline_clips()
def _draw_timeline_clips(self):
"""Draw clips on the timeline"""
if not hasattr(self, 'timeline_editor'):
return
track_index = 0
for track_name, clips in self.timeline_editor.tracks.items():
y = track_index * self.timeline_tracks_height + 20
for clip in clips:
self._draw_clip_on_timeline(clip, y)
track_index += 1
def _draw_clip_on_timeline(self, clip, y):
"""Draw a single clip on the timeline"""
x1 = clip['start_time'] * self.timeline_scale
x2 = (clip['start_time'] + clip['duration']) * self.timeline_scale
# Clip colors by type
clip_colors = {
'video': self.colors['accent_blue'],
'audio': self.colors['accent_green'],
'text': self.colors['accent_purple'],
'effect': self.colors['accent_orange']
}
color = clip_colors.get(clip['type'], self.colors['accent_gray'])
# Draw clip rectangle
clip_rect = self.timeline_canvas.create_rectangle(x1, y, x2, y + 30,
fill=color, outline=self.colors['text_primary'],
width=1, tags=f"clip_{clip['id']}")
# Clip label
clip_text = clip.get('file_path', 'Clip')
if clip_text:
clip_name = os.path.basename(clip_text) if clip['type'] in ['video', 'audio'] else clip.get('properties', {}).get('text', 'Text')
else:
clip_name = f"{clip['type'].title()} Clip"
# Truncate long names
if len(clip_name) > 15:
clip_name = clip_name[:15] + "..."
self.timeline_canvas.create_text(x1 + 5, y + 15, text=clip_name,
anchor="w", fill=self.colors['text_primary'],
font=self.fonts['small'], tags=f"clip_{clip['id']}")
def timeline_click(self, event):
"""Handle timeline click events"""
x = self.timeline_canvas.canvasx(event.x)
y = self.timeline_canvas.canvasy(event.y)
# Check if clicking on playhead area
if y < 20:
# Move playhead
self.timeline_playhead_pos = x / self.timeline_scale
self._draw_playhead()
# Update time display
minutes = int(self.timeline_playhead_pos // 60)
seconds = int(self.timeline_playhead_pos % 60)
milliseconds = int((self.timeline_playhead_pos % 1) * 100)
self.timeline_position_var.set(f"{minutes:02d}:{seconds:02d}:{milliseconds:02d}")
print(f"🎯 Playhead moved to {self.timeline_playhead_pos:.2f}s")
else:
# Check for clip selection
clicked_items = self.timeline_canvas.find_overlapping(x-1, y-1, x+1, y+1)
for item in clicked_items:
tags = self.timeline_canvas.gettags(item)
for tag in tags:
if tag.startswith("clip_"):
clip_id = int(tag.split("_")[1])
if clip_id not in self.timeline_selected_clips:
self.timeline_selected_clips.append(clip_id)
print(f"🎬 Selected clip {clip_id}")
break
def timeline_drag(self, event):
"""Handle timeline drag events"""
x = self.timeline_canvas.canvasx(event.x)
y = self.timeline_canvas.canvasy(event.y)
# Check if dragging in playhead area (top 20 pixels)
if y < 20:
# Update playhead position
self.timeline_playhead_pos = max(0, x / self.timeline_scale)
self._draw_playhead()
# Update time display
minutes = int(self.timeline_playhead_pos // 60)
seconds = int(self.timeline_playhead_pos % 60)
milliseconds = int((self.timeline_playhead_pos % 1) * 100)
self.timeline_position_var.set(f"{minutes:02d}:{seconds:02d}:{milliseconds:02d}")
print(f"🎯 Playhead dragged to {self.timeline_playhead_pos:.2f}s")
else:
# Handle clip dragging logic for other areas
print(f"🚚 Timeline drag at {event.x}, {event.y}")
def timeline_release(self, event):
"""Handle timeline mouse release events"""
print("👆 Timeline mouse release")
def timeline_double_click(self, event):
"""Handle timeline double click events"""
print("👆👆 Timeline double click - would open clip properties")
messagebox.showinfo("Timeline", "Double click - would open clip properties dialog")
def timeline_right_click(self, event):
"""Handle timeline right click context menu"""
print("👆 Timeline right click - would show context menu")
messagebox.showinfo("Timeline", "Right click - would show context menu with cut/copy/paste/delete options")
# GUI Components
class ShortsGeneratorGUI:
def __init__(self, root):
self.root = root
self.root.title("🎬 AI Shorts Generator - Advanced Video Moment Detection")
self.root.geometry("750x800")
self.root.minsize(600, 650)
# Modern color scheme
self.colors = {
'bg_primary': '#1a1a1a', # Dark background
'bg_secondary': '#2d2d2d', # Card backgrounds
'bg_tertiary': '#3d3d3d', # Elevated elements
'accent_blue': '#007acc', # Primary blue
'accent_green': '#28a745', # Success green
'accent_orange': '#fd7e14', # Warning orange
'accent_purple': '#6f42c1', # Secondary purple
'accent_red': '#dc3545', # Error red
'text_primary': '#ffffff', # Primary text
'text_secondary': '#b8b8b8', # Secondary text
'text_muted': '#6c757d', # Muted text
'border': '#404040', # Border color
'hover': '#4a4a4a' # Hover state
}
self.root.configure(bg=self.colors['bg_primary'])
# Modern fonts
self.fonts = {
'title': ('Segoe UI', 20, 'bold'),
'heading': ('Segoe UI', 14, 'bold'),
'subheading': ('Segoe UI', 12, 'bold'),
'body': ('Segoe UI', 10),
'caption': ('Segoe UI', 9),
'button': ('Segoe UI', 10, 'bold')
}
# Make window responsive
self.root.rowconfigure(0, weight=1)
self.root.columnconfigure(0, weight=1)
self.video_path = None
self.output_folder = "shorts"
self.max_clips = 3
self.threshold_db = -30
self.clip_duration = 5
# Bind resize event
self.root.bind('<Configure>', self.on_window_resize)
self.create_widgets()
def create_widgets(self):
# Create main scrollable container with modern styling
main_container = tk.Frame(self.root, bg=self.colors['bg_primary'])
main_container.pack(fill="both", expand=True, padx=25, pady=25)
main_container.rowconfigure(0, weight=1)
main_container.columnconfigure(0, weight=1)
# Create canvas and scrollbar for scrolling
canvas = tk.Canvas(main_container, bg=self.colors['bg_primary'], highlightthickness=0)
# Modern scrollbar styling
style = ttk.Style()
style.theme_use('clam')
style.configure("Modern.Vertical.TScrollbar",
background=self.colors['bg_tertiary'],
troughcolor=self.colors['bg_secondary'],
borderwidth=0,
arrowcolor=self.colors['text_secondary'],
darkcolor=self.colors['bg_tertiary'],
lightcolor=self.colors['bg_tertiary'])
scrollbar = ttk.Scrollbar(main_container, orient="vertical", command=canvas.yview,
style="Modern.Vertical.TScrollbar")
scrollable_frame = tk.Frame(canvas, bg=self.colors['bg_primary'])
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# Make scrollable frame responsive
scrollable_frame.columnconfigure(0, weight=1)
# Modern header section
header_frame = tk.Frame(scrollable_frame, bg=self.colors['bg_primary'])
header_frame.grid(row=0, column=0, pady=(0, 30), sticky="ew")
# Main title with modern typography
title_label = tk.Label(header_frame, text="🎬 AI Shorts Generator",
font=self.fonts['title'], bg=self.colors['bg_primary'],
fg=self.colors['text_primary'])
title_label.pack()
# Subtitle
subtitle_label = tk.Label(header_frame, text="Advanced Video Moment Detection & Generation",
font=self.fonts['caption'], bg=self.colors['bg_primary'],
fg=self.colors['text_secondary'])
subtitle_label.pack(pady=(5, 0))
# Video selection card
video_card = self.create_modern_card(scrollable_frame, "📁 Video Input")
video_card.grid(row=1, column=0, pady=15, sticky="ew")
# Output folder card
output_card = self.create_modern_card(scrollable_frame, "📂 Output Settings")
output_card.grid(row=2, column=0, pady=15, sticky="ew")
# Add content to video card
self.setup_video_selection(video_card)
# Add content to output card
self.setup_output_selection(output_card)
# Settings card
settings_card = self.create_modern_card(scrollable_frame, "⚙️ Generation Settings")
settings_card.grid(row=3, column=0, pady=15, sticky="ew")
self.setup_settings_panel(settings_card)
# Action buttons card
actions_card = self.create_modern_card(scrollable_frame, "🚀 Actions")
actions_card.grid(row=4, column=0, pady=15, sticky="ew")
self.setup_action_buttons(actions_card)
# Progress card
progress_card = self.create_modern_card(scrollable_frame, "📊 Progress")
progress_card.grid(row=5, column=0, pady=15, sticky="ew")
self.setup_progress_panel(progress_card)
# Pack the canvas and scrollbar
canvas.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")
def create_modern_card(self, parent, title):
"""Create a modern card-style container"""
card_frame = tk.Frame(parent, bg=self.colors['bg_secondary'], relief="flat", bd=0)
# Card header with modern styling
header_frame = tk.Frame(card_frame, bg=self.colors['bg_secondary'])
header_frame.pack(fill="x", padx=25, pady=(20, 10))
header_label = tk.Label(header_frame, text=title, font=self.fonts['heading'],
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'])
header_label.pack(anchor="w")
# Separator line
separator = tk.Frame(card_frame, bg=self.colors['border'], height=1)
separator.pack(fill="x", padx=25)
# Card content area
content_frame = tk.Frame(card_frame, bg=self.colors['bg_secondary'])
content_frame.pack(fill="both", expand=True, padx=25, pady=(15, 25))
return content_frame
def create_modern_button(self, parent, text, command, color, large=False):
"""Create a modern button with hover effects"""
font = self.fonts['button'] if not large else ('Segoe UI', 12, 'bold')
pady = 12 if not large else 16
button = tk.Button(parent, text=text, command=command,
bg=color, fg='white', font=font,
relief="flat", bd=0, pady=pady,
activebackground=self.adjust_color(color, -20),
activeforeground='white',
cursor="hand2")
# Add hover effects
def on_enter(e):
button.config(bg=self.adjust_color(color, 15))
def on_leave(e):
button.config(bg=color)
button.bind("<Enter>", on_enter)
button.bind("<Leave>", on_leave)
return button
def adjust_color(self, hex_color, adjustment):
"""Adjust color brightness for hover effects"""
hex_color = hex_color.lstrip('#')
rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
adjusted = tuple(max(0, min(255, c + adjustment)) for c in rgb)
return f"#{adjusted[0]:02x}{adjusted[1]:02x}{adjusted[2]:02x}"
def setup_video_selection(self, parent):
"""Setup the video selection interface"""
parent.columnconfigure(0, weight=1)
self.video_label = tk.Label(parent, text="No video selected",
font=self.fonts['body'], bg=self.colors['bg_tertiary'],
fg=self.colors['text_secondary'], relief="flat",
anchor="w", pady=12, padx=15, bd=1,
highlightbackground=self.colors['border'],
highlightthickness=1)
self.video_label.grid(row=0, column=0, sticky="ew", pady=(0, 15))
browse_btn = self.create_modern_button(parent, "📁 Browse Video",
self.select_video, self.colors['accent_blue'])
browse_btn.grid(row=1, column=0, sticky="ew")
def setup_output_selection(self, parent):
"""Setup the output folder selection interface"""
parent.columnconfigure(0, weight=1)
self.output_label = tk.Label(parent, text="shorts/",
font=self.fonts['body'], bg=self.colors['bg_tertiary'],
fg=self.colors['text_secondary'], relief="flat",
anchor="w", pady=12, padx=15, bd=1,
highlightbackground=self.colors['border'],
highlightthickness=1)
self.output_label.grid(row=0, column=0, sticky="ew", pady=(0, 15))
browse_btn = self.create_modern_button(parent, "📂 Browse Folder",
self.select_output_folder, self.colors['accent_blue'])
browse_btn.grid(row=1, column=0, sticky="ew")
def setup_settings_panel(self, parent):
"""Setup the settings panel with modern styling"""
parent.columnconfigure(0, weight=1)
# Max clips setting
clips_frame = tk.Frame(parent, bg=self.colors['bg_secondary'])
clips_frame.grid(row=0, column=0, sticky="ew", pady=(0, 20))
clips_frame.columnconfigure(1, weight=1)
self.use_max_clips = tk.BooleanVar(value=True)
clips_checkbox = tk.Checkbutton(clips_frame, variable=self.use_max_clips,
text="Limit clips:", font=self.fonts['body'],
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'],
selectcolor=self.colors['accent_blue'], relief="flat", bd=0)
clips_checkbox.grid(row=0, column=0, sticky="w", padx=(0, 15))
self.clips_var = tk.IntVar(value=3)
self.clips_spinbox = tk.Spinbox(clips_frame, from_=1, to=10, width=8,
textvariable=self.clips_var, font=self.fonts['body'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
relief="flat", bd=1, highlightbackground=self.colors['border'])
self.clips_spinbox.grid(row=0, column=2, sticky="e")
# Detection mode
detection_frame = tk.Frame(parent, bg=self.colors['bg_secondary'])
detection_frame.grid(row=1, column=0, sticky="ew", pady=(0, 20))
detection_frame.columnconfigure(1, weight=1)
tk.Label(detection_frame, text="Detection Mode:", font=self.fonts['subheading'],
bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).grid(row=0, column=0, sticky="w")
self.detection_mode_var = tk.StringVar(value="loud")
self.detection_display_var = tk.StringVar(value="🔊 Loud Moments")
# Modern combobox styling
detection_style = ttk.Style()
detection_style.configure("Modern.TCombobox",
fieldbackground=self.colors['bg_tertiary'],
background=self.colors['bg_tertiary'],
foreground=self.colors['text_primary'],
arrowcolor=self.colors['text_secondary'],
borderwidth=1,
relief="flat")
detection_dropdown = ttk.Combobox(detection_frame, textvariable=self.detection_display_var,
values=["🔊 Loud Moments", "🎬 Scene Changes", "🏃 Motion Intensity",
"😄 Emotional Speech", "🎵 Audio Peaks", "🎯 Smart Combined"],
state="readonly", width=25, font=self.fonts['body'],
style="Modern.TCombobox")
detection_dropdown.grid(row=0, column=1, sticky="e")
# Store the mapping between display text and internal values
self.mode_mapping = {
"🔊 Loud Moments": "loud",
"🎬 Scene Changes": "scene",
"🏃 Motion Intensity": "motion",
"😄 Emotional Speech": "speech",
"🎵 Audio Peaks": "peaks",
"🎯 Smart Combined": "combined"
}
# Audio threshold (for loud moments)
self.threshold_frame = tk.Frame(parent, bg=self.colors['bg_secondary'])
self.threshold_frame.grid(row=2, column=0, sticky="ew", pady=(0, 20))
self.threshold_frame.columnconfigure(1, weight=1)
tk.Label(self.threshold_frame, text="Audio Threshold (dB):", font=self.fonts['body'],
bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).grid(row=0, column=0, sticky="w")
self.threshold_var = tk.IntVar(value=-30)
threshold_spinbox = tk.Spinbox(self.threshold_frame, from_=-50, to=0, width=8,
textvariable=self.threshold_var, font=self.fonts['body'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
relief="flat", bd=1, highlightbackground=self.colors['border'])
threshold_spinbox.grid(row=0, column=2, sticky="e")
# Clip duration
duration_frame = tk.Frame(parent, bg=self.colors['bg_secondary'])
duration_frame.grid(row=3, column=0, sticky="ew")
duration_frame.columnconfigure(1, weight=1)
tk.Label(duration_frame, text="Clip Duration (seconds):", font=self.fonts['body'],
bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).grid(row=0, column=0, sticky="w")
self.duration_var = tk.IntVar(value=5)
duration_spinbox = tk.Spinbox(duration_frame, from_=3, to=120, width=8,
textvariable=self.duration_var, font=self.fonts['body'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
relief="flat", bd=1, highlightbackground=self.colors['border'])
duration_spinbox.grid(row=0, column=2, sticky="e")
# Bind dropdown change event
def on_detection_change(event):
selection = detection_dropdown.get()
self.detection_mode_var.set(self.mode_mapping.get(selection, "loud"))
# Show/hide threshold setting based on mode
if selection == "🔊 Loud Moments":
self.threshold_frame.grid(row=2, column=0, sticky="ew", pady=(0, 20))
else:
self.threshold_frame.grid_remove()
detection_dropdown.bind("<<ComboboxSelected>>", on_detection_change)
# Bind checkbox to enable/disable spinbox
def toggle_clips_limit():
if self.use_max_clips.get():
self.clips_spinbox.config(state="normal")
else:
self.clips_spinbox.config(state="disabled")
self.use_max_clips.trace("w", lambda *args: toggle_clips_limit())
clips_checkbox.config(command=toggle_clips_limit)
def setup_action_buttons(self, parent):
"""Setup the action buttons with modern styling"""
parent.columnconfigure(0, weight=1)
# Preview button
self.preview_btn = self.create_modern_button(parent, "🔍 Preview Clips",
self.preview_clips, self.colors['accent_blue'])
self.preview_btn.grid(row=0, column=0, sticky="ew", pady=(0, 10))
# Generate button - primary action
self.generate_btn = self.create_modern_button(parent, "🎬 Generate Shorts",
self.start_generation, self.colors['accent_green'],
large=True)
self.generate_btn.grid(row=1, column=0, sticky="ew", pady=(0, 15))
# Secondary actions
button_grid = tk.Frame(parent, bg=self.colors['bg_secondary'])
button_grid.grid(row=2, column=0, sticky="ew")
button_grid.columnconfigure(0, weight=1)
button_grid.columnconfigure(1, weight=1)
self.edit_btn = self.create_modern_button(button_grid, "✏️ Edit Shorts",
self.open_shorts_editor, self.colors['accent_orange'])
self.edit_btn.grid(row=0, column=0, sticky="ew", padx=(0, 5))
self.thumbnail_btn = self.create_modern_button(button_grid, "📸 Thumbnails",
self.open_thumbnail_editor, self.colors['accent_purple'])
self.thumbnail_btn.grid(row=0, column=1, sticky="ew", padx=(5, 0))
def setup_progress_panel(self, parent):
"""Setup the progress panel with modern styling"""
parent.columnconfigure(0, weight=1)
# Progress info
self.progress_label = tk.Label(parent, text="Ready to generate shorts",
font=self.fonts['body'], bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'])
self.progress_label.grid(row=0, column=0, sticky="ew", pady=(0, 10))
# Modern progress bar
progress_style = ttk.Style()
progress_style.configure("Modern.Horizontal.TProgressbar",
background=self.colors['accent_green'],
troughcolor=self.colors['bg_tertiary'],
borderwidth=0, lightcolor=self.colors['accent_green'],
darkcolor=self.colors['accent_green'])
self.progress_bar = ttk.Progressbar(parent, length=400, mode="determinate",
style="Modern.Horizontal.TProgressbar")
self.progress_bar.grid(row=1, column=0, sticky="ew", pady=(0, 10))
# Detection progress (initially hidden)
self.detection_progress_label = tk.Label(parent, text="", font=self.fonts['caption'],
bg=self.colors['bg_secondary'], fg=self.colors['accent_blue'])
self.detection_progress_bar = ttk.Progressbar(parent, length=400, mode="determinate",
style="Modern.Horizontal.TProgressbar")
# Initially hide detection progress
self.detection_progress_label.grid_remove()
self.detection_progress_bar.grid_remove()
# Settings frame
settings_frame = tk.LabelFrame(scrollable_frame, text="Settings", padx=10, pady=10)
settings_frame.grid(row=3, column=0, pady=10, sticky="ew")
settings_frame.columnconfigure(0, weight=1)
# Max clips with on/off toggle
clips_frame = tk.Frame(settings_frame)
clips_frame.grid(row=0, column=0, pady=5, sticky="ew")
clips_frame.columnconfigure(1, weight=1)
self.use_max_clips = tk.BooleanVar(value=True)
clips_checkbox = tk.Checkbutton(clips_frame, variable=self.use_max_clips, text="Max Clips to Generate:")
clips_checkbox.grid(row=0, column=0, sticky="w")
self.clips_var = tk.IntVar(value=3)
self.clips_spinbox = tk.Spinbox(clips_frame, from_=1, to=10, width=5, textvariable=self.clips_var)
self.clips_spinbox.grid(row=0, column=2, sticky="e")
# Bind checkbox to enable/disable spinbox
def toggle_clips_limit():
if self.use_max_clips.get():
self.clips_spinbox.config(state="normal")
else:
self.clips_spinbox.config(state="disabled")
self.use_max_clips.trace("w", lambda *args: toggle_clips_limit())
clips_checkbox.config(command=toggle_clips_limit)
# Add tooltip for max clips setting
clips_tooltip_text = """Max Clips Control:
• Checked: Limit the number of clips generated
• Unchecked: Generate all detected moments
• 1-3 clips: Quick highlights for social media
• 4-6 clips: Good variety pack
• 7-10 clips: Comprehensive highlight reel
Tip: Start with 3 clips, then increase if you want more content"""
ToolTip(self.clips_spinbox, clips_tooltip_text, side='right')
ToolTip(clips_checkbox, clips_tooltip_text, side='right')
# Detection Mode Selection
detection_frame = tk.Frame(settings_frame)
detection_frame.grid(row=1, column=0, pady=5, sticky="ew")
detection_frame.columnconfigure(1, weight=1)
tk.Label(detection_frame, text="Detection Mode:", font=("Arial", 9, "bold")).grid(row=0, column=0, sticky="w")
self.detection_mode_var = tk.StringVar(value="loud")
self.detection_display_var = tk.StringVar(value="🔊 Loud Moments")
detection_dropdown = ttk.Combobox(detection_frame, textvariable=self.detection_display_var,
values=["🔊 Loud Moments", "🎬 Scene Changes", "🏃 Motion Intensity",
"😄 Emotional Speech", "🎵 Audio Peaks", "🎯 Smart Combined"],
state="readonly", width=22)
detection_dropdown.grid(row=0, column=1, sticky="e")
# Store the mapping between display text and internal values
self.mode_mapping = {
"🔊 Loud Moments": "loud",
"🎬 Scene Changes": "scene",
"🏃 Motion Intensity": "motion",
"😄 Emotional Speech": "speech",
"🎵 Audio Peaks": "peaks",
"🎯 Smart Combined": "combined"
}
# Simple, clear descriptions for mode tooltips
mode_descriptions = {
"🔊 Loud Moments": """Analyzes audio volume levels to find the loudest parts of your video.
• Best for: Gaming reactions, music highlights, shouting moments
• Finds: High-volume audio segments above the threshold
• Ideal when: Your video has clear volume differences
• Tip: Adjust threshold if too many/few moments found""",
"🎬 Scene Changes": """Detects dramatic visual transitions and cuts in your video.
• Best for: Movie trailers, montages, location changes
• Finds: Major visual shifts between frames
• Ideal when: Video has multiple scenes or camera angles
• Tip: Great for content with quick cuts or transitions""",
"🏃 Motion Intensity": """Analyzes movement and action within video frames.
• Best for: Sports highlights, dance videos, action scenes
• Finds: High-movement moments with lots of visual activity
• Ideal when: Video contains physical action or movement
• Tip: Perfect for extracting the most dynamic moments""",
"😄 Emotional Speech": """Uses AI to detect excited, emotional, or emphatic speech patterns.
• Best for: Reactions, reviews, commentary, tutorials
• Finds: Words like 'wow', 'amazing', exclamations, excited tone
• Ideal when: Video has spoken content with emotional moments
• Tip: Captures the most engaging verbal reactions""",
"🎵 Audio Peaks": """Detects sudden audio spikes like bass drops, impacts, or sound effects.
• Best for: Music videos, sound effect moments, beat drops
• Finds: Sharp increases in audio frequency or volume
• Ideal when: Video has musical elements or sound effects
• Tip: Great for rhythm-based or audio-driven content""",
"🎯 Smart Combined": """Intelligently combines all detection methods for optimal results.
• Best for: Any video type, general content, unsure what to use
• Finds: Moments scoring high across multiple analysis methods
• Ideal when: You want the most 'interesting' overall moments
• Tip: Recommended starting point for most videos"""
}
# Create tooltip for the dropdown (updates when selection changes)
current_tooltip_text = mode_descriptions["🔊 Loud Moments"] # Default
dropdown_tooltip = ToolTip(detection_dropdown, current_tooltip_text)
# Update tooltip when selection changes
def on_detection_change(event):
selection = detection_dropdown.get()
mode_map = {
"🔊 Loud Moments": "loud",
"🎬 Scene Changes": "scene",
"🏃 Motion Intensity": "motion",
"😄 Emotional Speech": "speech",
"🎵 Audio Peaks": "peaks",
"🎯 Smart Combined": "combined"
}
self.detection_mode_var.set(mode_map.get(selection, "loud"))
# Update tooltip text for the selected mode
dropdown_tooltip.text = mode_descriptions.get(selection, "Select a detection mode")
# Show/hide threshold setting based on mode
if selection == "🔊 Loud Moments":
threshold_frame.grid(row=2, column=0, pady=5, sticky="ew")
else:
threshold_frame.grid_remove()
detection_dropdown.bind("<<ComboboxSelected>>", on_detection_change)
# Audio threshold (only shown for loud moments)
threshold_frame = tk.Frame(settings_frame)
threshold_frame.grid(row=2, column=0, pady=5, sticky="ew")
threshold_frame.columnconfigure(1, weight=1)
threshold_label = tk.Label(threshold_frame, text="Audio Threshold (dB):")
threshold_label.grid(row=0, column=0, sticky="w")
self.threshold_var = tk.IntVar(value=-30)
threshold_spinbox = tk.Spinbox(threshold_frame, from_=-50, to=0, width=5, textvariable=self.threshold_var)
threshold_spinbox.grid(row=0, column=2, sticky="e")
# Add tooltip for threshold setting
threshold_tooltip_text = """Audio Threshold Control:
• Higher values (closer to 0): Only very loud moments
• Lower values (closer to -50): More moments detected
• Default -30 dB: Good balance for most videos
• Adjust based on your video's audio levels
Example: Gaming videos might need -20 dB, quiet vlogs might need -40 dB"""
ToolTip(threshold_spinbox, threshold_tooltip_text, side='right')
# Clip duration (increased to 120 seconds max)
duration_frame = tk.Frame(settings_frame)
duration_frame.grid(row=3, column=0, pady=5, sticky="ew")
duration_frame.columnconfigure(1, weight=1)
duration_label = tk.Label(duration_frame, text="Clip Duration (seconds):")
duration_label.grid(row=0, column=0, sticky="w")
self.duration_var = tk.IntVar(value=5)
duration_spinbox = tk.Spinbox(duration_frame, from_=3, to=120, width=5, textvariable=self.duration_var)
duration_spinbox.grid(row=0, column=2, sticky="e")
# Add tooltip for duration setting
duration_tooltip_text = """Clip Duration Setting:
• 3-10 seconds: Perfect for TikTok/Instagram Reels
• 10-30 seconds: Good for YouTube Shorts
• 30-60 seconds: Longer form highlights
• 60+ seconds: Extended content clips
Shorter clips = more viral potential
Longer clips = more context and story"""
ToolTip(duration_spinbox, duration_tooltip_text, side='right')
# Preview button
self.preview_btn = tk.Button(scrollable_frame, text="🔍 Preview Clips",
command=self.preview_clips, bg="#2196F3", fg="white",
font=("Arial", 10, "bold"), pady=5)
self.preview_btn.grid(row=4, column=0, pady=5, sticky="ew")
# Add tooltip for preview button
preview_tooltip_text = """Preview Clips Feature:
• Analyzes your video using the selected detection mode
• Shows all detected moments with timestamps
• Lets you select specific clips to generate
• No video files created - just analysis
• Great for testing settings before full generation
Tip: Always preview first to see what the AI finds!"""
ToolTip(self.preview_btn, preview_tooltip_text, side='right')
# Generate button
self.generate_btn = tk.Button(scrollable_frame, text="🎬 Generate Shorts",
command=self.start_generation, bg="#4CAF50", fg="white",
font=("Arial", 12, "bold"), pady=10)
self.generate_btn.grid(row=5, column=0, pady=10, sticky="ew")
# Add tooltip for generate button
generate_tooltip_text = """Generate Shorts Feature:
• Creates actual video files from detected moments
• Adds AI-generated subtitles to each clip
• Formats videos for vertical social media (1080x1920)
• Saves clips to your selected output folder
• Takes longer but creates ready-to-post content
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(scrollable_frame, text="✏️ Edit Generated Shorts",
command=self.open_shorts_editor, bg="#FF9800", fg="white",
font=("Arial", 11, "bold"), pady=8)
self.edit_btn.grid(row=6, column=0, pady=5, sticky="ew")
# 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(scrollable_frame, text="📸 Create Thumbnails",
command=self.open_thumbnail_editor, bg="#9C27B0", fg="white",
font=("Arial", 11, "bold"), pady=8)
self.thumbnail_btn.grid(row=7, column=0, pady=5, sticky="ew")
# 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(scrollable_frame)
progress_frame.grid(row=8, column=0, pady=5, sticky="ew")
progress_frame.columnconfigure(0, weight=1)
self.progress_label = tk.Label(progress_frame, text="Ready to generate shorts")
self.progress_label.grid(row=0, column=0, sticky="ew")
self.progress_bar = ttk.Progressbar(progress_frame, length=400, mode="determinate")
self.progress_bar.grid(row=1, column=0, pady=3, sticky="ew")
# Detection progress (initially hidden)
self.detection_progress_label = tk.Label(progress_frame, text="", font=("Arial", 9), fg="gray")
self.detection_progress_label.grid(row=2, column=0, sticky="ew")
self.detection_progress_bar = ttk.Progressbar(progress_frame, length=400, mode="determinate")
self.detection_progress_bar.grid(row=3, column=0, pady=(0, 3), sticky="ew")
# Initially hide detection progress
self.detection_progress_label.grid_remove()
self.detection_progress_bar.grid_remove()
# Pack the canvas and scrollbar
canvas.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")
def on_window_resize(self, event):
"""Handle window resize events for responsive layout"""
if event.widget == self.root:
# Get current window size
width = self.root.winfo_width()
# Adjust progress bar length based on window width
progress_length = max(300, width - 150)
try:
self.progress_bar.config(length=progress_length)
self.detection_progress_bar.config(length=progress_length)
except:
pass
def select_video(self):
file_path = filedialog.askopenfilename(
title="Select Video File",
filetypes=[("Video files", "*.mp4 *.mov *.avi *.mkv *.wmv")]
)
if file_path:
self.video_path = file_path
self.video_label.config(text=os.path.basename(file_path))
def select_output_folder(self):
folder_path = filedialog.askdirectory(title="Select Output Folder")
if folder_path:
self.output_folder = folder_path
self.output_label.config(text=folder_path)
def preview_clips(self):
if not self.video_path:
messagebox.showwarning("Warning", "Please select a video file first!")
return
try:
# Validate video first
validate_video(self.video_path, min_duration=self.duration_var.get() * 2)
# Analyze using selected detection mode
self.preview_btn.config(state="disabled", text="Analyzing...")
self.root.update()
detection_mode = self.detection_mode_var.get()
if detection_mode == "loud":
moments = detect_loud_moments(
self.video_path,
chunk_duration=self.duration_var.get(),
threshold_db=self.threshold_var.get()
)
mode_name = "loud moments"
elif detection_mode == "scene":
moments = detect_scene_changes(self.video_path, chunk_duration=self.duration_var.get())
mode_name = "scene changes"
elif detection_mode == "motion":
moments = detect_motion_intensity(self.video_path, chunk_duration=self.duration_var.get())
mode_name = "motion moments"
elif detection_mode == "speech":
moments = detect_speech_emotion(self.video_path, chunk_duration=self.duration_var.get())
mode_name = "emotional speech"
elif detection_mode == "peaks":
moments = detect_audio_peaks(self.video_path, chunk_duration=self.duration_var.get())
mode_name = "audio peaks"
elif detection_mode == "combined":
moments = detect_combined_intensity(self.video_path, chunk_duration=self.duration_var.get())
mode_name = "interesting moments"
else:
moments = detect_loud_moments(
self.video_path,
chunk_duration=self.duration_var.get(),
threshold_db=self.threshold_var.get()
)
mode_name = "loud moments"
if not moments:
messagebox.showinfo("Preview", f"No {mode_name} found.\nTry a different detection mode or adjust settings.")
return
# Show preview window
preview_window = tk.Toplevel(self.root)
preview_window.title("Preview and Select Clips")
preview_window.geometry("500x400")
tk.Label(preview_window, text=f"Found {len(moments)} {mode_name}:", font=("Arial", 12, "bold")).pack(pady=10)
# Create scrollable frame for checkboxes
canvas = tk.Canvas(preview_window)
scrollbar = tk.Scrollbar(preview_window, orient="vertical", command=canvas.yview)
scrollable_frame = tk.Frame(canvas)
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# Store checkbox variables and clip data
self.clip_vars = []
# Use all clips if max clips is disabled, otherwise limit by setting
clips_to_show = moments if not self.use_max_clips.get() else moments[:self.clips_var.get()]
self.preview_clips_data = clips_to_show
# Add selectable clips with checkboxes
for i, (start, end) in enumerate(self.preview_clips_data, 1):
duration = end - start
time_str = f"Clip {i}: {start//60:02.0f}:{start%60:05.2f} - {end//60:02.0f}:{end%60:05.2f} ({duration:.1f}s)"
clip_var = tk.BooleanVar(value=True) # Default selected
self.clip_vars.append(clip_var)
clip_frame = tk.Frame(scrollable_frame)
clip_frame.pack(fill="x", padx=10, pady=2)
checkbox = tk.Checkbutton(clip_frame, variable=clip_var, text=time_str,
font=("Courier", 10), anchor="w")
checkbox.pack(fill="x")
canvas.pack(side="left", fill="both", expand=True, padx=10, pady=5)
scrollbar.pack(side="right", fill="y")
# Button frame
button_frame = tk.Frame(preview_window)
button_frame.pack(fill="x", padx=10, pady=10)
# Select/Deselect all buttons
control_frame = tk.Frame(button_frame)
control_frame.pack(fill="x", pady=5)
tk.Button(control_frame, text="Select All",
command=lambda: [var.set(True) for var in self.clip_vars]).pack(side="left", padx=5)
tk.Button(control_frame, text="Deselect All",
command=lambda: [var.set(False) for var in self.clip_vars]).pack(side="left", padx=5)
# Generate selected clips button (fixed size for full text visibility)
generate_selected_btn = tk.Button(button_frame, text="🎬 Generate Selected Clips",
command=lambda: self.generate_selected_clips(preview_window),
bg="#4CAF50", fg="white", font=("Arial", 11, "bold"),
pady=8, width=25)
generate_selected_btn.pack(fill="x", pady=5)
# Close button
tk.Button(button_frame, text="Close", command=preview_window.destroy).pack(pady=5)
except Exception as e:
messagebox.showerror("Preview Error", f"Error analyzing video: {str(e)}")
finally:
self.preview_btn.config(state="normal", text="🔍 Preview Clips")
def generate_selected_clips(self, preview_window):
"""Generate only the selected clips from preview"""
try:
# Get selected clips
selected_clips = []
for i, (clip_var, clip_data) in enumerate(zip(self.clip_vars, self.preview_clips_data)):
if clip_var.get():
selected_clips.append((i+1, clip_data)) # (clip_number, (start, end))
if not selected_clips:
messagebox.showwarning("Warning", "Please select at least one clip to generate!")
return
# Close preview window
preview_window.destroy()
# Show confirmation
clip_count = len(selected_clips)
clip_numbers = [str(num) for num, _ in selected_clips]
confirm_msg = f"Generate {clip_count} selected clips (#{', #'.join(clip_numbers)})?"
if not messagebox.askyesno("Confirm Generation", confirm_msg):
return
# Start generation in background thread
self.selected_clips_data = [clip_data for _, clip_data in selected_clips]
self.generate_btn.config(state="disabled", text="Generating Selected...")
thread = threading.Thread(target=self.selected_generation_worker)
thread.daemon = True
thread.start()
except Exception as e:
messagebox.showerror("Generation Error", f"Error starting generation: {str(e)}")
def selected_generation_worker(self):
"""Generate only selected clips"""
try:
# Check available disk space
import shutil
free_space_gb = shutil.disk_usage(self.output_folder)[2] / (1024**3)
if free_space_gb < 1:
raise RuntimeError(f"Insufficient disk space. Only {free_space_gb:.1f} GB available. Need at least 1 GB.")
# Validate video first
try:
video_duration = validate_video(self.video_path, min_duration=self.duration_var.get() * 2)
self.update_progress(f"✅ Video validated ({video_duration:.1f}s)", 5)
except Exception as e:
self.update_progress(f"❌ Video validation failed", 0)
raise e
os.makedirs(self.output_folder, exist_ok=True)
selected_count = len(self.selected_clips_data)
self.update_progress(f"📊 Generating {selected_count} selected clips", 10)
for i, (start, end) in enumerate(self.selected_clips_data):
self.update_progress(f"🗣️ Transcribing clip {i+1}/{selected_count}", 20 + (i * 30))
subtitles = transcribe_and_extract_subtitles(self.video_path, start, end)
out_path = os.path.join(self.output_folder, f"short_{i+1}.mp4")
self.update_progress(f"🎬 Creating video {i+1}/{selected_count}", 40 + (i * 30))
create_short_clip(self.video_path, start, end, subtitles, out_path)
self.update_progress("✅ Selected clips generated successfully!", 100)
messagebox.showinfo("Success", f"Successfully generated {selected_count} selected clips in '{self.output_folder}' folder!")
except FileNotFoundError as e:
messagebox.showerror("File Error", str(e))
except ValueError as e:
messagebox.showerror("Video Error", str(e))
except RuntimeError as e:
messagebox.showerror("System Error", str(e))
except Exception as e:
messagebox.showerror("Error", f"An unexpected error occurred: {str(e)}")
finally:
self.generate_btn.config(state="normal", text="🎬 Generate Shorts")
self.progress_bar["value"] = 0
self.progress_label.config(text="Ready to generate shorts")
def update_progress(self, message, percent):
self.progress_label.config(text=message)
self.progress_bar["value"] = percent
self.root.update()
def show_detection_progress(self):
"""Show the detection progress bar"""
self.detection_progress_label.pack(after=self.progress_bar)
self.detection_progress_bar.pack(after=self.detection_progress_label, pady=(0, 3))
self.root.update_idletasks()
def hide_detection_progress(self):
"""Hide the detection progress bar"""
self.detection_progress_label.pack_forget()
self.detection_progress_bar.pack_forget()
self.root.update_idletasks()
def update_detection_progress(self, message, percent):
"""Update detection progress bar and message"""
self.detection_progress_label.config(text=message)
self.detection_progress_bar["value"] = percent
self.root.update_idletasks()
def generation_worker(self):
try:
# Check available disk space
import shutil
free_space_gb = shutil.disk_usage(self.output_folder)[2] / (1024**3)
if free_space_gb < 1:
raise RuntimeError(f"Insufficient disk space. Only {free_space_gb:.1f} GB available. Need at least 1 GB.")
# Show detection progress for heavy modes
detection_mode = self.detection_mode_var.get()
if detection_mode in ["scene", "motion", "speech", "peaks", "combined"]:
self.show_detection_progress()
def detailed_progress_callback(status, percent):
# Update main progress
self.update_progress(status, percent)
def detection_progress_callback(detection_percent, detection_status):
# Update detection progress bar
self.update_detection_progress(detection_status, detection_percent)
# Pass both callbacks to generate_shorts
generate_shorts(
self.video_path,
max_clips=self.clips_var.get() if self.use_max_clips.get() else 10,
output_folder=self.output_folder,
progress_callback=detailed_progress_callback,
detection_progress_callback=detection_progress_callback,
threshold_db=self.threshold_var.get(),
clip_duration=self.duration_var.get(),
detection_mode=detection_mode
)
else:
# Use regular progress for loud moments mode
generate_shorts(
self.video_path,
max_clips=self.clips_var.get() if self.use_max_clips.get() else 10,
output_folder=self.output_folder,
progress_callback=self.update_progress,
threshold_db=self.threshold_var.get(),
clip_duration=self.duration_var.get(),
detection_mode=detection_mode
)
messagebox.showinfo("Success", f"Successfully generated shorts in '{self.output_folder}' folder!")
except FileNotFoundError as e:
messagebox.showerror("File Error", str(e))
except ValueError as e:
messagebox.showerror("Video Error", str(e))
except RuntimeError as e:
messagebox.showerror("System Error", str(e))
except Exception as e:
messagebox.showerror("Error", f"An unexpected error occurred: {str(e)}")
finally:
self.hide_detection_progress()
self.generate_btn.config(state="normal", text="🎬 Generate Shorts")
self.progress_bar["value"] = 0
self.progress_label.config(text="Ready to generate shorts")
def start_generation(self):
if not self.video_path:
messagebox.showwarning("Warning", "Please select a video file first!")
return
self.generate_btn.config(state="disabled", text="Generating...")
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()
app = ShortsGeneratorGUI(root)
root.mainloop()
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] != "--gui":
# Run command line mode
try:
generate_shorts(sys.argv[1])
print("✅ Shorts generation completed successfully!")
except Exception as e:
print(f"❌ Error: {str(e)}")
else:
# Run GUI mode (default)
run_gui()