An extractor converts a Document (raw bytes) into an ExtractionResult (markdown text). billfox ships with MistralExtractor, but you can implement your own.
from billfox._types import Document, ExtractionResult
class Extractor(Protocol):
async def extract(self, document: Document) -> ExtractionResult: ...import asyncio
import subprocess
import tempfile
from pathlib import Path
from billfox._types import Document, ExtractionResult, Page
class TesseractExtractor:
"""Extract text using Tesseract OCR."""
def __init__(self, lang: str = "eng") -> None:
self._lang = lang
async def extract(self, document: Document) -> ExtractionResult:
return await asyncio.to_thread(self._extract_sync, document)
def _extract_sync(self, document: Document) -> ExtractionResult:
with tempfile.NamedTemporaryFile(suffix=".png") as tmp:
tmp.write(document.content)
tmp.flush()
result = subprocess.run(
["tesseract", tmp.name, "stdout", "-l", self._lang],
capture_output=True, text=True, check=True,
)
markdown = result.stdout.strip()
return ExtractionResult(
markdown=markdown,
pages=[Page(index=0, markdown=markdown)],
)from billfox import Pipeline
from billfox.source import LocalFileSource
pipeline = Pipeline(
source=LocalFileSource(),
extractor=TesseractExtractor(lang="eng"),
parser=my_parser,
)- Use
asyncio.to_thread()to wrap synchronous SDK calls so your extractor works with the async pipeline. - Return one
Pageper logical page of the document. For single-page images, one page is fine. - The
ExtractionResult.metadatadict is a good place to store extractor-specific info (e.g. confidence scores, model version). - Your extractor is automatically compatible with
Pipelineandextract_only()without any registration.