The Morpheus release schedule / support-lifecycle matrix (sf000111242en_us) is entirely an image — the table of which version ships when and when each stream hits Maintenance/EOL is a JPEG. html_to_md dropped it, so the MCP could never answer "when does Morpheus 8.x.x reach end of life".
What this adds
scrape/vision.py (opt-in via VISION_OCR): transcribes qualifying images with a local Ollama vision model and appends a labeled markdown block to the page → chunked, embedded, retrievable.
Model chosen against ground truth (14×4-cell matrix): qwen2.5vl:7b + bare prompt → 55-56/56, ~18s, GPU-resident, prompt-robust. gemma3:12b needs an exact "read row by row, keep cells aligned" prompt or it shifts a column; qwen2.5vl:32b is no more accurate and 5× slower (CPU spill). All documented inline.
Reliability: temp 0 + 2-sample self-consistency gate (with an honest caveat — self-consistency catches random flakiness, not systematic misreads, so every block says "verify against the source image").
Cost control: content-hash cache in corpus/.vision-cache/ (each unique image OCR'd once ever); VISION_MAX_NEW bounds new OCRs/run so the first pass can't balloon into hours; deferred count logged. Every failure degrades to pre-vision behavior.
Adds the morpheus_release_schedule bundle + 2 eval golden queries; fixes single-doc title fallback; Pillow in requirements-vision.txt (scrape-only); VISION_* wired into refresh.yml.
Verification
Locally the matrix transcribes to the exact 14-row table; a second scrape is a cache hit (ocr=0). After deploy I'll confirm retrieval via search_docs on the live MCP.
## Problem
The Morpheus **release schedule / support-lifecycle matrix** (`sf000111242en_us`) is entirely an image — the table of which version ships when and when each stream hits Maintenance/EOL is a JPEG. `html_to_md` dropped it, so the MCP could never answer "when does Morpheus 8.x.x reach end of life".
## What this adds
- **`scrape/vision.py`** (opt-in via `VISION_OCR`): transcribes qualifying images with a local Ollama vision model and appends a labeled markdown block to the page → chunked, embedded, retrievable.
- **Model chosen against ground truth** (14×4-cell matrix): **qwen2.5vl:7b + bare prompt → 55-56/56, ~18s, GPU-resident, prompt-robust.** gemma3:12b needs an exact "read row by row, keep cells aligned" prompt or it shifts a column; qwen2.5vl:32b is no more accurate and 5× slower (CPU spill). All documented inline.
- **Reliability:** temp 0 + 2-sample self-consistency gate (with an honest caveat — self-consistency catches random flakiness, not systematic misreads, so every block says "verify against the source image").
- **Cost control:** content-hash cache in `corpus/.vision-cache/` (each unique image OCR'd once ever); `VISION_MAX_NEW` bounds new OCRs/run so the first pass can't balloon into hours; deferred count logged. Every failure degrades to pre-vision behavior.
- Adds the `morpheus_release_schedule` bundle + 2 eval golden queries; fixes single-doc title fallback; `Pillow` in `requirements-vision.txt` (scrape-only); `VISION_*` wired into `refresh.yml`.
## Verification
Locally the matrix transcribes to the exact 14-row table; a second scrape is a cache hit (`ocr=0`). After deploy I'll confirm retrieval via `search_docs` on the live MCP.
Some HPE pages carry information only inside an image. The Morpheus
release schedule / support-lifecycle matrix (sf000111242en_us) is the
case that surfaced this: the entire "which version ships when, when a
stream hits Maintenance/EOL" table is a JPEG, so html_to_md dropped
every value and retrieval could never answer "when does 8.x.x reach
end of life".
New scrape/vision.py (opt-in via VISION_OCR) transcribes qualifying
images with a local Ollama vision model and appends a labeled markdown
block to the page so the text is chunked, embedded and retrieved.
Model/prompt chosen against ground truth on the matrix (14x4 cells):
- qwen2.5vl:7b + a bare "transcribe the table" prompt: 55-56/56, ~18s,
GPU-resident, and prompt-ROBUST (accurate without hand-tuning).
- gemma3:12b needs an exact "read row by row, keep cells aligned"
prompt or it shifts a column by one row; qwen2.5vl:32b is no more
accurate and 5x slower (CPU spill). Both documented in the code.
- Note: self-consistency (2 samples must agree) only catches RANDOM
flakiness — a wrong read is stable across seeds — so every block also
ships a "verify against the source image" caveat.
Reliability/cost:
- content-hash cache in corpus/.vision-cache/ (committed) — each unique
image OCR'd once ever, shared across version bundles.
- VISION_MAX_NEW bounds NEW OCRs per run so the first pass can't balloon
into hours; the cache fills incrementally. Deferred count is logged.
- every failure path degrades to the pre-vision behavior; never blocks
the scrape.
Also:
- add the morpheus_release_schedule bundle (sf single-doc) + 2 eval
golden queries for it.
- fetch_single_doc: title falls back to the bundle title (not docId)
when a page has no <h1> (sf solution articles).
- Pillow in requirements-vision.txt (scrape-only; kept out of the
server image), installed + VISION_* wired into refresh.yml.
Verified locally: matrix transcribes to the exact 14-row table; second
run is a cache hit (ocr=0).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LFowQzJu7k97QLCRDSAeh1
claude
merged commit 698196bd63 into main2026-07-23 09:48:38 -04:00
claude
deleted branch feat/vision-ocr-image-pages2026-07-23 09:48:38 -04:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
The Morpheus release schedule / support-lifecycle matrix (
sf000111242en_us) is entirely an image — the table of which version ships when and when each stream hits Maintenance/EOL is a JPEG.html_to_mddropped it, so the MCP could never answer "when does Morpheus 8.x.x reach end of life".What this adds
scrape/vision.py(opt-in viaVISION_OCR): transcribes qualifying images with a local Ollama vision model and appends a labeled markdown block to the page → chunked, embedded, retrievable.corpus/.vision-cache/(each unique image OCR'd once ever);VISION_MAX_NEWbounds new OCRs/run so the first pass can't balloon into hours; deferred count logged. Every failure degrades to pre-vision behavior.morpheus_release_schedulebundle + 2 eval golden queries; fixes single-doc title fallback;Pillowinrequirements-vision.txt(scrape-only);VISION_*wired intorefresh.yml.Verification
Locally the matrix transcribes to the exact 14-row table; a second scrape is a cache hit (
ocr=0). After deploy I'll confirm retrieval viasearch_docson the live MCP.