Created
July 29, 2026 20:55
-
-
Save me-suzy/85b7539fd3b71a7228d1b2550a57ac87 to your computer and use it in GitHub Desktop.
subtitrare FINAL bun.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 json | |
| import re | |
| import time | |
| import tkinter as tk | |
| from tkinter import ttk, colorchooser, filedialog | |
| from pathlib import Path | |
| SRT_FILE = Path(r"G:\MedEx\Fin.2012.BRRip.XviD.5rFF.srt") | |
| CONFIG_FILE = Path(__file__).with_name("subtitrare_config.json") | |
| # 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, source_path): | |
| self.cues = cues | |
| self.source_path = source_path | |
| self.started_at = None | |
| self.offset = INITIAL_OFFSET_SECONDS | |
| self.paused_at = self.load_pause_position() | |
| 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") | |
| self.overlay.attributes("-transparentcolor", "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(True, True) | |
| self.controls.minsize(360, 400) | |
| self.controls.geometry("620x620") | |
| 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)) | |
| self.pause_info = tk.StringVar() | |
| tk.Label(self.controls, textvariable=self.pause_info, fg="#9c5700", font=("Arial", 10, "bold")).pack(padx=12, pady=(0, 6)) | |
| self.pause_caption = tk.StringVar() | |
| tk.Label(self.controls, textvariable=self.pause_caption, fg="#CC0000", font=("Arial", 11, "bold"), wraplength=570, justify="center").pack(padx=18, pady=(0, 8)) | |
| self.refresh_pause_info() | |
| self.file_name = tk.StringVar(value="Subtitrare: " + self.source_path.name) | |
| tk.Label(self.controls, textvariable=self.file_name, wraplength=440, fg="#1f4e78").pack(padx=12, pady=(0, 6)) | |
| tk.Button(self.controls, text="ÎNCARCĂ SUBTITRARE…", command=self.load_subtitle, bg="#cfe2f3").pack(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="PAUZĂ", command=self.pause_resume, bg="#f4cccc") | |
| self.stop_button.grid(row=3, column=0, columnspan=2, pady=(6, 0), padx=2) | |
| self.pause_button = tk.Button(actions, text="CONTINUĂ", command=self.pause_resume, bg="#93c47d", state="disabled") | |
| self.pause_button.grid(row=3, column=2, columnspan=2, pady=(6, 0), padx=2) | |
| if self.paused_at is not None: | |
| self.pause_button.configure(state="normal") | |
| self.stop_button.configure(state="disabled") | |
| self.reset_button = tk.Button(actions, text="RESET — DE LA ÎNCEPUT", command=self.reset_to_start, bg="#ead1dc") | |
| self.reset_button.grid(row=4, 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.Button(style, text="Alege…", command=self.choose_color).grid(row=1, column=1, sticky="e") | |
| 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 choose_color(self): | |
| _, hex_color = colorchooser.askcolor(color=self.font_color.get(), parent=self.controls, title="Alege culoarea subtitrării") | |
| if hex_color: | |
| self.font_color.set(hex_color.upper()) | |
| self.apply_style() | |
| def set_caption(self, text): | |
| self.label.configure(text=text) | |
| def read_config(self): | |
| try: | |
| return json.loads(CONFIG_FILE.read_text(encoding="utf-8")) | |
| except Exception: | |
| return {} | |
| def save_config(self): | |
| config = self.read_config() | |
| config["subtitle_file"] = str(self.source_path) | |
| pauses = config.setdefault("pause_times", {}) | |
| if self.paused_at is None: | |
| pauses.pop(str(self.source_path), None) | |
| else: | |
| pauses[str(self.source_path)] = self.paused_at | |
| CONFIG_FILE.write_text(json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8") | |
| def load_pause_position(self): | |
| value = self.read_config().get("pause_times", {}).get(str(self.source_path)) | |
| return float(value) if value is not None else None | |
| def refresh_pause_info(self): | |
| if self.paused_at is None: | |
| self.pause_info.set("Nu există o pauză salvată") | |
| self.pause_caption.set("") | |
| else: | |
| self.pause_info.set(f"Pauză salvată la: {self.time_text(self.paused_at)}") | |
| previous = next((text for start, _, text in reversed(self.cues) if start <= self.paused_at), "") | |
| self.pause_caption.set("Ultima subtitrare: " + previous.replace("\n", " / ")) | |
| def current_subtitle_time(self): | |
| if self.started_at is None: | |
| return self.paused_at or 0.0 | |
| return time.perf_counter() - self.started_at + self.offset | |
| def pause_resume(self): | |
| if self.started_at is not None: | |
| self.paused_at = self.current_subtitle_time() | |
| self.started_at = None | |
| self.set_caption("") | |
| self.save_config() | |
| self.refresh_pause_info() | |
| self.pause_button.configure(state="normal") | |
| self.stop_button.configure(state="disabled") | |
| self.status.set("PAUZAT — timpul a fost salvat") | |
| elif self.paused_at is not None: | |
| self.started_at = time.perf_counter() - self.paused_at - self.offset | |
| self.pause_button.configure(state="disabled") | |
| self.stop_button.configure(state="normal") | |
| self.status.set("RULEAZĂ DIN TIMPUL SALVAT") | |
| def reset_to_start(self): | |
| self.started_at = None | |
| self.paused_at = None | |
| self.offset = INITIAL_OFFSET_SECONDS | |
| self.set_caption("") | |
| self.save_config() | |
| self.refresh_pause_info() | |
| self.pause_button.configure(state="disabled") | |
| self.stop_button.configure(state="normal") | |
| self.status.set("RESETAT — apasă START când apare prima frază din film") | |
| def load_subtitle(self): | |
| selected = filedialog.askopenfilename( | |
| parent=self.controls, | |
| title="Alege fișierul de subtitrare", | |
| filetypes=(("Subtitrări SRT", "*.srt"), ("Toate fișierele", "*.*")), | |
| ) | |
| if not selected: | |
| return | |
| path = Path(selected) | |
| try: | |
| cues = load_srt(path) | |
| if not cues: | |
| raise ValueError("Fișierul nu conține replici SRT valide.") | |
| self.cues = cues | |
| self.source_path = path | |
| self.selected_cue_index = 0 | |
| self.paused_at = self.load_pause_position() | |
| self.stop() | |
| self.file_name.set("Subtitrare: " + path.name) | |
| self.save_config() | |
| self.refresh_pause_info() | |
| self.status.set(f"Încărcată și memorată: {len(cues)} replici") | |
| except Exception as error: | |
| self.status.set("Nu pot încărca subtitrarea: " + str(error)) | |
| 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.paused_at = None | |
| self.save_config() | |
| self.refresh_pause_info() | |
| self.pause_button.configure(state="disabled") | |
| self.stop_button.configure(state="normal") | |
| 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.pause_button.configure(state="disabled") | |
| self.stop_button.configure(state="normal") | |
| 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__": | |
| subtitle_path = SRT_FILE | |
| if CONFIG_FILE.exists(): | |
| try: | |
| saved = Path(json.loads(CONFIG_FILE.read_text(encoding="utf-8")).get("subtitle_file", "")) | |
| if saved.exists(): | |
| subtitle_path = saved | |
| except Exception: | |
| pass | |
| if not subtitle_path.exists(): | |
| raise SystemExit(f"Nu găsesc subtitrarea: {subtitle_path}") | |
| SubtitleOverlay(load_srt(subtitle_path), subtitle_path).run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment