feat: text-layer grounding (extractor authority, guard rescue tier, verifier oracle + hi-DPI crops)
- backend/text_layer.py: PyMuPDF text-layer extraction, fuzzy evidence
bbox matching, 300-DPI crop rendering, coverage-gap signal
- extractor (classic + agent): TEXT LAYER block appended at call sites;
grounding guard gains text-layer rescue tier (grounding=text_layer stamp)
- verifier: {text_layer} oracle excerpt + evidence-located hi-DPI crops
replacing full-page images (fallback preserved, I2 guard intact)
- coverage gaps: text-bearing pages with zero extraction -> failed-scope
gap findings (agent) / log-only (classic)
- config knobs: TEXT_LAYER_ENABLED/MIN_CHARS/MAX_CHARS, VERIFY_TEXT_MAX_CHARS,
VERIFY_HI_DPI_CROPS, VERIFY_CROP_DPI, VERIFY_CROP_MARGIN_PTS
- tests: 22 new (text_layer unit, grounding/render, runner-level flow)
Spec: docs/superpowers/specs/2026-08-12-text-layer-grounding-design.md
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
"""Text-layer extraction, evidence bbox matching, and crop rendering."""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
fitz = pytest.importorskip("pymupdf")
|
||||
|
||||
from backend import config
|
||||
from backend.text_layer import (
|
||||
attach_text_layers,
|
||||
coverage_gaps,
|
||||
extract_text_layers,
|
||||
find_evidence_bbox,
|
||||
render_crop,
|
||||
)
|
||||
|
||||
EVIDENCE = "(5) 2X6 STUD PACK @ 16 IN O.C."
|
||||
|
||||
|
||||
def _make_pdf(path, pages):
|
||||
"""pages: list of str ('' = effectively blank page)."""
|
||||
doc = fitz.open()
|
||||
for text in pages:
|
||||
page = doc.new_page(width=612, height=792)
|
||||
if text:
|
||||
page.insert_text((72, 72), text, fontsize=11)
|
||||
doc.save(str(path))
|
||||
doc.close()
|
||||
return str(path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def text_pdf(tmp_path):
|
||||
return _make_pdf(tmp_path / "set.pdf", [EVIDENCE, ""])
|
||||
|
||||
|
||||
def test_extract_text_layers(text_pdf):
|
||||
layers = extract_text_layers(text_pdf)
|
||||
assert set(layers) == {1, 2}
|
||||
assert layers[1]["has_text_layer"] is True
|
||||
assert "2X6 STUD PACK" in layers[1]["text"]
|
||||
assert layers[1]["words"], "expected word-level bboxes"
|
||||
assert all("bbox" in w and len(w["bbox"]) == 4 for w in layers[1]["words"])
|
||||
|
||||
|
||||
def test_blank_page_below_min_chars(text_pdf):
|
||||
layers = extract_text_layers(text_pdf)
|
||||
assert layers[2]["has_text_layer"] is False
|
||||
|
||||
|
||||
def test_disabled_returns_empty(text_pdf, monkeypatch):
|
||||
monkeypatch.setattr(config, "TEXT_LAYER_ENABLED", False)
|
||||
assert extract_text_layers(text_pdf) == {}
|
||||
|
||||
|
||||
def test_attach_text_layers(text_pdf, tmp_path):
|
||||
pages = [{"page_number": 1}, {"page_number": 2}]
|
||||
words = attach_text_layers(text_pdf, pages,
|
||||
text_dir=str(tmp_path / "text"))
|
||||
assert pages[0]["text_layer"] and "STUD PACK" in pages[0]["text_layer"]
|
||||
assert pages[1]["text_layer"] is None
|
||||
assert words[1] and not words[2]
|
||||
assert os.path.isfile(tmp_path / "text" / "page-001.txt")
|
||||
assert not os.path.exists(tmp_path / "text" / "page-002.txt")
|
||||
|
||||
|
||||
def test_find_evidence_bbox_exact(text_pdf):
|
||||
words = extract_text_layers(text_pdf)[1]["words"]
|
||||
bbox = find_evidence_bbox(words, EVIDENCE)
|
||||
assert bbox is not None
|
||||
assert bbox[2] > bbox[0] and bbox[3] > bbox[1]
|
||||
|
||||
|
||||
def test_find_evidence_bbox_fuzzy(text_pdf):
|
||||
# Vision quotes imperfectly: wrong count token, rest exact.
|
||||
words = extract_text_layers(text_pdf)[1]["words"]
|
||||
bbox = find_evidence_bbox(words, "(2) 2X6 STUD PACK @ 16 IN O.C.")
|
||||
assert bbox is not None
|
||||
|
||||
|
||||
def test_find_evidence_bbox_miss(text_pdf):
|
||||
words = extract_text_layers(text_pdf)[1]["words"]
|
||||
assert find_evidence_bbox(words, "PENTHOUSE EXHAUST FAN EF-9") is None
|
||||
assert find_evidence_bbox([], EVIDENCE) is None
|
||||
assert find_evidence_bbox(words, "") is None
|
||||
|
||||
|
||||
def test_render_crop(text_pdf):
|
||||
words = extract_text_layers(text_pdf)[1]["words"]
|
||||
bbox = find_evidence_bbox(words, EVIDENCE)
|
||||
crop = render_crop(text_pdf, 1, bbox)
|
||||
assert crop is not None
|
||||
# Decodes as an image of plausible size (margin around the text line).
|
||||
doc = fitz.open(stream=crop, filetype="jpeg")
|
||||
pix = doc[0].get_pixmap()
|
||||
assert pix.width > 100 and pix.height > 20
|
||||
doc.close()
|
||||
|
||||
|
||||
def test_render_crop_bad_page(text_pdf):
|
||||
assert render_crop(text_pdf, 99, (0, 0, 10, 10)) is None
|
||||
|
||||
|
||||
def test_coverage_gaps():
|
||||
pages = [{"page_number": 1, "text_layer": "some real text"},
|
||||
{"page_number": 2, "text_layer": "more text"},
|
||||
{"page_number": 3, "text_layer": None}]
|
||||
sheets = [{"page_number": 1, "assertions": [{"id": "a"}]},
|
||||
{"page_number": 2, "assertions": []}]
|
||||
assert coverage_gaps(pages, sheets) == [2]
|
||||
Reference in New Issue
Block a user