ShortGenerator/app2.py
klop51 c82130ec6e feat: Implement subtitle positioning tool with GUI
- 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.
2025-08-06 20:41:10 +02:00

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()