Coverage for src / quber / core / extractors / dual / models.py: 100%
28 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-09-23 22:14 -0400
« prev ^ index » next coverage.py v7.14.0, created at 2026-09-23 22:14 -0400
1"""Result types for the two-flow extractor.
3`VisualTable` is one table as identified by looking at the page (the
4authoritative count and identity). `CamelotTable` is one cell grid pulled
5by Camelot, carrying its classifier verdict as metadata only — the verdict
6never drops a candidate here, because identity is the visual flow's job.
7`DualResult` holds both lists for one document, unmatched.
8"""
10from __future__ import annotations
12from typing import List, Optional, Tuple
14from pydantic import BaseModel, Field
16from quber.agents.classifier import ClassifierResult
17from quber.core.extractors.camelot.acquire import CamelotCandidate
20class VisualTable(BaseModel):
21 """A table the visual flow found on a page, with its full-anatomy box."""
23 page: int = Field(ge=1, description="1-indexed page the table sits on")
24 ordinal: int = Field(ge=1, description="1-based reading-order position within the page")
25 title: str = Field(default="", description="Visible caption, else a one-line column summary")
26 region: Tuple[float, float, float, float] = Field(
27 description="Boundary box, normalized 0..1 with the page top-left as origin (x1, y1, x2, y2)"
28 )
29 tightened: bool = Field(
30 description="True if the box was snapped to the text layer; False if the coarse grid cell was kept"
31 )
34class CamelotTable(BaseModel):
35 """A Camelot cell grid plus its is-table verdict (metadata, not a gate)."""
37 candidate: CamelotCandidate = Field(description="Raw Camelot output: cells, markdown, bbox, accuracy")
38 classification: Optional[ClassifierResult] = Field(
39 default=None,
40 description="Classifier's is_table verdict, kept for later cross-reference; it does not drop the candidate",
41 )
44class DualResult(BaseModel):
45 """Both flows' output for one document, side by side and unmatched."""
47 source: str = Field(description="Path of the source PDF")
48 visual: List[VisualTable] = Field(
49 default_factory=list, description="Tables identified by the visual flow"
50 )
51 camelot: List[CamelotTable] = Field(default_factory=list, description="Cell grids extracted by Camelot")
53 def visual_count_by_page(self) -> dict[int, int]:
54 counts: dict[int, int] = {}
55 for t in self.visual:
56 counts[t.page] = counts.get(t.page, 0) + 1
57 return counts
59 def camelot_count_by_page(self) -> dict[int, int]:
60 counts: dict[int, int] = {}
61 for t in self.camelot:
62 counts[t.candidate.page] = counts.get(t.candidate.page, 0) + 1
63 return counts