OCR Caching

doc2mark provides a request-scoped OCR result cache that avoids redundant (and expensive) calls to vision-model providers when the same image is processed more than once. Cached values use the cache v4 schema, which stores the full structured OCRPage document alongside the legacy plain-text representation, so downstream consumers always receive complete results regardless of whether the value was served from cache or computed live.

Caching is wired into the processing pipeline through the ocr_cache parameter accepted by load() and UnifiedDocumentLoader. You can create a cache with the create_ocr_cache() factory, or instantiate a backend class directly.

Cache backends

OCRCache (abstract base)

All cache backends implement the OCRCache interface.

from doc2mark import OCRCache

Abstract methods

get(key)

Return the cached OCRResult for key, or None on a miss.

set(key, result, ttl_seconds=None)

Store an OCRResult. When ttl_seconds is None the backend’s default TTL applies.

cleanup()

Remove expired entries. Returns the number of entries removed.

clear()

Drop every entry from the cache.

stats()

Return a dict of runtime counters (hits, misses, sets, evictions, etc.) and configuration metadata such as the backend name.

class doc2mark.OCRCache[source]

Bases: ABC

Interface for OCR result caches.

abstractmethod cleanup()[source]

Remove expired entries and return the number removed.

Return type:

int

abstractmethod clear()[source]

Clear all cached entries.

Return type:

None

abstractmethod get(key)[source]

Return a cached OCR result, or None on miss.

Parameters:

key (str)

Return type:

OCRResult | None

abstractmethod set(key, result, ttl_seconds=None)[source]

Store an OCR result.

Parameters:
  • key (str)

  • result (OCRResult)

  • ttl_seconds (float | None)

Return type:

None

abstractmethod stats()[source]

Return cache statistics.

Return type:

Dict[str, Any]

MemoryOCRCache

Thread-safe, in-process LRU cache with TTL, absolute max-age, and refresh-on-access semantics. This is the default backend returned by create_ocr_cache() when provider="memory".

from doc2mark import MemoryOCRCache

cache = MemoryOCRCache(
    ttl_seconds=3600,
    max_age_seconds=43200,
    max_entries=1024,
    max_refreshes=10,
)

Constructor parameters

ttl_seconds (float, default 3600)

Time-to-live for each entry in seconds. An entry that is accessed before expiry has its deadline extended (up to max_refreshes times).

max_age_seconds (Optional[float], default 43200)

Absolute upper bound on how long an entry may live, regardless of refresh activity. Set to None to disable the hard ceiling.

max_entries (int, default 1024)

Maximum number of entries. When exceeded the least-recently-used entry is evicted.

max_refreshes (Optional[int], default 10)

Maximum number of times an entry’s TTL may be extended on access. None allows unlimited refreshes.

time_func (Optional[Callable[[], float]], default None)

Clock function used for timestamps. Defaults to time.time(). Mainly useful in tests.

class doc2mark.MemoryOCRCache(ttl_seconds=3600, max_age_seconds=43200, max_entries=1024, max_refreshes=10, time_func=None)[source]

Bases: OCRCache

Thread-safe in-memory OCR cache with TTL, max-age, and refresh bounds.

Parameters:
  • ttl_seconds (float)

  • max_age_seconds (float | None)

  • max_entries (int)

  • max_refreshes (int | None)

  • time_func (Callable[[], float] | None)

cleanup()[source]

Remove expired entries and return the number removed.

Return type:

int

clear()[source]

Clear all cached entries.

Return type:

None

get(key)[source]

Return a cached OCR result, or None on miss.

Parameters:

key (str)

Return type:

OCRResult | None

set(key, result, ttl_seconds=None)[source]

Store an OCR result.

Parameters:
  • key (str)

  • result (OCRResult)

  • ttl_seconds (float | None)

Return type:

None

stats()[source]

Return cache statistics.

Return type:

Dict[str, Any]

RedisOCRCache

Persistent, cross-process cache backed by Redis. Requires the optional redis dependency (install with pip install doc2mark[redis]).

