feat(scrape): vision OCR of image-only doc pages (support matrices) #5

Merged
claude merged 1 commits from feat/vision-ocr-image-pages into main 2026-07-23 09:48:38 -04:00
Contributor

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.

## 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.
claude added 1 commit 2026-07-23 09:35:17 -04:00
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 main 2026-07-23 09:48:38 -04:00
claude deleted branch feat/vision-ocr-image-pages 2026-07-23 09:48:38 -04:00
Sign in to join this conversation.