File size: 9,353 Bytes
6b8ed55
 
 
 
 
bbbfba8
6b8ed55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbbfba8
6b8ed55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbbfba8
 
 
6b8ed55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
"""Tests for the ViewerProjection builder and overlay generation."""

from __future__ import annotations

import json
from typing import TYPE_CHECKING

import pytest
from src.app.domain.models import (
    AltoReadiness,
    CanonicalDocument,
    EvidenceType,
    ExportEligibility,
    Geometry,
    GeometryStatus,
    NonTextRegion,
    OverlayLevel,
    Page,
    PageXmlReadiness,
    Provenance,
    RawProviderPayload,
    ReadinessLevel,
    Source,
    TextLine,
    TextRegion,
    Word,
)
from src.app.domain.models.geometry import GeometryContext
from src.app.domain.models.status import BlockRole, InputType, NonTextKind
from src.app.normalization.pipeline import normalize
from src.app.viewer.overlays import (
    line_to_overlay,
    region_to_overlay,
    word_to_inspection,
    word_to_overlay,
)
from src.app.viewer.projection_builder import build_all_projections, build_projection

if TYPE_CHECKING:
    from pathlib import Path


def _prov() -> Provenance:
    return Provenance(
        provider="test", adapter="v1", source_ref="$",
        evidence_type=EvidenceType.PROVIDER_NATIVE,
    )


def _geo(x: float = 10, y: float = 10, w: float = 100, h: float = 30) -> Geometry:
    return Geometry(bbox=(x, y, w, h), status=GeometryStatus.EXACT)


def _simple_doc() -> CanonicalDocument:
    return CanonicalDocument(
        document_id="viewer_test",
        source=Source(input_type=InputType.IMAGE, filename="test.png"),
        pages=[Page(
            id="p1", page_index=0, width=2480, height=3508,
            alto_readiness=AltoReadiness(level=ReadinessLevel.FULL),
            page_readiness=PageXmlReadiness(level=ReadinessLevel.FULL),
            reading_order=["tb1"],
            text_regions=[TextRegion(
                id="tb1", role=BlockRole.BODY,
                geometry=_geo(100, 200, 1200, 900), provenance=_prov(), lang="fra",
                lines=[
                    TextLine(id="tl1", geometry=_geo(110, 220, 1100, 42),
                             provenance=_prov(), lang="fra",
                             words=[
                                 Word(id="w1", text="Bonjour", geometry=_geo(110, 220, 90, 40),
                                      provenance=_prov(), confidence=0.96, lang="fra"),
                                 Word(id="w2", text="monde", geometry=_geo(220, 220, 80, 40),
                                      provenance=_prov(), confidence=0.94, lang="fra"),
                             ]),
                    TextLine(id="tl2", geometry=_geo(110, 280, 1100, 42),
                             provenance=_prov(),
                             words=[
                                 Word(id="w3", text="Test", geometry=_geo(110, 280, 60, 40),
                                      provenance=_prov(), confidence=0.90),
                             ]),
                ],
            )],
            non_text_regions=[NonTextRegion(
                id="ntr1", kind=NonTextKind.ILLUSTRATION,
                geometry=_geo(1500, 200, 400, 300), provenance=_prov(),
            )],
        )],
    )


# -- Overlay unit tests -------------------------------------------------------


class TestOverlayGeneration:
    def test_word_overlay(self) -> None:
        w = Word(id="w1", text="Hello", geometry=_geo(10, 20, 50, 30),
                 provenance=_prov(), confidence=0.95, lang="eng")
        ov = word_to_overlay(w)
        assert ov.id == "w1"
        assert ov.level == OverlayLevel.WORD
        assert ov.text == "Hello"
        assert ov.bbox == (10, 20, 50, 30)
        assert ov.confidence == 0.95
        assert ov.provenance_type == EvidenceType.PROVIDER_NATIVE
        assert ov.geometry_status == GeometryStatus.EXACT

    def test_line_overlay(self) -> None:
        line = TextLine(id="tl1", geometry=_geo(10, 20, 200, 30),
                        provenance=_prov(),
                        words=[Word(id="w1", text="Hello", geometry=_geo(),
                                    provenance=_prov())])
        ov = line_to_overlay(line)
        assert ov.id == "tl1"
        assert ov.level == OverlayLevel.LINE
        assert ov.text == "Hello"

    def test_region_overlay(self) -> None:
        r = TextRegion(id="tb1", role=BlockRole.HEADING,
                       geometry=_geo(0, 0, 500, 100), provenance=_prov(),
                       lines=[TextLine(id="tl1", geometry=_geo(),
                                       provenance=_prov(),
                                       words=[Word(id="w1", text="Title",
                                                   geometry=_geo(),
                                                   provenance=_prov())])])
        ov = region_to_overlay(r)
        assert ov.id == "tb1"
        assert ov.level == OverlayLevel.BLOCK
        assert ov.label == "heading"

    def test_word_inspection(self) -> None:
        w = Word(id="w1", text="Hello", geometry=_geo(10, 20, 50, 30),
                 provenance=_prov(), confidence=0.95, lang="eng")
        insp = word_to_inspection(w)
        assert insp.id == "w1"
        assert insp.text == "Hello"
        assert insp.lang == "eng"
        assert insp.provenance_provider == "test"