The constructor lazily imports redis, connects using the provided URL, and verifies reachability with ping(). If the connection fails at construction time and a fallback is configured via create_ocr_cache(), the factory falls back transparently.

from doc2mark import RedisOCRCache

cache = RedisOCRCache(
    redis_url="redis://localhost:6379/0",
    ttl_seconds=3600,
    max_age_seconds=43200,
    max_refreshes=10,
    key_prefix="doc2mark:ocr:ocr-cache-v4",
)

Constructor parameters

redis_url (str)

Redis connection URL (e.g. "redis://localhost:6379/0"). Required; an empty string raises ValueError.

ttl_seconds (float, default 3600)

Per-entry time-to-live in seconds, extended on access.

max_age_seconds (Optional[float], default 43200)

Absolute entry lifetime cap. None disables the limit.

max_refreshes (Optional[int], default 10)

Maximum refresh count per entry.

key_prefix (str, default “doc2mark:ocr:ocr-cache-v4”)

Prefix prepended to every Redis key. Change this to namespace multiple applications sharing the same Redis instance.

time_func (Optional[Callable[[], float]], default None)

Clock override, as in MemoryOCRCache.

Note

cleanup() is a no-op for the Redis backend because Redis manages key expiration natively via the EX argument on SET.

class doc2mark.RedisOCRCache(redis_url, ttl_seconds=3600, max_age_seconds=43200, max_refreshes=10, key_prefix='doc2mark:ocr:ocr-cache-v4', time_func=None)[source]

Bases: OCRCache

Redis-backed OCR cache.

Redis is imported lazily so doc2mark remains importable without the optional redis extra. The constructor verifies the connection with ping().

Parameters:
  • redis_url (str)

  • ttl_seconds (float)

  • max_age_seconds (float | None)

  • max_refreshes (int | None)

  • key_prefix (str)

  • time_func (Callable[[], float] | None)

cleanup()[source]

Remove expired entries and return the number removed.

Return type:

int

clear()[source]

Clear all cached entries.

Return type:

None

get(key)[source]

Return a cached OCR result, or None on miss.

Parameters:

key (str)

Return type:

OCRResult | None

set(key, result, ttl_seconds=None)[source]

Store an OCR result.

Parameters:
  • key (str)

  • result (OCRResult)

  • ttl_seconds (float | None)

Return type:

None

stats()[source]

Return cache statistics.

Return type:

Dict[str, Any]

NoOpOCRCache

A cache that never stores anything. Every get() returns None. Useful in testing or when you want to satisfy a type constraint without actually caching.

from doc2mark import NoOpOCRCache

cache = NoOpOCRCache()  # always misses
class doc2mark.NoOpOCRCache[source]

Bases: OCRCache

Cache implementation that always misses.

cleanup()[source]

Remove expired entries and return the number removed.

Return type:

int

clear()[source]

Clear all cached entries.

Return type:

None

get(key)[source]

Return a cached OCR result, or None on miss.

Parameters:

key (str)

Return type:

OCRResult | None

set(key, result, ttl_seconds=None)[source]

Store an OCR result.

Parameters:
  • key (str)

  • result (OCRResult)

  • ttl_seconds (float | None)

Return type:

None

stats()[source]

Return cache statistics.

Return type:

Dict[str, Any]

CachedOCR

A transparent BaseOCR wrapper that intercepts process_image and batch_process_images calls, checks the cache first, and only forwards misses to the underlying provider. Duplicate images within the same batch are de-duplicated so the provider sees each unique image at most once.

You rarely need to instantiate CachedOCR yourself – the loader creates one internally when you pass ocr_cache to UnifiedDocumentLoader.

from doc2mark.ocr.cache import CachedOCR
from doc2mark import MemoryOCRCache, OCRFactory, OCRConfig

provider = OCRFactory.create("openai", api_key="sk-...")
cache = MemoryOCRCache()
cached_provider = CachedOCR(wrapped=provider, cache=cache)

# Use cached_provider like any BaseOCR instance
result = cached_provider.process_image(image_bytes)

Constructor parameters

wrapped (:class:`~doc2mark.ocr.base.BaseOCR`)

