- Added app.py for subtitle positioning tool using Tkinter and MoviePy. - Integrated font selection and adjustable subtitle positioning. - Implemented loading and saving of presets in JSON format. - Added functionality to preview subtitles on video clips. - Enhanced subtitle rendering with highlight effects. - Created app2.py for advanced subtitle handling with SRT file support. - Implemented SRT parsing and subtitle navigation in app2.py. - Added system font detection for better font compatibility. - Updated shorts_generator2.py to include GUI for shorts generation. - Enhanced error handling and progress tracking in shorts generation. - Created subtitle_generator.py for automatic subtitle generation from video. - Added progress bar and user feedback in subtitle generation GUI. - Updated subtitle_gui_presets.json and subtitles.srt for testing.
323 lines
12 KiB
Python
323 lines
12 KiB
Python
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()
|