File size: 4,473 Bytes
7b2177e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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
129
# GENERATED by hub/build_hub_package.py from src/bodhan_genai/ocr/templates/contract.py -- do not edit.
# Vendored so this repo is self-contained: `pip install transformers torch pillow` is the
# whole install. See indic_doc_parser.py for usage.

"""IndicDocParser's contract: prompts, block taxonomy, output schema.

Two vocabularies meet here and mixing them fails silently. **Labels** are what IndicDocLayout
emits (``ocr.layout.labels.CLASSES``); MARGINALIA and HEAD_FOOT match them case-SENSITIVELY.
**Types** are the coarse categories a label maps to, and select the prompt. OCR_SKIP_LABELS is
the exception: matched case-insensitively.

stdlib-only -- importable with no GPU stack and no PIL.
"""

from __future__ import annotations

from enum import StrEnum


class TableFormat(StrEnum):
    """HTML is the default: colspan/rowspan and in-cell breaks have no GFM spelling, so a
    merged-cell table rendered as markdown silently loses its structure."""

    HTML = "html"
    MARKDOWN = "markdown"


TEXT_PROMPT = (
    "Transcribe the text in this image. Write any mathematical expressions in LaTeX, "
    "using $...$ for inline math and $$...$$ for display equations."
)
EQUATION_PROMPT = "Output only the LaTeX for this equation image."
TABLE_PROMPTS = {
    TableFormat.HTML: (
        "Convert this table image to HTML. Preserve the structure exactly, using colspan and "
        "rowspan for merged cells and <br/> for line breaks within a cell. "
        "Output only the HTML table."
    ),
    TableFormat.MARKDOWN: (
        "Convert this table image to a GitHub-flavored markdown table. Output only the table."
    ),
}


def prompt_for(block_type: str, table_format: TableFormat = TableFormat.HTML) -> str:
    if block_type == "Table":
        return TABLE_PROMPTS[TableFormat(table_format)]
    if block_type == "Equation":
        return EQUATION_PROMPT
    return TEXT_PROMPT


# Labels absent from this map fall through to "Text" by design -- Question, Paragraph, Answer,
# List, MCQ, Code, Reference and the rest all carry prose.
LABEL_TO_TYPE = {
    "table": "Table",
    "table-caption": "Caption",
    "equation": "Equation",
    "expression": "Equation",
    "diagram": "Figure",
    "chart": "Figure",
    "image": "Picture",
    "image-caption": "Caption",
    "title": "Title",
    "chapter-title": "Title",
    "section-title": "SectionHeader",
    "sub-section-title": "SectionHeader",
    "sub-sub-section-title": "SectionHeader",
    "header": "PageHeader",
    "footer": "PageFooter",
    "page-number": "PageNumber",
    "folio": "PageNumber",
    "footnote": "Footnote",
}


def map_label(label) -> str:
    """Label -> pipeline type. Unknown labels are ``Text``."""
    return LABEL_TO_TYPE.get(str(label).strip().lower(), "Text")


# Exactly the types map_label can produce, less DROP_TYPES. test_contract asserts this, so a
# dead or undocumented type cannot creep in.
KEPT_BLOCK_TYPES = (
    "Text",
    "Title",
    "SectionHeader",
    "Table",
    "Equation",
    "Caption",
    "Footnote",
    "PageHeader",
    "PageFooter",
    "PageNumber",
)

# Never cropped, never reconstructed: pictorial regions have no text and invite hallucination.
# Deliberately narrow -- page numbers and margin text DO reach the recognizer.
DROP_TYPES = frozenset({"Figure", "Picture"})

# Cleaned as their own group so a page-spanning paragraph cannot swallow a page number.
MARGINALIA = frozenset({"Header", "Footer", "Page-number", "Folio"})
HEAD_FOOT = ("Header", "Footer")

# Never sent to the recognizer, but NOT deleted: these keep their place in the output with
# text "", so a consumer can still see what was detected and where.
OCR_SKIP_LABELS = frozenset({"header", "footer", "diagram", "image", "chart", "advertisement"})


def is_transcribed(label) -> bool:
    return str(label).strip().lower() not in OCR_SKIP_LABELS


OUTPUT_BLOCK_SCHEMA = {
    "order": "int    -- reading-order rank (0 = first)",
    "label": "str    -- raw IndicDocLayout label",
    "type": "str    -- pipeline type (see KEPT_BLOCK_TYPES)",
    "bbox_xyxy": "[float x4] -- pixel [x0, y0, x1, y1], clamped to the page",
    "conf": "float  -- detection confidence",
    "text": "str    -- transcription; '' when not sent to the recognizer",
}

OUTPUT_PAGE_SCHEMA = {
    "image": "str   -- source image filename",
    "width": "int   -- page width in pixels",
    "height": "int   -- page height in pixels",
    "blocks": "[BLOCK] -- in reading order",
}