Coverage for src / quber / core / extractors / dual / orchestrator.py: 62%
21 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"""Run the visual and Camelot flows side by side for one document.
3Both flows run concurrently and each returns its own result; the extractor
4hands back a `DualResult` carrying both, unmatched. There is no detector,
5no box repair, no targeted recovery, and no grid escalation — the two
6flows are independent first-class inputs, to be cross-referenced later.
7"""
9from __future__ import annotations
11import asyncio
12from pathlib import Path
13from typing import Optional
15from quber.agents.classifier import TableClassifier, get_classifier
16from quber.agents.grid_locator import GridLocator, get_grid_locator
17from quber.core.extractors.dual.camelot import run_camelot_flow
18from quber.core.extractors.dual.models import DualResult
19from quber.core.extractors.dual.vision import run_vision_flow
22class DualFlowExtractor:
23 """Produce a document's visual tables and Camelot grids as parallel flows.
25 `locator` and `classifier` default to the configured API backends but
26 can be injected (e.g. mocks in tests). `classify_camelot` controls only
27 whether the Camelot grids get an is-table verdict attached; it never
28 changes which grids are returned.
29 """
31 def __init__(
32 self,
33 locator: Optional[GridLocator] = None,
34 classifier: Optional[TableClassifier] = None,
35 dpi: int = 200,
36 classify_camelot: bool = True,
37 ) -> None:
38 self.locator = locator or get_grid_locator()
39 self.classifier = classifier or get_classifier()
40 self.dpi = dpi
41 self.classify_camelot = classify_camelot
43 async def run(self, source: Path) -> DualResult:
44 classifier = self.classifier if self.classify_camelot else None
45 visual, camelot = await asyncio.gather(
46 run_vision_flow(source, self.locator, dpi=self.dpi),
47 run_camelot_flow(source, classifier),
48 )
49 return DualResult(source=str(source), visual=visual, camelot=camelot)
51 def run_sync(self, source: Path) -> DualResult:
52 return asyncio.run(self.run(source))