- Improved the rendering of subtitles in preview mode to highlight the currently selected word. - Enhanced export functionality to create highlights for all words in sequence using preset settings. - Updated the GUI to reflect changes in highlight word selection and preset slot navigation. - Added error handling for subtitle creation and improved logging for better debugging. - Introduced a new script for extracting subtitles and generating short clips based on loud moments in the audio. - Updated JSON preset files to remove obsolete fields and ensure compatibility with the new features.
770 lines
29 KiB
Python
770 lines
29 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
|
||
|
||
def debug_moviepy_text():
|
||
"""Debug MoviePy TextClip functionality"""
|
||
print("🔍 Debugging MoviePy TextClip...")
|
||
|
||
# Set the correct ImageMagick path
|
||
imagemagick_path = r"C:\Program Files\ImageMagick-7.1.2-Q16-HDRI\magick.exe"
|
||
os.environ["IMAGEMAGICK_BINARY"] = imagemagick_path
|
||
print(f"<EFBFBD> Setting ImageMagick path: {imagemagick_path}")
|
||
|
||
# Test different approaches to fix the font issue
|
||
methods_to_try = [
|
||
{"name": "Basic TextClip", "params": {"font_size": 20}},
|
||
{"name": "TextClip with method='caption'", "params": {"font_size": 20, "method": "caption"}},
|
||
{"name": "TextClip with method='label'", "params": {"font_size": 20, "method": "label"}},
|
||
{"name": "TextClip color only", "params": {"color": "white"}},
|
||
{"name": "TextClip minimal", "params": {}},
|
||
]
|
||
|
||
for i, method in enumerate(methods_to_try, 1):
|
||
try:
|
||
print(f"Test {i}: {method['name']}...")
|
||
clip = TextClip("test", **method["params"])
|
||
clip.close()
|
||
print(f"✅ {method['name']} works")
|
||
return method["params"]
|
||
except Exception as e:
|
||
print(f"❌ {method['name']} failed: {e}")
|
||
|
||
# Try additional environment variables
|
||
print("🔧 Attempting additional font fixes...")
|
||
try:
|
||
os.environ["FONTCONFIG_PATH"] = "C:/Windows/Fonts"
|
||
os.environ["MAGICK_FONT_PATH"] = "C:/Windows/Fonts"
|
||
|
||
clip = TextClip("test", font_size=20)
|
||
clip.close()
|
||
print("✅ Font fix successful")
|
||
return {"font_size": 20}
|
||
except Exception as e:
|
||
print(f"❌ Font fix failed: {e}")
|
||
|
||
print("❌ All TextClip methods failed")
|
||
print("🆘 For now, the app will work without text overlays")
|
||
return None
|
||
|
||
# Find working font at startup
|
||
SYSTEM_FONT = find_system_font()
|
||
|
||
# Global settings with defaults
|
||
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_word_index": -1, # Index of word to highlight (-1 = last word)
|
||
"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
|
||
current_preset_slot = 1 # Currently selected preset slot
|
||
|
||
|
||
def save_presets():
|
||
global current_preset_slot
|
||
preset_file_numbered = f"subtitle_gui_presets_slot_{current_preset_slot}.json"
|
||
word_presets_file_numbered = f"word_presets_slot_{current_preset_slot}.json"
|
||
|
||
with open(preset_file_numbered, "w") as f:
|
||
json.dump(settings, f)
|
||
|
||
# Save word-specific presets
|
||
with open(word_presets_file_numbered, "w") as f:
|
||
json.dump(word_presets, f)
|
||
|
||
print(f"📂 Preset slot {current_preset_slot} saved!")
|
||
update_preset_display()
|
||
|
||
|
||
def load_presets():
|
||
global settings, word_presets, current_preset_slot
|
||
preset_file_numbered = f"subtitle_gui_presets_slot_{current_preset_slot}.json"
|
||
word_presets_file_numbered = f"word_presets_slot_{current_preset_slot}.json"
|
||
|
||
try:
|
||
with open(preset_file_numbered, "r") as f:
|
||
loaded = json.load(f)
|
||
settings.update(loaded)
|
||
|
||
# Load word-specific presets
|
||
try:
|
||
with open(word_presets_file_numbered, "r") as f:
|
||
word_presets = json.load(f)
|
||
except FileNotFoundError:
|
||
word_presets = {}
|
||
|
||
print(f"✅ Preset slot {current_preset_slot} loaded!")
|
||
sync_gui()
|
||
update_preset_display()
|
||
except FileNotFoundError:
|
||
print(f"⚠️ No preset found in slot {current_preset_slot}.")
|
||
|
||
|
||
def change_preset_slot(slot_number):
|
||
global current_preset_slot
|
||
current_preset_slot = slot_number
|
||
print(f"🔄 Switched to preset slot {current_preset_slot}")
|
||
update_preset_display()
|
||
|
||
|
||
def update_preset_display():
|
||
preset_label.config(text=f"Current Preset Slot: {current_preset_slot}")
|
||
|
||
# Check if preset file exists and update button colors
|
||
preset_file_numbered = f"subtitle_gui_presets_slot_{current_preset_slot}.json"
|
||
if os.path.exists(preset_file_numbered):
|
||
preset_label.config(bg="lightgreen")
|
||
load_preset_btn.config(text=f"📂 Load Preset {current_preset_slot}", state="normal")
|
||
else:
|
||
preset_label.config(bg="lightcoral")
|
||
load_preset_btn.config(text=f"📂 Load Preset {current_preset_slot} (Empty)", state="disabled")
|
||
|
||
save_btn.config(text=f"📂 Save Preset {current_preset_slot}")
|
||
|
||
|
||
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()
|
||
update_preset_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 settings["highlight_word_index"]
|
||
|
||
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]:
|
||
return word_presets[srt_key][subtitle_key]
|
||
else:
|
||
return settings["highlight_word_index"] # Default
|
||
|
||
|
||
def set_current_word_index(word_index):
|
||
"""Set the word index for the current subtitle"""
|
||
if not subtitles or not settings["srt_path"]:
|
||
settings["highlight_word_index"] = word_index
|
||
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()
|
||
|
||
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
|
||
|
||
# For preview mode (not export), show only current selected word
|
||
if save_path is None:
|
||
# Get highlight word based on index for preview
|
||
if len(words) > 0:
|
||
word_index = get_current_word_index() # Use subtitle-specific word index
|
||
if word_index < 0 or word_index >= len(words):
|
||
word_index = len(words) - 1 # Default to last word
|
||
highlight_word = words[word_index]
|
||
else:
|
||
highlight_word = "word" # Fallback
|
||
|
||
full_text = subtitle_text.upper()
|
||
words_upper = full_text.split()
|
||
if highlight_word.upper() not in words_upper:
|
||
highlight_word = words_upper[-1] # Fallback
|
||
highlight_index = words_upper.index(highlight_word.upper())
|
||
chars_before = sum(len(w) + 1 for w in words_upper[: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 single highlighted word for preview
|
||
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"✅ Preview highlighted word created: '{highlight_word}'")
|
||
except Exception as e:
|
||
print(f"❌ Highlighted word creation failed: {e}")
|
||
highlighted_word = None
|
||
|
||
# Compose preview video
|
||
if highlighted_word is not None:
|
||
final = CompositeVideoClip([vertical_clip, base_subtitle, highlighted_word], size=(1080, 1920))
|
||
print("✅ Preview video composed with highlight")
|
||
else:
|
||
final = CompositeVideoClip([vertical_clip, base_subtitle], size=(1080, 1920))
|
||
print("✅ Preview video composed without highlight")
|
||
|
||
else:
|
||
# For export mode, create highlights for ALL words in sequence
|
||
print(f"🎬 Creating export with ALL {len(words)} words highlighted in sequence...")
|
||
|
||
highlighted_clips = []
|
||
full_text = subtitle_text.upper()
|
||
words_upper = full_text.split()
|
||
|
||
# Store current settings to restore later
|
||
original_slot = current_preset_slot
|
||
original_settings = settings.copy()
|
||
|
||
# Calculate timing for each word highlight
|
||
if len(words) > 0:
|
||
time_per_word = clip.duration / len(words)
|
||
|
||
for i, word in enumerate(words):
|
||
try:
|
||
# Calculate preset slot for this word (cycle through slots 1-5)
|
||
word_slot = (i % 5) + 1
|
||
|
||
# Load preset for this word's slot (without changing global current_preset_slot)
|
||
preset_file_numbered = f"subtitle_gui_presets_slot_{word_slot}.json"
|
||
if os.path.exists(preset_file_numbered):
|
||
with open(preset_file_numbered, "r") as f:
|
||
slot_settings = json.load(f)
|
||
# Apply slot settings for this word
|
||
word_font_size = slot_settings.get("font_size_highlight", original_settings["font_size_highlight"])
|
||
word_y_offset = slot_settings.get("highlight_offset", original_settings["highlight_offset"])
|
||
word_x_offset = slot_settings.get("highlight_x_offset", original_settings["highlight_x_offset"])
|
||
print(f"📂 Using preset slot {word_slot} for word '{word}' (font: {word_font_size}, y: {word_y_offset}, x: {word_x_offset})")
|
||
else:
|
||
# Use original settings if preset doesn't exist
|
||
word_font_size = original_settings["font_size_highlight"]
|
||
word_y_offset = original_settings["highlight_offset"]
|
||
word_x_offset = original_settings["highlight_x_offset"]
|
||
print(f"⚠️ No preset in slot {word_slot}, using defaults for word '{word}'")
|
||
|
||
# Calculate position for this word
|
||
chars_before = sum(len(w) + 1 for w in words_upper[:i])
|
||
char_width = 35
|
||
total_width = len(full_text) * char_width
|
||
x_offset = (chars_before * char_width) - (total_width // 2) + word_x_offset
|
||
|
||
# Calculate timing for this word
|
||
word_start_time = i * time_per_word
|
||
word_duration = min(time_per_word * 1.5, original_settings["highlight_duration"]) # Slight overlap
|
||
|
||
# Create highlighted word clip with slot-specific settings
|
||
highlighted_word = create_safe_textclip(
|
||
word,
|
||
font_size=word_font_size,
|
||
color='#FFD700',
|
||
stroke_color='#FF6B35',
|
||
stroke_width=5
|
||
).with_duration(word_duration).with_start(word_start_time).with_position((540 + x_offset, original_settings["subtitle_y_px"] + word_y_offset))
|
||
|
||
highlighted_clips.append(highlighted_word)
|
||
print(f"✅ Created highlight {i+1}/{len(words)}: '{word}' at {word_start_time:.1f}s with slot {word_slot} settings")
|
||
|
||
except Exception as e:
|
||
print(f"❌ Failed to create highlight for word '{word}': {e}")
|
||
|
||
# No need to restore settings since we didn't modify the global variables
|
||
|
||
# Compose final video with all highlights
|
||
all_clips = [vertical_clip, base_subtitle] + highlighted_clips
|
||
final = CompositeVideoClip(all_clips, size=(1080, 1920))
|
||
print(f"✅ Export video composed with {len(highlighted_clips)} word highlights using different presets")
|
||
|
||
if save_path:
|
||
srt_output_path = os.path.splitext(save_path)[0] + ".srt"
|
||
with open(srt_output_path, "w", encoding='utf-8') as srt_file:
|
||
srt_file.write(str(sub))
|
||
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
|
||
update_highlight_word_display()
|
||
start_preview_thread()
|
||
|
||
|
||
def next_sub():
|
||
global current_index
|
||
if current_index < len(subtitles) - 1:
|
||
current_index += 1
|
||
update_highlight_word_display()
|
||
start_preview_thread()
|
||
|
||
|
||
def prev_highlight_word():
|
||
"""Switch to the previous preset slot, select previous word, AND load the preset"""
|
||
global current_preset_slot
|
||
|
||
# First, change the highlighted word
|
||
if subtitles:
|
||
sub = subtitles[current_index]
|
||
words = sub.text.replace("\n", " ").strip().split()
|
||
if len(words) > 1:
|
||
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()
|
||
|
||
# Then, switch to previous preset slot
|
||
new_slot = current_preset_slot - 1
|
||
if new_slot < 1:
|
||
new_slot = 5 # Wrap to last slot
|
||
|
||
change_preset_slot(new_slot)
|
||
|
||
# Automatically load the preset for this slot
|
||
load_presets()
|
||
|
||
# Start preview to show changes
|
||
start_preview_thread()
|
||
|
||
|
||
def next_highlight_word():
|
||
"""Switch to the next preset slot, select next word, AND load the preset"""
|
||
global current_preset_slot
|
||
|
||
# First, change the highlighted word
|
||
if subtitles:
|
||
sub = subtitles[current_index]
|
||
words = sub.text.replace("\n", " ").strip().split()
|
||
if len(words) > 1:
|
||
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()
|
||
|
||
# Then, switch to next preset slot
|
||
new_slot = current_preset_slot + 1
|
||
if new_slot > 5:
|
||
new_slot = 1 # Wrap to first slot
|
||
|
||
change_preset_slot(new_slot)
|
||
|
||
# Automatically load the preset for this slot
|
||
load_presets()
|
||
|
||
# Start preview to show changes
|
||
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)})")
|
||
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
|
||
print(f"📜 Dropped subtitles: {path}")
|
||
|
||
|
||
# GUI Setup
|
||
root = tk.Tk()
|
||
root.title("Subtitle Positioning Tool")
|
||
root.geometry("420x850")
|
||
|
||
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)
|
||
|
||
# Preset Slot Navigation + Word Selection
|
||
tk.Label(root, text="Preset Slot Navigation + Word Selection").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 Slot+Word", command=prev_highlight_word).pack(side="left", padx=2)
|
||
tk.Button(word_nav_frame, text="⏭️ Next Slot+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)
|
||
|
||
# Preset slot selection
|
||
tk.Label(root, text="Preset Slots").pack()
|
||
preset_label = tk.Label(root, text="Current Preset Slot: 1", bg="lightcoral", relief="sunken")
|
||
preset_label.pack(pady=2)
|
||
|
||
preset_slot_frame = tk.Frame(root)
|
||
preset_slot_frame.pack(pady=2)
|
||
tk.Button(preset_slot_frame, text="Slot 1", command=lambda: change_preset_slot(1)).pack(side="left", padx=1)
|
||
tk.Button(preset_slot_frame, text="Slot 2", command=lambda: change_preset_slot(2)).pack(side="left", padx=1)
|
||
tk.Button(preset_slot_frame, text="Slot 3", command=lambda: change_preset_slot(3)).pack(side="left", padx=1)
|
||
tk.Button(preset_slot_frame, text="Slot 4", command=lambda: change_preset_slot(4)).pack(side="left", padx=1)
|
||
tk.Button(preset_slot_frame, text="Slot 5", command=lambda: change_preset_slot(5)).pack(side="left", padx=1)
|
||
|
||
save_btn = tk.Button(root, text="📂 Save Preset 1", command=save_presets)
|
||
save_btn.pack(pady=5)
|
||
|
||
load_preset_btn = tk.Button(root, text="📂 Load Preset 1", command=load_presets)
|
||
load_preset_btn.pack(pady=5)
|
||
|
||
root.mainloop()
|