File size: 4,186 Bytes
0580de5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbbfba8
 
0580de5
 
 
 
 
 
 
 
 
 
bbbfba8
 
 
0580de5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbbfba8
 
 
 
 
0580de5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbbfba8
0580de5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
130
131
132
133
"""text_only adapter — handles providers that return structured text without geometry.

Expected payload format:
    {
        "text": "full page text",
        "blocks": [                     # optional
            {"text": "block text"},
            ...
        ]
    }
    or simply:
    {"text": "full text"}

This adapter is honest: geometry is marked as 'unknown' since the
provider doesn't supply coordinates. ALTO export will be refused
(no word geometry), but PAGE export may be partial and the viewer
will show text without positioned overlays.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from src.app.domain.models import (
    CanonicalDocument,
    Geometry,
    Provenance,
    RawProviderPayload,
)
from src.app.domain.models.status import EvidenceType, GeometryStatus, InputType
from src.app.normalization.canonical_builder import CanonicalBuilder
from src.app.providers.adapters.base import BaseAdapter

if TYPE_CHECKING:
    from src.app.domain.models.geometry import GeometryContext


class TextOnlyAdapter(BaseAdapter):
    """Adapter for the text_only family (mLLM without geometry)."""

    @property
    def family(self) -> str:
        return "text_only"

    @property
    def version(self) -> str:
        return "adapter.text_only.v1"

    def normalize(
        self,
        raw: RawProviderPayload,
        geometry_context: GeometryContext,
        *,
        document_id: str,
        source_filename: str | None = None,
    ) -> CanonicalDocument:
        payload = raw.payload
        if not isinstance(payload, dict):
            raise ValueError(
                f"text_only expects a dict payload, got {type(payload).__name__}"
            )

        builder = CanonicalBuilder(
            document_id=document_id,
            input_type=InputType.IMAGE,
            filename=source_filename,
        )

        page_w = geometry_context.source_width
        page_h = geometry_context.source_height

        page = builder.add_page("p1", 0, page_w, page_h)

        # Placeholder bbox covering the full page — marked unknown
        full_page_geo = Geometry(
            bbox=(0, 0, page_w, page_h),
            status=GeometryStatus.UNKNOWN,
        )

        # Extract text blocks
        blocks = payload.get("blocks")
        if blocks and isinstance(blocks, list):
            texts = [
                str(b.get("text", ""))
                for b in blocks
                if isinstance(b, dict) and b.get("text")
            ]
        else:
            # Single text blob — split into paragraphs
            full_text = str(payload.get("text", ""))
            if not full_text.strip():
                raise ValueError("text_only payload has no text content")
            texts = [p.strip() for p in full_text.split("\n\n") if p.strip()]
            if not texts:
                texts = [full_text.strip()]

        for bi, block_text in enumerate(texts):
            block_id = f"tb{bi + 1}"
            prov = Provenance(
                provider=raw.provider_id,
                adapter=self.version,
                source_ref=f"$.blocks[{bi}]" if blocks else f"$.text.paragraph[{bi}]",
                evidence_type=EvidenceType.PROVIDER_NATIVE,
            )

            region = page.add_text_region(
                region_id=block_id,
                geometry=full_page_geo,
                provenance=prov,
            )

            # Split block into lines
            lines = [ln.strip() for ln in block_text.split("\n") if ln.strip()]
            if not lines:
                lines = [block_text]

            for li, line_text in enumerate(lines):
                line_id = f"tl{bi + 1}_{li + 1}"
                word_id = f"w{bi + 1}_{li + 1}"

                line = region.add_line(
                    line_id, geometry=full_page_geo, provenance=prov,
                )
                # Each line becomes a single word (no word segmentation available)
                line.add_word(
                    word_id,
                    text=line_text,
                    geometry=full_page_geo,
                    provenance=prov,
                )

        return builder.build()