# -- Projection builder -------------------------------------------------------


class TestProjectionBuilder:
    def test_build_projection(self) -> None:
        doc = _simple_doc()
        vp = build_projection(doc)
        assert vp.image_ref == "test.png"
        assert vp.image_width == 2480
        assert vp.image_height == 3508

    def test_block_overlays(self) -> None:
        doc = _simple_doc()
        vp = build_projection(doc)
        assert len(vp.block_overlays) == 1
        assert vp.block_overlays[0].id == "tb1"

    def test_line_overlays(self) -> None:
        doc = _simple_doc()
        vp = build_projection(doc)
        assert len(vp.line_overlays) == 2

    def test_word_overlays(self) -> None:
        doc = _simple_doc()
        vp = build_projection(doc)
        assert len(vp.word_overlays) == 3
        word_ids = {o.id for o in vp.word_overlays}
        assert word_ids == {"w1", "w2", "w3"}

    def test_non_text_overlays(self) -> None:
        doc = _simple_doc()
        vp = build_projection(doc)
        assert len(vp.non_text_overlays) == 1
        assert vp.non_text_overlays[0].id == "ntr1"

    def test_inspection_index(self) -> None:
        doc = _simple_doc()
        vp = build_projection(doc)
        # Should contain all blocks, lines, and words
        assert "tb1" in vp.inspection_index
        assert "tl1" in vp.inspection_index
        assert "tl2" in vp.inspection_index
        assert "w1" in vp.inspection_index
        assert "w2" in vp.inspection_index
        assert "w3" in vp.inspection_index

    def test_inspection_data_content(self) -> None:
        doc = _simple_doc()
        vp = build_projection(doc)
        w1 = vp.inspection_index["w1"]
        assert w1.text == "Bonjour"
        assert w1.confidence == 0.96
        assert w1.lang == "fra"
        assert w1.provenance_provider == "test"

    def test_export_status_default(self) -> None:
        doc = _simple_doc()
        vp = build_projection(doc)
        assert vp.export_status.alto_export == ReadinessLevel.NONE

    def test_export_status_custom(self) -> None:
        doc = _simple_doc()
        elig = ExportEligibility(
            alto_export=ReadinessLevel.FULL,
            page_export=ReadinessLevel.FULL,
            viewer_render=ReadinessLevel.FULL,
        )
        vp = build_projection(doc, export_status=elig)
        assert vp.export_status.alto_export == ReadinessLevel.FULL

    def test_page_index_out_of_range(self) -> None:
        doc = _simple_doc()
        with pytest.raises(ValueError, match="out of range"):
            build_projection(doc, page_index=5)

    def test_build_all_projections(self) -> None:
        doc = _simple_doc()
        projections = build_all_projections(doc)
        assert len(projections) == 1
        assert projections[0].image_width == 2480

    def test_json_roundtrip(self) -> None:
        doc = _simple_doc()
        vp = build_projection(doc)
        data = vp.model_dump(mode="json")
        # Verify it's fully serializable
        import json
        json_str = json.dumps(data)
        restored = json.loads(json_str)
        assert restored["image_width"] == 2480
        assert len(restored["word_overlays"]) == 3
        assert "w1" in restored["inspection_index"]


class TestProjectionFromPipeline:
    """Integration: PaddleOCR raw → normalize → build_projection."""

    def test_from_paddle(self, fixtures_dir: Path) -> None:
        with open(fixtures_dir / "paddle_ocr_sample.json") as f:
            payload = json.load(f)

        raw = RawProviderPayload(
            provider_id="paddleocr", adapter_id="v1", runtime_type="local",
            payload=payload, image_width=2480, image_height=3508,
        )
        geo_ctx = GeometryContext(source_width=2480, source_height=3508)

        doc = normalize(raw, family="word_box_json", geometry_context=geo_ctx,
                        document_id="vp_test", source_filename="page.png")

        vp = build_projection(doc)
        assert vp.image_ref == "page.png"
        assert len(vp.word_overlays) == 5
        assert vp.word_overlays[0].text == "Bonjour"
        assert len(vp.inspection_index) > 0