Skip to content

Add vision extraction for scanned PDFs - #40

Merged
gavinbee merged 1 commit into
mainfrom
8-vision-extraction
May 28, 2026
Merged

Add vision extraction for scanned PDFs#40
gavinbee merged 1 commit into
mainfrom
8-vision-extraction

Conversation

@gavinbee

@gavinbee gavinbee commented May 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • `src/vision_extract.py` — per-page extraction via a local vision model (Qwen2.5-VL through Ollama). Rasterizes each page, prompts for a JSON object, parses into the same `PageExtraction` shape the form-field path emits, so merge / output stay path-agnostic.
  • Prompt: canonical-schema JSON skeleton built from `src.schema` constants (can't drift) + the template's `vision_prompt_addendum` + the source filename so the model can resolve `session_number` from form text / filename / both and report which via `source`.
  • Model call: `generate(format="json", temperature=0)` with the page PNG. One retry on structural failure with a clarifying nudge; `VisionExtractionError` if the retry also fails. Strips markdown fences some models add.
  • Parsing: typed `FieldValue` with clamped [0,1] confidence; nulls dropped; bare scalars get default 0.5. `successful` coerced to true/false/null defensively — a model emitting the string `"false"` must not become a truthy non-empty string downstream (regression-tested). `session_number` keeps its source provenance.
  • Cache: raw responses → `.raw.json` sidecar keyed by page + tagged with the model. Re-runs reuse it (fast parsing iteration without re-invoking the slow model); `use_cache=False` (`--no-cache`) forces fresh; model-mismatch ignores the cache.
  • `VisionClient` is a `Protocol` — tests inject a fake client returning canned JSON. Not wired into the CLI yet (that's End-to-end vision CLI #10).

Test plan

  • `pytest tests/ -q` — 231 passing (190 prior + 41 new). Prompt content, good-response parsing, successful/confidence coercion incl. the stringy-`false` regression, retry-then-succeed / retry-then-fail, fence stripping, missing-`rows` retry, cache round-trip (write → reuse → --no-cache → model-mismatch).
  • CI goes green.
  • Real-model smoke test happens when the CLI wires this in (End-to-end vision CLI #10) against an actual scanned PDF.

Closes #8.

🤖 Generated with Claude Code

- src/vision_extract.py: per-page extraction via a local vision model
  (Qwen2.5-VL through Ollama). Rasterizes each page, prompts the model
  for a JSON object, parses it into the same PageExtraction shape the
  form-field path emits — so merge / output stay path-agnostic.

- Prompt construction: a canonical-schema JSON skeleton built from
  src.schema constants (can't drift), plus the template's
  vision_prompt_addendum, plus the source filename so the model can
  resolve session_number from the form text, the filename, or both
  (and report which via the "source" field).

- Model call: client.generate(format="json", options temperature=0)
  with the page PNG. One retry on a structural parse failure with a
  clarifying nudge appended; raises VisionExtractionError if the second
  attempt also fails. Tolerates markdown code fences some models wrap
  JSON in despite instructions.

- Parsing: each field becomes a typed FieldValue with clamped [0,1]
  confidence; null values are dropped; bare scalars (model ignored the
  {value, confidence} shape) get a default 0.5 confidence. successful
  is coerced to true/false/null defensively — a model emitting the
  string "false" must NOT become a truthy non-empty string downstream
  (regression-tested). session_number preserves its source provenance.

- Caching: raw model responses are written to a .raw.json sidecar keyed
  by page number and tagged with the model. Re-runs reuse the cache
  (fast iteration on parsing logic without re-invoking the slow model);
  use_cache=False (the CLI's --no-cache) forces a fresh call; a cache
  built with a different model is ignored.

- VisionClient is a Protocol so the module doesn't hard-depend on the
  ollama package surface; tests inject a fake client returning canned
  JSON.

- tests/test_vision_extract.py: 41 tests over a mocked client. Prompt
  content, good-response parsing, successful/confidence coercion
  (incl. the stringy-false regression), retry-then-succeed and
  retry-then-fail, markdown-fence stripping, missing-rows retry, and
  the cache round-trip (write → reuse → --no-cache → model-mismatch).

- docs/architecture.md: marked implemented; noted it's not yet wired
  into the CLI (that's #10).

Closes #8.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@gavinbee
gavinbee merged commit 6a2496c into main May 28, 2026
1 check passed
@gavinbee
gavinbee deleted the 8-vision-extraction branch May 28, 2026 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vision extraction

1 participant