diff --git a/README.md b/README.md
index 62dffa2..9076550 100644
--- a/README.md
+++ b/README.md
@@ -62,7 +62,7 @@ To automatically create or reuse a personal fork using an authenticated GitHub C
The installer first reuses a sibling `../enhancer` checkout when one exists. Otherwise it creates a managed checkout under `~/Library/Application Support/PDF Magic/Enhancer` on macOS or `~/.local/share/pdf-magic/enhancer` on Linux. It registers the native bridge under `~/.config/pdf-magic/native` for both Chrome and Brave. When setup runs from a viewer checkout, this stable launcher loads that checkout's current helper on every enhancement, so helper updates take effect automatically on the next request without reinstalling or reloading the extension. Running jobs finish with the version they started with. If the checkout is moved or unavailable, the launcher uses its installed fallback; rerun setup from the new location to relink it. Download-only setup installs a standalone copy and must be rerun to update it.
-After setup completes, click Enhance again. The host locates Homebrew tools even when the browser is launched from Finder and keeps OCR logs separate from native-messaging responses.
+After setup completes, click Enhance again. When the selected Enhancer checkout supports `setup-python-env.sh`, the installer creates or repairs `enhancer/.venv` and installs its Python dependencies there. The OCR scripts and native host prefer this isolated Python for tagging, AI review, and PDF metadata stamping; Homebrew's OCRmyPDF and qpdf remain separate executables. Existing older forks without the setup script continue to work with their previous interpreter discovery. The host locates Homebrew tools even when the browser is launched from Finder and keeps OCR logs separate from native-messaging responses.
For web PDFs, the viewer sends the already-loaded document to the host in acknowledged chunks. Enhancement does not make another HTTP download, so it can use PDFs that the browser loaded from sites that reject standalone requests. Local PDFs are read directly from disk. Both enhancement modes use `--force-ocr --ai-review` to re-read every page and review recognized text against page images with the local AI models, including when the PDF already contains text. This rasterizes pages in the enhanced copy and takes longer than preserving existing text. The source remains unchanged. The tagger builds heading structure and section bookmarks from the reviewed text. Cached results from the previous pipeline are regenerated on the next enhancement.
diff --git a/native/install-host.sh b/native/install-host.sh
old mode 100755
new mode 100644
index 9ebd1fc..26f12db
--- a/native/install-host.sh
+++ b/native/install-host.sh
@@ -197,6 +197,13 @@ if [[ ! -f "$enhancer_dir/ocr-scanned-pdf.sh" ]]; then
exit 1
fi
+# The user's explicit installer run also provisions isolated Python packages.
+# Older forks without the setup script retain the legacy interpreter fallback.
+if [[ -f "$enhancer_dir/setup-python-env.sh" ]]; then
+ printf 'Setting up the PDF Enhancer Python virtual environment...\n'
+ /bin/bash "$enhancer_dir/setup-python-env.sh"
+fi
+
config_dir="$HOME/.config/pdf-magic"
config_path="$config_dir/enhancer-host.json"
host_install_dir="$config_dir/native"
diff --git a/native/pdfmagic-enhancer-host.py b/native/pdfmagic-enhancer-host.py
index 3e4cf1c..94dbf2e 100644
--- a/native/pdfmagic-enhancer-host.py
+++ b/native/pdfmagic-enhancer-host.py
@@ -367,7 +367,20 @@ def input_from_url(source_url: str, temporary_directory: Path) -> tuple[Path, bo
def pikepdf_python() -> list[str]:
- candidates: list[list[str]] = [[sys.executable], ["python3"]]
+ candidates: list[list[str]] = []
+ # Use the same Enhancer-owned environment for metadata stamping as OCR.
+ # The host itself may run on an OS Python without the PDF dependencies.
+ if HOST_CONFIG.is_file():
+ try:
+ config = json.loads(HOST_CONFIG.read_text())
+ enhancer_dir = config.get("enhancer_dir")
+ if isinstance(enhancer_dir, str) and enhancer_dir.strip():
+ venv_python = Path(enhancer_dir).expanduser() / ".venv" / "bin" / "python"
+ if venv_python.is_file():
+ candidates.append([str(venv_python)])
+ except (OSError, ValueError, TypeError):
+ pass
+ candidates.extend([[sys.executable], ["python3"]])
ocrmypdf = shutil.which("ocrmypdf")
if ocrmypdf:
first_line = Path(ocrmypdf).read_text(errors="ignore").splitlines()[0]
diff --git a/package.json b/package.json
index 814a18e..c97c216 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"scripts": {
"build": "node scripts/build.mjs",
"check": "node scripts/check-extension.mjs",
- "test": "node --test tests/enhancer-setup.test.mjs tests/document-title.test.mjs tests/metadata-doi.test.mjs tests/local-file-favicon.test.mjs tests/page-tab-labels.test.mjs tests/minimap.test.mjs tests/clickable-reference-links.test.mjs tests/printed-page-jump.test.mjs tests/markdown-copy.test.mjs tests/terminal-punctuation-selection.test.mjs tests/footnote-click-copy.test.mjs tests/chatgpt-summary-popup.test.mjs tests/chatgpt-pdf-relay.test.mjs tests/selection-gaps.test.mjs"
+ "test": "node --test tests/enhance-failure.test.mjs tests/enhancer-setup.test.mjs tests/document-title.test.mjs tests/metadata-doi.test.mjs tests/local-file-favicon.test.mjs tests/page-tab-labels.test.mjs tests/minimap.test.mjs tests/clickable-reference-links.test.mjs tests/printed-page-jump.test.mjs tests/markdown-copy.test.mjs tests/terminal-punctuation-selection.test.mjs tests/footnote-click-copy.test.mjs tests/chatgpt-summary-popup.test.mjs tests/chatgpt-pdf-relay.test.mjs tests/selection-gaps.test.mjs"
},
"dependencies": {
"pdfjs-dist": "6.3.289",
diff --git a/src/viewer.html b/src/viewer.html
index eeb4de1..4f4f2fa 100644
--- a/src/viewer.html
+++ b/src/viewer.html
@@ -189,6 +189,15 @@
+