Coverage for src / quber / core / extractors / set_of_mark / orchestrator.py: 53%
32 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"""SetOfMarkExtractor (Set-of-Mark): vision identifies tables, Camelot fills them in-region.
3Thin wrapper that renders the document's pages once, wires `SetOfMarkDeps`, and
4runs the vision-guided extraction graph built in `pipeline`. The graph does the
5work: locate every table, capture each in its own region, clean structure under
6the no-number guard, and reading-order the result. Emits one `ExtractedTable`
7per visual table; a region where Camelot finds nothing is still emitted with
8its visual identity so a table is never silently dropped.
9"""
11from __future__ import annotations
13import asyncio
14import tempfile
15from pathlib import Path
16from typing import List, Optional
18import fitz
20from quber.agents.capture_advisor import CaptureAdvisor, get_capture_advisor
21from quber.agents.grid_locator import GridLocator, get_grid_locator
22from quber.agents.llm_client import LLMClient, get_llm_client
23from quber.agents.status_inspector import StatusInspector, get_status_inspector
24from quber.core.extractors.base import ExtractedTable
25from quber.core.extractors.camelot.acquire import render_pages
26from quber.core.extractors.set_of_mark.pipeline import SetOfMarkDeps, SetOfMarkState, build_set_of_mark_graph
29class SetOfMarkExtractor:
30 """Set-of-Mark extractor: vision locates, Camelot fills in-region, correction cleans."""
32 def __init__(
33 self,
34 locator: Optional[GridLocator] = None,
35 llm: Optional[LLMClient] = None,
36 dpi: int = 200,
37 page_concurrency: int = 16,
38 correct_concurrency: int = 16,
39 advisor: Optional[CaptureAdvisor] = None,
40 inspector: Optional[StatusInspector] = None,
41 ) -> None:
42 self.locator = locator or get_grid_locator()
43 self.llm = llm or get_llm_client(None)
44 self.dpi = dpi
45 self.page_concurrency = page_concurrency
46 self.correct_concurrency = correct_concurrency
47 self.advisor = advisor if advisor is not None else get_capture_advisor()
48 self.inspector = inspector if inspector is not None else get_status_inspector()
50 async def extract_tables(self, source: Path) -> List[ExtractedTable]:
51 doc = fitz.open(str(source))
52 page_dims = {i + 1: (doc[i].rect.width, doc[i].rect.height) for i in range(doc.page_count)}
53 with tempfile.TemporaryDirectory(prefix="quber-som-") as tmp:
54 images = await asyncio.to_thread(render_pages, source, self.dpi, Path(tmp))
55 page_images = {i + 1: img for i, img in enumerate(images)}
56 deps = SetOfMarkDeps(
57 locator=self.locator,
58 llm=self.llm,
59 source=source,
60 page_images=page_images,
61 page_dims=page_dims,
62 dpi=self.dpi,
63 correct_sem=asyncio.Semaphore(self.correct_concurrency),
64 page_sem=asyncio.Semaphore(self.page_concurrency),
65 advisor=self.advisor,
66 inspector=self.inspector,
67 )
68 return await build_set_of_mark_graph().run(state=SetOfMarkState(), deps=deps, inputs=source)
70 def extract_tables_sync(self, source: Path) -> List[ExtractedTable]:
71 return asyncio.run(self.extract_tables(source))