API Reference¶
Core functions¶
extract_document¶
from multixtract import extract_document
document, images = extract_document(
doc_path, # path to any supported file
image_filter=None, # optional ImageFilterPipeline
registry=None, # optional ExtractorRegistry (defaults to process-wide registry)
)
Returns (document, prepared_images).
document structure:
{
"metadata": {
"author": str, "title": str, "created": str, "modified": str,
"page_count": int, "table_count": int, ...
},
"_base_name": str,
"pgs": [
{
"pg_num": int,
"kind": "page",
"title": str, # slide title (PPTX) or sheet name (XLSX); "" for PDF/DOCX
"txt": str,
"tables": [[[str]]], # list of tables, each a list of row-lists
"imgs": [...], # populated after vision; empty before
"hyperlinks": [str], # deduplicated HTTP/HTTPS/FTP URLs
}
],
}
prepared_images — list of dicts, one per image that passed filtering:
{
"image_id": str,
"page_number": int,
"img_idx": int,
"image_bytes": bytes,
"ext": str, # "png", "jpg", etc.
"width": int,
"height": int,
"img_path": str,
}
chunk_document¶
from multixtract import chunk_document
chunks = chunk_document(
document, # from extract_document
base_name, # used to build deterministic chunk_id values
image_embeddings=None, # {img_id: vector} to reuse pre-computed embeddings
target_tokens=500, # target tokens per text chunk
overlap_tokens=50, # overlap tokens between adjacent text chunks
)
Each chunk:
{
"chunk_id": str, # deterministic — e.g. "report__p1_text_0"
"chunk_type": str, # "text" | "table" | "image"
"pg_num": int,
"chunk_idx": int,
"content": str,
"token_cnt": int,
"metadata": dict, # type-specific; see data-model.md
"embedding": list | None,
}
build_index_document¶
from multixtract import build_index_document
index_doc = build_index_document(
chunk, # one chunk dict from chunk_document() or _chunks.json
header, # the _header dict from _chunks.json
timestamp, # ISO-8601 UTC string, e.g. "2026-08-14T10:00:00Z"
)
Transforms a raw chunk into a flat document ready for Azure AI Search (or any document store). metadata is dissolved — type-specific fields are promoted to the top level. embedding is renamed to content_vector.
{
# identity
"id": str, # safe_index_key(chunk_id)
"doc_id": str, # stem of chunk_id before "__"
# provenance (from _header)
"file_name": str,
"file_path": str,
"file_type": str, # extension without dot, e.g. "pdf"
"total_pgs": int,
# chunk fields
"chunk_type": str,
"pg_num": int,
"chunk_idx": int,
"token_cnt": int,
"content": str,
"content_vector": list | None,
"last_updated": str,
# type-specific flat fields (only present for matching chunk_type)
"total_txt_chunks_on_pg": int, # text chunks only
"num_rows": int, "num_col": int, # table chunks only
"img_id": str, "img_path": str, # image chunks only
}
safe_index_key¶
from multixtract import safe_index_key
key: str = safe_index_key("report.pdf__p1_text_0")
# "report_pdf__p1_text_0"
Replaces every character outside [A-Za-z0-9_\-=] with _. Applied automatically to all chunk_id values and the id field in index documents.
split_text_into_chunks¶
from multixtract import split_text_into_chunks
chunks: list[str] = split_text_into_chunks(
text,
target_tokens=500,
overlap_tokens=50,
)
Sentence-boundary-aware sliding-window text splitter. Works on any string.
table_to_markdown¶
from multixtract import table_to_markdown
md: str = table_to_markdown(table) # table = list[list[str | None]]
Pipeline¶
Pipeline¶
from multixtract import Pipeline, PipelineConfig
pipeline = Pipeline(
vision=None, # VisionModel or None
embedder=None, # Embedder or None
store=None, # BlobStore or None
config=None, # PipelineConfig or None (uses defaults)
)
result = pipeline.process(
doc_path,
skip_if_exists=True, # skip if output JSON already exists in the store
split_chunks=False, # when True, also write individual per-chunk documents
)
When split_chunks=True, result.split_stats is populated.
Pipeline.split_chunks_file¶
stats = pipeline.split_chunks_file(
chunks_data, # dict — parsed _chunks.json content
timestamp=None, # ISO-8601 str; defaults to current UTC time
skip_if_exists=True, # skip chunks whose output path already exists
upload_workers=4, # max concurrent store writes
)
Reads a _chunks.json dict and writes one flat build_index_document JSON per chunk to {individual_chunks_subdir}/{doc_name}/{id}.json. Returns a SplitStats.
SplitStats¶
from multixtract import SplitStats
stats.created # int — chunk files written this call
stats.skipped # int — skipped because output already existed
stats.failed # int — store write errors
stats.deduped # int — image chunks whose echo content was deduplicated
Pipeline.process_batch¶
summary = pipeline.process_batch(
"report.pdf", # one or more files and/or directories
"./documents",
max_workers=4, # concurrent documents
)
# summary: BatchResult
Thin facade over BatchProcessor. For full control (custom BatchConfig, on_progress, custom DocumentSource) use BatchProcessor directly.
ExtractionResult¶
result.base_name # str — document stem
result.document # dict — full document structure
result.chunks # list[dict]
result.image_index # list[dict] — flat list of all images with vision metadata
result.filter_stats # dict — {"kept": n, "dimension": n, "solid_color": n, ...}
result.split_stats # SplitStats | None — populated when split_chunks=True
result.degradations # list[dict] — partial failures; see below
degradations entries have the shape {"stage": str, "id": str, "error": str | None}:
stage |
When populated | error |
|---|---|---|
"vision" |
VisionModel.analyze raised |
exception string |
"embed_image" |
embedder returned None for an image |
None |
"embed_chunk" |
embedder returned None for a chunk |
None |
Only populated when the corresponding provider is configured. An empty list means no partial failures occurred.
PipelineConfig¶
from multixtract import PipelineConfig
PipelineConfig(
min_image_size=100,
min_image_size_minor=75,
reference_img_dir="",
vision_workers=6,
embed_text_limit=8000,
chunk_target_tokens=500,
chunk_overlap_tokens=50,
images_subdir="extracted_images",
doc_json_subdir="jsons",
image_json_subdir="image_jsons",
chunks_subdir="chunks",
individual_chunks_subdir="individual_chunks",
)
PipelineConfig.from_env¶
config = PipelineConfig.from_env()
# reads MULTIXTRACT_VISION_WORKERS, MULTIXTRACT_CHUNK_TARGET_TOKENS, etc.
config = PipelineConfig.from_env(prefix="APP_")
# reads APP_VISION_WORKERS, APP_CHUNK_TARGET_TOKENS, etc.
Every field maps to <prefix><FIELD_NAME_UPPER>. Integer fields are coerced; malformed or absent variables fall back to dataclass defaults.
| Environment variable | Field | Default |
|---|---|---|
MULTIXTRACT_MIN_IMAGE_SIZE |
min_image_size |
100 |
MULTIXTRACT_MIN_IMAGE_SIZE_MINOR |
min_image_size_minor |
75 |
MULTIXTRACT_REFERENCE_IMG_DIR |
reference_img_dir |
"" |
MULTIXTRACT_VISION_WORKERS |
vision_workers |
6 |
MULTIXTRACT_EMBED_TEXT_LIMIT |
embed_text_limit |
8000 |
MULTIXTRACT_CHUNK_TARGET_TOKENS |
chunk_target_tokens |
500 |
MULTIXTRACT_CHUNK_OVERLAP_TOKENS |
chunk_overlap_tokens |
50 |
MULTIXTRACT_IMAGES_SUBDIR |
images_subdir |
"extracted_images" |
MULTIXTRACT_DOC_JSON_SUBDIR |
doc_json_subdir |
"jsons" |
MULTIXTRACT_IMAGE_JSON_SUBDIR |
image_json_subdir |
"image_jsons" |
MULTIXTRACT_CHUNKS_SUBDIR |
chunks_subdir |
"chunks" |
MULTIXTRACT_INDIVIDUAL_CHUNKS_SUBDIR |
individual_chunks_subdir |
"individual_chunks" |
Batch processing¶
BatchProcessor¶
from multixtract.batch import BatchProcessor, BatchConfig
processor = BatchProcessor(pipeline, config=BatchConfig(max_workers=8))
# From mixed file / directory paths (CLI-style)
result = processor.process_inputs(["report.pdf", "./docs"])
# From any DocumentSource implementation
result = processor.process_source(my_source)
# From a pre-built path iterator
result = processor.process_paths(iter([Path("a.pdf"), Path("b.pdf")]))
BatchConfig¶
BatchConfig(
max_workers=4, # concurrent documents
skip_if_exists=True, # skip docs already in the store
split_chunks=False, # write per-chunk documents after each doc
on_progress=None, # Callable[[Path, ExtractionResult | Exception], None]
)
on_progress is called after every document — succeeded, skipped, or failed. The second argument is the ExtractionResult on success/skip, or the Exception on failure. Callback exceptions are caught and logged; they never abort the batch.
from tqdm import tqdm
bar = tqdm(unit="doc")
config = BatchConfig(max_workers=8, on_progress=lambda path, r: bar.update(1))
processor = BatchProcessor(pipeline, config)
processor.process_inputs(["./documents"])
BatchResult¶
result.succeeded # int
result.failed # int
result.skipped # int
result.total # int — succeeded + failed + skipped
result.failures # list[DocumentFailure]
DocumentFailure¶
result.failures[0].path # Path — absolute path to the failed document
result.failures[0].error # Exception
Formatters¶
AzureAISearchFormatter¶
from multixtract.formatters import AzureAISearchFormatter
# From an in-process ExtractionResult
docs = AzureAISearchFormatter.from_result(result)
docs = AzureAISearchFormatter.from_result(result, timestamp="2026-08-19T10:00:00Z", skip_empty=True)
# From a _chunks.json dict (post-processing / offline path)
docs = AzureAISearchFormatter.from_chunks_file(chunks_data)
# Upload to Azure AI Search
search_client.upload_documents(documents=docs)
Each document in docs is a flat dict produced by build_index_document — same schema as documented under build_index_document above.
AzureAISearchFormatter.index_schema¶
from azure.search.documents.indexes import SearchIndexClient
schema = AzureAISearchFormatter.index_schema(
index_name="my-index",
vector_dim=1024, # must match your embedding model's output dim
)
index_client.create_or_update_index(schema)
Returns a SearchIndex configured for hybrid (keyword + vector) retrieval. Requires azure-search-documents to be installed; the import is deferred so the package is not needed unless this method is called.
Token utilities¶
count_tokens¶
Uses tiktoken (cl100k_base encoding) when installed (pip install "multixtract[tiktoken]"); falls back to the fast heuristic otherwise. Used internally for all final token_cnt stamps on chunks.
estimate_tokens¶
Fast heuristic (no external dependency). Used in the splitting hot path where tiktoken would be too slow.
Interfaces (protocols)¶
All providers use structural typing — implement the methods, no subclassing required.
VisionModel¶
class VisionModel(Protocol):
def analyze(
self,
image_bytes: bytes,
ext: str = "png",
width: int = 0,
height: int = 0,
) -> VisionResult: ...
VisionResult¶
@dataclass
class VisionResult:
caption: str = ""
ocr_text: str = ""
description: str = ""
def best_text(self) -> str: ... # description or caption, whichever is set
Embedder¶
class Embedder(Protocol):
dim: int
def embed(self, texts: list[str]) -> list[list[float] | None]: ...
BlobStore¶
class BlobStore(Protocol):
def put_bytes(self, path: str, data: bytes, content_type: str = "") -> str: ...
def put_json(self, path: str, obj: object, compact: bool = False) -> str: ...
def exists(self, path: str) -> bool: ...
DocumentExtractor¶
class DocumentExtractor(Protocol):
extensions: tuple[str, ...]
def extract(
self,
path: str,
image_filter=None,
) -> tuple[dict, list[dict]]: ...
Extractor registry¶
from multixtract import register_extractor, default_registry
register_extractor(MyExtractor()) # register on the global registry
register_extractor(MyExtractor(), extensions=[".x"]) # override extensions
default_registry.supported_extensions # ['.csv', '.doc', '.docx', ...]
Image filtering¶
from multixtract.filters import ImageFilterPipeline
f = ImageFilterPipeline(
min_image_size=100,
min_image_size_minor=75,
reference_img_dir="", # path to folder of reference logo images
)
# Tunable class-level thresholds
f.SOLID_RANGE_MAX = 35 # pixel value range below which image is "solid colour"
f.ICON_MAX_DIM = 200 # max dimension for tiny-icon colour check
f.ICON_MAX_COLORS = 8 # max distinct colours for tiny-icon rejection
f.LOGO_PHASH_THRESHOLD = 60 # perceptual hash distance for logo matching
f.filter_stats # {"kept": n, "dimension": n, "solid_color": n, "ref_logo": n, ...}
f.reset() # clear per-document stats