- Implemented a new subtitle generator tool using Tkinter for GUI. - Added functionality to load video and SRT files, and display subtitles. - Created text rendering using PIL for better font handling. - Introduced preset saving and loading for subtitle settings and word selections. - Added support for highlighting specific words in subtitles. - Included multiple preset slots for different configurations. - Created JSON files for storing presets and word selections.
601 lines
21 KiB
Python
601 lines
21 KiB
Python
import tkinter as tk
|
|
from tkinter import filedialog
|
|
from moviepy import VideoFileClip, ImageClip, CompositeVideoClip
|
|
import threading
|
|
import json
|
|
import pysrt
|
|
import os
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
import numpy as np
|
|
|
|
def find_system_font():
|
|
"""Find a working system font for PIL"""
|
|
print("Finding system fonts for PIL...")
|
|
fonts_to_try = [
|
|
"C:/Windows/Fonts/arial.ttf",
|
|
"C:/Windows/Fonts/calibri.ttf",
|
|
"C:/Windows/Fonts/times.ttf",
|
|
"C:/Windows/Fonts/verdana.ttf",
|
|
"C:/Windows/Fonts/tahoma.ttf",
|
|
"C:/Windows/Fonts/segoeui.ttf",
|
|
]
|
|
|
|
for font_path in fonts_to_try:
|
|
try:
|
|
if os.path.exists(font_path):
|
|
# Test the font by creating a small text image
|
|
font = ImageFont.truetype(font_path, 20)
|
|
img = Image.new('RGB', (100, 50), color='white')
|
|
draw = ImageDraw.Draw(img)
|
|
draw.text((10, 10), "Test", font=font, fill='black')
|
|
print(f"Using font: {font_path}")
|
|
return font_path
|
|
except Exception as e:
|
|
print(f"Font test failed for {font_path}: {e}")
|
|
continue
|
|
|
|
print("Using default font")
|
|
return None
|
|
|
|
def create_text_image(text, font_size=50, color='white', stroke_color='black', stroke_width=3, font_path=None):
|
|
"""Create a text image using PIL"""
|
|
try:
|
|
# Load font
|
|
if font_path and os.path.exists(font_path):
|
|
font = ImageFont.truetype(font_path, font_size)
|
|
else:
|
|
# Try to use default font
|
|
try:
|
|
font = ImageFont.load_default()
|
|
except:
|
|
# Last resort - create a basic font
|
|
font = ImageFont.load_default()
|
|
|
|
# Get text dimensions
|
|
temp_img = Image.new('RGB', (1, 1))
|
|
temp_draw = ImageDraw.Draw(temp_img)
|
|
|
|
try:
|
|
bbox = temp_draw.textbbox((0, 0), text, font=font)
|
|
text_width = bbox[2] - bbox[0]
|
|
text_height = bbox[3] - bbox[1]
|
|
except:
|
|
# Fallback method for older PIL versions
|
|
text_width, text_height = temp_draw.textsize(text, font=font)
|
|
|
|
# Add padding
|
|
padding = max(stroke_width * 2, 10)
|
|
img_width = text_width + padding * 2
|
|
img_height = text_height + padding * 2
|
|
|
|
# Create transparent image
|
|
img = Image.new('RGBA', (img_width, img_height), (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
# Calculate text position (centered)
|
|
x = (img_width - text_width) // 2
|
|
y = (img_height - text_height) // 2
|
|
|
|
# Convert color names to RGB
|
|
if isinstance(color, str):
|
|
if color == 'white':
|
|
color = (255, 255, 255, 255)
|
|
elif color.startswith('#'):
|
|
# Convert hex to RGB
|
|
hex_color = color.lstrip('#')
|
|
color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + (255,)
|
|
|
|
if isinstance(stroke_color, str):
|
|
if stroke_color == 'black':
|
|
stroke_color = (0, 0, 0, 255)
|
|
elif stroke_color.startswith('#'):
|
|
hex_color = stroke_color.lstrip('#')
|
|
stroke_color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + (255,)
|
|
|
|
# Draw text with stroke
|
|
if stroke_width > 0 and stroke_color:
|
|
# Draw stroke by drawing text in multiple positions
|
|
for dx in range(-stroke_width, stroke_width + 1):
|
|
for dy in range(-stroke_width, stroke_width + 1):
|
|
if dx*dx + dy*dy <= stroke_width*stroke_width:
|
|
draw.text((x + dx, y + dy), text, font=font, fill=stroke_color)
|
|
|
|
# Draw main text
|
|
draw.text((x, y), text, font=font, fill=color)
|
|
|
|
# Convert to numpy array for MoviePy
|
|
img_array = np.array(img)
|
|
|
|
return img_array, img_width, img_height
|
|
|
|
except Exception as e:
|
|
print(f"Text image creation failed: {e}")
|
|
# Create a simple colored rectangle as fallback
|
|
img = Image.new('RGBA', (200, 50), (255, 255, 255, 255))
|
|
return np.array(img), 200, 50
|
|
|
|
# Find working font at startup
|
|
SYSTEM_FONT = find_system_font()
|
|
|
|
# Global settings with defaults (no highlight_word_index here!)
|
|
settings = {
|
|
"subtitle_y_px": 1550,
|
|
"highlight_offset": -8,
|
|
"font_size_subtitle": 65,
|
|
"font_size_highlight": 68,
|
|
"highlight_x_offset": 0,
|
|
"video_path": None,
|
|
"font": "Arial",
|
|
"srt_path": None,
|
|
"highlight_start_time": 0.1, # When highlight appears (seconds)
|
|
"highlight_duration": 1.5 # How long highlight lasts (seconds)
|
|
}
|
|
|
|
preset_file = "subtitle_gui_presets.json"
|
|
word_presets_file = "word_presets.json" # Store word selections per subtitle
|
|
subtitles = []
|
|
current_index = 0
|
|
word_presets = {} # Dictionary to store word index for each subtitle
|
|
|
|
|
|
def save_presets():
|
|
with open(preset_file, "w") as f:
|
|
json.dump(settings, f)
|
|
|
|
# Save word-specific presets
|
|
with open(word_presets_file, "w") as f:
|
|
json.dump(word_presets, f)
|
|
|
|
print("📂 Presets and word selections saved!")
|
|
|
|
|
|
def load_presets():
|
|
global settings, word_presets
|
|
try:
|
|
with open(preset_file, "r") as f:
|
|
loaded = json.load(f)
|
|
settings.update(loaded)
|
|
|
|
# Load word-specific presets
|
|
try:
|
|
with open(word_presets_file, "r") as f:
|
|
word_presets = json.load(f)
|
|
except FileNotFoundError:
|
|
word_presets = {}
|
|
|
|
print("✅ Presets and word selections loaded!")
|
|
sync_gui()
|
|
except FileNotFoundError:
|
|
print("⚠️ No presets found.")
|
|
|
|
|
|
def sync_gui():
|
|
sub_y_slider.set(settings["subtitle_y_px"])
|
|
highlight_slider.set(settings["highlight_offset"])
|
|
highlight_x_slider.set(settings["highlight_x_offset"])
|
|
sub_font_slider.set(settings["font_size_subtitle"])
|
|
highlight_font_slider.set(settings["font_size_highlight"])
|
|
font_dropdown_var.set(settings["font"])
|
|
highlight_start_slider.set(settings["highlight_start_time"])
|
|
highlight_duration_slider.set(settings["highlight_duration"])
|
|
update_highlight_word_display()
|
|
|
|
|
|
def load_srt():
|
|
global subtitles, current_index, word_presets
|
|
path = filedialog.askopenfilename(filetypes=[("SRT files", "*.srt")])
|
|
if path:
|
|
settings["srt_path"] = path
|
|
subtitles = pysrt.open(path)
|
|
current_index = 0
|
|
|
|
# Load word presets for this SRT file
|
|
load_word_presets_for_srt(path)
|
|
|
|
print(f"📜 Loaded subtitles: {path}")
|
|
update_highlight_word_display()
|
|
|
|
|
|
def load_word_presets_for_srt(srt_path):
|
|
"""Load word presets specific to this SRT file"""
|
|
global word_presets
|
|
try:
|
|
srt_key = os.path.basename(srt_path)
|
|
if srt_key in word_presets:
|
|
print(f"✅ Loaded word selections for {srt_key}")
|
|
else:
|
|
word_presets[srt_key] = {}
|
|
print(f"📝 Created new word selection storage for {srt_key}")
|
|
except Exception as e:
|
|
print(f"⚠️ Error loading word presets: {e}")
|
|
word_presets = {}
|
|
|
|
|
|
def get_current_word_index():
|
|
"""Get the word index for the current subtitle"""
|
|
if not subtitles or not settings["srt_path"]:
|
|
return -1 # Default to last word
|
|
|
|
srt_key = os.path.basename(settings["srt_path"])
|
|
subtitle_key = str(current_index)
|
|
|
|
if srt_key in word_presets and subtitle_key in word_presets[srt_key]:
|
|
saved_index = word_presets[srt_key][subtitle_key]
|
|
print(f"🔄 Loading saved word index {saved_index} for subtitle {current_index + 1}")
|
|
return saved_index
|
|
else:
|
|
print(f"🆕 No saved word for subtitle {current_index + 1}, using default (last word)")
|
|
return -1 # Default to last word
|
|
|
|
|
|
def set_current_word_index(word_index):
|
|
"""Set the word index for the current subtitle"""
|
|
if not subtitles or not settings["srt_path"]:
|
|
print("⚠️ Cannot save word index - no subtitles loaded")
|
|
return
|
|
|
|
srt_key = os.path.basename(settings["srt_path"])
|
|
subtitle_key = str(current_index)
|
|
|
|
if srt_key not in word_presets:
|
|
word_presets[srt_key] = {}
|
|
|
|
word_presets[srt_key][subtitle_key] = word_index
|
|
print(f"💾 Saved word selection for subtitle {current_index + 1}: word {word_index + 1}")
|
|
|
|
|
|
def create_safe_textclip(text, font_size=50, color='white', stroke_color=None, stroke_width=0):
|
|
"""Create a text clip using PIL instead of MoviePy TextClip"""
|
|
try:
|
|
# Create text image using PIL
|
|
if stroke_color is None:
|
|
stroke_color = 'black'
|
|
if stroke_width == 0:
|
|
stroke_width = 3
|
|
|
|
img_array, img_width, img_height = create_text_image(
|
|
text=text,
|
|
font_size=font_size,
|
|
color=color,
|
|
stroke_color=stroke_color,
|
|
stroke_width=stroke_width,
|
|
font_path=SYSTEM_FONT
|
|
)
|
|
|
|
# Convert PIL image to MoviePy ImageClip
|
|
from moviepy import ImageClip
|
|
clip = ImageClip(img_array, duration=1)
|
|
|
|
print(f"Created text clip: '{text}' ({img_width}x{img_height})")
|
|
return clip
|
|
|
|
except Exception as e:
|
|
print(f"Text clip creation failed: {e}")
|
|
# Return a simple colored clip as placeholder
|
|
from moviepy import ColorClip
|
|
placeholder = ColorClip(size=(800, 100), color=(255, 255, 255)).with_duration(1)
|
|
return placeholder
|
|
|
|
def render_preview(save_path=None):
|
|
if not settings["video_path"]:
|
|
print("⚠️ No video selected.")
|
|
return
|
|
if not subtitles:
|
|
print("⚠️ No subtitles loaded.")
|
|
return
|
|
|
|
sub = subtitles[current_index]
|
|
subtitle_text = sub.text.replace("\n", " ").strip()
|
|
words = subtitle_text.split()
|
|
|
|
# Get highlight word based on INDIVIDUAL subtitle word index
|
|
if len(words) > 0:
|
|
word_index = get_current_word_index() # Get subtitle-specific word index
|
|
if word_index < 0 or word_index >= len(words):
|
|
word_index = len(words) - 1 # Default to last word
|
|
set_current_word_index(word_index) # Save the default
|
|
highlight_word = words[word_index]
|
|
print(f"🎯 Using word '{highlight_word}' (index {word_index}) for subtitle {current_index + 1}")
|
|
else:
|
|
highlight_word = "word" # Fallback
|
|
|
|
start_time = sub.start.ordinal / 1000.0
|
|
end_time = sub.end.ordinal / 1000.0
|
|
|
|
clip = VideoFileClip(settings["video_path"]).subclipped(start_time, min(end_time, start_time + 3))
|
|
vertical_clip = clip.resized(height=1920).cropped(width=1080, x_center=clip.w / 2)
|
|
|
|
# Create base subtitle using safe method
|
|
try:
|
|
base_subtitle = create_safe_textclip(
|
|
subtitle_text,
|
|
font_size=settings["font_size_subtitle"],
|
|
color='white',
|
|
stroke_color='black',
|
|
stroke_width=5
|
|
).with_duration(clip.duration).with_position(('center', settings["subtitle_y_px"]))
|
|
print("✅ Base subtitle created successfully")
|
|
except Exception as e:
|
|
print(f"❌ Base subtitle creation failed: {e}")
|
|
return
|
|
|
|
full_text = subtitle_text.upper()
|
|
words = full_text.split()
|
|
if highlight_word.upper() not in words:
|
|
highlight_word = words[-1] # Fallback
|
|
highlight_index = words.index(highlight_word.upper())
|
|
chars_before = sum(len(w) + 1 for w in words[:highlight_index])
|
|
char_width = 35
|
|
total_width = len(full_text) * char_width
|
|
x_offset = (chars_before * char_width) - (total_width // 2) + settings["highlight_x_offset"]
|
|
|
|
# Create highlighted word using safe method
|
|
try:
|
|
highlighted_word = create_safe_textclip(
|
|
highlight_word,
|
|
font_size=settings["font_size_highlight"],
|
|
color='#FFD700',
|
|
stroke_color='#FF6B35',
|
|
stroke_width=5
|
|
).with_duration(min(settings["highlight_duration"], clip.duration)).with_start(settings["highlight_start_time"]).with_position((540 + x_offset, settings["subtitle_y_px"] + settings["highlight_offset"]))
|
|
print(f"✅ Highlighted word created successfully (start: {settings['highlight_start_time']}s, duration: {settings['highlight_duration']}s)")
|
|
except Exception as e:
|
|
print(f"❌ Highlighted word creation failed: {e}")
|
|
# Create without highlight if it fails
|
|
highlighted_word = None
|
|
print("⚠️ Continuing without highlight")
|
|
|
|
# Compose final video with or without highlight
|
|
if highlighted_word is not None:
|
|
final = CompositeVideoClip([vertical_clip, base_subtitle, highlighted_word], size=(1080, 1920))
|
|
print("✅ Final video composed with highlight")
|
|
else:
|
|
final = CompositeVideoClip([vertical_clip, base_subtitle], size=(1080, 1920))
|
|
print("✅ Final video composed without highlight")
|
|
|
|
if save_path:
|
|
srt_output_path = os.path.splitext(save_path)[0] + ".srt"
|
|
with open(srt_output_path, "w") as srt_file:
|
|
srt_file.write(sub.__unicode__())
|
|
final.write_videofile(save_path, fps=24)
|
|
print(f"✅ Exported SRT: {srt_output_path}")
|
|
else:
|
|
# Create a smaller preview version to fit 1080p screen better
|
|
# Scale down to 50% size: 540x960 instead of 1080x1920
|
|
preview_final = final.resized(0.5)
|
|
print("🎬 Opening preview window (540x960 - scaled to fit 1080p screen)")
|
|
preview_final.preview(fps=24)
|
|
preview_final.close()
|
|
|
|
clip.close()
|
|
final.close()
|
|
|
|
|
|
def update_setting(var_name, value):
|
|
if var_name in ["highlight_start_time", "highlight_duration"]:
|
|
settings[var_name] = float(value)
|
|
else:
|
|
settings[var_name] = int(value) if var_name.startswith("font_size") or "offset" in var_name or "y_px" in var_name else value
|
|
|
|
|
|
def update_font(value):
|
|
settings["font"] = value
|
|
|
|
|
|
def open_video():
|
|
file_path = filedialog.askopenfilename(filetypes=[("MP4 files", "*.mp4")])
|
|
if file_path:
|
|
settings["video_path"] = file_path
|
|
print(f"📂 Loaded video: {file_path}")
|
|
|
|
|
|
def start_preview_thread():
|
|
threading.Thread(target=render_preview).start()
|
|
|
|
|
|
def export_clip():
|
|
if settings["video_path"]:
|
|
base_name = os.path.splitext(os.path.basename(settings["video_path"]))[0]
|
|
out_path = os.path.join(os.path.dirname(settings["video_path"]), f"{base_name}_clip_exported.mp4")
|
|
threading.Thread(target=render_preview, args=(out_path,)).start()
|
|
print(f"💾 Exporting to: {out_path}")
|
|
|
|
|
|
def prev_sub():
|
|
global current_index
|
|
if current_index > 0:
|
|
current_index -= 1
|
|
print(f"📍 Switched to subtitle {current_index + 1}")
|
|
update_highlight_word_display()
|
|
start_preview_thread()
|
|
|
|
|
|
def next_sub():
|
|
global current_index
|
|
if current_index < len(subtitles) - 1:
|
|
current_index += 1
|
|
print(f"📍 Switched to subtitle {current_index + 1}")
|
|
update_highlight_word_display()
|
|
start_preview_thread()
|
|
|
|
|
|
def prev_highlight_word():
|
|
"""Select the previous word to highlight"""
|
|
if not subtitles:
|
|
return
|
|
|
|
sub = subtitles[current_index]
|
|
words = sub.text.replace("\n", " ").strip().split()
|
|
if len(words) <= 1:
|
|
return
|
|
|
|
current_word_index = get_current_word_index()
|
|
if current_word_index < 0:
|
|
current_word_index = len(words) - 1
|
|
|
|
new_index = current_word_index - 1
|
|
if new_index < 0:
|
|
new_index = len(words) - 1 # Wrap to last word
|
|
|
|
set_current_word_index(new_index)
|
|
update_highlight_word_display()
|
|
start_preview_thread()
|
|
|
|
|
|
def next_highlight_word():
|
|
"""Select the next word to highlight"""
|
|
if not subtitles:
|
|
return
|
|
|
|
sub = subtitles[current_index]
|
|
words = sub.text.replace("\n", " ").strip().split()
|
|
if len(words) <= 1:
|
|
return
|
|
|
|
current_word_index = get_current_word_index()
|
|
if current_word_index < 0:
|
|
current_word_index = len(words) - 1
|
|
|
|
new_index = (current_word_index + 1) % len(words)
|
|
set_current_word_index(new_index)
|
|
update_highlight_word_display()
|
|
start_preview_thread()
|
|
|
|
|
|
def update_highlight_word_display():
|
|
"""Update the display showing which word is selected for highlight"""
|
|
if not subtitles:
|
|
highlight_word_label.config(text="Highlighted Word: None")
|
|
return
|
|
|
|
sub = subtitles[current_index]
|
|
words = sub.text.replace("\n", " ").strip().split()
|
|
|
|
if len(words) > 0:
|
|
word_index = get_current_word_index()
|
|
if word_index < 0 or word_index >= len(words):
|
|
word_index = len(words) - 1
|
|
set_current_word_index(word_index) # Save the default
|
|
|
|
highlight_word = words[word_index]
|
|
highlight_word_label.config(text=f"Highlighted Word: '{highlight_word}' ({word_index + 1}/{len(words)})")
|
|
print(f"🔄 Display updated: subtitle {current_index + 1}, word '{highlight_word}' ({word_index + 1}/{len(words)})")
|
|
else:
|
|
highlight_word_label.config(text="Highlighted Word: None")
|
|
|
|
|
|
def handle_drop(event):
|
|
path = event.data
|
|
if path.endswith(".mp4"):
|
|
settings["video_path"] = path
|
|
print(f"🎥 Dropped video: {path}")
|
|
elif path.endswith(".srt"):
|
|
settings["srt_path"] = path
|
|
global subtitles, current_index
|
|
subtitles = pysrt.open(path)
|
|
current_index = 0
|
|
load_word_presets_for_srt(path)
|
|
print(f"📜 Dropped subtitles: {path}")
|
|
update_highlight_word_display()
|
|
|
|
|
|
# GUI Setup
|
|
root = tk.Tk()
|
|
root.title("Subtitle Positioning Tool - Fixed Version")
|
|
root.geometry("420x800")
|
|
|
|
root.drop_target_register = getattr(root, 'drop_target_register', lambda *args: None)
|
|
root.dnd_bind = getattr(root, 'dnd_bind', lambda *args, **kwargs: None)
|
|
try:
|
|
import tkinterdnd2 as tkdnd
|
|
root.drop_target_register(tkdnd.DND_FILES)
|
|
root.dnd_bind('<<Drop>>', handle_drop)
|
|
except:
|
|
pass
|
|
|
|
load_btn = tk.Button(root, text="🎥 Load Video", command=open_video)
|
|
load_btn.pack(pady=5)
|
|
|
|
load_srt_btn = tk.Button(root, text="📜 Load SRT", command=load_srt)
|
|
load_srt_btn.pack(pady=5)
|
|
|
|
tk.Label(root, text="Subtitle Y Position").pack()
|
|
sub_y_slider = tk.Scale(root, from_=1000, to=1800, orient="horizontal", command=lambda v: update_setting("subtitle_y_px", v))
|
|
sub_y_slider.set(settings["subtitle_y_px"])
|
|
sub_y_slider.pack()
|
|
|
|
tk.Label(root, text="Highlight Y Offset").pack()
|
|
highlight_slider = tk.Scale(root, from_=-100, to=100, orient="horizontal", command=lambda v: update_setting("highlight_offset", v))
|
|
highlight_slider.set(settings["highlight_offset"])
|
|
highlight_slider.pack()
|
|
|
|
tk.Label(root, text="Highlight X Offset").pack()
|
|
highlight_x_slider = tk.Scale(root, from_=-300, to=300, orient="horizontal", command=lambda v: update_setting("highlight_x_offset", v))
|
|
highlight_x_slider.set(settings["highlight_x_offset"])
|
|
highlight_x_slider.pack()
|
|
|
|
tk.Label(root, text="Subtitle Font Size").pack()
|
|
sub_font_slider = tk.Scale(root, from_=30, to=100, orient="horizontal", command=lambda v: update_setting("font_size_subtitle", v))
|
|
sub_font_slider.set(settings["font_size_subtitle"])
|
|
sub_font_slider.pack()
|
|
|
|
tk.Label(root, text="Highlight Font Size").pack()
|
|
highlight_font_slider = tk.Scale(root, from_=30, to=100, orient="horizontal", command=lambda v: update_setting("font_size_highlight", v))
|
|
highlight_font_slider.set(settings["font_size_highlight"])
|
|
highlight_font_slider.pack()
|
|
|
|
tk.Label(root, text="Highlight Start Time (seconds)").pack()
|
|
highlight_start_slider = tk.Scale(root, from_=0.0, to=3.0, resolution=0.1, orient="horizontal", command=lambda v: update_setting("highlight_start_time", v))
|
|
highlight_start_slider.set(settings["highlight_start_time"])
|
|
highlight_start_slider.pack()
|
|
|
|
tk.Label(root, text="Highlight Duration (seconds)").pack()
|
|
highlight_duration_slider = tk.Scale(root, from_=0.1, to=5.0, resolution=0.1, orient="horizontal", command=lambda v: update_setting("highlight_duration", v))
|
|
highlight_duration_slider.set(settings["highlight_duration"])
|
|
highlight_duration_slider.pack()
|
|
|
|
font_dropdown_var = tk.StringVar(value=settings["font"])
|
|
tk.Label(root, text="Font").pack()
|
|
font_dropdown = tk.OptionMenu(
|
|
root,
|
|
font_dropdown_var,
|
|
"Arial",
|
|
"Courier",
|
|
"Times-Roman",
|
|
"Helvetica-Bold",
|
|
"Verdana",
|
|
"Georgia",
|
|
"Impact",
|
|
command=update_font
|
|
)
|
|
font_dropdown.pack(pady=5)
|
|
|
|
# Highlight word selection
|
|
tk.Label(root, text="Select Highlight Word").pack()
|
|
highlight_word_label = tk.Label(root, text="Highlighted Word: None", bg="lightgray", relief="sunken")
|
|
highlight_word_label.pack(pady=5)
|
|
|
|
word_nav_frame = tk.Frame(root)
|
|
word_nav_frame.pack(pady=5)
|
|
tk.Button(word_nav_frame, text="⏮️ Prev Word", command=prev_highlight_word).pack(side="left", padx=2)
|
|
tk.Button(word_nav_frame, text="⏭️ Next Word", command=next_highlight_word).pack(side="left", padx=2)
|
|
|
|
preview_btn = tk.Button(root, text="▶️ Preview Clip", command=start_preview_thread)
|
|
preview_btn.pack(pady=10)
|
|
|
|
export_btn = tk.Button(root, text="💾 Export Clip", command=export_clip)
|
|
export_btn.pack(pady=5)
|
|
|
|
nav_frame = tk.Frame(root)
|
|
nav_frame.pack(pady=5)
|
|
tk.Button(nav_frame, text="⏮️ Prev", command=prev_sub).pack(side="left", padx=5)
|
|
tk.Button(nav_frame, text="⏭️ Next", command=next_sub).pack(side="left", padx=5)
|
|
|
|
save_btn = tk.Button(root, text="📂 Save Preset", command=save_presets)
|
|
save_btn.pack(pady=5)
|
|
|
|
load_preset_btn = tk.Button(root, text="📂 Load Preset", command=load_presets)
|
|
load_preset_btn.pack(pady=5)
|
|
|
|
root.mainloop()
|