kennethzychew's picture
phase 2.1: modality detection
e9e88a5
Raw
History Blame Contribute Delete
4.44 kB
"""Modality detection: map a document file to its parse path (pure, no I/O).
The first stage of the core pipeline (architecture section 3, step 1) is intake
and type detection: decide whether a file is a *native PDF* -- which Docling
parses into text/layout directly -- or an *image* (a scan or a phone photo),
which goes through the vision-direct or OCR strategy. That single choice,
``"native_pdf"`` vs ``"image"``, is the only thing this module produces; the
acquisition stage downstream uses it to pick a parser.
Classification is by file extension first, with a standard-library MIME-type
fallback for files whose extension is not enumerated explicitly. No bytes are
read -- the result is a pure function of the path string -- so detection stays
fast, deterministic for the supported extensions, and testable offline. An
unrecognised type raises :class:`UnsupportedModalityError` with an actionable
message; the runner catches it per document and routes to review (CLAUDE.md
rule 6), so a stray file never halts the loop.
"""
from __future__ import annotations
import mimetypes
from pathlib import Path
from typing import Literal
# The two parse paths the pipeline supports. ``image`` covers both flatbed scans
# and phone photos -- they share the same downstream handling (architecture
# section 7), so the distinction is not made here.
Modality = Literal["native_pdf", "image"]
# Extensions parsed as native PDFs (the Docling text/layout path).
NATIVE_PDF_EXTENSIONS: frozenset[str] = frozenset({".pdf"})
# Raster image extensions treated as scans/photos (vision-direct or OCR path).
IMAGE_EXTENSIONS: frozenset[str] = frozenset(
{".png", ".jpg", ".jpeg", ".tif", ".tiff", ".bmp", ".gif", ".webp"}
)
class UnsupportedModalityError(ValueError):
"""Raised when a file's type maps to none of the supported modalities."""
def _modality_from_mime(mime_type: str | None) -> Modality | None:
"""Classify a MIME type into a modality, or ``None`` if it maps to neither.
Args:
mime_type: A MIME type such as ``"application/pdf"`` or ``"image/png"``,
or ``None`` when the type could not be guessed.
Returns:
``"native_pdf"`` for PDF, ``"image"`` for any ``image/*`` type, or
``None`` when the MIME type is unknown or unsupported.
"""
if mime_type == "application/pdf":
return "native_pdf"
if mime_type is not None and mime_type.startswith("image/"):
return "image"
return None
def detect_modality(path: str | Path) -> Modality:
"""Determine the parse modality of a document from its filename.
Extension matching (case-insensitively) is the authoritative signal for the
supported types; for any other extension the standard library's MIME guess
is consulted as a fallback before giving up. The file is never opened, so
``path`` need not exist.
Args:
path: The document path. Only the filename and its extension are
inspected; directories and the file's contents are ignored.
Returns:
``"native_pdf"`` for PDFs, ``"image"`` for supported raster images.
Raises:
UnsupportedModalityError: If the extension and MIME guess both fail to
resolve to a supported modality.
"""
name = Path(path).name
suffix = Path(name).suffix.lower()
if suffix in NATIVE_PDF_EXTENSIONS:
return "native_pdf"
if suffix in IMAGE_EXTENSIONS:
return "image"
guessed = _modality_from_mime(mimetypes.guess_type(name)[0])
if guessed is not None:
return guessed
shown_suffix = suffix or "(none)"
raise UnsupportedModalityError(
f"Cannot determine modality for {name!r}: extension {shown_suffix} is not "
f"a supported document type. Supported: PDF "
f"({', '.join(sorted(NATIVE_PDF_EXTENSIONS))}) or image "
f"({', '.join(sorted(IMAGE_EXTENSIONS))})."
)
def is_supported(path: str | Path) -> bool:
"""Report whether ``path`` resolves to a supported modality.
A convenience predicate over :func:`detect_modality` for callers (e.g. the
watcher) that want to skip unsupported files without handling an exception.
Args:
path: The document path to test.
Returns:
``True`` if :func:`detect_modality` would succeed, ``False`` otherwise.
"""
try:
detect_modality(path)
except UnsupportedModalityError:
return False
return True