Skip to content

OCR API

OCR providers translate images to candidate lines. The decode pipeline remains responsible for integrity and correction.

glyphive.restore.ocr

Lazy OCR provider registry and voting orchestration.

__all__ = ['OcrLine', 'OcrProvider', 'available', 'available_engines', 'get', 'names', 'ocr_image', 'ocr_pages', 'ocr_vote'] module-attribute

OcrLine

Bases: NamedTuple

One OCR-read line: its text plus optional per-character confidence.

char_conf is None when the provider (or a non-OCR text/QR path) has no per-character confidence to offer -- callers MUST keep tolerating that (plan 3: OCR-confidence-assisted char-level erasures is an optimization, never a requirement). When present, char_conf has exactly len(text) entries, one per character of text (spaces included -- a provider gives whitespace a confidence of 1.0), each in 0.0..1.0 (or None for a single character the provider itself could not score). It is deliberately RAW: aligned to the full printed line as read, not yet sliced down to the codec's payload region -- see :func:glyphive.codec.engine.align_payload_char_conf, which does that alignment once the codec's frame shape is known.

char_conf = None class-attribute instance-attribute

text instance-attribute

OcrProvider

Bases: ABC

Base class for stateless, no-argument image OCR providers.

name class-attribute

__init_subclass__(**kwargs)

Source code in src/glyphive/restore/ocr/_base.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def __init_subclass__(cls, **kwargs: _ty.Any) -> None:
    super().__init_subclass__(**kwargs)
    if getattr(getattr(cls, "ocr_image", None), "__isabstractmethod__", False):
        return
    name = getattr(cls, "name", None)
    if not isinstance(name, str) or not re.fullmatch(r"[a-z][a-z0-9_-]*", name):
        raise ValueError(f"OCR provider {cls.__qualname__} has an invalid name")
    if name in OcrProvider._registry:
        existing = OcrProvider._registry[name]
        raise ValueError(
            f"duplicate OCR provider name {name!r}: "
            f"{existing.__module__}.{existing.__qualname__} and "
            f"{cls.__module__}.{cls.__qualname__}"
        )
    OcrProvider._registry[name] = cls

available() classmethod

Source code in src/glyphive/restore/ocr/_base.py
57
58
59
60
61
62
63
@classmethod
def available(cls) -> _ty.List[str]:
    return sorted(
        name
        for name, implementation in OcrProvider._registry.items()
        if implementation.is_available()
    )

get(name) classmethod

Source code in src/glyphive/restore/ocr/_base.py
65
66
67
68
69
70
71
72
73
74
@classmethod
def get(cls, name: str) -> "OcrProvider":
    try:
        implementation = OcrProvider._registry[name]
    except (KeyError, TypeError):
        valid = ", ".join(OcrProvider.names()) or "(none)"
        raise ValueError(
            f"unknown OCR engine {name!r}; available engines: {valid}"
        ) from None
    return implementation()

is_available() classmethod

Source code in src/glyphive/restore/ocr/_base.py
76
77
78
@classmethod
def is_available(cls) -> bool:
    return True

names() classmethod

Source code in src/glyphive/restore/ocr/_base.py
53
54
55
@classmethod
def names(cls) -> _ty.List[str]:
    return sorted(OcrProvider._registry)

ocr_image(image_path) abstractmethod

Return candidate lines (text + optional per-char confidence) from one image.

Source code in src/glyphive/restore/ocr/_base.py
100
101
102
@abstractmethod
def ocr_image(self, image_path: _ty.Any) -> _ty.List[OcrLine]:
    """Return candidate lines (text + optional per-char confidence) from one image."""

available()

Return registered OCR provider names currently available to use.

Source code in src/glyphive/restore/ocr/__init__.py
51
52
53
def available() -> list[str]:
    """Return registered OCR provider names currently available to use."""
    return OcrProvider.available()

available_engines()

Return available providers in the documented preference order.

Source code in src/glyphive/restore/ocr/__init__.py
56
57
58
59
60
61
62
63
64
65
66
67
def available_engines() -> list[str]:
    """Return available providers in the documented preference order."""
    result = []
    ordered_names = list(_ENGINE_PREFERENCE)
    ordered_names.extend(name for name in names() if name not in ordered_names)
    for name in ordered_names:
        try:
            if get(name).is_available():
                result.append(name)
        except Exception:
            continue
    return result

get(name)

Source code in src/glyphive/restore/ocr/__init__.py
43
44
def get(name: str) -> OcrProvider:
    return OcrProvider.get(name)

names()

Source code in src/glyphive/restore/ocr/__init__.py
47
48
def names() -> list[str]:
    return OcrProvider.names()

ocr_image(image_path, *, engine=None)

OCR one image with a selected or highest-preference provider.

Source code in src/glyphive/restore/ocr/__init__.py
90
91
92
def ocr_image(image_path, *, engine: Optional[str] = None) -> List[OcrLine]:
    """OCR one image with a selected or highest-preference provider."""
    return get(_select_engine(engine)).ocr_image(image_path)

ocr_pages(image_paths, *, engine=None)

OCR several images, resolving one provider before any page work.

Source code in src/glyphive/restore/ocr/__init__.py
 95
 96
 97
 98
 99
100
def ocr_pages(
    image_paths: Iterable, *, engine: Optional[str] = None
) -> List[List[OcrLine]]:
    """OCR several images, resolving one provider before any page work."""
    provider = get(_select_engine(engine))
    return [provider.ocr_image(path) for path in image_paths]

ocr_vote(image_path, *, engines)

Return a majority-vote hint; CRC/RS remains the correctness oracle.

Votes on each line's TEXT only; the winning line's char_conf (from whichever engine's reading was chosen) is carried through unchanged -- voting picks which text wins, it never blends or discards confidence.

Source code in src/glyphive/restore/ocr/__init__.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def ocr_vote(image_path, *, engines: List[str]) -> List[OcrLine]:
    """Return a majority-vote hint; CRC/RS remains the correctness oracle.

    Votes on each line's TEXT only; the winning line's ``char_conf`` (from
    whichever engine's reading was chosen) is carried through unchanged --
    voting picks which text wins, it never blends or discards confidence.
    """
    if not engines:
        raise ValueError("ocr_vote requires at least one engine")
    providers = [get(_select_engine(name)) for name in engines]
    per_engine = [provider.ocr_image(image_path) for provider in providers]
    base = per_engine[0]
    voted: List[OcrLine] = []
    for index, base_line in enumerate(base):
        votes: Counter[str] = Counter(
            lines[index].text for lines in per_engine if index < len(lines)
        )
        if not votes:
            voted.append(base_line)
            continue
        top = max(votes.values())
        for lines in per_engine:
            if index < len(lines) and votes[lines[index].text] == top:
                voted.append(lines[index])
                break
    return voted