Skip to content

Commit eabf161

Browse files
committed
Add PGS to SRT OCR subtitle extraction feature
Implements OCR-based conversion of PGS (Presentation Graphic Stream) subtitles to SRT format with automatic tool detection. Features: - Auto-detect Tesseract OCR from PATH or Subtitle Edit installations - Auto-detect MKVToolNix from standard install locations - Support multiple language codes (ISO 639-2/3, language names) - GUI checkbox to enable/disable OCR for PGS subtitles - Automatic cleanup of intermediate .sup files after conversion Dependencies: - pgsrip: PGS subtitle OCR engine - pytesseract: Tesseract OCR wrapper - babelfish: Language code handling - opencv-python, cleanit, trakit: Image/metadata processing Known limitation: This feature works when running from source (python -m fastflix) but not in PyInstaller-built executables due to subprocess environment issues with pgsrip. Users needing PGS OCR should run from source.
1 parent 250db2f commit eabf161

File tree

11 files changed

+916
-11
lines changed

11 files changed

+916
-11
lines changed

FastFlix_Windows_Installer.spec

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- mode: python ; coding: utf-8 -*-
2-
from PyInstaller.utils.hooks import collect_submodules
2+
from PyInstaller.utils.hooks import collect_submodules, copy_metadata, collect_data_files
33
import toml
44

55
block_cipher = None
@@ -24,9 +24,12 @@ all_imports.remove("python-box")
2424
all_imports.append("box")
2525
all_imports.append("iso639")
2626