The real OCR provider to delegate cache misses to.

cache (Optional[:class:`~doc2mark.OCRCache`])

The cache backend. If None, a NoOpOCRCache is substituted (effectively disabling caching).

cache_version (str, default “ocr-cache-v4”)

Schema version embedded in every cache key. Changing this value invalidates all existing entries, which is useful after a breaking change to the serialization format.

class doc2mark.ocr.cache.CachedOCR(wrapped, cache, cache_version='ocr-cache-v4')[source]

Bases: BaseOCR

OCR provider wrapper that checks a cache before calling the provider.

Parameters:
batch_process_images(images, **kwargs)[source]

Process multiple images in batch using LangChain.

This is the primary method for OCR processing. All implementations must use LangChain for efficient batch processing.

Parameters:
  • images (List[bytes]) – List of image data as bytes

  • **kwargs – Additional provider-specific options

Returns:

List of OCRResult objects in the same order as input

Return type:

List[OCRResult]

preprocess_image(image_data)[source]

Preprocess image before OCR (optional).

Parameters:

image_data (bytes) – Raw image data

Returns:

Preprocessed image data

Return type:

bytes

property provider_name: str

Get the provider name.

property requires_api_key: bool

Check if this provider requires an API key.

validate_api_key()[source]

Validate that the API key is set if required.

Returns:

True if valid, False otherwise

Return type:

bool

Factory function

create_ocr_cache

The recommended way to obtain a cache backend. It maps a short string name to the corresponding class, forwarding common tuning parameters.

from doc2mark import create_ocr_cache

# In-memory cache (default settings)
cache = create_ocr_cache("memory")

# Redis-backed cache (requires pip install doc2mark[redis])
cache = create_ocr_cache(
    "redis",
    redis_url="redis://localhost:6379/0",
    fallback="memory",
)

# Disable caching explicitly
cache = create_ocr_cache("none")  # returns None

Parameters

provider (Optional[str], default “none”)

Backend name. Recognized values:

redis_url (Optional[str], default None)

Connection URL when provider is "redis".

fallback (str, default “memory”)

Backend to use when Redis is unreachable. "memory" silently degrades to an in-process cache; "none" disables caching; "raise" re-raises the original connection error.

ttl_seconds (float, default 3600)

Per-entry time-to-live forwarded to the backend.

max_age_seconds (Optional[float], default 43200)

Absolute entry lifetime forwarded to the backend.

max_refreshes (Optional[int], default 10)

Refresh cap forwarded to the backend.

max_entries (int, default 1024)

Maximum entries (memory backend only).

key_prefix (str, default “doc2mark:ocr:ocr-cache-v4”)

Redis key namespace (Redis backend only).

Returns

An OCRCache instance, or None when caching is disabled.

doc2mark.create_ocr_cache(provider='none', *, redis_url=None, fallback='memory', ttl_seconds=3600, max_age_seconds=43200, max_refreshes=10, max_entries=1024, key_prefix='doc2mark:ocr:ocr-cache-v4')[source]

Create an OCR cache backend from a small provider name.

Parameters:
  • provider (str | None)

  • redis_url (str | None)

  • fallback (str)

  • ttl_seconds (float)

  • max_age_seconds (float | None)

  • max_refreshes (int | None)

  • max_entries (int)

  • key_prefix (str)

Return type:

OCRCache | None

Wiring caching into the pipeline

Pass the cache to load() or construct a UnifiedDocumentLoader with it:

from doc2mark import load, create_ocr_cache

cache = create_ocr_cache("memory")
doc = load(
    "report.pdf",
    extract_images=True,
    ocr_images=True,
    ocr_cache=cache,
)
from doc2mark import UnifiedDocumentLoader, create_ocr_cache

cache = create_ocr_cache(
    "redis",
    redis_url="redis://localhost:6379/0",
    fallback="memory",
)
loader = UnifiedDocumentLoader(
    ocr_provider="openai",
    api_key="sk-...",
    ocr_cache=cache,
)
doc = loader.load("report.pdf", extract_images=True, ocr_images=True)