import tkinter as tk from tkinter import filedialog from moviepy import VideoFileClip, TextClip, CompositeVideoClip import threading import json import re import os import platform def get_system_fonts(): """Get list of available system fonts""" fonts = [] if platform.system() == "Windows": # Common Windows font paths font_paths = [ "C:/Windows/Fonts/", "C:/Windows/System32/Fonts/" ] common_fonts = [] for font_path in font_paths: if os.path.exists(font_path): for file in os.listdir(font_path): if file.endswith(('.ttf', '.otf')): # Extract font name without extension font_name = os.path.splitext(file)[0] # Clean up common variations if 'arial' in font_name.lower() and 'bold' not in font_name.lower(): common_fonts.append('arial.ttf') elif 'times' in font_name.lower() and 'bold' not in font_name.lower(): common_fonts.append('times.ttf') elif 'courier' in font_name.lower() and 'bold' not in font_name.lower(): common_fonts.append('cour.ttf') elif 'comic' in font_name.lower(): common_fonts.append('comic.ttf') elif 'impact' in font_name.lower(): common_fonts.append('impact.ttf') elif 'verdana' in font_name.lower(): common_fonts.append('verdana.ttf') elif 'tahoma' in font_name.lower(): common_fonts.append('tahoma.ttf') # Add found fonts, fallback to common Windows fonts fonts = list(set(common_fonts)) if common_fonts else [ 'arial.ttf', 'times.ttf', 'cour.ttf', 'comic.ttf', 'impact.ttf', 'verdana.ttf', 'tahoma.ttf' ] # Add option to use no font (system default) fonts.insert(0, 'System Default') return fonts AVAILABLE_FONTS = get_system_fonts() # 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": "System Default", "subtitles": [], "current_index": 0 } # Compatible fonts that work across different systems COMPATIBLE_FONTS = [ "Arial", "Times-Roman", "Helvetica", "Courier", "Comic-Sans-MS", "Impact", "Verdana", "Tahoma", "Georgia", "Trebuchet-MS" ] preset_file = "subtitle_gui_presets.json" # === SRT PARSER === def parse_srt(file_path): with open(file_path, 'r', encoding='utf-8') as f: contents = f.read() pattern = r"(\d+)\s+(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})\s+([\s\S]*?)(?=\n\d+|\Z)" matches = re.findall(pattern, contents) subtitles = [] for _, start, end, text in matches: subtitles.append({ "start": srt_time_to_seconds(start), "end": srt_time_to_seconds(end), "text": text.replace('\n', ' ') }) return subtitles def srt_time_to_seconds(time_str): h, m, s_ms = time_str.split(':') s, ms = s_ms.split(',') return int(h)*3600 + int(m)*60 + int(s) + int(ms)/1000 # === PRESETS === def save_presets(): with open(preset_file, "w") as f: json.dump(settings, f) print("📂 Presets saved!") def load_presets(): global settings try: with open(preset_file, "r") as f: loaded = json.load(f) settings.update(loaded) print("✅ Presets loaded!") sync_gui() except FileNotFoundError: print("⚠️ No presets found.") # === SYNC === 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"]) def render_preview(): if not settings["video_path"] or not settings["subtitles"]: print("⚠️ Video or subtitles not loaded.") return sub = settings["subtitles"][settings["current_index"]] subtitle_text = sub["text"] start_time = sub["start"] end_time = sub["end"] duration = end_time - start_time clip = VideoFileClip(settings["video_path"]).subclipped(start_time, end_time) vertical_clip = clip.resized(height=1920).cropped(width=1080, x_center=clip.w / 2) highlight_word = subtitle_text.split()[-1] # Highlight last word for now # Create TextClip with font if specified, otherwise use system default if settings["font"] == "System Default": base_subtitle = TextClip( text=subtitle_text, font_size=settings["font_size_subtitle"], color='white', stroke_color='black', stroke_width=5 ).with_duration(duration).with_position(('center', settings["subtitle_y_px"])) else: try: base_subtitle = TextClip( text=subtitle_text, font=settings["font"], font_size=settings["font_size_subtitle"], color='white', stroke_color='black', stroke_width=5 ).with_duration(duration).with_position(('center', settings["subtitle_y_px"])) except: # Fallback to system default if font fails print(f"⚠️ Font {settings['font']} failed, using system default") base_subtitle = TextClip( text=subtitle_text, font_size=settings["font_size_subtitle"], color='white', stroke_color='black', stroke_width=5 ).with_duration(duration).with_position(('center', settings["subtitle_y_px"])) full_text = subtitle_text.upper() words = full_text.split() try: highlight_index = words.index(highlight_word.upper()) except ValueError: highlight_index = len(words) - 1 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 with same font logic if settings["font"] == "System Default": highlighted_word = TextClip( text=highlight_word, font_size=settings["font_size_highlight"], color='#FFD700', stroke_color='#FF6B35', stroke_width=5 ).with_duration(duration / 2).with_start(duration / 4).with_position((540 + x_offset, settings["subtitle_y_px"] + settings["highlight_offset"])) else: try: highlighted_word = TextClip( text=highlight_word, font=settings["font"], font_size=settings["font_size_highlight"], color='#FFD700', stroke_color='#FF6B35', stroke_width=5 ).with_duration(duration / 2).with_start(duration / 4).with_position((540 + x_offset, settings["subtitle_y_px"] + settings["highlight_offset"])) except: # Fallback to system default if font fails highlighted_word = TextClip( text=highlight_word, font_size=settings["font_size_highlight"], color='#FFD700', stroke_color='#FF6B35', stroke_width=5 ).with_duration(duration / 2).with_start(duration / 4).with_position((540 + x_offset, settings["subtitle_y_px"] + settings["highlight_offset"])) final = CompositeVideoClip([vertical_clip, base_subtitle, highlighted_word], size=(1080, 1920)) # Scale down the preview to fit 1080p monitor (max height ~900px to leave room for taskbar) preview_scale = 900 / 1920 # Scale factor to fit height preview_width = int(1080 * preview_scale) preview_height = int(1920 * preview_scale) preview_clip = final.resized((preview_width, preview_height)) preview_clip.preview(fps=24, audio=False) clip.close() final.close() preview_clip.close() def update_setting(var_name, value): 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 load_srt(): file_path = filedialog.askopenfilename(filetypes=[("SRT Subtitle", "*.srt")]) if file_path: settings["subtitles"] = parse_srt(file_path) settings["current_index"] = 0 print(f"📝 Loaded {len(settings['subtitles'])} subtitles from {file_path}") def next_sub(): if settings["current_index"] < len(settings["subtitles"]) - 1: settings["current_index"] += 1 start_preview_thread() def prev_sub(): if settings["current_index"] > 0: settings["current_index"] -= 1 start_preview_thread() def start_preview_thread(): threading.Thread(target=render_preview).start() # === GUI === root = tk.Tk() root.title("Subtitle Positioning Tool") root.geometry("420x700") load_btn = tk.Button(root, text="🎥 Load Video", command=open_video) load_btn.pack(pady=10) load_srt_btn = tk.Button(root, text="📑 Load SRT Subtitles", 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="Font").pack() font_dropdown_var = tk.StringVar(value=settings["font"]) font_dropdown = tk.OptionMenu(root, font_dropdown_var, *AVAILABLE_FONTS, command=update_font) font_dropdown.pack(pady=5) preview_btn = tk.Button(root, text="▶️ Preview Clip", command=start_preview_thread) preview_btn.pack(pady=10) nav_frame = tk.Frame(root) 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="right", padx=5) nav_frame.pack(pady=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()