565 lines
24 KiB
Python
565 lines
24 KiB
Python
import os
|
|
import numpy as np
|
|
from moviepy import VideoFileClip, TextClip, CompositeVideoClip
|
|
from faster_whisper import WhisperModel
|
|
import tkinter as tk
|
|
from tkinter import filedialog, messagebox, ttk
|
|
import threading
|
|
|
|
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 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, threshold_db=-30, clip_duration=5):
|
|
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
|
|
|
|
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)
|
|
selected = best_moments[:max_clips]
|
|
|
|
if not selected:
|
|
raise ValueError(f"No loud moments found with threshold {threshold_db} dB. Try lowering the threshold or use a different video.")
|
|
|
|
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)
|
|
|
|
# GUI Components
|
|
class ShortsGeneratorGUI:
|
|
def __init__(self, root):
|
|
self.root = root
|
|
self.root.title("AI Shorts Generator")
|
|
self.root.geometry("500x400")
|
|
|
|
self.video_path = None
|
|
self.output_folder = "shorts"
|
|
self.max_clips = 3
|
|
self.threshold_db = -30
|
|
self.clip_duration = 5
|
|
|
|
self.create_widgets()
|
|
|
|
def create_widgets(self):
|
|
# Title
|
|
title_label = tk.Label(self.root, text="🎬 AI Shorts Generator", font=("Arial", 16, "bold"))
|
|
title_label.pack(pady=10)
|
|
|
|
# Video selection
|
|
video_frame = tk.Frame(self.root)
|
|
video_frame.pack(pady=10, padx=20, fill="x")
|
|
|
|
tk.Label(video_frame, text="Select Video File:").pack(anchor="w")
|
|
video_select_frame = tk.Frame(video_frame)
|
|
video_select_frame.pack(fill="x", pady=5)
|
|
|
|
self.video_label = tk.Label(video_select_frame, text="No video selected", bg="white", relief="sunken")
|
|
self.video_label.pack(side="left", fill="x", expand=True, padx=(0, 5))
|
|
|
|
tk.Button(video_select_frame, text="Browse", command=self.select_video).pack(side="right")
|
|
|
|
# Output folder selection
|
|
output_frame = tk.Frame(self.root)
|
|
output_frame.pack(pady=10, padx=20, fill="x")
|
|
|
|
tk.Label(output_frame, text="Output Folder:").pack(anchor="w")
|
|
output_select_frame = tk.Frame(output_frame)
|
|
output_select_frame.pack(fill="x", pady=5)
|
|
|
|
self.output_label = tk.Label(output_select_frame, text="shorts/", bg="white", relief="sunken")
|
|
self.output_label.pack(side="left", fill="x", expand=True, padx=(0, 5))
|
|
|
|
tk.Button(output_select_frame, text="Browse", command=self.select_output_folder).pack(side="right")
|
|
|
|
# Settings frame
|
|
settings_frame = tk.LabelFrame(self.root, text="Settings", padx=10, pady=10)
|
|
settings_frame.pack(pady=10, padx=20, fill="x")
|
|
|
|
# Max clips with on/off toggle
|
|
clips_frame = tk.Frame(settings_frame)
|
|
clips_frame.pack(fill="x", pady=5)
|
|
|
|
clips_left_frame = tk.Frame(clips_frame)
|
|
clips_left_frame.pack(side="left")
|
|
|
|
self.use_max_clips = tk.BooleanVar(value=True)
|
|
clips_checkbox = tk.Checkbutton(clips_left_frame, variable=self.use_max_clips, text="Max Clips to Generate:")
|
|
clips_checkbox.pack(side="left")
|
|
|
|
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.pack(side="right")
|
|
|
|
# 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)
|
|
|
|
# Audio threshold
|
|
threshold_frame = tk.Frame(settings_frame)
|
|
threshold_frame.pack(fill="x", pady=5)
|
|
tk.Label(threshold_frame, text="Audio Threshold (dB):").pack(side="left")
|
|
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.pack(side="right")
|
|
|
|
# Clip duration (increased to 120 seconds max)
|
|
duration_frame = tk.Frame(settings_frame)
|
|
duration_frame.pack(fill="x", pady=5)
|
|
tk.Label(duration_frame, text="Clip Duration (seconds):").pack(side="left")
|
|
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.pack(side="right")
|
|
|
|
# Preview button
|
|
self.preview_btn = tk.Button(self.root, text="🔍 Preview Clips",
|
|
command=self.preview_clips, bg="#2196F3", fg="white",
|
|
font=("Arial", 10, "bold"), pady=5)
|
|
self.preview_btn.pack(pady=10)
|
|
|
|
# Generate button
|
|
self.generate_btn = tk.Button(self.root, text="🎬 Generate Shorts",
|
|
command=self.start_generation, bg="#4CAF50", fg="white",
|
|
font=("Arial", 12, "bold"), pady=10)
|
|
self.generate_btn.pack(pady=20)
|
|
|
|
# Progress frame
|
|
progress_frame = tk.Frame(self.root)
|
|
progress_frame.pack(pady=10, padx=20, fill="x")
|
|
|
|
self.progress_label = tk.Label(progress_frame, text="Ready to generate shorts")
|
|
self.progress_label.pack()
|
|
|
|
self.progress_bar = ttk.Progressbar(progress_frame, length=400, mode="determinate")
|
|
self.progress_bar.pack(pady=5)
|
|
|
|
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 for loud moments
|
|
self.preview_btn.config(state="disabled", text="Analyzing...")
|
|
self.root.update()
|
|
|
|
loud_moments = detect_loud_moments(
|
|
self.video_path,
|
|
chunk_duration=self.duration_var.get(),
|
|
threshold_db=self.threshold_var.get()
|
|
)
|
|
|
|
if not loud_moments:
|
|
messagebox.showinfo("Preview", f"No loud moments found with threshold {self.threshold_var.get()} dB.\nTry lowering the threshold.")
|
|
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(loud_moments)} loud moments:", 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 = loud_moments if not self.use_max_clips.get() else loud_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 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.")
|
|
|
|
generate_shorts(
|
|
self.video_path,
|
|
max_clips=self.clips_var.get() if self.use_max_clips.get() else len(detect_loud_moments(self.video_path, chunk_duration=self.duration_var.get(), threshold_db=self.threshold_var.get())),
|
|
output_folder=self.output_folder,
|
|
progress_callback=self.update_progress,
|
|
threshold_db=self.threshold_var.get(),
|
|
clip_duration=self.duration_var.get()
|
|
)
|
|
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.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 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 GUI mode
|
|
run_gui()
|
|
elif len(sys.argv) < 2:
|
|
print("Usage: python shorts_generator2.py your_video.mp4")
|
|
print(" or: python shorts_generator2.py --gui")
|
|
run_gui() # Default to GUI if no args
|
|
else:
|
|
# Run command line mode
|
|
try:
|
|
generate_shorts(sys.argv[1])
|
|
print("✅ Shorts generation completed successfully!")
|
|
except Exception as e:
|
|
print(f"❌ Error: {str(e)}")
|
|
|