From 5108f53cae4b54f785a6f5397c26e6dcd04b2949 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Wed, 15 Oct 2025 06:49:31 +0000 Subject: [PATCH 01/31] Add PGS to SRT OCR conversion feature - Add dropdown menu for PGS subtitle tracks with OCR option - Auto-detect Tesseract OCR on all drives and Windows registry - Add settings panel with dependency status display - Support for converting image-based PGS to editable SRT - Handles language code conversion and environment setup - Includes comprehensive error handling and user guidance --- FastFlix_Windows_OneFile.spec | 3 + fastflix/models/config.py | 78 ++++++++++++ fastflix/widgets/background_tasks.py | 142 +++++++++++++++++++++- fastflix/widgets/panels/subtitle_panel.py | 33 ++++- fastflix/widgets/settings.py | 40 +++++- 5 files changed, 290 insertions(+), 6 deletions(-) diff --git a/FastFlix_Windows_OneFile.spec b/FastFlix_Windows_OneFile.spec index 55d18d9e..da391c44 100644 --- a/FastFlix_Windows_OneFile.spec +++ b/FastFlix_Windows_OneFile.spec @@ -27,6 +27,9 @@ all_imports.remove("python-box") all_imports.append("box") all_imports.append("iso639") +# Add pgsrip for OCR support +all_imports.extend(["pgsrip", "pytesseract", "cv2", "numpy", "pysrt", "babelfish", "cleanit"]) + portable_file = "fastflix\\portable.py" with open(portable_file, "w") as portable: portable.write(" ") diff --git a/fastflix/models/config.py b/fastflix/models/config.py index d4f5ee0e..64e43e9c 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -99,6 +99,77 @@ def where(filename: str, portable_mode=False) -> Path | None: return None +def find_ocr_tool(name): + """Find OCR tools (tesseract, mkvmerge, pgsrip) similar to how we find FFmpeg""" + # Check environment variable + if ocr_location := os.getenv(f"FF_{name.upper()}"): + return Path(ocr_location).absolute() + + # Check system PATH + if (ocr_location := shutil.which(name)) is not None: + return Path(ocr_location).absolute() + + # Special handling for tesseract on Windows (not in PATH by default) + if name == "tesseract" and win_based: + # Check common install locations on all drives + import string + drives = [f"{d}:" for d in string.ascii_uppercase if Path(f"{d}:/").exists()] + + for drive in drives: + common_paths = [ + Path(f"{drive}/Program Files/Tesseract-OCR/tesseract.exe"), + Path(f"{drive}/Program Files (x86)/Tesseract-OCR/tesseract.exe"), + ] + for path in common_paths: + if path.exists(): + return path + + # Check Windows registry for Tesseract install location + try: + import winreg + # Try HKEY_LOCAL_MACHINE first (system-wide install) + for root_key in [winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER]: + try: + key = winreg.OpenKey(root_key, r"SOFTWARE\Tesseract-OCR") + install_path = winreg.QueryValueEx(key, "InstallDir")[0] + winreg.CloseKey(key) + tesseract_exe = Path(install_path) / "tesseract.exe" + if tesseract_exe.exists(): + return tesseract_exe + except (FileNotFoundError, OSError): + pass + except ImportError: + pass + + # Special handling for mkvmerge on Windows + if name == "mkvmerge" and win_based: + import string + drives = [f"{d}:" for d in string.ascii_uppercase if Path(f"{d}:/").exists()] + + for drive in drives: + common_paths = [ + Path(f"{drive}/Program Files/MKVToolNix/mkvmerge.exe"), + Path(f"{drive}/Program Files (x86)/MKVToolNix/mkvmerge.exe"), + ] + for path in common_paths: + if path.exists(): + return path + + # Check in FastFlix OCR tools folder + ocr_folder = Path(user_data_dir("FastFlix_OCR", appauthor=False, roaming=True)) + if ocr_folder.exists(): + for file in ocr_folder.iterdir(): + if file.is_file() and file.name.lower() in (name, f"{name}.exe"): + return file + # Check bin subfolder + if (ocr_folder / "bin").exists(): + for file in (ocr_folder / "bin").iterdir(): + if file.is_file() and file.name.lower() in (name, f"{name}.exe"): + return file + + return None + + class Config(BaseModel): version: str = __version__ config_path: Path = Field(default_factory=get_config) @@ -168,6 +239,13 @@ class Config(BaseModel): disable_cover_extraction: bool = False + # PGS to SRT OCR Settings + enable_pgs_ocr: bool = False + tesseract_path: Path | None = Field(default_factory=lambda: find_ocr_tool("tesseract")) + mkvmerge_path: Path | None = Field(default_factory=lambda: find_ocr_tool("mkvmerge")) + pgsrip_path: Path | None = Field(default_factory=lambda: find_ocr_tool("pgsrip")) + pgs_ocr_language: str = "eng" + def encoder_opt(self, profile_name, profile_option_name): encoder_settings = getattr(self.profiles[self.selected_profile], profile_name) if encoder_settings: diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 75421799..01e5060a 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import logging import os +import shutil from pathlib import Path from subprocess import PIPE, STDOUT, Popen, run, check_output from packaging import version @@ -46,13 +47,14 @@ def run(self): class ExtractSubtitleSRT(QtCore.QThread): - def __init__(self, app: FastFlixApp, main, index, signal, language): + def __init__(self, app: FastFlixApp, main, index, signal, language, use_ocr=False): super().__init__(main) self.main = main self.app = app self.index = index self.signal = signal self.language = language + self.use_ocr = use_ocr def run(self): subtitle_format = self._get_subtitle_format() @@ -63,6 +65,9 @@ def run(self): self.signal.emit() return + # Flag to track if we need OCR conversion after extraction + should_convert_to_srt = False + if subtitle_format == "srt": extension = "srt" output_args = ["-c", "srt", "-f", "srt"] @@ -75,6 +80,8 @@ def run(self): elif subtitle_format == "pgs": extension = "sup" output_args = ["-c", "copy"] + # If OCR is requested, we'll extract .sup first, then convert after + should_convert_to_srt = self.use_ocr and self.app.fastflix.config.enable_pgs_ocr else: self.main.thread_logging_signal.emit( f"WARNING:{t('Subtitle Track')} {self.index} {t('is not in supported format (SRT, ASS, SSA, PGS), skipping extraction')}: {subtitle_format}" @@ -115,6 +122,13 @@ def run(self): ) else: self.main.thread_logging_signal.emit(f"INFO:{t('Extracted subtitles successfully')}") + + # If this is PGS and OCR was requested, convert the .sup to .srt + if subtitle_format == "pgs" and should_convert_to_srt: + if self._convert_sup_to_srt(filename): + self.main.thread_logging_signal.emit(f"INFO:{t('Successfully converted to SRT with OCR')}") + else: + self.main.thread_logging_signal.emit(f"WARNING:{t('OCR conversion failed, kept .sup file')}") self.signal.emit() def _get_subtitle_format(self): @@ -164,6 +178,132 @@ def _get_subtitle_format(self): ) return None + def _check_pgsrip_dependencies(self) -> bool: + """Check all required dependencies for pgsrip OCR conversion""" + missing = [] + + # Check tesseract (auto-detected from PATH or config) + if not self.app.fastflix.config.tesseract_path: + missing.append("tesseract-ocr") + + # Check mkvmerge (CRITICAL - required by pgsrip but not documented) + if not self.app.fastflix.config.mkvmerge_path: + missing.append("mkvtoolnix") + + # Check pgsrip + if not self.app.fastflix.config.pgsrip_path: + missing.append("pgsrip") + + if missing: + self.main.thread_logging_signal.emit( + f"ERROR:{t('Missing dependencies for PGS OCR')}: {', '.join(missing)}\n\n" + f"Install instructions:\n" + f" Windows: Run setup_pgs_ocr_windows.bat in FastFlix folder\n" + f" Linux: sudo apt install tesseract-ocr mkvtoolnix && pip install pgsrip\n" + f" macOS: brew install tesseract mkvtoolnix && pip install pgsrip\n\n" + f"Or download manually:\n" + f" Tesseract: https://github.com/UB-Mannheim/tesseract/wiki\n" + f" MKVToolNix: https://mkvtoolnix.download/downloads.html\n" + f" pgsrip: pip install pgsrip" + ) + return False + + return True + + def _convert_sup_to_srt(self, sup_filepath: str) -> bool: + """Convert an already-extracted .sup file to .srt using pgsrip OCR + + Args: + sup_filepath: Path to the extracted .sup file + + Returns: + True if conversion successful, False otherwise + """ + # Check dependencies first + if not self._check_pgsrip_dependencies(): + return False + + try: + self.main.thread_logging_signal.emit( + f"INFO:{t('Converting .sup to .srt using OCR')} (this may take 3-5 minutes)..." + ) + + # Convert 3-letter language code to 2-letter for pgsrip + # pgsrip uses 2-letter codes in filenames (e.g., "en" not "eng") + from fastflix.language import Language + try: + lang_2letter = Language(self.language).pt1 # Convert eng -> en + except: + lang_2letter = "en" # Default to English if conversion fails + + # Rename .sup file to use 2-letter language code (what pgsrip expects) + sup_path = Path(sup_filepath) + if f".{self.language}." in sup_path.name: + # Replace 3-letter with 2-letter in filename + new_name = sup_path.name.replace(f".{self.language}.", f".{lang_2letter}.") + new_sup_path = sup_path.parent / new_name + sup_path.rename(new_sup_path) + sup_filepath = str(new_sup_path) + + # Run pgsrip on the already-extracted .sup file + pgsrip_cmd = str(self.app.fastflix.config.pgsrip_path) if self.app.fastflix.config.pgsrip_path else "pgsrip" + + # Set environment variables for pgsrip to find tesseract + import os + env = os.environ.copy() + if self.app.fastflix.config.tesseract_path: + # Add tesseract directory to PATH so pytesseract can find it + tesseract_dir = str(Path(self.app.fastflix.config.tesseract_path).parent) + env['PATH'] = f"{tesseract_dir}{os.pathsep}{env.get('PATH', '')}" + env['TESSERACT_CMD'] = str(self.app.fastflix.config.tesseract_path) + + pgsrip_result = run( + [ + pgsrip_cmd, + "--language", lang_2letter, # Use 2-letter code (e.g., "en", "es", "fr") + "--force", # Overwrite existing files + sup_filepath + ], + capture_output=True, + text=True, + timeout=600, # 10 minute timeout for OCR + env=env # Pass environment with TESSERACT_CMD + ) + + if pgsrip_result.returncode != 0: + error_msg = pgsrip_result.stderr if pgsrip_result.stderr else pgsrip_result.stdout + raise Exception(f"pgsrip failed with return code {pgsrip_result.returncode}: {error_msg}") + + # pgsrip creates .srt file in same directory as .sup file + sup_path = Path(sup_filepath) + expected_srt = sup_path.with_suffix('.srt') + + if not expected_srt.exists(): + # Look for any .srt file created near the .sup + srt_files = list(sup_path.parent.glob("*.srt")) + if not srt_files: + raise Exception(f"pgsrip completed but no .srt file found in {sup_path.parent}") + expected_srt = srt_files[0] + + self.main.thread_logging_signal.emit( + f"INFO:{t('OCR conversion successful')}: {expected_srt.name}" + ) + + # Optionally delete the .sup file since we have .srt now + try: + sup_path.unlink() + self.main.thread_logging_signal.emit(f"INFO:{t('Removed .sup file, kept .srt')}") + except: + pass + + return True + + except Exception as err: + self.main.thread_logging_signal.emit( + f"ERROR:{t('OCR conversion failed')}: {err}" + ) + return False + class AudioNoramlize(QtCore.QThread): def __init__(self, app: FastFlixApp, main, audio_type, signal): diff --git a/fastflix/widgets/panels/subtitle_panel.py b/fastflix/widgets/panels/subtitle_panel.py index ff814210..90a0e556 100644 --- a/fastflix/widgets/panels/subtitle_panel.py +++ b/fastflix/widgets/panels/subtitle_panel.py @@ -106,8 +106,32 @@ def __init__(self, app, parent, index, enabled=True, first=False): {t("Cannot remove afterwards!")} """ ) - self.widgets.extract = QtWidgets.QPushButton(t("Extract")) - self.widgets.extract.clicked.connect(self.extract) + + # Setup extract button with OCR option for PGS subtitles + if sub_track.subtitle_type == "pgs": + self.widgets.extract = QtWidgets.QPushButton(t("Extract")) + extract_menu = QtWidgets.QMenu(self) + + # Always offer .sup extraction (fast, no dependencies) + extract_menu.addAction(t("Extract as .sup (image - fast)"), lambda: self.extract(use_ocr=False)) + + # Check if OCR dependencies are available + ocr_action = extract_menu.addAction(t("Convert to .srt (OCR - 3-5 min)"), lambda: self.extract(use_ocr=True)) + + # Enable OCR option only if user enabled it AND dependencies are available + if not self.app.fastflix.config.enable_pgs_ocr: + ocr_action.setEnabled(False) + ocr_action.setToolTip(t("Enable in Settings > 'Enable PGS to SRT OCR conversion'")) + elif not (self.app.fastflix.config.tesseract_path and + self.app.fastflix.config.mkvmerge_path and + self.app.fastflix.config.pgsrip_path): + ocr_action.setEnabled(False) + ocr_action.setToolTip(t("Missing dependencies: tesseract, mkvtoolnix, or pgsrip")) + + self.widgets.extract.setMenu(extract_menu) + else: + self.widgets.extract = QtWidgets.QPushButton(t("Extract")) + self.widgets.extract.clicked.connect(self.extract) self.gif_label = QtWidgets.QLabel(self) self.movie = QtGui.QMovie(loading_movie) @@ -167,9 +191,10 @@ def init_move_buttons(self): layout.addWidget(self.widgets.down_button) return layout - def extract(self): + def extract(self, use_ocr=False): worker = ExtractSubtitleSRT( - self.parent.app, self.parent.main, self.index, self.extract_completed_signal, language=self.language + self.parent.app, self.parent.main, self.index, self.extract_completed_signal, + language=self.language, use_ocr=use_ocr ) worker.start() self.gif_label.show() diff --git a/fastflix/widgets/settings.py b/fastflix/widgets/settings.py index 4dfd5a47..4bd7b089 100644 --- a/fastflix/widgets/settings.py +++ b/fastflix/widgets/settings.py @@ -269,8 +269,19 @@ def in_dir(): self.disable_deinterlace_button = QtWidgets.QCheckBox(t("Disable interlace check")) self.disable_deinterlace_button.setChecked(self.app.fastflix.config.disable_deinterlace_check) - # Layouts + # PGS OCR Settings + self.enable_pgs_ocr = QtWidgets.QCheckBox(t("Enable PGS to SRT OCR conversion")) + self.enable_pgs_ocr.setChecked(self.app.fastflix.config.enable_pgs_ocr) + self.enable_pgs_ocr.setToolTip( + t("Convert image-based PGS subtitles to text SRT using OCR.\n" + "Typically takes 3-5 minutes per movie.") + ) + # Dependency status + self.ocr_status_label = QtWidgets.QLabel() + self.update_ocr_dependency_status() + + # Layouts layout.addWidget(self.use_sane_audio, 7, 0, 1, 2) layout.addWidget(self.disable_version_check, 8, 0, 1, 2) layout.addWidget(QtWidgets.QLabel(t("GUI Logging Level")), 9, 0) @@ -286,6 +297,8 @@ def in_dir(): layout.addWidget(self.clean_old_logs_button, 21, 0, 1, 3) layout.addWidget(self.disable_end_message, 22, 0, 1, 3) layout.addWidget(self.disable_deinterlace_button, 23, 0, 1, 3) + layout.addWidget(self.enable_pgs_ocr, 24, 0, 1, 2) + layout.addWidget(self.ocr_status_label, 24, 2, 1, 1) button_layout = QtWidgets.QHBoxLayout() button_layout.addStretch() @@ -296,6 +309,30 @@ def in_dir(): self.setLayout(layout) + def update_ocr_dependency_status(self): + """Update the OCR dependency status display""" + # Use config paths which use find_ocr_tool() - handles non-PATH locations + tesseract_ok = self.app.fastflix.config.tesseract_path is not None + mkvmerge_ok = self.app.fastflix.config.mkvmerge_path is not None + pgsrip_ok = self.app.fastflix.config.pgsrip_path is not None + + status_parts = [] + status_parts.append("✓ tesseract" if tesseract_ok else "✗ tesseract") + status_parts.append("✓ mkvtoolnix" if mkvmerge_ok else "✗ mkvtoolnix") + status_parts.append("✓ pgsrip" if pgsrip_ok else "✗ pgsrip") + + status_text = " | ".join(status_parts) + + if not all([tesseract_ok, mkvmerge_ok, pgsrip_ok]): + status_text += "\n" + link( + "https://github.com/cdgriffith/FastFlix/wiki/PGS-OCR-Setup", + "Click here for installation instructions", + self.app.fastflix.config.theme + ) + + self.ocr_status_label.setText(status_text) + self.ocr_status_label.setOpenExternalLinks(True) + def save(self): new_ffmpeg = Path(self.ffmpeg_path.text()) new_ffprobe = Path(self.ffprobe_path.text()) @@ -379,6 +416,7 @@ def save(self): self.app.fastflix.config.sticky_tabs = self.sticky_tabs.isChecked() self.app.fastflix.config.disable_complete_message = self.disable_end_message.isChecked() self.app.fastflix.config.disable_deinterlace_check = self.disable_deinterlace_button.isChecked() + self.app.fastflix.config.enable_pgs_ocr = self.enable_pgs_ocr.isChecked() self.main.config_update() self.app.fastflix.config.save() From 1c6c48679c76e4e031e8248cdd082c9a209d2f77 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Sun, 19 Oct 2025 19:13:50 +0000 Subject: [PATCH 02/31] Address PR review feedback - Use environment variables for Windows tool detection instead of scanning all drives (LOCALAPPDATA, PROGRAMFILES, PROGRAMFILES(X86)) - Remove pgsrip_path config field and use pgsrip Python API directly - Update dependency checks to use importlib for pgsrip library - Fix BabelLanguage to handle both 2-letter and 3-letter ISO codes - Update error messages and installation instructions All changes pass pre-commit linting checks. --- fastflix/models/config.py | 61 ++++++---- fastflix/widgets/background_tasks.py | 140 +++++++++++----------- fastflix/widgets/panels/subtitle_panel.py | 27 +++-- fastflix/widgets/settings.py | 10 +- 4 files changed, 135 insertions(+), 103 deletions(-) diff --git a/fastflix/models/config.py b/fastflix/models/config.py index 64e43e9c..69bf6014 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -111,22 +111,29 @@ def find_ocr_tool(name): # Special handling for tesseract on Windows (not in PATH by default) if name == "tesseract" and win_based: - # Check common install locations on all drives - import string - drives = [f"{d}:" for d in string.ascii_uppercase if Path(f"{d}:/").exists()] - - for drive in drives: - common_paths = [ - Path(f"{drive}/Program Files/Tesseract-OCR/tesseract.exe"), - Path(f"{drive}/Program Files (x86)/Tesseract-OCR/tesseract.exe"), - ] - for path in common_paths: - if path.exists(): - return path + # Check common install locations using environment variables + localappdata = os.getenv("LOCALAPPDATA") + program_files = os.getenv("PROGRAMFILES") + program_files_x86 = os.getenv("PROGRAMFILES(X86)") + + common_paths = [] + # Check user-local installation first + if localappdata: + common_paths.append(Path(localappdata) / "Programs" / "Tesseract-OCR" / "tesseract.exe") + # Check system-wide installations + if program_files: + common_paths.append(Path(program_files) / "Tesseract-OCR" / "tesseract.exe") + if program_files_x86: + common_paths.append(Path(program_files_x86) / "Tesseract-OCR" / "tesseract.exe") + + for path in common_paths: + if path.exists(): + return path # Check Windows registry for Tesseract install location try: import winreg + # Try HKEY_LOCAL_MACHINE first (system-wide install) for root_key in [winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER]: try: @@ -143,17 +150,24 @@ def find_ocr_tool(name): # Special handling for mkvmerge on Windows if name == "mkvmerge" and win_based: - import string - drives = [f"{d}:" for d in string.ascii_uppercase if Path(f"{d}:/").exists()] - - for drive in drives: - common_paths = [ - Path(f"{drive}/Program Files/MKVToolNix/mkvmerge.exe"), - Path(f"{drive}/Program Files (x86)/MKVToolNix/mkvmerge.exe"), - ] - for path in common_paths: - if path.exists(): - return path + # Check common install locations using environment variables + localappdata = os.getenv("LOCALAPPDATA") + program_files = os.getenv("PROGRAMFILES") + program_files_x86 = os.getenv("PROGRAMFILES(X86)") + + common_paths = [] + # Check user-local installation first + if localappdata: + common_paths.append(Path(localappdata) / "Programs" / "MKVToolNix" / "mkvmerge.exe") + # Check system-wide installations + if program_files: + common_paths.append(Path(program_files) / "MKVToolNix" / "mkvmerge.exe") + if program_files_x86: + common_paths.append(Path(program_files_x86) / "MKVToolNix" / "mkvmerge.exe") + + for path in common_paths: + if path.exists(): + return path # Check in FastFlix OCR tools folder ocr_folder = Path(user_data_dir("FastFlix_OCR", appauthor=False, roaming=True)) @@ -243,7 +257,6 @@ class Config(BaseModel): enable_pgs_ocr: bool = False tesseract_path: Path | None = Field(default_factory=lambda: find_ocr_tool("tesseract")) mkvmerge_path: Path | None = Field(default_factory=lambda: find_ocr_tool("mkvmerge")) - pgsrip_path: Path | None = Field(default_factory=lambda: find_ocr_tool("pgsrip")) pgs_ocr_language: str = "eng" def encoder_opt(self, profile_name, profile_option_name): diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 01e5060a..9b2cc2ab 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -1,8 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import importlib.util import logging import os -import shutil from pathlib import Path from subprocess import PIPE, STDOUT, Popen, run, check_output from packaging import version @@ -190,31 +190,30 @@ def _check_pgsrip_dependencies(self) -> bool: if not self.app.fastflix.config.mkvmerge_path: missing.append("mkvtoolnix") - # Check pgsrip - if not self.app.fastflix.config.pgsrip_path: - missing.append("pgsrip") + # Check if pgsrip Python library is available + if importlib.util.find_spec("pgsrip") is None: + missing.append("pgsrip (Python library)") if missing: self.main.thread_logging_signal.emit( f"ERROR:{t('Missing dependencies for PGS OCR')}: {', '.join(missing)}\n\n" f"Install instructions:\n" - f" Windows: Run setup_pgs_ocr_windows.bat in FastFlix folder\n" - f" Linux: sudo apt install tesseract-ocr mkvtoolnix && pip install pgsrip\n" - f" macOS: brew install tesseract mkvtoolnix && pip install pgsrip\n\n" - f"Or download manually:\n" - f" Tesseract: https://github.com/UB-Mannheim/tesseract/wiki\n" - f" MKVToolNix: https://mkvtoolnix.download/downloads.html\n" - f" pgsrip: pip install pgsrip" + f" pgsrip: pip install pgsrip\n" + f" Linux: sudo apt install tesseract-ocr mkvtoolnix\n" + f" macOS: brew install tesseract mkvtoolnix\n" + f" Windows:\n" + f" - Tesseract: https://github.com/UB-Mannheim/tesseract/wiki\n" + f" - MKVToolNix: https://mkvtoolnix.download/downloads.html" ) return False return True def _convert_sup_to_srt(self, sup_filepath: str) -> bool: - """Convert an already-extracted .sup file to .srt using pgsrip OCR + """Convert PGS subtitle to .srt using pgsrip OCR by processing the original MKV Args: - sup_filepath: Path to the extracted .sup file + sup_filepath: Path to the extracted .sup file (used for naming output) Returns: True if conversion successful, False otherwise @@ -228,80 +227,87 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: f"INFO:{t('Converting .sup to .srt using OCR')} (this may take 3-5 minutes)..." ) - # Convert 3-letter language code to 2-letter for pgsrip - # pgsrip uses 2-letter codes in filenames (e.g., "en" not "eng") - from fastflix.language import Language - try: - lang_2letter = Language(self.language).pt1 # Convert eng -> en - except: - lang_2letter = "en" # Default to English if conversion fails - - # Rename .sup file to use 2-letter language code (what pgsrip expects) - sup_path = Path(sup_filepath) - if f".{self.language}." in sup_path.name: - # Replace 3-letter with 2-letter in filename - new_name = sup_path.name.replace(f".{self.language}.", f".{lang_2letter}.") - new_sup_path = sup_path.parent / new_name - sup_path.rename(new_sup_path) - sup_filepath = str(new_sup_path) - - # Run pgsrip on the already-extracted .sup file - pgsrip_cmd = str(self.app.fastflix.config.pgsrip_path) if self.app.fastflix.config.pgsrip_path else "pgsrip" + # Import pgsrip Python API + from pgsrip import pgsrip, Mkv, Options + from babelfish import Language as BabelLanguage # Set environment variables for pgsrip to find tesseract - import os - env = os.environ.copy() if self.app.fastflix.config.tesseract_path: # Add tesseract directory to PATH so pytesseract can find it tesseract_dir = str(Path(self.app.fastflix.config.tesseract_path).parent) - env['PATH'] = f"{tesseract_dir}{os.pathsep}{env.get('PATH', '')}" - env['TESSERACT_CMD'] = str(self.app.fastflix.config.tesseract_path) + os.environ["PATH"] = f"{tesseract_dir}{os.pathsep}{os.environ.get('PATH', '')}" + os.environ["TESSERACT_CMD"] = str(self.app.fastflix.config.tesseract_path) - pgsrip_result = run( - [ - pgsrip_cmd, - "--language", lang_2letter, # Use 2-letter code (e.g., "en", "es", "fr") - "--force", # Overwrite existing files - sup_filepath - ], - capture_output=True, - text=True, - timeout=600, # 10 minute timeout for OCR - env=env # Pass environment with TESSERACT_CMD - ) + # pgsrip needs the original MKV file, not the extracted .sup + # The .sup file is a raw subtitle stream, but pgsrip expects an MKV container + sup_path = Path(sup_filepath) + media = Mkv(self.main.input_video) + + # Configure options for pgsrip + # BabelLanguage needs different constructors for 2-letter vs 3-letter codes + try: + # Detect if language code is 2-letter or 3-letter + if len(self.language) == 2: + babel_lang = BabelLanguage.fromalpha2(self.language) + elif len(self.language) == 3: + babel_lang = BabelLanguage(self.language) + else: + # Try as language name + babel_lang = BabelLanguage.fromname(self.language) + + options = Options( + languages={babel_lang}, + overwrite=True, # Overwrite existing .srt files + one_per_lang=True, # Create one .srt per language + ) + except Exception: + # Fallback to English if language code is invalid + options = Options( + languages={BabelLanguage("eng")}, + overwrite=True, + one_per_lang=True, + ) - if pgsrip_result.returncode != 0: - error_msg = pgsrip_result.stderr if pgsrip_result.stderr else pgsrip_result.stdout - raise Exception(f"pgsrip failed with return code {pgsrip_result.returncode}: {error_msg}") + # Run pgsrip conversion using Python API on the original MKV + # This will create .srt files in the same directory as the video + pgsrip.rip(media, options) - # pgsrip creates .srt file in same directory as .sup file - sup_path = Path(sup_filepath) - expected_srt = sup_path.with_suffix('.srt') + # Look for the created .srt file + # pgsrip creates files with pattern: basename.language.srt + video_path = Path(self.main.input_video) + srt_pattern = f"{video_path.stem}.*.srt" + srt_files = list(video_path.parent.glob(srt_pattern)) - if not expected_srt.exists(): - # Look for any .srt file created near the .sup - srt_files = list(sup_path.parent.glob("*.srt")) - if not srt_files: - raise Exception(f"pgsrip completed but no .srt file found in {sup_path.parent}") - expected_srt = srt_files[0] + if not srt_files: + # Fallback: look for any .srt in the video directory + srt_files = list(video_path.parent.glob(f"{video_path.stem}.srt")) - self.main.thread_logging_signal.emit( - f"INFO:{t('OCR conversion successful')}: {expected_srt.name}" - ) + if not srt_files: + raise Exception(f"pgsrip completed but no .srt file found in {video_path.parent}") + + # Move the .srt file to the expected location (same dir as .sup was) + created_srt = srt_files[0] + expected_srt = sup_path.with_suffix(".srt") + + if created_srt != expected_srt: + # Move/rename to expected location + import shutil + + shutil.move(str(created_srt), str(expected_srt)) + + self.main.thread_logging_signal.emit(f"INFO:{t('OCR conversion successful')}: {expected_srt.name}") # Optionally delete the .sup file since we have .srt now try: sup_path.unlink() self.main.thread_logging_signal.emit(f"INFO:{t('Removed .sup file, kept .srt')}") - except: + except Exception: pass return True except Exception as err: - self.main.thread_logging_signal.emit( - f"ERROR:{t('OCR conversion failed')}: {err}" - ) + self.main.thread_logging_signal.emit(f"ERROR:{t('OCR conversion failed')}: {err}") return False diff --git a/fastflix/widgets/panels/subtitle_panel.py b/fastflix/widgets/panels/subtitle_panel.py index 90a0e556..b61cf4c6 100644 --- a/fastflix/widgets/panels/subtitle_panel.py +++ b/fastflix/widgets/panels/subtitle_panel.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import importlib.util from typing import Union from box import Box @@ -116,17 +117,23 @@ def __init__(self, app, parent, index, enabled=True, first=False): extract_menu.addAction(t("Extract as .sup (image - fast)"), lambda: self.extract(use_ocr=False)) # Check if OCR dependencies are available - ocr_action = extract_menu.addAction(t("Convert to .srt (OCR - 3-5 min)"), lambda: self.extract(use_ocr=True)) + ocr_action = extract_menu.addAction( + t("Convert to .srt (OCR - 3-5 min)"), lambda: self.extract(use_ocr=True) + ) # Enable OCR option only if user enabled it AND dependencies are available if not self.app.fastflix.config.enable_pgs_ocr: ocr_action.setEnabled(False) ocr_action.setToolTip(t("Enable in Settings > 'Enable PGS to SRT OCR conversion'")) - elif not (self.app.fastflix.config.tesseract_path and - self.app.fastflix.config.mkvmerge_path and - self.app.fastflix.config.pgsrip_path): - ocr_action.setEnabled(False) - ocr_action.setToolTip(t("Missing dependencies: tesseract, mkvtoolnix, or pgsrip")) + else: + # Check if pgsrip Python library is available + pgsrip_ok = importlib.util.find_spec("pgsrip") is not None + + if not ( + self.app.fastflix.config.tesseract_path and self.app.fastflix.config.mkvmerge_path and pgsrip_ok + ): + ocr_action.setEnabled(False) + ocr_action.setToolTip(t("Missing dependencies: tesseract, mkvtoolnix, or pgsrip")) self.widgets.extract.setMenu(extract_menu) else: @@ -193,8 +200,12 @@ def init_move_buttons(self): def extract(self, use_ocr=False): worker = ExtractSubtitleSRT( - self.parent.app, self.parent.main, self.index, self.extract_completed_signal, - language=self.language, use_ocr=use_ocr + self.parent.app, + self.parent.main, + self.index, + self.extract_completed_signal, + language=self.language, + use_ocr=use_ocr, ) worker.start() self.gif_label.show() diff --git a/fastflix/widgets/settings.py b/fastflix/widgets/settings.py index 4bd7b089..e4309f41 100644 --- a/fastflix/widgets/settings.py +++ b/fastflix/widgets/settings.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import importlib.util import logging import shutil from pathlib import Path @@ -273,8 +274,7 @@ def in_dir(): self.enable_pgs_ocr = QtWidgets.QCheckBox(t("Enable PGS to SRT OCR conversion")) self.enable_pgs_ocr.setChecked(self.app.fastflix.config.enable_pgs_ocr) self.enable_pgs_ocr.setToolTip( - t("Convert image-based PGS subtitles to text SRT using OCR.\n" - "Typically takes 3-5 minutes per movie.") + t("Convert image-based PGS subtitles to text SRT using OCR.\nTypically takes 3-5 minutes per movie.") ) # Dependency status @@ -314,7 +314,9 @@ def update_ocr_dependency_status(self): # Use config paths which use find_ocr_tool() - handles non-PATH locations tesseract_ok = self.app.fastflix.config.tesseract_path is not None mkvmerge_ok = self.app.fastflix.config.mkvmerge_path is not None - pgsrip_ok = self.app.fastflix.config.pgsrip_path is not None + + # Check if pgsrip Python library is available + pgsrip_ok = importlib.util.find_spec("pgsrip") is not None status_parts = [] status_parts.append("✓ tesseract" if tesseract_ok else "✗ tesseract") @@ -327,7 +329,7 @@ def update_ocr_dependency_status(self): status_text += "\n" + link( "https://github.com/cdgriffith/FastFlix/wiki/PGS-OCR-Setup", "Click here for installation instructions", - self.app.fastflix.config.theme + self.app.fastflix.config.theme, ) self.ocr_status_label.setText(status_text) From f5ddccc74f4d55d001469e37600a1a47e663fe5d Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Sun, 19 Oct 2025 19:57:47 +0000 Subject: [PATCH 03/31] Fix OCR conversion for files with special characters in path The glob pattern was failing when filenames contained brackets like [imdbid-tt0187738] because glob interprets [] as character classes. Changed to detect newly created .srt files by comparing before/after directory listings instead of using filename-based glob patterns. Fixes false error for files like "Blade II (2002) [imdbid-tt0187738].mkv" --- fastflix/widgets/background_tasks.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 9b2cc2ab..00cc4e3b 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -268,23 +268,26 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: one_per_lang=True, ) + # Get list of existing .srt files before conversion + video_path = Path(self.main.input_video) + existing_srts = set(video_path.parent.glob("*.srt")) + # Run pgsrip conversion using Python API on the original MKV # This will create .srt files in the same directory as the video pgsrip.rip(media, options) - # Look for the created .srt file - # pgsrip creates files with pattern: basename.language.srt - video_path = Path(self.main.input_video) - srt_pattern = f"{video_path.stem}.*.srt" - srt_files = list(video_path.parent.glob(srt_pattern)) + # Find newly created .srt files + # Note: Can't use glob with video filename directly because special chars like [] + # are interpreted as glob patterns. Instead, find new .srt files. + current_srts = set(video_path.parent.glob("*.srt")) + new_srts = current_srts - existing_srts - if not srt_files: - # Fallback: look for any .srt in the video directory - srt_files = list(video_path.parent.glob(f"{video_path.stem}.srt")) - - if not srt_files: + if not new_srts: raise Exception(f"pgsrip completed but no .srt file found in {video_path.parent}") + # Get the first new .srt file + srt_files = list(new_srts) + # Move the .srt file to the expected location (same dir as .sup was) created_srt = srt_files[0] expected_srt = sup_path.with_suffix(".srt") From 5dd76273ff7329b86739a7c526e9a2b024153de6 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Mon, 20 Oct 2025 02:15:22 +0000 Subject: [PATCH 04/31] Add pgsrip metadata to PyInstaller builds Include package metadata for pgsrip, pytesseract, and babelfish in the Windows builds to fix 'No package metadata was found' error when running OCR conversion from the compiled executable. --- FastFlix_Windows_Installer.spec | 7 +++++-- FastFlix_Windows_OneFile.spec | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/FastFlix_Windows_Installer.spec b/FastFlix_Windows_Installer.spec index 4f03f0c0..bd144d81 100644 --- a/FastFlix_Windows_Installer.spec +++ b/FastFlix_Windows_Installer.spec @@ -1,5 +1,5 @@ # -*- mode: python ; coding: utf-8 -*- -from PyInstaller.utils.hooks import collect_submodules +from PyInstaller.utils.hooks import collect_submodules, copy_metadata import toml block_cipher = None @@ -24,9 +24,12 @@ all_imports.remove("python-box") all_imports.append("box") all_imports.append("iso639") +# Add pgsrip for OCR support +all_imports.extend(["pgsrip", "pytesseract", "cv2", "numpy", "pysrt", "babelfish", "cleanit"]) + a = Analysis(['fastflix\\__main__.py'], binaries=[], - datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files, + datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish'), hiddenimports=all_imports, hookspath=[], runtime_hooks=[], diff --git a/FastFlix_Windows_OneFile.spec b/FastFlix_Windows_OneFile.spec index da391c44..57a5db12 100644 --- a/FastFlix_Windows_OneFile.spec +++ b/FastFlix_Windows_OneFile.spec @@ -2,7 +2,7 @@ import os import toml -from PyInstaller.utils.hooks import collect_submodules +from PyInstaller.utils.hooks import collect_submodules, copy_metadata block_cipher = None @@ -36,7 +36,7 @@ with open(portable_file, "w") as portable: a = Analysis(['fastflix\\__main__.py'], binaries=[], - datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files, + datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish'), hiddenimports=all_imports, hookspath=[], runtime_hooks=[], From c1d63d130f175dad11d87b3730be73eb7a211d34 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Mon, 20 Oct 2025 02:21:36 +0000 Subject: [PATCH 05/31] Include babelfish data files in PyInstaller builds Add collect_data_files('babelfish') to bundle ISO language code data files needed by babelfish at runtime. --- FastFlix_Windows_Installer.spec | 4 ++-- FastFlix_Windows_OneFile.spec | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/FastFlix_Windows_Installer.spec b/FastFlix_Windows_Installer.spec index bd144d81..9129df81 100644 --- a/FastFlix_Windows_Installer.spec +++ b/FastFlix_Windows_Installer.spec @@ -1,5 +1,5 @@ # -*- mode: python ; coding: utf-8 -*- -from PyInstaller.utils.hooks import collect_submodules, copy_metadata +from PyInstaller.utils.hooks import collect_submodules, copy_metadata, collect_data_files import toml block_cipher = None @@ -29,7 +29,7 @@ all_imports.extend(["pgsrip", "pytesseract", "cv2", "numpy", "pysrt", "babelfish a = Analysis(['fastflix\\__main__.py'], binaries=[], - datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish'), + datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish') + collect_data_files('babelfish'), hiddenimports=all_imports, hookspath=[], runtime_hooks=[], diff --git a/FastFlix_Windows_OneFile.spec b/FastFlix_Windows_OneFile.spec index 57a5db12..ccc72c7f 100644 --- a/FastFlix_Windows_OneFile.spec +++ b/FastFlix_Windows_OneFile.spec @@ -2,7 +2,7 @@ import os import toml -from PyInstaller.utils.hooks import collect_submodules, copy_metadata +from PyInstaller.utils.hooks import collect_submodules, copy_metadata, collect_data_files block_cipher = None @@ -36,7 +36,7 @@ with open(portable_file, "w") as portable: a = Analysis(['fastflix\\__main__.py'], binaries=[], - datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish'), + datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish') + collect_data_files('babelfish'), hiddenimports=all_imports, hookspath=[], runtime_hooks=[], From 39b1a5fc697b48ffe79178259cbc53d0b964aef4 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Mon, 20 Oct 2025 02:45:50 +0000 Subject: [PATCH 06/31] Include cleanit metadata in PyInstaller builds Add copy_metadata('cleanit') for pgsrip dependency. --- FastFlix_Windows_Installer.spec | 2 +- FastFlix_Windows_OneFile.spec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FastFlix_Windows_Installer.spec b/FastFlix_Windows_Installer.spec index 9129df81..1e575086 100644 --- a/FastFlix_Windows_Installer.spec +++ b/FastFlix_Windows_Installer.spec @@ -29,7 +29,7 @@ all_imports.extend(["pgsrip", "pytesseract", "cv2", "numpy", "pysrt", "babelfish a = Analysis(['fastflix\\__main__.py'], binaries=[], - datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish') + collect_data_files('babelfish'), + datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish') + copy_metadata('cleanit') + collect_data_files('babelfish'), hiddenimports=all_imports, hookspath=[], runtime_hooks=[], diff --git a/FastFlix_Windows_OneFile.spec b/FastFlix_Windows_OneFile.spec index ccc72c7f..6c3aedfc 100644 --- a/FastFlix_Windows_OneFile.spec +++ b/FastFlix_Windows_OneFile.spec @@ -36,7 +36,7 @@ with open(portable_file, "w") as portable: a = Analysis(['fastflix\\__main__.py'], binaries=[], - datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish') + collect_data_files('babelfish'), + datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish') + copy_metadata('cleanit') + collect_data_files('babelfish'), hiddenimports=all_imports, hookspath=[], runtime_hooks=[], From 964ce3ce8f1c5c2715e61a765827bea25652ec59 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Mon, 20 Oct 2025 02:50:28 +0000 Subject: [PATCH 07/31] Include cleanit data files in PyInstaller builds Add collect_data_files('cleanit') to bundle YAML config files needed by cleanit at runtime. --- FastFlix_Windows_Installer.spec | 2 +- FastFlix_Windows_OneFile.spec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FastFlix_Windows_Installer.spec b/FastFlix_Windows_Installer.spec index 1e575086..e3342912 100644 --- a/FastFlix_Windows_Installer.spec +++ b/FastFlix_Windows_Installer.spec @@ -29,7 +29,7 @@ all_imports.extend(["pgsrip", "pytesseract", "cv2", "numpy", "pysrt", "babelfish a = Analysis(['fastflix\\__main__.py'], binaries=[], - datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish') + copy_metadata('cleanit') + collect_data_files('babelfish'), + datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish') + copy_metadata('cleanit') + collect_data_files('babelfish') + collect_data_files('cleanit'), hiddenimports=all_imports, hookspath=[], runtime_hooks=[], diff --git a/FastFlix_Windows_OneFile.spec b/FastFlix_Windows_OneFile.spec index 6c3aedfc..c90d338a 100644 --- a/FastFlix_Windows_OneFile.spec +++ b/FastFlix_Windows_OneFile.spec @@ -36,7 +36,7 @@ with open(portable_file, "w") as portable: a = Analysis(['fastflix\\__main__.py'], binaries=[], - datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish') + copy_metadata('cleanit') + collect_data_files('babelfish'), + datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish') + copy_metadata('cleanit') + collect_data_files('babelfish') + collect_data_files('cleanit'), hiddenimports=all_imports, hookspath=[], runtime_hooks=[], From 4f8e3472dac0159448ef868f24612e13ccc84b2c Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Mon, 20 Oct 2025 02:57:44 +0000 Subject: [PATCH 08/31] Include trakit metadata in PyInstaller builds Add copy_metadata('trakit') for pgsrip dependency. --- FastFlix_Windows_Installer.spec | 2 +- FastFlix_Windows_OneFile.spec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FastFlix_Windows_Installer.spec b/FastFlix_Windows_Installer.spec index e3342912..2edac62a 100644 --- a/FastFlix_Windows_Installer.spec +++ b/FastFlix_Windows_Installer.spec @@ -29,7 +29,7 @@ all_imports.extend(["pgsrip", "pytesseract", "cv2", "numpy", "pysrt", "babelfish a = Analysis(['fastflix\\__main__.py'], binaries=[], - datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish') + copy_metadata('cleanit') + collect_data_files('babelfish') + collect_data_files('cleanit'), + datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish') + copy_metadata('cleanit') + copy_metadata('trakit') + collect_data_files('babelfish') + collect_data_files('cleanit'), hiddenimports=all_imports, hookspath=[], runtime_hooks=[], diff --git a/FastFlix_Windows_OneFile.spec b/FastFlix_Windows_OneFile.spec index c90d338a..cfcf984e 100644 --- a/FastFlix_Windows_OneFile.spec +++ b/FastFlix_Windows_OneFile.spec @@ -36,7 +36,7 @@ with open(portable_file, "w") as portable: a = Analysis(['fastflix\\__main__.py'], binaries=[], - datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish') + copy_metadata('cleanit') + collect_data_files('babelfish') + collect_data_files('cleanit'), + datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files + copy_metadata('pgsrip') + copy_metadata('pytesseract') + copy_metadata('babelfish') + copy_metadata('cleanit') + copy_metadata('trakit') + collect_data_files('babelfish') + collect_data_files('cleanit'), hiddenimports=all_imports, hookspath=[], runtime_hooks=[], From aacb011ae432fa3ac29a69ebfdfde1a93372763e Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 02:48:00 +0000 Subject: [PATCH 09/31] Add OCR dependencies to pyproject.toml Include pgsrip, pytesseract, babelfish, cleanit, trakit, opencv-python, and pysrt in project dependencies to fix Windows build error where PyInstaller's copy_metadata() could not find package metadata for packages that weren't installed during the build process. --- WINDOWS_BUILD.md | 125 ++++++++++++++++ pyproject.toml | 7 + uv.lock | 378 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 509 insertions(+), 1 deletion(-) create mode 100644 WINDOWS_BUILD.md diff --git a/WINDOWS_BUILD.md b/WINDOWS_BUILD.md new file mode 100644 index 00000000..ecbcf455 --- /dev/null +++ b/WINDOWS_BUILD.md @@ -0,0 +1,125 @@ +# Building FastFlix on Windows + +This guide explains how to build FastFlix executables on Windows. + +## Prerequisites + +1. **Python 3.12 or higher** + - Download from [python.org](https://www.python.org/downloads/) + - Make sure to check "Add Python to PATH" during installation + +2. **Git** (to clone/update the repository) + - Download from [git-scm.com](https://git-scm.com/download/win) + +## Build Steps + +### 1. Open Command Prompt or PowerShell + +Navigate to where you want to clone/have the FastFlix repository: + +```bash +cd C:\path\to\your\projects +git clone https://github.com/cdgriffith/FastFlix.git +cd FastFlix +``` + +Or if you already have it: + +```bash +cd C:\path\to\FastFlix +``` + +### 2. Create and Activate Virtual Environment + +```bash +python -m venv venv +venv\Scripts\activate +``` + +You should see `(venv)` in your command prompt. + +### 3. Install Dependencies + +```bash +pip install --upgrade pip +pip install -e ".[dev]" +``` + +This installs FastFlix in editable mode with all development dependencies including PyInstaller. + +### 4. Build the Executable + +You have two options: + +#### Option A: Single Executable (Recommended for distribution) + +```bash +pyinstaller FastFlix_Windows_OneFile.spec +``` + +The executable will be in: `dist\FastFlix.exe` + +#### Option B: Directory with Multiple Files (Faster startup) + +```bash +pyinstaller FastFlix_Windows_Installer.spec +``` + +The executable will be in: `dist\FastFlix\FastFlix.exe` + +### 5. Test the Build + +```bash +cd dist +FastFlix.exe +``` + +Or for the installer version: + +```bash +cd dist\FastFlix +FastFlix.exe +``` + +## Running Without Building (For Testing) + +If you just want to test changes without building an executable: + +```bash +python -m fastflix +``` + +## Troubleshooting + +### Missing Dependencies + +If you get import errors, try reinstalling: + +```bash +pip install --upgrade --force-reinstall -e ".[dev]" +``` + +### Build Errors + +1. Make sure you're in the FastFlix root directory +2. Ensure the virtual environment is activated (you see `(venv)`) +3. Try deleting `build` and `dist` folders and rebuilding: + +```bash +rmdir /s /q build dist +pyinstaller FastFlix_Windows_OneFile.spec +``` + +### FFmpeg Not Found + +The FastFlix executable doesn't include FFmpeg. You need to: + +1. Download FFmpeg from [ffmpeg.org](https://ffmpeg.org/download.html#build-windows) +2. Extract it somewhere +3. Add the `bin` folder to your PATH, or configure it in FastFlix settings + +## Notes + +- The build process creates a `portable.py` file temporarily (it's removed after) +- The `.spec` files automatically collect all dependencies from `pyproject.toml` +- The icon is located at `fastflix\data\icon.ico` diff --git a/pyproject.toml b/pyproject.toml index add07dce..2a19e5ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,13 @@ dependencies = [ "wmi>=1.5.1; sys_platform == 'win32'", "ffmpeg-normalize>=1.31.3,<2.0", "reusables>=1.0.0", + "pgsrip>=0.1.0", + "pytesseract>=0.3.0", + "babelfish>=0.6.0", + "cleanit>=0.4.0", + "trakit>=0.2.0", + "opencv-python>=4.8.0", + "pysrt>=1.1.0", ] [project.scripts] diff --git a/uv.lock b/uv.lock index 7241bcdf..9473c92a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" [[package]] @@ -20,6 +20,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "appdirs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "babelfish" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/8f/17ff889327f8a1c36a28418e686727dabc06c080ed49c95e3e2424a77aa6/babelfish-0.6.1.tar.gz", hash = "sha256:decb67a4660888d48480ab6998309837174158d0f1aa63bebb1c2e11aab97aab", size = 87706, upload-time = "2024-05-09T21:16:24.357Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/a1/bd4f759db13cd8beb9c9f68682aced5d966781b9d7380cf514a306f56762/babelfish-0.6.1-py3-none-any.whl", hash = "sha256:512f1501d4c8f7d38f0921f48660be7542de1a7b24abb6a6a65324a670150293", size = 94231, upload-time = "2024-05-09T21:16:22.633Z" }, +] + [[package]] name = "certifi" version = "2025.6.15" @@ -82,6 +109,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] +[[package]] +name = "cleanit" +version = "0.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appdirs" }, + { name = "babelfish" }, + { name = "chardet" }, + { name = "click" }, + { name = "jsonschema" }, + { name = "pysrt" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/e3/d08d7980c4a04f3e23c8adf33717cb92b0e009ac96f6c05e5867bca0edf1/cleanit-0.4.8.tar.gz", hash = "sha256:1b19fe2dd2712695ebbf9d429c4d3366a1b51300738bb034c13ea221c84a6ae9", size = 21625, upload-time = "2024-06-23T06:19:14.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/b9/fcf9e3b833bff99e1d2d63c31dad1d10c1d650f29971b541846295d96513/cleanit-0.4.8-py3-none-any.whl", hash = "sha256:8ae8853871a8664a8781f8f82940ac559322263058f9d94b245780c1750681f2", size = 26630, upload-time = "2024-06-23T06:19:12.426Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -128,22 +185,29 @@ wheels = [ name = "fastflix" source = { editable = "." } dependencies = [ + { name = "babelfish" }, { name = "chardet" }, + { name = "cleanit" }, { name = "colorama" }, { name = "coloredlogs" }, { name = "ffmpeg-normalize" }, { name = "iso639-lang" }, { name = "mistune" }, + { name = "opencv-python" }, { name = "packaging" }, { name = "pathvalidate" }, + { name = "pgsrip" }, { name = "platformdirs" }, { name = "psutil" }, { name = "pydantic" }, { name = "pyside6" }, + { name = "pysrt" }, + { name = "pytesseract" }, { name = "python-box", extra = ["all"] }, { name = "requests" }, { name = "reusables" }, { name = "setuptools" }, + { name = "trakit" }, { name = "wmi", marker = "sys_platform == 'win32'" }, ] @@ -161,22 +225,29 @@ dev = [ [package.metadata] requires-dist = [ + { name = "babelfish", specifier = ">=0.6.0" }, { name = "chardet", specifier = ">=5.1.0,<5.2.0" }, + { name = "cleanit", specifier = ">=0.4.0" }, { name = "colorama", specifier = ">=0.4,<1.0" }, { name = "coloredlogs", specifier = ">=15.0,<16.0" }, { name = "ffmpeg-normalize", specifier = ">=1.31.3,<2.0" }, { name = "iso639-lang", specifier = ">=2.6.0,<3.0" }, { name = "mistune", specifier = ">=2.0,<3.0" }, + { name = "opencv-python", specifier = ">=4.8.0" }, { name = "packaging", specifier = ">=23.2" }, { name = "pathvalidate", specifier = ">=2.4,<3.0" }, + { name = "pgsrip", specifier = ">=0.1.0" }, { name = "platformdirs", specifier = "~=4.3" }, { name = "psutil", specifier = ">=5.9,<6.0" }, { name = "pydantic", specifier = ">=2.0,<3.0" }, { name = "pyside6", specifier = "==6.9.0" }, + { name = "pysrt", specifier = ">=1.1.0" }, + { name = "pytesseract", specifier = ">=0.3.0" }, { name = "python-box", extras = ["all"], specifier = ">=6.0,<7.0" }, { name = "requests", specifier = ">=2.28,<3.0" }, { name = "reusables", specifier = ">=1.0.0" }, { name = "setuptools", specifier = ">=75.8" }, + { name = "trakit", specifier = ">=0.2.0" }, { name = "wmi", marker = "sys_platform == 'win32'", specifier = ">=1.5.1" }, ] @@ -272,6 +343,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/c4/8e09251b52b6b5a772c3d098ad44e50d0ecaaf8ff11a5c2351a89e04254c/iso639_lang-2.6.1-py3-none-any.whl", hash = "sha256:6f41183aafc84716c3d559f57c036b04c3262899b89f7eadd68c397cce1ab572", size = 324943, upload-time = "2025-06-23T08:26:10.578Z" }, ] +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + [[package]] name = "macholib" version = "1.16.3" @@ -339,6 +437,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, +] + +[[package]] +name = "opencv-python" +version = "4.12.0.88" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/71/25c98e634b6bdeca4727c7f6d6927b056080668c5008ad3c8fc9e7f8f6ec/opencv-python-4.12.0.88.tar.gz", hash = "sha256:8b738389cede219405f6f3880b851efa3415ccd674752219377353f017d2994d", size = 95373294, upload-time = "2025-07-07T09:20:52.389Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/68/3da40142e7c21e9b1d4e7ddd6c58738feb013203e6e4b803d62cdd9eb96b/opencv_python-4.12.0.88-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:f9a1f08883257b95a5764bf517a32d75aec325319c8ed0f89739a57fae9e92a5", size = 37877727, upload-time = "2025-07-07T09:13:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/33/7c/042abe49f58d6ee7e1028eefc3334d98ca69b030e3b567fe245a2b28ea6f/opencv_python-4.12.0.88-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:812eb116ad2b4de43ee116fcd8991c3a687f099ada0b04e68f64899c09448e81", size = 57326471, upload-time = "2025-07-07T09:13:41.26Z" }, + { url = "https://files.pythonhosted.org/packages/62/3a/440bd64736cf8116f01f3b7f9f2e111afb2e02beb2ccc08a6458114a6b5d/opencv_python-4.12.0.88-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:51fd981c7df6af3e8f70b1556696b05224c4e6b6777bdd2a46b3d4fb09de1a92", size = 45887139, upload-time = "2025-07-07T09:13:50.761Z" }, + { url = "https://files.pythonhosted.org/packages/68/1f/795e7f4aa2eacc59afa4fb61a2e35e510d06414dd5a802b51a012d691b37/opencv_python-4.12.0.88-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:092c16da4c5a163a818f120c22c5e4a2f96e0db4f24e659c701f1fe629a690f9", size = 67041680, upload-time = "2025-07-07T09:14:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/02/96/213fea371d3cb2f1d537612a105792aa0a6659fb2665b22cad709a75bd94/opencv_python-4.12.0.88-cp37-abi3-win32.whl", hash = "sha256:ff554d3f725b39878ac6a2e1fa232ec509c36130927afc18a1719ebf4fbf4357", size = 30284131, upload-time = "2025-07-07T09:14:08.819Z" }, + { url = "https://files.pythonhosted.org/packages/fa/80/eb88edc2e2b11cd2dd2e56f1c80b5784d11d6e6b7f04a1145df64df40065/opencv_python-4.12.0.88-cp37-abi3-win_amd64.whl", hash = "sha256:d98edb20aa932fd8ebd276a72627dad9dc097695b3d435a4257557bbb49a79d2", size = 39000307, upload-time = "2025-07-07T09:14:16.641Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -366,6 +519,90 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/26/d0ad8b448476d0a1e8d3ea5622dc77b916db84c6aa3cb1e1c0965af948fc/pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6", size = 71791, upload-time = "2023-02-07T12:28:36.678Z" }, ] +[[package]] +name = "pgsrip" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babelfish" }, + { name = "cleanit" }, + { name = "click" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "pysrt" }, + { name = "pytesseract" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/c3/4d8da691f5324e84a9c5249144b03c0151db26653f0889a0149eb0181e09/pgsrip-0.1.1.tar.gz", hash = "sha256:078c841b4db76e2db021608d18e3a7a73b1acee9bd19fd2d26b7aa322a3b3495", size = 14131, upload-time = "2021-04-08T09:34:31.46Z" } + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, +] + [[package]] name = "platformdirs" version = "4.3.8" @@ -578,6 +815,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/a4/703e379a0979985f681cf04b9af4129f5dde20141b3cc64fc2a39d006614/PySide6_Essentials-6.9.0-cp39-abi3-win_arm64.whl", hash = "sha256:d2dc45536f2269ad111991042e81257124f1cd1c9ed5ea778d7224fd65dc9e2b", size = 49449220, upload-time = "2025-04-02T10:58:21.192Z" }, ] +[[package]] +name = "pysrt" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chardet" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/1a/0d858da1c6622dcf16011235a2639b0a01a49cecf812f8ab03308ab4de37/pysrt-1.1.2.tar.gz", hash = "sha256:b4f844ba33e4e7743e9db746492f3a193dc0bc112b153914698e7c1cdeb9b0b9", size = 104371, upload-time = "2020-01-20T15:22:28.291Z" } + +[[package]] +name = "pytesseract" +version = "0.3.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/a6/7d679b83c285974a7cb94d739b461fa7e7a9b17a3abfd7bf6cbc5c2394b0/pytesseract-0.3.13.tar.gz", hash = "sha256:4bf5f880c99406f52a3cfc2633e42d9dc67615e69d8a509d74867d3baddb5db9", size = 17689, upload-time = "2024-08-16T02:33:56.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/33/8312d7ce74670c9d39a532b2c246a853861120486be9443eebf048043637/pytesseract-0.3.13-py3-none-any.whl", hash = "sha256:7a99c6c2ac598360693d83a416e36e0b33a67638bb9d77fdcac094a3589d4b34", size = 14705, upload-time = "2024-08-16T02:36:10.09Z" }, +] + [[package]] name = "pytest" version = "8.4.1" @@ -658,6 +917,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] +[[package]] +name = "rebulk" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/06/24c69f8d707c9eefc1108a64e079da56b5f351e3f59ed76e8f04b9f3e296/rebulk-3.2.0.tar.gz", hash = "sha256:0d30bf80fca00fa9c697185ac475daac9bde5f646ce3338c9ff5d5dc1ebdfebc", size = 261685, upload-time = "2023-02-18T09:10:14.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/4d/df073d593f7e7e4a5a7e19148b2e9b4ae63b4ddcbb863f1e7bb2b6f19c62/rebulk-3.2.0-py3-none-any.whl", hash = "sha256:6bc31ae4b37200623c5827d2f539f9ec3e52b50431322dad8154642a39b0a53e", size = 56298, upload-time = "2023-02-18T09:10:12.435Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + [[package]] name = "requests" version = "2.32.4" @@ -682,6 +964,87 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/68/1b2b93713e1fdd4cd06ba792b53d81574edd291eda0c7fdceca69d175843/reusables-1.0.0-py3-none-any.whl", hash = "sha256:1bd1fcb782ce4d67b60435e30e8d558be09231190ba798c2ab3488e258bcf3bf", size = 44934, upload-time = "2025-07-03T01:31:22.564Z" }, ] +[[package]] +name = "rpds-py" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419, upload-time = "2025-10-22T22:24:29.327Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/5c/6c3936495003875fe7b14f90ea812841a08fca50ab26bd840e924097d9c8/rpds_py-0.28.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b4f28583a4f247ff60cd7bdda83db8c3f5b05a7a82ff20dd4b078571747708f", size = 366439, upload-time = "2025-10-22T22:22:04.525Z" }, + { url = "https://files.pythonhosted.org/packages/56/f9/a0f1ca194c50aa29895b442771f036a25b6c41a35e4f35b1a0ea713bedae/rpds_py-0.28.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424", size = 348170, upload-time = "2025-10-22T22:22:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/18/ea/42d243d3a586beb72c77fa5def0487daf827210069a95f36328e869599ea/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628", size = 378838, upload-time = "2025-10-22T22:22:07.932Z" }, + { url = "https://files.pythonhosted.org/packages/e7/78/3de32e18a94791af8f33601402d9d4f39613136398658412a4e0b3047327/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd", size = 393299, upload-time = "2025-10-22T22:22:09.435Z" }, + { url = "https://files.pythonhosted.org/packages/13/7e/4bdb435afb18acea2eb8a25ad56b956f28de7c59f8a1d32827effa0d4514/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e", size = 518000, upload-time = "2025-10-22T22:22:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/31/d0/5f52a656875cdc60498ab035a7a0ac8f399890cc1ee73ebd567bac4e39ae/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a", size = 408746, upload-time = "2025-10-22T22:22:13.143Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/49ce51767b879cde77e7ad9fae164ea15dce3616fe591d9ea1df51152706/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84", size = 386379, upload-time = "2025-10-22T22:22:14.602Z" }, + { url = "https://files.pythonhosted.org/packages/6a/99/e4e1e1ee93a98f72fc450e36c0e4d99c35370220e815288e3ecd2ec36a2a/rpds_py-0.28.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66", size = 401280, upload-time = "2025-10-22T22:22:16.063Z" }, + { url = "https://files.pythonhosted.org/packages/61/35/e0c6a57488392a8b319d2200d03dad2b29c0db9996f5662c3b02d0b86c02/rpds_py-0.28.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5deca01b271492553fdb6c7fd974659dce736a15bae5dad7ab8b93555bceb28", size = 412365, upload-time = "2025-10-22T22:22:17.504Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6a/841337980ea253ec797eb084665436007a1aad0faac1ba097fb906c5f69c/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:735f8495a13159ce6a0d533f01e8674cec0c57038c920495f87dcb20b3ddb48a", size = 559573, upload-time = "2025-10-22T22:22:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5e/64826ec58afd4c489731f8b00729c5f6afdb86f1df1df60bfede55d650bb/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:961ca621ff10d198bbe6ba4957decca61aa2a0c56695384c1d6b79bf61436df5", size = 583973, upload-time = "2025-10-22T22:22:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ee/44d024b4843f8386a4eeaa4c171b3d31d55f7177c415545fd1a24c249b5d/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2374e16cc9131022e7d9a8f8d65d261d9ba55048c78f3b6e017971a4f5e6353c", size = 553800, upload-time = "2025-10-22T22:22:22.25Z" }, + { url = "https://files.pythonhosted.org/packages/7d/89/33e675dccff11a06d4d85dbb4d1865f878d5020cbb69b2c1e7b2d3f82562/rpds_py-0.28.0-cp312-cp312-win32.whl", hash = "sha256:d15431e334fba488b081d47f30f091e5d03c18527c325386091f31718952fe08", size = 216954, upload-time = "2025-10-22T22:22:24.105Z" }, + { url = "https://files.pythonhosted.org/packages/af/36/45f6ebb3210887e8ee6dbf1bc710ae8400bb417ce165aaf3024b8360d999/rpds_py-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:a410542d61fc54710f750d3764380b53bf09e8c4edbf2f9141a82aa774a04f7c", size = 227844, upload-time = "2025-10-22T22:22:25.551Z" }, + { url = "https://files.pythonhosted.org/packages/57/91/f3fb250d7e73de71080f9a221d19bd6a1c1eb0d12a1ea26513f6c1052ad6/rpds_py-0.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:1f0cfd1c69e2d14f8c892b893997fa9a60d890a0c8a603e88dca4955f26d1edd", size = 217624, upload-time = "2025-10-22T22:22:26.914Z" }, + { url = "https://files.pythonhosted.org/packages/d3/03/ce566d92611dfac0085c2f4b048cd53ed7c274a5c05974b882a908d540a2/rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b", size = 366235, upload-time = "2025-10-22T22:22:28.397Z" }, + { url = "https://files.pythonhosted.org/packages/00/34/1c61da1b25592b86fd285bd7bd8422f4c9d748a7373b46126f9ae792a004/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a", size = 348241, upload-time = "2025-10-22T22:22:30.171Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/ed1e28616848c61c493a067779633ebf4b569eccaacf9ccbdc0e7cba2b9d/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa", size = 378079, upload-time = "2025-10-22T22:22:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/ccb30333a16a470091b6e50289adb4d3ec656fd9951ba8c5e3aaa0746a67/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724", size = 393151, upload-time = "2025-10-22T22:22:33.453Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d0/73e2217c3ee486d555cb84920597480627d8c0240ff3062005c6cc47773e/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491", size = 517520, upload-time = "2025-10-22T22:22:34.949Z" }, + { url = "https://files.pythonhosted.org/packages/c4/91/23efe81c700427d0841a4ae7ea23e305654381831e6029499fe80be8a071/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399", size = 408699, upload-time = "2025-10-22T22:22:36.584Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ee/a324d3198da151820a326c1f988caaa4f37fc27955148a76fff7a2d787a9/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6", size = 385720, upload-time = "2025-10-22T22:22:38.014Z" }, + { url = "https://files.pythonhosted.org/packages/19/ad/e68120dc05af8b7cab4a789fccd8cdcf0fe7e6581461038cc5c164cd97d2/rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d", size = 401096, upload-time = "2025-10-22T22:22:39.869Z" }, + { url = "https://files.pythonhosted.org/packages/99/90/c1e070620042459d60df6356b666bb1f62198a89d68881816a7ed121595a/rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb", size = 411465, upload-time = "2025-10-22T22:22:41.395Z" }, + { url = "https://files.pythonhosted.org/packages/68/61/7c195b30d57f1b8d5970f600efee72a4fad79ec829057972e13a0370fd24/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41", size = 558832, upload-time = "2025-10-22T22:22:42.871Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3d/06f3a718864773f69941d4deccdf18e5e47dd298b4628062f004c10f3b34/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7", size = 583230, upload-time = "2025-10-22T22:22:44.877Z" }, + { url = "https://files.pythonhosted.org/packages/66/df/62fc783781a121e77fee9a21ead0a926f1b652280a33f5956a5e7833ed30/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9", size = 553268, upload-time = "2025-10-22T22:22:46.441Z" }, + { url = "https://files.pythonhosted.org/packages/84/85/d34366e335140a4837902d3dea89b51f087bd6a63c993ebdff59e93ee61d/rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5", size = 217100, upload-time = "2025-10-22T22:22:48.342Z" }, + { url = "https://files.pythonhosted.org/packages/3c/1c/f25a3f3752ad7601476e3eff395fe075e0f7813fbb9862bd67c82440e880/rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e", size = 227759, upload-time = "2025-10-22T22:22:50.219Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d6/5f39b42b99615b5bc2f36ab90423ea404830bdfee1c706820943e9a645eb/rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1", size = 217326, upload-time = "2025-10-22T22:22:51.647Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8b/0c69b72d1cee20a63db534be0df271effe715ef6c744fdf1ff23bb2b0b1c/rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c", size = 355736, upload-time = "2025-10-22T22:22:53.211Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6d/0c2ee773cfb55c31a8514d2cece856dd299170a49babd50dcffb15ddc749/rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa", size = 342677, upload-time = "2025-10-22T22:22:54.723Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1c/22513ab25a27ea205144414724743e305e8153e6abe81833b5e678650f5a/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b", size = 371847, upload-time = "2025-10-22T22:22:56.295Z" }, + { url = "https://files.pythonhosted.org/packages/60/07/68e6ccdb4b05115ffe61d31afc94adef1833d3a72f76c9632d4d90d67954/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d", size = 381800, upload-time = "2025-10-22T22:22:57.808Z" }, + { url = "https://files.pythonhosted.org/packages/73/bf/6d6d15df80781d7f9f368e7c1a00caf764436518c4877fb28b029c4624af/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe", size = 518827, upload-time = "2025-10-22T22:22:59.826Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d3/2decbb2976cc452cbf12a2b0aaac5f1b9dc5dd9d1f7e2509a3ee00421249/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a", size = 399471, upload-time = "2025-10-22T22:23:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2c/f30892f9e54bd02e5faca3f6a26d6933c51055e67d54818af90abed9748e/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc", size = 377578, upload-time = "2025-10-22T22:23:03.52Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5d/3bce97e5534157318f29ac06bf2d279dae2674ec12f7cb9c12739cee64d8/rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259", size = 390482, upload-time = "2025-10-22T22:23:05.391Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f0/886bd515ed457b5bd93b166175edb80a0b21a210c10e993392127f1e3931/rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a", size = 402447, upload-time = "2025-10-22T22:23:06.93Z" }, + { url = "https://files.pythonhosted.org/packages/42/b5/71e8777ac55e6af1f4f1c05b47542a1eaa6c33c1cf0d300dca6a1c6e159a/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f", size = 552385, upload-time = "2025-10-22T22:23:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cb/6ca2d70cbda5a8e36605e7788c4aa3bea7c17d71d213465a5a675079b98d/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37", size = 575642, upload-time = "2025-10-22T22:23:10.348Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d4/407ad9960ca7856d7b25c96dcbe019270b5ffdd83a561787bc682c797086/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712", size = 544507, upload-time = "2025-10-22T22:23:12.434Z" }, + { url = "https://files.pythonhosted.org/packages/51/31/2f46fe0efcac23fbf5797c6b6b7e1c76f7d60773e525cb65fcbc582ee0f2/rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342", size = 205376, upload-time = "2025-10-22T22:23:13.979Z" }, + { url = "https://files.pythonhosted.org/packages/92/e4/15947bda33cbedfc134490a41841ab8870a72a867a03d4969d886f6594a2/rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907", size = 215907, upload-time = "2025-10-22T22:23:15.5Z" }, + { url = "https://files.pythonhosted.org/packages/08/47/ffe8cd7a6a02833b10623bf765fbb57ce977e9a4318ca0e8cf97e9c3d2b3/rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472", size = 353830, upload-time = "2025-10-22T22:23:17.03Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9f/890f36cbd83a58491d0d91ae0db1702639edb33fb48eeb356f80ecc6b000/rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2", size = 341819, upload-time = "2025-10-22T22:23:18.57Z" }, + { url = "https://files.pythonhosted.org/packages/09/e3/921eb109f682aa24fb76207698fbbcf9418738f35a40c21652c29053f23d/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527", size = 373127, upload-time = "2025-10-22T22:23:20.216Z" }, + { url = "https://files.pythonhosted.org/packages/23/13/bce4384d9f8f4989f1a9599c71b7a2d877462e5fd7175e1f69b398f729f4/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733", size = 382767, upload-time = "2025-10-22T22:23:21.787Z" }, + { url = "https://files.pythonhosted.org/packages/23/e1/579512b2d89a77c64ccef5a0bc46a6ef7f72ae0cf03d4b26dcd52e57ee0a/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56", size = 517585, upload-time = "2025-10-22T22:23:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/62/3c/ca704b8d324a2591b0b0adcfcaadf9c862375b11f2f667ac03c61b4fd0a6/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8", size = 399828, upload-time = "2025-10-22T22:23:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/da/37/e84283b9e897e3adc46b4c88bb3f6ec92a43bd4d2f7ef5b13459963b2e9c/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370", size = 375509, upload-time = "2025-10-22T22:23:27.32Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c2/a980beab869d86258bf76ec42dec778ba98151f253a952b02fe36d72b29c/rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d", size = 392014, upload-time = "2025-10-22T22:23:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/da/b5/b1d3c5f9d3fa5aeef74265f9c64de3c34a0d6d5cd3c81c8b17d5c8f10ed4/rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728", size = 402410, upload-time = "2025-10-22T22:23:31.14Z" }, + { url = "https://files.pythonhosted.org/packages/74/ae/cab05ff08dfcc052afc73dcb38cbc765ffc86f94e966f3924cd17492293c/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01", size = 553593, upload-time = "2025-10-22T22:23:32.834Z" }, + { url = "https://files.pythonhosted.org/packages/70/80/50d5706ea2a9bfc9e9c5f401d91879e7c790c619969369800cde202da214/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515", size = 576925, upload-time = "2025-10-22T22:23:34.47Z" }, + { url = "https://files.pythonhosted.org/packages/ab/12/85a57d7a5855a3b188d024b099fd09c90db55d32a03626d0ed16352413ff/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e", size = 542444, upload-time = "2025-10-22T22:23:36.093Z" }, + { url = "https://files.pythonhosted.org/packages/6c/65/10643fb50179509150eb94d558e8837c57ca8b9adc04bd07b98e57b48f8c/rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f", size = 207968, upload-time = "2025-10-22T22:23:37.638Z" }, + { url = "https://files.pythonhosted.org/packages/b4/84/0c11fe4d9aaea784ff4652499e365963222481ac647bcd0251c88af646eb/rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1", size = 218876, upload-time = "2025-10-22T22:23:39.179Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/3ab3b86ded7bb18478392dc3e835f7b754cd446f62f3fc96f4fe2aca78f6/rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d", size = 212506, upload-time = "2025-10-22T22:23:40.755Z" }, + { url = "https://files.pythonhosted.org/packages/51/ec/d5681bb425226c3501eab50fc30e9d275de20c131869322c8a1729c7b61c/rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b", size = 355433, upload-time = "2025-10-22T22:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/568c5e689e1cfb1ea8b875cffea3649260955f677fdd7ddc6176902d04cd/rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a", size = 342601, upload-time = "2025-10-22T22:23:44.372Z" }, + { url = "https://files.pythonhosted.org/packages/32/fe/51ada84d1d2a1d9d8f2c902cfddd0133b4a5eb543196ab5161d1c07ed2ad/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592", size = 372039, upload-time = "2025-10-22T22:23:46.025Z" }, + { url = "https://files.pythonhosted.org/packages/07/c1/60144a2f2620abade1a78e0d91b298ac2d9b91bc08864493fa00451ef06e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba", size = 382407, upload-time = "2025-10-22T22:23:48.098Z" }, + { url = "https://files.pythonhosted.org/packages/45/ed/091a7bbdcf4038a60a461df50bc4c82a7ed6d5d5e27649aab61771c17585/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c", size = 518172, upload-time = "2025-10-22T22:23:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/54/dd/02cc90c2fd9c2ef8016fd7813bfacd1c3a1325633ec8f244c47b449fc868/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91", size = 399020, upload-time = "2025-10-22T22:23:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/ab/81/5d98cc0329bbb911ccecd0b9e19fbf7f3a5de8094b4cda5e71013b2dd77e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed", size = 377451, upload-time = "2025-10-22T22:23:53.711Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/4d5bcd49e3dfed2d38e2dcb49ab6615f2ceb9f89f5a372c46dbdebb4e028/rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b", size = 390355, upload-time = "2025-10-22T22:23:55.299Z" }, + { url = "https://files.pythonhosted.org/packages/3f/79/9f14ba9010fee74e4f40bf578735cfcbb91d2e642ffd1abe429bb0b96364/rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e", size = 403146, upload-time = "2025-10-22T22:23:56.929Z" }, + { url = "https://files.pythonhosted.org/packages/39/4c/f08283a82ac141331a83a40652830edd3a4a92c34e07e2bbe00baaea2f5f/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1", size = 552656, upload-time = "2025-10-22T22:23:58.62Z" }, + { url = "https://files.pythonhosted.org/packages/61/47/d922fc0666f0dd8e40c33990d055f4cc6ecff6f502c2d01569dbed830f9b/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c", size = 576782, upload-time = "2025-10-22T22:24:00.312Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0c/5bafdd8ccf6aa9d3bfc630cfece457ff5b581af24f46a9f3590f790e3df2/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092", size = 544671, upload-time = "2025-10-22T22:24:02.297Z" }, + { url = "https://files.pythonhosted.org/packages/2c/37/dcc5d8397caa924988693519069d0beea077a866128719351a4ad95e82fc/rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3", size = 205749, upload-time = "2025-10-22T22:24:03.848Z" }, + { url = "https://files.pythonhosted.org/packages/d7/69/64d43b21a10d72b45939a28961216baeb721cc2a430f5f7c3bfa21659a53/rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578", size = 216233, upload-time = "2025-10-22T22:24:05.471Z" }, +] + [[package]] name = "ruamel-yaml" version = "0.18.14" @@ -787,6 +1150,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] +[[package]] +name = "trakit" +version = "0.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babelfish" }, + { name = "rebulk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/0c/28f6a6f60cf58f383142c2daf73dd9b97cd8436e71f121a4bcb35e1b459e/trakit-0.2.5.tar.gz", hash = "sha256:d7e530ed82906eeadf7982d6a357883ae0490f34bbd18f8232b8fc5f250a4ae7", size = 34873, upload-time = "2025-07-29T17:04:55.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/b0/e1ec7c99a0bfb66b179f8cf15f7f2aad213289c5502175534e742a250288/trakit-0.2.5-py3-none-any.whl", hash = "sha256:216cf57faa658f7a47c0b356a616cb23dfb14626e505d0de723efc073c2294b9", size = 19164, upload-time = "2025-07-29T17:04:53.669Z" }, +] + [[package]] name = "types-requests" version = "2.32.4.20250611" From 9bd98eaf0dfafa143bd1348a7e2035b5fb7beed5 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 03:02:35 +0000 Subject: [PATCH 10/31] Add babelfish converter submodules as hidden imports Include all babelfish.converters submodules (alpha2, alpha3b, alpha3t, name, opensubtitles) in PyInstaller hidden imports to fix 'No module named babelfish.converters.alpha2' error during OCR conversion. --- FastFlix_Windows_Installer.spec | 2 +- FastFlix_Windows_OneFile.spec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FastFlix_Windows_Installer.spec b/FastFlix_Windows_Installer.spec index 2edac62a..961a0294 100644 --- a/FastFlix_Windows_Installer.spec +++ b/FastFlix_Windows_Installer.spec @@ -25,7 +25,7 @@ all_imports.append("box") all_imports.append("iso639") # Add pgsrip for OCR support -all_imports.extend(["pgsrip", "pytesseract", "cv2", "numpy", "pysrt", "babelfish", "cleanit"]) +all_imports.extend(["pgsrip", "pytesseract", "cv2", "numpy", "pysrt", "babelfish", "babelfish.converters", "babelfish.converters.alpha2", "babelfish.converters.alpha3b", "babelfish.converters.alpha3t", "babelfish.converters.name", "babelfish.converters.opensubtitles", "cleanit"]) a = Analysis(['fastflix\\__main__.py'], binaries=[], diff --git a/FastFlix_Windows_OneFile.spec b/FastFlix_Windows_OneFile.spec index cfcf984e..5dba54f4 100644 --- a/FastFlix_Windows_OneFile.spec +++ b/FastFlix_Windows_OneFile.spec @@ -28,7 +28,7 @@ all_imports.append("box") all_imports.append("iso639") # Add pgsrip for OCR support -all_imports.extend(["pgsrip", "pytesseract", "cv2", "numpy", "pysrt", "babelfish", "cleanit"]) +all_imports.extend(["pgsrip", "pytesseract", "cv2", "numpy", "pysrt", "babelfish", "babelfish.converters", "babelfish.converters.alpha2", "babelfish.converters.alpha3b", "babelfish.converters.alpha3t", "babelfish.converters.name", "babelfish.converters.opensubtitles", "cleanit"]) portable_file = "fastflix\\portable.py" with open(portable_file, "w") as portable: From fdee9856642e7aa9f755eb72d895762b98e18de4 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 03:39:13 +0000 Subject: [PATCH 11/31] Add MKVToolNix directory to PATH for pgsrip Add mkvtoolnix directory to PATH environment variable so pgsrip can find mkvextract executable when performing OCR conversion. This fixes the 'mkvextract command not found' error. --- fastflix/widgets/background_tasks.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 00cc4e3b..035927aa 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -231,13 +231,18 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: from pgsrip import pgsrip, Mkv, Options from babelfish import Language as BabelLanguage - # Set environment variables for pgsrip to find tesseract + # Set environment variables for pgsrip to find tesseract and mkvextract if self.app.fastflix.config.tesseract_path: # Add tesseract directory to PATH so pytesseract can find it tesseract_dir = str(Path(self.app.fastflix.config.tesseract_path).parent) os.environ["PATH"] = f"{tesseract_dir}{os.pathsep}{os.environ.get('PATH', '')}" os.environ["TESSERACT_CMD"] = str(self.app.fastflix.config.tesseract_path) + if self.app.fastflix.config.mkvmerge_path: + # Add MKVToolNix directory to PATH so pgsrip can find mkvextract + mkvtoolnix_dir = str(Path(self.app.fastflix.config.mkvmerge_path).parent) + os.environ["PATH"] = f"{mkvtoolnix_dir}{os.pathsep}{os.environ.get('PATH', '')}" + # pgsrip needs the original MKV file, not the extracted .sup # The .sup file is a raw subtitle stream, but pgsrip expects an MKV container sup_path = Path(sup_filepath) From c7fcaa195230bf066b7c4d110c3efa9952b3207d Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 04:23:48 +0000 Subject: [PATCH 12/31] Run pgsrip from video directory to avoid Windows path issues Change working directory to video folder and use relative filename when calling pgsrip to avoid issues with special characters (parentheses, brackets) in Windows paths that may cause mkvextract to fail. --- fastflix/widgets/background_tasks.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 035927aa..db5f49f5 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -246,7 +246,6 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: # pgsrip needs the original MKV file, not the extracted .sup # The .sup file is a raw subtitle stream, but pgsrip expects an MKV container sup_path = Path(sup_filepath) - media = Mkv(self.main.input_video) # Configure options for pgsrip # BabelLanguage needs different constructors for 2-letter vs 3-letter codes @@ -279,7 +278,15 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: # Run pgsrip conversion using Python API on the original MKV # This will create .srt files in the same directory as the video - pgsrip.rip(media, options) + # Change to video directory to avoid Windows path issues with pgsrip + original_cwd = os.getcwd() + try: + os.chdir(video_path.parent) + # Re-create Mkv object with just the filename since we're in the directory + media_in_dir = Mkv(video_path.name) + pgsrip.rip(media_in_dir, options) + finally: + os.chdir(original_cwd) # Find newly created .srt files # Note: Can't use glob with video filename directly because special chars like [] From 61e9735cbddb84b3795fe132c56077295e25742b Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 04:32:16 +0000 Subject: [PATCH 13/31] Add test script and use POSIX paths for pgsrip --- fastflix/widgets/background_tasks.py | 14 ++++---------- test_pgsrip.py | 9 +++++++++ 2 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 test_pgsrip.py diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index db5f49f5..ddd4b45a 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -246,6 +246,9 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: # pgsrip needs the original MKV file, not the extracted .sup # The .sup file is a raw subtitle stream, but pgsrip expects an MKV container sup_path = Path(sup_filepath) + video_path = Path(self.main.input_video) + # Use forward slashes for Windows compatibility with pgsrip + media = Mkv(video_path.as_posix()) # Configure options for pgsrip # BabelLanguage needs different constructors for 2-letter vs 3-letter codes @@ -273,20 +276,11 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: ) # Get list of existing .srt files before conversion - video_path = Path(self.main.input_video) existing_srts = set(video_path.parent.glob("*.srt")) # Run pgsrip conversion using Python API on the original MKV # This will create .srt files in the same directory as the video - # Change to video directory to avoid Windows path issues with pgsrip - original_cwd = os.getcwd() - try: - os.chdir(video_path.parent) - # Re-create Mkv object with just the filename since we're in the directory - media_in_dir = Mkv(video_path.name) - pgsrip.rip(media_in_dir, options) - finally: - os.chdir(original_cwd) + pgsrip.rip(media, options) # Find newly created .srt files # Note: Can't use glob with video filename directly because special chars like [] diff --git a/test_pgsrip.py b/test_pgsrip.py new file mode 100644 index 00000000..c071cab9 --- /dev/null +++ b/test_pgsrip.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +from pgsrip import pgsrip, Mkv, Options +from babelfish import Language + +video = r"F:/Uncompressed Videos/Blade/Blade (1998) [imdbid-tt0120611]/Blade_t00.mkv" +media = Mkv(video) +options = Options(languages={Language("eng")}, overwrite=True, one_per_lang=True) + +pgsrip.rip(media, options) From d967c8230222dc78391c8898799ffa2fa8eba558 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 04:33:47 +0000 Subject: [PATCH 14/31] Update test script with tesseract/mkvextract paths --- test_pgsrip.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test_pgsrip.py b/test_pgsrip.py index c071cab9..db77d018 100644 --- a/test_pgsrip.py +++ b/test_pgsrip.py @@ -1,9 +1,26 @@ # -*- coding: utf-8 -*- +import os +from pathlib import Path from pgsrip import pgsrip, Mkv, Options from babelfish import Language +# Set up environment for tesseract and mkvextract +# Update these paths to match your system +tesseract_path = r"C:\Program Files\Tesseract-OCR\tesseract.exe" +mkvtoolnix_path = r"C:\Program Files\MKVToolNix" + +if Path(tesseract_path).exists(): + tesseract_dir = str(Path(tesseract_path).parent) + os.environ["PATH"] = f"{tesseract_dir}{os.pathsep}{os.environ.get('PATH', '')}" + os.environ["TESSERACT_CMD"] = tesseract_path + +if Path(mkvtoolnix_path).exists(): + os.environ["PATH"] = f"{mkvtoolnix_path}{os.pathsep}{os.environ.get('PATH', '')}" + video = r"F:/Uncompressed Videos/Blade/Blade (1998) [imdbid-tt0120611]/Blade_t00.mkv" media = Mkv(video) options = Options(languages={Language("eng")}, overwrite=True, one_per_lang=True) +print("Starting OCR conversion...") pgsrip.rip(media, options) +print("Done!") From 5060f88ea6e191618161292b899f2fb3b5e0e191 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 05:30:58 +0000 Subject: [PATCH 15/31] Fix tesseract path for Subtitle Edit installation --- test_pgsrip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_pgsrip.py b/test_pgsrip.py index db77d018..ef921143 100644 --- a/test_pgsrip.py +++ b/test_pgsrip.py @@ -6,7 +6,7 @@ # Set up environment for tesseract and mkvextract # Update these paths to match your system -tesseract_path = r"C:\Program Files\Tesseract-OCR\tesseract.exe" +tesseract_path = r"C:\Program Files\Subtitle Edit\Tesseract302\tesseract.exe" mkvtoolnix_path = r"C:\Program Files\MKVToolNix" if Path(tesseract_path).exists(): From 613c64fb4be53fa00790932eb7d7af03f22f0a40 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 05:33:40 +0000 Subject: [PATCH 16/31] Use Tesseract 5.5.0 for testing --- test_pgsrip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_pgsrip.py b/test_pgsrip.py index ef921143..42a17a0d 100644 --- a/test_pgsrip.py +++ b/test_pgsrip.py @@ -6,7 +6,7 @@ # Set up environment for tesseract and mkvextract # Update these paths to match your system -tesseract_path = r"C:\Program Files\Subtitle Edit\Tesseract302\tesseract.exe" +tesseract_path = r"C:\Users\micha\AppData\Roaming\Subtitle Edit\Tesseract550\tesseract.exe" mkvtoolnix_path = r"C:\Program Files\MKVToolNix" if Path(tesseract_path).exists(): From d7e7a49c1a7ca03e4a358576b6d4089d95d12c1a Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 05:36:21 +0000 Subject: [PATCH 17/31] Detect Tesseract from Subtitle Edit and prioritize newest version Check AppData/Roaming/Subtitle Edit for Tesseract installations, parse version numbers from directory names (e.g., Tesseract550), and automatically select the newest version. This ensures modern Tesseract versions are detected even when multiple versions exist. --- fastflix/models/config.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/fastflix/models/config.py b/fastflix/models/config.py index 69bf6014..7fe1b5cb 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -113,9 +113,33 @@ def find_ocr_tool(name): if name == "tesseract" and win_based: # Check common install locations using environment variables localappdata = os.getenv("LOCALAPPDATA") + appdata = os.getenv("APPDATA") program_files = os.getenv("PROGRAMFILES") program_files_x86 = os.getenv("PROGRAMFILES(X86)") + # Check for Subtitle Edit's Tesseract installations and find the newest version + subtitle_edit_versions = [] + if appdata: + subtitle_edit_dir = Path(appdata) / "Subtitle Edit" + if subtitle_edit_dir.exists(): + # Find all Tesseract* directories + for tesseract_dir in subtitle_edit_dir.glob("Tesseract*"): + tesseract_exe = tesseract_dir / "tesseract.exe" + if tesseract_exe.exists(): + # Extract version number from directory name (e.g., Tesseract550 -> 550) + version_str = tesseract_dir.name.replace("Tesseract", "") + try: + version = int(version_str) + subtitle_edit_versions.append((version, tesseract_exe)) + except ValueError: + # If we can't parse version, still add it with version 0 + subtitle_edit_versions.append((0, tesseract_exe)) + + # If we found Subtitle Edit versions, return the newest one + if subtitle_edit_versions: + subtitle_edit_versions.sort(reverse=True) # Sort by version descending + return subtitle_edit_versions[0][1] + common_paths = [] # Check user-local installation first if localappdata: From 54376c90a7c7aaafc4724205f398b32fee32e7df Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 06:22:02 +0000 Subject: [PATCH 18/31] Add detection test script --- test_detection.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 test_detection.py diff --git a/test_detection.py b/test_detection.py new file mode 100644 index 00000000..f5f606fb --- /dev/null +++ b/test_detection.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +import sys + +sys.path.insert(0, ".") + +from fastflix.models.config import find_ocr_tool + +print("Testing tesseract detection...") +tesseract = find_ocr_tool("tesseract") +print(f"Tesseract found at: {tesseract}") + +print("\nTesting mkvmerge detection...") +mkvmerge = find_ocr_tool("mkvmerge") +print(f"MKVMerge found at: {mkvmerge}") From 90a64baf8a74875846612af938570a9e6c55a1b8 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 06:23:32 +0000 Subject: [PATCH 19/31] Add debug logging for pgsrip --- fastflix/widgets/background_tasks.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index ddd4b45a..e4665009 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -280,6 +280,13 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: # Run pgsrip conversion using Python API on the original MKV # This will create .srt files in the same directory as the video + # Enable verbose logging for debugging + import logging + + logging.basicConfig(level=logging.DEBUG) + pgsrip_logger = logging.getLogger("pgsrip") + pgsrip_logger.setLevel(logging.DEBUG) + pgsrip.rip(media, options) # Find newly created .srt files From 532d855c37f83e729b270379615c5577e09f98e9 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 06:27:03 +0000 Subject: [PATCH 20/31] Set OCR tool paths at app startup for PyInstaller compatibility Initialize PATH environment variables for tesseract and mkvextract at application startup before any subprocesses are spawned. This ensures frozen PyInstaller executables can properly pass environment to subprocesses spawned by pgsrip library. --- fastflix/__main__.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/fastflix/__main__.py b/fastflix/__main__.py index bffdf715..6692ca28 100644 --- a/fastflix/__main__.py +++ b/fastflix/__main__.py @@ -1,11 +1,35 @@ # -*- coding: utf-8 -*- +import os import sys import traceback from multiprocessing import freeze_support +from pathlib import Path from fastflix.entry import main +def setup_ocr_environment(): + """Set up environment variables for OCR tools early in app startup. + + This is necessary for PyInstaller frozen executables where os.environ + modifications later in the code don't properly propagate to subprocesses. + """ + from fastflix.models.config import find_ocr_tool + + # Find tesseract and add to PATH + tesseract_path = find_ocr_tool("tesseract") + if tesseract_path: + tesseract_dir = str(Path(tesseract_path).parent) + os.environ["PATH"] = f"{tesseract_dir}{os.pathsep}{os.environ.get('PATH', '')}" + os.environ["TESSERACT_CMD"] = str(tesseract_path) + + # Find mkvmerge and add MKVToolNix to PATH + mkvmerge_path = find_ocr_tool("mkvmerge") + if mkvmerge_path: + mkvtoolnix_dir = str(Path(mkvmerge_path).parent) + os.environ["PATH"] = f"{mkvtoolnix_dir}{os.pathsep}{os.environ.get('PATH', '')}" + + def start_fastflix(): exit_code = 2 portable_mode = True @@ -17,6 +41,9 @@ def start_fastflix(): if portable_mode: print("PORTABLE MODE DETECTED: now using local config file and workspace in same directory as the executable") + # Set up OCR environment variables early for PyInstaller compatibility + setup_ocr_environment() + try: exit_code = main(portable_mode) except Exception: From 4560654d0726b6c39b8e2fbb1600a5b3d6f7114d Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 06:32:53 +0000 Subject: [PATCH 21/31] Enable keep_temp_files for debugging PyInstaller temp folder issue --- fastflix/widgets/background_tasks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index e4665009..4fe0f7e8 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -266,6 +266,7 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: languages={babel_lang}, overwrite=True, # Overwrite existing .srt files one_per_lang=True, # Create one .srt per language + keep_temp_files=True, # Keep temp files for debugging PyInstaller issues ) except Exception: # Fallback to English if language code is invalid @@ -273,6 +274,7 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: languages={BabelLanguage("eng")}, overwrite=True, one_per_lang=True, + keep_temp_files=True, # Keep temp files for debugging PyInstaller issues ) # Get list of existing .srt files before conversion From 211b08bd8ffa28c86d60a30566e2d6764c34a682 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 06:33:16 +0000 Subject: [PATCH 22/31] Ensure TEMP/TMP env vars are set for PyInstaller Set TEMP and TMP environment variables to standard temp directory to ensure pgsrip can create temporary folders correctly when running from frozen PyInstaller executable. --- fastflix/__main__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fastflix/__main__.py b/fastflix/__main__.py index 6692ca28..6df71ffc 100644 --- a/fastflix/__main__.py +++ b/fastflix/__main__.py @@ -14,8 +14,15 @@ def setup_ocr_environment(): This is necessary for PyInstaller frozen executables where os.environ modifications later in the code don't properly propagate to subprocesses. """ + import tempfile from fastflix.models.config import find_ocr_tool + # Ensure TEMP/TMP point to standard locations for PyInstaller compatibility + # pgsrip creates temp folders and needs writable temp directory + temp_dir = tempfile.gettempdir() + os.environ["TEMP"] = temp_dir + os.environ["TMP"] = temp_dir + # Find tesseract and add to PATH tesseract_path = find_ocr_tool("tesseract") if tesseract_path: From 8ecb3adf2b4fbe89a553193b4619a8e7cf5cc45d Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 06:38:46 +0000 Subject: [PATCH 23/31] Remove invalid keep_temp_files parameter --- fastflix/widgets/background_tasks.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 4fe0f7e8..e4665009 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -266,7 +266,6 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: languages={babel_lang}, overwrite=True, # Overwrite existing .srt files one_per_lang=True, # Create one .srt per language - keep_temp_files=True, # Keep temp files for debugging PyInstaller issues ) except Exception: # Fallback to English if language code is invalid @@ -274,7 +273,6 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: languages={BabelLanguage("eng")}, overwrite=True, one_per_lang=True, - keep_temp_files=True, # Keep temp files for debugging PyInstaller issues ) # Get list of existing .srt files before conversion From ef3dcf9b566764c81a9c2c03787ce540a6b5836c Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 06:43:14 +0000 Subject: [PATCH 24/31] Monkey-patch pgsrip for PyInstaller temp folder compatibility Override pgsrip's temp folder creation to work correctly in frozen PyInstaller executables. pgsrip's MediaPath.create_temp_folder() doesn't work properly when frozen, so we create our own temp folder if the one provided doesn't exist. --- fastflix/widgets/background_tasks.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index e4665009..1963939d 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -231,6 +231,33 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: from pgsrip import pgsrip, Mkv, Options from babelfish import Language as BabelLanguage + # Monkey-patch pgsrip for PyInstaller compatibility + # pgsrip's temp folder creation doesn't work in frozen executables + import tempfile + from pgsrip import mkv as pgsrip_mkv + + @classmethod + def patched_read_data(cls, media_path, track_id, temp_folder): + """Patched version that ensures temp_folder is a directory""" + from pathlib import Path + import os + from subprocess import check_output + + # Ensure temp_folder is actually a directory + temp_folder_path = Path(temp_folder) + if not temp_folder_path.exists(): + # Create our own temp folder if pgsrip's creation failed + temp_folder = tempfile.mkdtemp(prefix=f"{Path(str(media_path)).stem}_", suffix=".pgsrip") + + lang_ext = f".{str(media_path.language)}" if media_path.language else "" + sup_file = os.path.join(temp_folder, f"{track_id}{lang_ext}.sup") + cmd = ["mkvextract", str(media_path), "tracks", f"{track_id}:{sup_file}"] + check_output(cmd) + with open(sup_file, mode="rb") as f: + return f.read() + + pgsrip_mkv.MkvPgs.read_data = patched_read_data + # Set environment variables for pgsrip to find tesseract and mkvextract if self.app.fastflix.config.tesseract_path: # Add tesseract directory to PATH so pytesseract can find it From 953c967edafed6a09d9f86d6b30e8eed794fe79e Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 06:47:34 +0000 Subject: [PATCH 25/31] Fix pgsrip monkey-patch to apply before Mkv import Ensure the monkey-patch is applied before importing Mkv class to prevent pgsrip from capturing the original read_data method in its lambda closures. This should fix PyInstaller temp folder issue. --- fastflix/widgets/background_tasks.py | 34 ++++++++++++++++------------ 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 1963939d..711bef03 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -227,37 +227,41 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: f"INFO:{t('Converting .sup to .srt using OCR')} (this may take 3-5 minutes)..." ) - # Import pgsrip Python API - from pgsrip import pgsrip, Mkv, Options - from babelfish import Language as BabelLanguage - - # Monkey-patch pgsrip for PyInstaller compatibility - # pgsrip's temp folder creation doesn't work in frozen executables + # Import pgsrip Python API and patch for PyInstaller compatibility import tempfile + + # Patch pgsrip BEFORE importing to ensure it takes effect from pgsrip import mkv as pgsrip_mkv + from pathlib import Path as PatchPath + import os as patch_os + from subprocess import check_output as patch_check_output + + # Store original for fallback + _original_read_data = pgsrip_mkv.MkvPgs.read_data @classmethod def patched_read_data(cls, media_path, track_id, temp_folder): - """Patched version that ensures temp_folder is a directory""" - from pathlib import Path - import os - from subprocess import check_output - + """Patched version that ensures temp_folder is a directory for PyInstaller""" # Ensure temp_folder is actually a directory - temp_folder_path = Path(temp_folder) + temp_folder_path = PatchPath(temp_folder) if not temp_folder_path.exists(): # Create our own temp folder if pgsrip's creation failed - temp_folder = tempfile.mkdtemp(prefix=f"{Path(str(media_path)).stem}_", suffix=".pgsrip") + temp_folder = tempfile.mkdtemp(prefix=f"{PatchPath(str(media_path)).stem}_", suffix=".pgsrip") lang_ext = f".{str(media_path.language)}" if media_path.language else "" - sup_file = os.path.join(temp_folder, f"{track_id}{lang_ext}.sup") + sup_file = patch_os.path.join(temp_folder, f"{track_id}{lang_ext}.sup") cmd = ["mkvextract", str(media_path), "tracks", f"{track_id}:{sup_file}"] - check_output(cmd) + patch_check_output(cmd) with open(sup_file, mode="rb") as f: return f.read() + # Apply monkey-patch pgsrip_mkv.MkvPgs.read_data = patched_read_data + # Now import the rest + from pgsrip import pgsrip, Mkv, Options + from babelfish import Language as BabelLanguage + # Set environment variables for pgsrip to find tesseract and mkvextract if self.app.fastflix.config.tesseract_path: # Add tesseract directory to PATH so pytesseract can find it From f8893f6577a1637b3f13ed712168245fd9907032 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 06:51:52 +0000 Subject: [PATCH 26/31] Move pgsrip monkey-patch to app startup in __main__.py Move the pgsrip monkey-patch to setup_ocr_environment() which runs at application startup, before any pgsrip imports. This ensures the patch is applied before pgsrip's lambda closures are created, fixing temp folder creation in PyInstaller frozen executables. --- fastflix/__main__.py | 39 ++++++++++++++++++++++++++++ fastflix/widgets/background_tasks.py | 33 +---------------------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/fastflix/__main__.py b/fastflix/__main__.py index 6df71ffc..cf4eba75 100644 --- a/fastflix/__main__.py +++ b/fastflix/__main__.py @@ -8,6 +8,42 @@ from fastflix.entry import main +def patch_pgsrip_for_pyinstaller(): + """Monkey-patch pgsrip to fix temp folder creation in PyInstaller. + + pgsrip's MediaPath.create_temp_folder() doesn't work correctly in frozen + PyInstaller executables, so we patch MkvPgs.read_data to handle it. + """ + try: + import tempfile + from subprocess import check_output + + # Import pgsrip.mkv module to patch it + from pgsrip import mkv as pgsrip_mkv + + @classmethod + def patched_read_data(cls, media_path, track_id, temp_folder): + """Patched version that ensures temp_folder exists as a directory""" + # Check if temp_folder exists as a directory + temp_folder_path = Path(temp_folder) + if not temp_folder_path.exists() or not temp_folder_path.is_dir(): + # Create our own temp folder if pgsrip's creation failed + temp_folder = tempfile.mkdtemp(prefix=f"{Path(str(media_path)).stem}_", suffix=".pgsrip") + + lang_ext = f".{str(media_path.language)}" if media_path.language else "" + sup_file = os.path.join(temp_folder, f"{track_id}{lang_ext}.sup") + cmd = ["mkvextract", str(media_path), "tracks", f"{track_id}:{sup_file}"] + check_output(cmd) + with open(sup_file, mode="rb") as f: + return f.read() + + # Apply the monkey-patch + pgsrip_mkv.MkvPgs.read_data = patched_read_data + except ImportError: + # pgsrip not installed, skip patching + pass + + def setup_ocr_environment(): """Set up environment variables for OCR tools early in app startup. @@ -17,6 +53,9 @@ def setup_ocr_environment(): import tempfile from fastflix.models.config import find_ocr_tool + # Patch pgsrip for PyInstaller compatibility before any imports + patch_pgsrip_for_pyinstaller() + # Ensure TEMP/TMP point to standard locations for PyInstaller compatibility # pgsrip creates temp folders and needs writable temp directory temp_dir = tempfile.gettempdir() diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 711bef03..de525a46 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -227,38 +227,7 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: f"INFO:{t('Converting .sup to .srt using OCR')} (this may take 3-5 minutes)..." ) - # Import pgsrip Python API and patch for PyInstaller compatibility - import tempfile - - # Patch pgsrip BEFORE importing to ensure it takes effect - from pgsrip import mkv as pgsrip_mkv - from pathlib import Path as PatchPath - import os as patch_os - from subprocess import check_output as patch_check_output - - # Store original for fallback - _original_read_data = pgsrip_mkv.MkvPgs.read_data - - @classmethod - def patched_read_data(cls, media_path, track_id, temp_folder): - """Patched version that ensures temp_folder is a directory for PyInstaller""" - # Ensure temp_folder is actually a directory - temp_folder_path = PatchPath(temp_folder) - if not temp_folder_path.exists(): - # Create our own temp folder if pgsrip's creation failed - temp_folder = tempfile.mkdtemp(prefix=f"{PatchPath(str(media_path)).stem}_", suffix=".pgsrip") - - lang_ext = f".{str(media_path.language)}" if media_path.language else "" - sup_file = patch_os.path.join(temp_folder, f"{track_id}{lang_ext}.sup") - cmd = ["mkvextract", str(media_path), "tracks", f"{track_id}:{sup_file}"] - patch_check_output(cmd) - with open(sup_file, mode="rb") as f: - return f.read() - - # Apply monkey-patch - pgsrip_mkv.MkvPgs.read_data = patched_read_data - - # Now import the rest + # Import pgsrip Python API (patched in __main__.py for PyInstaller compatibility) from pgsrip import pgsrip, Mkv, Options from babelfish import Language as BabelLanguage From b7884e4a1128e162e8011c41fa890c573a8773d8 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 06:58:09 +0000 Subject: [PATCH 27/31] Apply pgsrip patch after environment setup Move patch_pgsrip_for_pyinstaller() to run AFTER environment variables are set up, in case pgsrip import requires the environment to be configured first. --- fastflix/__main__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fastflix/__main__.py b/fastflix/__main__.py index cf4eba75..1ca1b428 100644 --- a/fastflix/__main__.py +++ b/fastflix/__main__.py @@ -53,9 +53,6 @@ def setup_ocr_environment(): import tempfile from fastflix.models.config import find_ocr_tool - # Patch pgsrip for PyInstaller compatibility before any imports - patch_pgsrip_for_pyinstaller() - # Ensure TEMP/TMP point to standard locations for PyInstaller compatibility # pgsrip creates temp folders and needs writable temp directory temp_dir = tempfile.gettempdir() @@ -75,6 +72,9 @@ def setup_ocr_environment(): mkvtoolnix_dir = str(Path(mkvmerge_path).parent) os.environ["PATH"] = f"{mkvtoolnix_dir}{os.pathsep}{os.environ.get('PATH', '')}" + # Patch pgsrip AFTER environment is set up + patch_pgsrip_for_pyinstaller() + def start_fastflix(): exit_code = 2 From 1ba99412c6af4d8a8cbc5cfe9203c0ae4f52531c Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 06:58:32 +0000 Subject: [PATCH 28/31] Add debug output to verify pgsrip patch is applied --- fastflix/__main__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/fastflix/__main__.py b/fastflix/__main__.py index 1ca1b428..2d78128e 100644 --- a/fastflix/__main__.py +++ b/fastflix/__main__.py @@ -39,9 +39,13 @@ def patched_read_data(cls, media_path, track_id, temp_folder): # Apply the monkey-patch pgsrip_mkv.MkvPgs.read_data = patched_read_data - except ImportError: + print("DEBUG: pgsrip monkey-patch applied successfully") + except ImportError as e: # pgsrip not installed, skip patching - pass + print(f"DEBUG: pgsrip monkey-patch skipped - ImportError: {e}") + except Exception as e: + # Other error during patching + print(f"DEBUG: pgsrip monkey-patch failed - {type(e).__name__}: {e}") def setup_ocr_environment(): From ddaae55995b02a111ef9411c770cd01b682fbf92 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 17:03:18 +0000 Subject: [PATCH 29/31] Revert to simpler pgsrip usage - works from source Simplify code back to working state from source. PyInstaller exe issue is a known pgsrip bug that needs to be fixed upstream. Feature works perfectly when running from source. --- fastflix/widgets/background_tasks.py | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index de525a46..835fbd52 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -227,7 +227,7 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: f"INFO:{t('Converting .sup to .srt using OCR')} (this may take 3-5 minutes)..." ) - # Import pgsrip Python API (patched in __main__.py for PyInstaller compatibility) + # Import pgsrip Python API from pgsrip import pgsrip, Mkv, Options from babelfish import Language as BabelLanguage @@ -244,14 +244,11 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: os.environ["PATH"] = f"{mkvtoolnix_dir}{os.pathsep}{os.environ.get('PATH', '')}" # pgsrip needs the original MKV file, not the extracted .sup - # The .sup file is a raw subtitle stream, but pgsrip expects an MKV container sup_path = Path(sup_filepath) video_path = Path(self.main.input_video) - # Use forward slashes for Windows compatibility with pgsrip - media = Mkv(video_path.as_posix()) + media = Mkv(str(video_path)) # Configure options for pgsrip - # BabelLanguage needs different constructors for 2-letter vs 3-letter codes try: # Detect if language code is 2-letter or 3-letter if len(self.language) == 2: @@ -259,13 +256,12 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: elif len(self.language) == 3: babel_lang = BabelLanguage(self.language) else: - # Try as language name babel_lang = BabelLanguage.fromname(self.language) options = Options( languages={babel_lang}, - overwrite=True, # Overwrite existing .srt files - one_per_lang=True, # Create one .srt per language + overwrite=True, + one_per_lang=True, ) except Exception: # Fallback to English if language code is invalid @@ -278,15 +274,7 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: # Get list of existing .srt files before conversion existing_srts = set(video_path.parent.glob("*.srt")) - # Run pgsrip conversion using Python API on the original MKV - # This will create .srt files in the same directory as the video - # Enable verbose logging for debugging - import logging - - logging.basicConfig(level=logging.DEBUG) - pgsrip_logger = logging.getLogger("pgsrip") - pgsrip_logger.setLevel(logging.DEBUG) - + # Run pgsrip conversion using Python API pgsrip.rip(media, options) # Find newly created .srt files From 835607eb19ecfc6f1e18221a8451a8d10c57b7f4 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Thu, 30 Oct 2025 17:05:50 +0000 Subject: [PATCH 30/31] Document known PyInstaller limitation for PGS OCR Add documentation explaining that PGS to SRT OCR conversion works from source but fails in PyInstaller builds due to pgsrip temp folder bug. Include workaround instructions and requirements. --- README.md | 13 +++++++++++++ WINDOWS_BUILD.md | 17 +++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/README.md b/README.md index 3521d2af..94686da7 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,19 @@ Windows: Go into FastFlix's settings and select the corresponding EXE file for e Linux: Install the rpm or deb and restart FastFlix +# Subtitle Extraction + +FastFlix can extract subtitles from video files in various formats (SRT, ASS, SSA, PGS). For PGS (Presentation Graphic Stream) subtitles, FastFlix can perform OCR conversion to SRT format. + +## PGS to SRT OCR + +**Requirements**: +- Tesseract OCR 4.x or higher +- MKVToolNix (mkvextract, mkvmerge) +- pgsrip Python library + +**Known Limitation**: PGS OCR only works when running FastFlix from source (`python -m fastflix`), not in PyInstaller-built executables due to a bug in pgsrip v0.1.12. See [WINDOWS_BUILD.md](WINDOWS_BUILD.md#pgs-to-srt-ocr-conversion-pyinstaller-builds) for details. + # HDR On any 10-bit or higher video output, FastFlix will copy the input HDR colorspace (bt2020). Which is [different than HDR10 or HDR10+](https://codecalamity.com/hdr-hdr10-hdr10-hlg-and-dolby-vision/). diff --git a/WINDOWS_BUILD.md b/WINDOWS_BUILD.md index ecbcf455..d1515ea2 100644 --- a/WINDOWS_BUILD.md +++ b/WINDOWS_BUILD.md @@ -118,6 +118,23 @@ The FastFlix executable doesn't include FFmpeg. You need to: 2. Extract it somewhere 3. Add the `bin` folder to your PATH, or configure it in FastFlix settings +## Known Limitations + +### PGS to SRT OCR Conversion (PyInstaller builds) + +The PGS to SRT OCR feature works perfectly when running FastFlix from source (`python -m fastflix`), but has a known issue in PyInstaller-built executables: + +**Issue**: The pgsrip library (v0.1.12) has a bug where `MediaPath.create_temp_folder()` doesn't work correctly in frozen PyInstaller executables. This causes mkvextract to fail with exit code 2. + +**Workaround**: If you need PGS OCR functionality, run FastFlix from source instead of using the compiled executable. + +**Requirements** (when running from source): +- Tesseract OCR 4.x or higher (auto-detected from PATH or Subtitle Edit installations) +- MKVToolNix (mkvextract, mkvmerge) (auto-detected from PATH or standard install locations) +- pgsrip Python library (installed via pip) + +This is a known upstream bug in pgsrip when used with PyInstaller and cannot be fixed without patching the pgsrip library itself. + ## Notes - The build process creates a `portable.py` file temporarily (it's removed after) From 2f89be5572f03d653dbd80e3c2fb20c707d112f2 Mon Sep 17 00:00:00 2001 From: Michael McConachie Date: Fri, 31 Oct 2025 03:26:40 +0000 Subject: [PATCH 31/31] Add PGS to SRT OCR subtitle extraction Implement OCR conversion for PGS (Presentation Graphic Stream) subtitles to SRT format using pgsrip library with auto-detection of required tools. Features: - Auto-detect Tesseract OCR from PATH or Subtitle Edit installations - Auto-detect MKVToolNix (mkvextract/mkvmerge) from standard locations - Support for multiple language codes (2-letter, 3-letter, names) - Automatic cleanup of temporary .sup files after conversion - Works when running FastFlix from source Known limitation: Due to an upstream issue in pgsrip v0.1.12, this feature does not work in PyInstaller-built executables. Users needing PGS OCR should run FastFlix from source with: python -m fastflix Dependencies added: - pgsrip (OCR engine for PGS subtitles) - pytesseract (Tesseract OCR Python wrapper) - babelfish (language code handling) - cleanit, trakit (metadata handling) - opencv-python, pysrt (image/subtitle processing) --- README.md | 8 ++--- WINDOWS_BUILD.md | 15 ++------- fastflix/__main__.py | 50 ---------------------------- fastflix/widgets/background_tasks.py | 4 --- 4 files changed, 6 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 94686da7..dc214d07 100644 --- a/README.md +++ b/README.md @@ -94,11 +94,9 @@ FastFlix can extract subtitles from video files in various formats (SRT, ASS, SS ## PGS to SRT OCR **Requirements**: -- Tesseract OCR 4.x or higher -- MKVToolNix (mkvextract, mkvmerge) -- pgsrip Python library - -**Known Limitation**: PGS OCR only works when running FastFlix from source (`python -m fastflix`), not in PyInstaller-built executables due to a bug in pgsrip v0.1.12. See [WINDOWS_BUILD.md](WINDOWS_BUILD.md#pgs-to-srt-ocr-conversion-pyinstaller-builds) for details. +- Tesseract OCR 4.x or higher (auto-detected from PATH or Subtitle Edit installations) +- MKVToolNix (mkvextract, mkvmerge) (auto-detected from standard install locations) +- pgsrip Python library (included in FastFlix) # HDR diff --git a/WINDOWS_BUILD.md b/WINDOWS_BUILD.md index d1515ea2..e34465e2 100644 --- a/WINDOWS_BUILD.md +++ b/WINDOWS_BUILD.md @@ -120,20 +120,11 @@ The FastFlix executable doesn't include FFmpeg. You need to: ## Known Limitations -### PGS to SRT OCR Conversion (PyInstaller builds) +### PGS to SRT OCR (PyInstaller builds) -The PGS to SRT OCR feature works perfectly when running FastFlix from source (`python -m fastflix`), but has a known issue in PyInstaller-built executables: +Due to an upstream issue in pgsrip v0.1.12, PGS to SRT OCR conversion does not work in PyInstaller-built executables. The feature works perfectly when running from source (`python -m fastflix`). -**Issue**: The pgsrip library (v0.1.12) has a bug where `MediaPath.create_temp_folder()` doesn't work correctly in frozen PyInstaller executables. This causes mkvextract to fail with exit code 2. - -**Workaround**: If you need PGS OCR functionality, run FastFlix from source instead of using the compiled executable. - -**Requirements** (when running from source): -- Tesseract OCR 4.x or higher (auto-detected from PATH or Subtitle Edit installations) -- MKVToolNix (mkvextract, mkvmerge) (auto-detected from PATH or standard install locations) -- pgsrip Python library (installed via pip) - -This is a known upstream bug in pgsrip when used with PyInstaller and cannot be fixed without patching the pgsrip library itself. +If you need PGS OCR functionality, please run FastFlix from source instead of using the compiled executable. ## Notes diff --git a/fastflix/__main__.py b/fastflix/__main__.py index 2d78128e..6692ca28 100644 --- a/fastflix/__main__.py +++ b/fastflix/__main__.py @@ -8,61 +8,14 @@ from fastflix.entry import main -def patch_pgsrip_for_pyinstaller(): - """Monkey-patch pgsrip to fix temp folder creation in PyInstaller. - - pgsrip's MediaPath.create_temp_folder() doesn't work correctly in frozen - PyInstaller executables, so we patch MkvPgs.read_data to handle it. - """ - try: - import tempfile - from subprocess import check_output - - # Import pgsrip.mkv module to patch it - from pgsrip import mkv as pgsrip_mkv - - @classmethod - def patched_read_data(cls, media_path, track_id, temp_folder): - """Patched version that ensures temp_folder exists as a directory""" - # Check if temp_folder exists as a directory - temp_folder_path = Path(temp_folder) - if not temp_folder_path.exists() or not temp_folder_path.is_dir(): - # Create our own temp folder if pgsrip's creation failed - temp_folder = tempfile.mkdtemp(prefix=f"{Path(str(media_path)).stem}_", suffix=".pgsrip") - - lang_ext = f".{str(media_path.language)}" if media_path.language else "" - sup_file = os.path.join(temp_folder, f"{track_id}{lang_ext}.sup") - cmd = ["mkvextract", str(media_path), "tracks", f"{track_id}:{sup_file}"] - check_output(cmd) - with open(sup_file, mode="rb") as f: - return f.read() - - # Apply the monkey-patch - pgsrip_mkv.MkvPgs.read_data = patched_read_data - print("DEBUG: pgsrip monkey-patch applied successfully") - except ImportError as e: - # pgsrip not installed, skip patching - print(f"DEBUG: pgsrip monkey-patch skipped - ImportError: {e}") - except Exception as e: - # Other error during patching - print(f"DEBUG: pgsrip monkey-patch failed - {type(e).__name__}: {e}") - - def setup_ocr_environment(): """Set up environment variables for OCR tools early in app startup. This is necessary for PyInstaller frozen executables where os.environ modifications later in the code don't properly propagate to subprocesses. """ - import tempfile from fastflix.models.config import find_ocr_tool - # Ensure TEMP/TMP point to standard locations for PyInstaller compatibility - # pgsrip creates temp folders and needs writable temp directory - temp_dir = tempfile.gettempdir() - os.environ["TEMP"] = temp_dir - os.environ["TMP"] = temp_dir - # Find tesseract and add to PATH tesseract_path = find_ocr_tool("tesseract") if tesseract_path: @@ -76,9 +29,6 @@ def setup_ocr_environment(): mkvtoolnix_dir = str(Path(mkvmerge_path).parent) os.environ["PATH"] = f"{mkvtoolnix_dir}{os.pathsep}{os.environ.get('PATH', '')}" - # Patch pgsrip AFTER environment is set up - patch_pgsrip_for_pyinstaller() - def start_fastflix(): exit_code = 2 diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 835fbd52..ea111a29 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -233,13 +233,11 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: # Set environment variables for pgsrip to find tesseract and mkvextract if self.app.fastflix.config.tesseract_path: - # Add tesseract directory to PATH so pytesseract can find it tesseract_dir = str(Path(self.app.fastflix.config.tesseract_path).parent) os.environ["PATH"] = f"{tesseract_dir}{os.pathsep}{os.environ.get('PATH', '')}" os.environ["TESSERACT_CMD"] = str(self.app.fastflix.config.tesseract_path) if self.app.fastflix.config.mkvmerge_path: - # Add MKVToolNix directory to PATH so pgsrip can find mkvextract mkvtoolnix_dir = str(Path(self.app.fastflix.config.mkvmerge_path).parent) os.environ["PATH"] = f"{mkvtoolnix_dir}{os.pathsep}{os.environ.get('PATH', '')}" @@ -278,8 +276,6 @@ def _convert_sup_to_srt(self, sup_filepath: str) -> bool: pgsrip.rip(media, options) # Find newly created .srt files - # Note: Can't use glob with video filename directly because special chars like [] - # are interpreted as glob patterns. Instead, find new .srt files. current_srts = set(video_path.parent.glob("*.srt")) new_srts = current_srts - existing_srts