27+
# Add pgsrip for OCR support
28+
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"])
29+
2730
a = Analysis(['fastflix\\__main__.py'],
2831
binaries=[],
29-
datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files,
32+
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'),
3033
hiddenimports=all_imports,
3134
hookspath=[],
3235
runtime_hooks=[],

FastFlix_Windows_OneFile.spec

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33
import toml
44

5-
from PyInstaller.utils.hooks import collect_submodules
5+
from PyInstaller.utils.hooks import collect_submodules, copy_metadata, collect_data_files
66

77
block_cipher = None
88

@@ -27,13 +27,16 @@ all_imports.remove("python-box")
2727
all_imports.append("box")
2828
all_imports.append("iso639")
2929

30+
# Add pgsrip for OCR support
31+
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"])
32+
3033
portable_file = "fastflix\\portable.py"
3134
with open(portable_file, "w") as portable:
3235
portable.write(" ")
3336

3437
a = Analysis(['fastflix\\__main__.py'],
3538
binaries=[],
36-
datas=[('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files,
39+
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'),
3740
hiddenimports=all_imports,
3841
hookspath=[],
3942
runtime_hooks=[],

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,25 @@ Windows: Go into FastFlix's settings and select the corresponding EXE file for e
8787

8888
Linux: Install the rpm or deb and restart FastFlix
8989

90+
# Subtitle Extraction
91+
92+
FastFlix can extract subtitles from video files in various formats including SRT, ASS, SSA, and PGS.
93+
94+
## PGS to SRT OCR Conversion
95+
96+
FastFlix includes experimental support for converting PGS (Presentation Graphic Stream) subtitles to SRT format using OCR. This feature automatically detects and uses installed OCR tools.
97+
98+
**Requirements (auto-detected)**:
99+
- Tesseract OCR 4.x or higher
100+
- MKVToolNix (mkvextract/mkvmerge)
101+
102+
**Important**: This feature only works when running FastFlix from source:
103+
```bash
104+
python -m fastflix
105+
```
106+
107+
The Windows/Mac executable builds do not support PGS OCR due to environment limitations with the pgsrip library. If you need this feature, install FastFlix via pip and run from source.
108+
90109
# HDR
91110

92111
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/).

WINDOWS_BUILD.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Building FastFlix on Windows
2+
3+
This guide explains how to build FastFlix executables on Windows.
4+
5+
## Prerequisites
6+
7+
1. **Python 3.12 or higher**
8+
- Download from [python.org](https://www.python.org/downloads/)
9+
- Make sure to check "Add Python to PATH" during installation
10+
11+
2. **Git** (to clone/update the repository)
12+
- Download from [git-scm.com](https://git-scm.com/download/win)
13+
14+
## Build Steps
15+
16+
### 1. Open Command Prompt or PowerShell
17+
18+
Navigate to where you want to clone/have the FastFlix repository:
19+
20+
```bash
21+
cd C:\path\to\your\projects
22+
git clone https://github.com/cdgriffith/FastFlix.git
23+
cd FastFlix
24+
```
25+
26+
Or if you already have it:
27+
28+
```bash
29+
cd C:\path\to\FastFlix
30+
```
31+
32+
### 2. Create and Activate Virtual Environment
33+
34+
```bash
35+
python -m venv venv
36+
venv\Scripts\activate
37+
```
38+
39+
You should see `(venv)` in your command prompt.
40+
41+
### 3. Install Dependencies
42+
43+
```bash
44+
pip install --upgrade pip
45+
pip install -e ".[dev]"
46+
```
47+
48+
This installs FastFlix in editable mode with all development dependencies including PyInstaller.
49+
50+
### 4. Build the Executable
51+
52+
You have two options:
53+
54+
#### Option A: Single Executable (Recommended for distribution)
55+
56+
```bash
57+
pyinstaller FastFlix_Windows_OneFile.spec
58+
```
59+
60+
The executable will be in: `dist\FastFlix.exe`
61+
62+
#### Option B: Directory with Multiple Files (Faster startup)
63+
64+
```bash
65+
pyinstaller FastFlix_Windows_Installer.spec
66+
```
67+
68+
The executable will be in: `dist\FastFlix\FastFlix.exe`
69+
70+
### 5. Test the Build
71+
72+
```bash
73+
cd dist
74+
FastFlix.exe
75+
```
76+
77+
Or for the installer version:
78+
79+
```bash
80+
cd dist\FastFlix
81+
FastFlix.exe
82+
```
83+
84+
## Running Without Building (For Testing)
85+
86+
If you just want to test changes without building an executable:
87+
88+
```bash
89+
python -m fastflix
90+
```
91+
92+
## Troubleshooting
93+
94+
### Missing Dependencies
95+
96+
If you get import errors, try reinstalling:
97+
98+
```bash
99+
pip install --upgrade --force-reinstall -e ".[dev]"
100+
```
101+
102+
### Build Errors
103+
104+
1. Make sure you're in the FastFlix root directory
105+
2. Ensure the virtual environment is activated (you see `(venv)`)
106+
3. Try deleting `build` and `dist` folders and rebuilding:
107+
108+
```bash
109+
rmdir /s /q build dist
110+
pyinstaller FastFlix_Windows_OneFile.spec
111+
```
112+
113+
### FFmpeg Not Found
114+
115+
The FastFlix executable doesn't include FFmpeg. You need to:
116+
117+
1. Download FFmpeg from [ffmpeg.org](https://ffmpeg.org/download.html#build-windows)
118+
2. Extract it somewhere
119+
3. Add the `bin` folder to your PATH, or configure it in FastFlix settings
120+
121+
## Known Limitations
122+
123+
### PGS to SRT OCR (PyInstaller builds)
124+
125+
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`).
126+
127+
If you need PGS OCR functionality, please run FastFlix from source instead of using the compiled executable.
128+
129+
## Notes
130+
131+
- The build process creates a `portable.py` file temporarily (it's removed after)
132+
- The `.spec` files automatically collect all dependencies from `pyproject.toml`
133+
- The icon is located at `fastflix\data\icon.ico`

fastflix/__main__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,35 @@
11
# -*- coding: utf-8 -*-
2+
import os
23
import sys
34
import traceback
45
from multiprocessing import freeze_support
6+
from pathlib import Path
57

68
from fastflix.entry import main
79

810

11+
def setup_ocr_environment():
12+
"""Set up environment variables for OCR tools early in app startup.
13+
14+
This is necessary for PyInstaller frozen executables where os.environ
15+
modifications later in the code don't properly propagate to subprocesses.
16+
"""
17+
from fastflix.models.config import find_ocr_tool
18+
19+
# Find tesseract and add to PATH
20+
tesseract_path = find_ocr_tool("tesseract")
21+
if tesseract_path:
22+
tesseract_dir = str(Path(tesseract_path).parent)
23+
os.environ["PATH"] = f"{tesseract_dir}{os.pathsep}{os.environ.get('PATH', '')}"
24+
os.environ["TESSERACT_CMD"] = str(tesseract_path)
25+
26+
# Find mkvmerge and add MKVToolNix to PATH
27+
mkvmerge_path = find_ocr_tool("mkvmerge")
28+
if mkvmerge_path:
29+
mkvtoolnix_dir = str(Path(mkvmerge_path).parent)
30+
os.environ["PATH"] = f"{mkvtoolnix_dir}{os.pathsep}{os.environ.get('PATH', '')}"
31+
32+
933
def start_fastflix():
1034
exit_code = 2
1135
portable_mode = True
@@ -17,6 +41,9 @@ def start_fastflix():
1741
if portable_mode:
1842
print("PORTABLE MODE DETECTED: now using local config file and workspace in same directory as the executable")
1943

44+
# Set up OCR environment variables early for PyInstaller compatibility
45+
setup_ocr_environment()
46+
2047
try:
2148
exit_code = main(portable_mode)
2249
except Exception:

fastflix/models/config.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,115 @@ def where(filename: str, portable_mode=False) -> Path | None:
9999
return None
100100

101101

102+
def find_ocr_tool(name):
103+
"""Find OCR tools (tesseract, mkvmerge, pgsrip) similar to how we find FFmpeg"""
104+
# Check environment variable
105+
if ocr_location := os.getenv(f"FF_{name.upper()}"):
106+
return Path(ocr_location).absolute()
107+
108+
# Check system PATH
109+
if (ocr_location := shutil.which(name)) is not None:
110+
return Path(ocr_location).absolute()
111+
112+
# Special handling for tesseract on Windows (not in PATH by default)
113+
if name == "tesseract" and win_based:
114+
# Check common install locations using environment variables
115+
localappdata = os.getenv("LOCALAPPDATA")
116+
appdata = os.getenv("APPDATA")
117+
program_files = os.getenv("PROGRAMFILES")
118+
program_files_x86 = os.getenv("PROGRAMFILES(X86)")
119+
120+
# Check for Subtitle Edit's Tesseract installations and find the newest version
121+
subtitle_edit_versions = []
122+
if appdata:
123+
subtitle_edit_dir = Path(appdata) / "Subtitle Edit"
124+
if subtitle_edit_dir.exists():
125+
# Find all Tesseract* directories
126+
for tesseract_dir in subtitle_edit_dir.glob("Tesseract*"):
127+
tesseract_exe = tesseract_dir / "tesseract.exe"
128+
if tesseract_exe.exists():
129+
# Extract version number from directory name (e.g., Tesseract550 -> 550)
130+
version_str = tesseract_dir.name.replace("Tesseract", "")
131+
try:
132+
version = int(version_str)
133+
subtitle_edit_versions.append((version, tesseract_exe))
134+
except ValueError:
135+
# If we can't parse version, still add it with version 0
136+
subtitle_edit_versions.append((0, tesseract_exe))
137+
138+
# If we found Subtitle Edit versions, return the newest one
139+
if subtitle_edit_versions:
140+
subtitle_edit_versions.sort(reverse=True) # Sort by version descending
141+
return subtitle_edit_versions[0][1]
142+
143+
common_paths = []
144+
# Check user-local installation first
145+
if localappdata:
146+
common_paths.append(Path(localappdata) / "Programs" / "Tesseract-OCR" / "tesseract.exe")
147+
# Check system-wide installations
148+
if program_files:
149+
common_paths.append(Path(program_files) / "Tesseract-OCR" / "tesseract.exe")
150+
if program_files_x86:
151+
common_paths.append(Path(program_files_x86) / "Tesseract-OCR" / "tesseract.exe")
152+
153+
for path in common_paths:
154+
if path.exists():
155+
return path
156+
157+
# Check Windows registry for Tesseract install location
158+
try:
159+
import winreg
160+
161+
# Try HKEY_LOCAL_MACHINE first (system-wide install)
162+
for root_key in [winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER]:
163+
try:
164+
key = winreg.OpenKey(root_key, r"SOFTWARE\Tesseract-OCR")
165+
install_path = winreg.QueryValueEx(key, "InstallDir")[0]
166+
winreg.CloseKey(key)
167+
tesseract_exe = Path(install_path) / "tesseract.exe"
168+
if tesseract_exe.exists():
169+
return tesseract_exe
170+
except (FileNotFoundError, OSError):
171+
pass
172+
except ImportError:
173+
pass
174+
175+
# Special handling for mkvmerge on Windows
176+
if name == "mkvmerge" and win_based:
177+
# Check common install locations using environment variables
178+
localappdata = os.getenv("LOCALAPPDATA")
179+
program_files = os.getenv("PROGRAMFILES")
180+
program_files_x86 = os.getenv("PROGRAMFILES(X86)")
181+
182+
common_paths = []
183+
# Check user-local installation first
184+
if localappdata:
185+
common_paths.append(Path(localappdata) / "Programs" / "MKVToolNix" / "mkvmerge.exe")
186+
# Check system-wide installations
187+
if program_files:
188+
common_paths.append(Path(program_files) / "MKVToolNix" / "mkvmerge.exe")
189+
if program_files_x86:
190+
common_paths.append(Path(program_files_x86) / "MKVToolNix" / "mkvmerge.exe")
191+
192+
for path in common_paths:
193+
if path.exists():
194+
return path
195+
196+
# Check in FastFlix OCR tools folder
197+
ocr_folder = Path(user_data_dir("FastFlix_OCR", appauthor=False, roaming=True))
198+
if ocr_folder.exists():
199+
for file in ocr_folder.iterdir():
200+
if file.is_file() and file.name.lower() in (name, f"{name}.exe"):
201+
return file
202+
# Check bin subfolder
203+
if (ocr_folder / "bin").exists():
204+
for file in (ocr_folder / "bin").iterdir():
205+
if file.is_file() and file.name.lower() in (name, f"{name}.exe"):
206+
return file
207+
208+
return None
209+
210+
102211
class Config(BaseModel):
103212
version: str = __version__
104213
config_path: Path = Field(default_factory=get_config)
@@ -168,6 +277,12 @@ class Config(BaseModel):
168277

169278
disable_cover_extraction: bool = False
170279

280+
# PGS to SRT OCR Settings
281+
enable_pgs_ocr: bool = False
282+
tesseract_path: Path | None = Field(default_factory=lambda: find_ocr_tool("tesseract"))
283+
mkvmerge_path: Path | None = Field(default_factory=lambda: find_ocr_tool("mkvmerge"))
284+
pgs_ocr_language: str = "eng"
285+
171286
def encoder_opt(self, profile_name, profile_option_name):
172287
encoder_settings = getattr(self.profiles[self.selected_profile], profile_name)
173288
if encoder_settings:

0 commit comments

Comments
 (0)