Created
July 27, 2026 21:19
-
-
Save me-suzy/2c91b64c0eecb91aa8eaa51020b8f91d to your computer and use it in GitHub Desktop.
subtitrare.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """Overlay pentru subtitrari SRT, sincronizat manual cu un film redat in browser. | |
| Pornire: dublu-click pe acest fisier sau: python subtitrare.py | |
| Apasa START exact cand pornesti/redai filmul. Foloseste -/+ pentru corectie fina. | |
| """ | |
| import ctypes | |
| import re | |
| import time | |
| import tkinter as tk | |
| from tkinter import ttk | |
| from pathlib import Path | |
| SRT_FILE = Path(r"G:\MedEx\Fin.2012.BRRip.XviD.5rFF.srt") | |
| # Schimbă această valoare dacă subtitrarea este mereu înainte sau în urmă. | |
| # Exemplu: 2.0 = subtitrarea apare cu 2 secunde mai târziu; -2.0 = mai devreme. | |
| INITIAL_OFFSET_SECONDS = 0.0 | |
| def timestamp(value: str) -> float: | |
| hours, minutes, seconds, milliseconds = re.split(r"[:,]", value.strip()) | |
| return int(hours) * 3600 + int(minutes) * 60 + int(seconds) + int(milliseconds) / 1000 | |
| def load_srt(path: Path): | |
| text = path.read_text(encoding="utf-8-sig", errors="replace").replace("\r\n", "\n") | |
| cues = [] | |
| for block in re.split(r"\n\s*\n", text.strip()): | |
| lines = [line.strip("\ufeff") for line in block.split("\n")] | |
| timing_index = next((i for i, line in enumerate(lines) if "-->" in line), None) | |
| if timing_index is None: | |
| continue | |
| start, end = [part.strip() for part in lines[timing_index].split("-->", 1)] | |
| caption = "\n".join(lines[timing_index + 1:]).strip() | |
| if caption: | |
| cues.append((timestamp(start), timestamp(end), caption)) | |
| return cues | |
| class SubtitleOverlay: | |
| def __init__(self, cues): | |
| self.cues = cues | |
| self.started_at = None | |
| self.offset = INITIAL_OFFSET_SECONDS | |
| self.preview_until = 0.0 | |
| self.selected_cue_index = 0 | |
| self.root = tk.Tk() | |
| self.root.withdraw() | |
| self.font_name = tk.StringVar(master=self.root, value="Arial") | |
| self.font_size = tk.IntVar(master=self.root, value=26) | |
| self.font_color = tk.StringVar(master=self.root, value="#FFFFFF") | |
| self.bold = tk.BooleanVar(master=self.root, value=True) | |
| self.italic = tk.BooleanVar(master=self.root, value=False) | |
| self.bottom_offset = tk.IntVar(master=self.root, value=265) | |
| self.screen_w = self.root.winfo_screenwidth() | |
| self.screen_h = self.root.winfo_screenheight() | |
| self.overlay = tk.Toplevel(self.root) | |
| self.overlay.overrideredirect(True) | |
| self.overlay.attributes("-topmost", True) | |
| self.overlay.configure(bg="black") | |
| overlay_width = min(1500, self.screen_w - 80) | |
| overlay_x = (self.screen_w - overlay_width) // 2 | |
| self.overlay_width = overlay_width | |
| self.overlay_x = overlay_x | |
| self.overlay.geometry(f"{overlay_width}x150+{overlay_x}+{self.screen_h - self.bottom_offset.get()}") | |
| self.label = tk.Label(self.overlay, text="", fg="white", bg="black", justify="center", wraplength=overlay_width - 80, padx=28, pady=14) | |
| self.label.pack(fill="both", expand=True) | |
| self.apply_style() | |
| self.controls = tk.Toplevel(self.root) | |
| self.controls.title("Subtitrare") | |
| self.controls.attributes("-topmost", True) | |
| self.controls.resizable(False, False) | |
| self.controls.geometry("480x460") | |
| tk.Label(self.controls, text="Sincronizare subtitrare", font=("Arial", 11, "bold")).pack(padx=12, pady=(10, 4)) | |
| self.status = tk.StringVar(value="Apasă START când pornești filmul") | |
| tk.Label(self.controls, textvariable=self.status).pack(padx=12, pady=(0, 8)) | |
| actions = tk.Frame(self.controls) | |
| actions.pack(padx=10, pady=(0, 10)) | |
| self.start_button = tk.Button(actions, text="START — SINCRONIZEAZĂ ACUM", command=self.sync_first_cue, width=29, bg="#9fc5e8") | |
| self.start_button.grid(row=0, column=0, columnspan=4, pady=(0, 6)) | |
| self.test_button = tk.Button(actions, text="TESTEAZĂ FONTUL ȘI POZIȚIA", command=self.preview_first_cue, bg="#d9d2e9") | |
| self.test_button.grid(row=1, column=0, columnspan=4, pady=(0, 6)) | |
| self.adjust_buttons = [] | |
| for column, (caption, seconds) in enumerate((("− 1 sec", -1.0), ("− 0,1 sec", -0.1), ("+ 0,1 sec", 0.1), ("+ 1 sec", 1.0))): | |
| button = tk.Button(actions, text=caption, bg="#eeeeee") | |
| button.configure(command=lambda value=seconds, source=button: self.adjust(value, source)) | |
| button.grid(row=2, column=column, padx=2) | |
| self.adjust_buttons.append(button) | |
| self.stop_button = tk.Button(actions, text="Oprește", command=self.stop, bg="#f4cccc") | |
| self.stop_button.grid(row=3, column=0, columnspan=4, pady=(6, 0)) | |
| style = tk.LabelFrame(self.controls, text="Aspect subtitrare") | |
| style.pack(fill="x", padx=10, pady=(0, 10)) | |
| tk.Label(style, text="Font:").grid(row=0, column=0, sticky="w", padx=6, pady=4) | |
| ttk.Combobox(style, textvariable=self.font_name, values=("Arial", "Calibri", "Verdana", "Tahoma", "Times New Roman"), width=18).grid(row=0, column=1, sticky="w") | |
| tk.Label(style, text="Mărime:").grid(row=0, column=2, sticky="w", padx=(12, 4)) | |
| tk.Spinbox(style, from_=14, to=54, textvariable=self.font_size, width=5).grid(row=0, column=3, sticky="w") | |
| tk.Label(style, text="Culoare:").grid(row=1, column=0, sticky="w", padx=6, pady=4) | |
| tk.Entry(style, textvariable=self.font_color, width=10).grid(row=1, column=1, sticky="w") | |
| tk.Checkbutton(style, text="Bold", variable=self.bold).grid(row=1, column=2, sticky="w", padx=(12, 0)) | |
| tk.Checkbutton(style, text="Italic", variable=self.italic).grid(row=1, column=3, sticky="w") | |
| tk.Label(style, text="Distanță de jos:").grid(row=2, column=0, sticky="w", padx=6, pady=4) | |
| tk.Spinbox(style, from_=120, to=500, increment=10, textvariable=self.bottom_offset, width=7).grid(row=2, column=1, sticky="w") | |
| tk.Button(style, text="Aplică aspectul", command=self.apply_style, bg="#cfe2f3").grid(row=2, column=2, columnspan=2, pady=4) | |
| tk.Button(self.controls, text="Închide", command=self.close).pack(pady=(0, 10)) | |
| self.controls.protocol("WM_DELETE_WINDOW", self.close) | |
| def apply_style(self): | |
| style = [] | |
| if self.bold.get(): style.append("bold") | |
| if self.italic.get(): style.append("italic") | |
| self.label.configure(font=(self.font_name.get(), self.font_size.get(), *style), fg=self.font_color.get() or "#FFFFFF") | |
| self.overlay.geometry(f"{self.overlay_width}x150+{self.overlay_x}+{self.screen_h - self.bottom_offset.get()}") | |
| def set_caption(self, text): | |
| self.label.configure(text=text) | |
| def start(self): | |
| self.started_at = time.perf_counter() | |
| self.offset = INITIAL_OFFSET_SECONDS | |
| self.start_button.configure(bg="#93c47d", activebackground="#93c47d") | |
| for button in self.adjust_buttons: | |
| button.configure(bg="#eeeeee") | |
| self.status.set("RULEAZĂ — așteaptă prima replică") | |
| def sync_first_cue(self): | |
| """Align the first cue in whatever SRT file is currently configured.""" | |
| if not self.cues: | |
| return | |
| self.started_at = time.perf_counter() - self.cues[0][0] | |
| self.offset = 0.0 | |
| self.start_button.configure(bg="#93c47d", activebackground="#93c47d") | |
| self.start_button.configure(bg="#76a5af", activebackground="#76a5af") | |
| for button in self.adjust_buttons: | |
| button.configure(bg="#eeeeee") | |
| self.status.set("SINCRONIZAT CU PRIMA FRAZĂ DIN SUBTITRARE") | |
| def adjust(self, seconds, source=None): | |
| if self.started_at is None: | |
| self.start() | |
| self.offset += seconds | |
| for button in self.adjust_buttons: | |
| button.configure(bg="#eeeeee") | |
| if source is not None: | |
| source.configure(bg="#ffe599", activebackground="#ffe599") | |
| direction = "+" if self.offset >= 0 else "" | |
| self.status.set(f"CORECȚIE APLICATĂ: {direction}{self.offset:.1f} sec") | |
| def preview_first_cue(self): | |
| if not self.cues: | |
| return | |
| self.set_caption(self.cues[0][2]) | |
| self.preview_until = time.perf_counter() + 4 | |
| self.test_button.configure(bg="#b4a7d6", activebackground="#b4a7d6") | |
| self.status.set("TEST ACTIV — textul trebuie să fie vizibil jos pe ecran") | |
| def stop(self): | |
| self.started_at = None | |
| self.set_caption("") | |
| self.start_button.configure(bg="#d9d9d9", activebackground="#d9d9d9") | |
| self.start_button.configure(bg="#9fc5e8", activebackground="#9fc5e8") | |
| self.status.set("OPRIT") | |
| @staticmethod | |
| def time_text(seconds): | |
| seconds = max(0, int(seconds)) | |
| return f"{seconds // 60:02d}:{seconds % 60:02d}" | |
| def tick(self): | |
| self.overlay.lift() | |
| if self.preview_until and time.perf_counter() >= self.preview_until: | |
| self.preview_until = 0.0 | |
| self.set_caption("") | |
| self.test_button.configure(bg="#d9d2e9", activebackground="#d9d2e9") | |
| if self.started_at is not None: | |
| current = time.perf_counter() - self.started_at + self.offset | |
| caption = next((text for start, end, text in self.cues if start <= current <= end), "") | |
| if not self.preview_until: | |
| self.set_caption(caption) | |
| if not caption: | |
| self.status.set(f"RULEAZĂ — subtitrare: {self.time_text(current)}") | |
| self.root.after(80, self.tick) | |
| def close(self): | |
| self.root.destroy() | |
| def run(self): | |
| self.tick() | |
| self.root.mainloop() | |
| if __name__ == "__main__": | |
| if not SRT_FILE.exists(): | |
| raise SystemExit(f"Nu găsesc subtitrarea: {SRT_FILE}") | |
| SubtitleOverlay(load_srt(SRT_FILE)).run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment