File size: 16,419 Bytes
5ec8f42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
979f3c3
5ec8f42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
"""Tests Sprint 66 โ€” DAG branchant via ``inputs_from``.

Couvre :

1. ``PipelineStep.inputs_from`` acceptรฉ par dรฉfaut (vide).
2. ``PipelineSpec.validate`` :
   - ``inputs_from`` vers une รฉtape antรฉrieure connue qui produit
     le type โ†’ OK
   - ``inputs_from`` vers une รฉtape inconnue โ†’ erreur explicite
   - ``inputs_from`` vers une รฉtape qui ne produit pas ce type โ†’
     erreur explicite
   - ``inputs_from`` pour un type que le module ne consomme pas โ†’
     erreur explicite
   - ``inputs_from = {TYPE: "__initial__"}`` valide si ce type est
     dans les entrรฉes initiales
3. ``PipelineRunner.run`` :
   - DAG fork : 2 corrections en parallรจle d'un mรชme OCR (chacune
     dรฉmarre depuis OCR, pas l'une de l'autre) โ†’ mรฉtriques
     indรฉpendantes
   - Rรฉtrocompat : sans ``inputs_from``, comportement Sprint 63
     prรฉservรฉ (chaรฎne)
   - ``inputs_from`` vers une รฉtape qui a รฉchouรฉ โ†’ entrรฉe
     manquante explicite avec marqueur ``@step``
4. ``PipelineResult.junction_metrics_for`` retourne la derniรจre
   รฉtape rรฉussie ayant produit le type, indรฉpendamment du DAG.
5. Philosophie inchangรฉe : tous les modules sont des **mocks**.
"""

from __future__ import annotations

from typing import Any

from picarones.core.corpus import Document, GTLevel, TextGT
from picarones.core.modules import ArtifactType, BaseModule
from picarones.core.pipeline import (
    PipelineRunner,
    PipelineSpec,
    PipelineStep,
)


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Mocks
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€


class MockOCR(BaseModule):
    input_types = (ArtifactType.IMAGE,)
    output_types = (ArtifactType.TEXT,)
    execution_mode: Any = "io"

    def __init__(self, output: str) -> None:
        self._out = output

    @property
    def name(self) -> str:
        return "mock-ocr"

    def process(self, inputs):
        return {ArtifactType.TEXT: self._out}


class TextFixer(BaseModule):
    """Rewriter qui applique un dict de remplacements."""

    input_types = (ArtifactType.TEXT,)
    output_types = (ArtifactType.TEXT,)
    execution_mode: Any = "cpu"

    def __init__(self, name: str, replacements: dict[str, str]) -> None:
        self._name = name
        self._replacements = replacements

    @property
    def name(self) -> str:
        return self._name

    def process(self, inputs):
        text = inputs[ArtifactType.TEXT]
        for src, dst in self._replacements.items():
            text = text.replace(src, dst)
        return {ArtifactType.TEXT: text}


class TextDoubler(BaseModule):
    """Module qui consomme TEXT et produit TEXT (concatรจne 2 fois)."""

    input_types = (ArtifactType.TEXT,)
    output_types = (ArtifactType.TEXT,)
    execution_mode: Any = "cpu"

    @property
    def name(self) -> str:
        return "doubler"

    def process(self, inputs):
        return {ArtifactType.TEXT: inputs[ArtifactType.TEXT] * 2}


class AlwaysFails(BaseModule):
    input_types = (ArtifactType.TEXT,)
    output_types = (ArtifactType.TEXT,)
    execution_mode: Any = "cpu"

    @property
    def name(self) -> str:
        return "fail"

    def process(self, inputs):
        raise RuntimeError("boom")


def _make_doc(text: str = "hello world") -> Document:
    return Document(
        image_path="/tmp/x.png", ground_truth=text, doc_id="d1",
        ground_truths={GTLevel.TEXT: TextGT(text=text)},
    )


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 1. PipelineStep.inputs_from default
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€


class TestStepDefaults:
    def test_inputs_from_default_empty(self) -> None:
        step = PipelineStep("ocr", MockOCR("x"))
        assert step.inputs_from == {}


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 2. Validation รฉtendue
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€


class TestValidateInputsFrom:
    def test_valid_reference_to_prior_step(self) -> None:
        spec = PipelineSpec(
            name="ok",
            steps=[
                PipelineStep("ocr", MockOCR("x")),
                PipelineStep(
                    "fix",
                    TextFixer("fix", {}),
                    inputs_from={ArtifactType.TEXT: "ocr"},
                ),
            ],
        )
        problems = spec.validate((ArtifactType.IMAGE,))
        assert problems == []

    def test_reference_to_initial_input(self) -> None:
        # Une pipeline dรฉmarrant par TEXT (factory custom) peut
        # rรฉfรฉrencer "__initial__"
        spec = PipelineSpec(
            name="ok",
            steps=[
                PipelineStep(
                    "fix",
                    TextFixer("fix", {}),
                    inputs_from={ArtifactType.TEXT: "__initial__"},
                ),
            ],
        )
        problems = spec.validate((ArtifactType.TEXT,))
        assert problems == []

    def test_reference_to_unknown_step(self) -> None:
        spec = PipelineSpec(
            name="bad",
            steps=[
                PipelineStep("ocr", MockOCR("x")),
                PipelineStep(
                    "fix",
                    TextFixer("fix", {}),
                    inputs_from={ArtifactType.TEXT: "non_existing"},
                ),
            ],
        )
        problems = spec.validate((ArtifactType.IMAGE,))
        assert any("non_existing" in p for p in problems)

    def test_reference_to_step_not_producing_type(self) -> None:
        # Un step qui produit TEXT, on rรฉfรฉrence un type ALTO qu'il
        # n'a pas โ€” mais le module en aval ne consomme pas ALTO,
        # donc on test directement avec un type que le module
        # consomme bien.  Pour ce test on simule en rรฉfรฉrenรงant
        # un type que le module en aval consomme mais que l'รฉtape
        # source n'a pas produit.
        spec = PipelineSpec(
            name="bad",
            steps=[
                PipelineStep("ocr", MockOCR("x")),  # produit TEXT
                # Le step suivant consomme TEXT et inputs_from
                # rรฉfรฉrence l'รฉtape "ocr" mais via un type qu'elle
                # ne produit pas.  Pour faire รงa il faut un module
                # qui consomme un autre type.  On ne couvre pas ce
                # cas ici (il faudrait un mock multi-type) ;
                # on valide via test_reference_type_not_consumed.
            ],
        )
        # Ce test est vide intentionnellement โ€” couvert par le
        # suivant.
        assert spec.validate((ArtifactType.IMAGE,)) == []

    def test_reference_type_not_consumed(self) -> None:
        # Le module ne consomme pas IMAGE, mais on dรฉclare
        # inputs_from[IMAGE] = "ocr" โ€” erreur.
        spec = PipelineSpec(
            name="bad",
            steps=[
                PipelineStep("ocr", MockOCR("x")),
                PipelineStep(
                    "fix",
                    TextFixer("fix", {}),
                    inputs_from={
                        ArtifactType.IMAGE: "ocr",  # IMAGE n'est pas dans input_types de TextFixer
                    },
                ),
            ],
        )
        problems = spec.validate((ArtifactType.IMAGE,))
        assert any("ne consomme pas" in p for p in problems)


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 3. DAG branchant : fork explicite
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€


class TestForkBranch:
    def test_two_fixers_from_same_ocr(self) -> None:
        """OCR โ†’ fix_a (depuis OCR), OCR โ†’ fix_b (depuis OCR).

        Sans inputs_from, fix_b consommerait la sortie de fix_a
        (chaรฎne).  Avec inputs_from explicite, chaque fixer part de
        l'OCR original.
        """
        doc = _make_doc("hello world")
        # OCR produit du texte fautif corrigible de plusieurs
        # faรงons :
        # - fix_a corrige "hellb" โ†’ "hello"
        # - fix_b corrige "wlrd" โ†’ "world"
        # Si fix_b avait reรงu la sortie de fix_a (qui n'a corrigรฉ
        # que "hellb"), il aurait pu corriger "wlrd" en "world"
        # mais "hellb" reste incorrect.  Avec le DAG branchant,
        # fix_a et fix_b appliquent chacun leur correction sur
        # l'OCR original, indรฉpendamment.
        spec = PipelineSpec(
            name="fork",
            steps=[
                PipelineStep("ocr", MockOCR("hellb wlrd")),
                PipelineStep(
                    "fix_a",
                    TextFixer("fix_a", {"hellb": "hello"}),
                    inputs_from={ArtifactType.TEXT: "ocr"},
                ),
                PipelineStep(
                    "fix_b",
                    TextFixer("fix_b", {"wlrd": "world"}),
                    inputs_from={ArtifactType.TEXT: "ocr"},
                ),
            ],
        )
        result = PipelineRunner.run(
            spec, doc, {ArtifactType.IMAGE: "/tmp/x.png"},
        )
        assert result.succeeded
        # fix_a a corrigรฉ "hellb" โ†’ "hello wlrd" (CER รฉlevรฉ)
        # fix_b a corrigรฉ "wlrd" โ†’ "hellb world" (CER รฉlevรฉ)
        # Aucun ne ramรจne ร  "hello world", mais on vรฉrifie que
        # chacun a bien dรฉmarrรฉ depuis l'OCR original.
        cer_a = result.steps[1].junction_metrics["text"]["cer"]
        cer_b = result.steps[2].junction_metrics["text"]["cer"]
        # Les deux CER sont strictement > 0 (puisque chaque fixer
        # ne corrige qu'une partie du texte fautif)
        assert cer_a > 0.0
        assert cer_b > 0.0

    def test_fork_vs_chain_diverge(self) -> None:
        """Fork explicite vs chain implicite produisent des rรฉsultats
        diffรฉrents quand les transformations ne sont pas commutatives."""
        doc = _make_doc("hello world")
        # chain : ocr โ†’ doubler โ†’ fixer (le fixer voit le texte doublรฉ)
        chain_spec = PipelineSpec(
            name="chain",
            steps=[
                PipelineStep("ocr", MockOCR("hello wrold")),
                PipelineStep("doubler", TextDoubler()),
                PipelineStep(
                    "fix",
                    TextFixer("fix", {"wrold": "world"}),
                ),
            ],
        )
        # fork : doubler depuis ocr ; fix DEPUIS ocr (pas depuis
        # doubler) โ†’ fix corrige sans le doubling
        fork_spec = PipelineSpec(
            name="fork",
            steps=[
                PipelineStep("ocr", MockOCR("hello wrold")),
                PipelineStep(
                    "doubler", TextDoubler(),
                    inputs_from={ArtifactType.TEXT: "ocr"},
                ),
                PipelineStep(
                    "fix",
                    TextFixer("fix", {"wrold": "world"}),
                    inputs_from={ArtifactType.TEXT: "ocr"},
                ),
            ],
        )
        chain_result = PipelineRunner.run(
            chain_spec, doc, {ArtifactType.IMAGE: "/tmp/x.png"},
        )
        fork_result = PipelineRunner.run(
            fork_spec, doc, {ArtifactType.IMAGE: "/tmp/x.png"},
        )
        # En chain, le fixer voit le texte doublรฉ "hello wroldhello wrold"
        # โ†’ "hello worldhello world" โ€” CER รฉlevรฉ vs GT "hello world".
        # En fork, le fixer voit l'OCR original "hello wrold" โ†’
        # "hello world" โ€” CER 0 vs GT "hello world".
        chain_fix_cer = chain_result.steps[2].junction_metrics["text"]["cer"]
        fork_fix_cer = fork_result.steps[2].junction_metrics["text"]["cer"]
        assert fork_fix_cer == 0.0
        assert chain_fix_cer > 0.0


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 4. Rรฉfรฉrence vers une รฉtape qui a รฉchouรฉ
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€


class TestReferenceToFailedStep:
    def test_inputs_from_failed_step_propagates_missing(self) -> None:
        doc = _make_doc("hello world")
        spec = PipelineSpec(
            name="fail_then_ref",
            steps=[
                PipelineStep("ocr", MockOCR("hello world")),
                PipelineStep(
                    "fail", AlwaysFails(),
                    inputs_from={ArtifactType.TEXT: "ocr"},
                ),
                # Cette รฉtape rรฉfรฉrence "fail" qui a รฉchouรฉ
                PipelineStep(
                    "after_fail",
                    TextFixer("after", {}),
                    inputs_from={ArtifactType.TEXT: "fail"},
                ),
            ],
        )
        result = PipelineRunner.run(
            spec, doc, {ArtifactType.IMAGE: "/tmp/x.png"},
        )
        # ocr OK, fail รฉchoue, after_fail signale entrรฉe manquante
        assert result.steps[0].error is None
        assert result.steps[1].error is not None
        assert "RuntimeError" in result.steps[1].error
        assert result.steps[2].error is not None
        assert "@fail" in result.steps[2].error


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 5. Rรฉtrocompat : sans inputs_from, comportement Sprint 63
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€


class TestBackwardsCompat:
    def test_chain_without_inputs_from_still_works(self) -> None:
        doc = _make_doc("hello world")
        spec = PipelineSpec(
            name="legacy",
            steps=[
                PipelineStep("ocr", MockOCR("hello wrold")),
                PipelineStep(
                    "fix",
                    TextFixer("fix", {"wrold": "world"}),
                ),
            ],
        )
        result = PipelineRunner.run(
            spec, doc, {ArtifactType.IMAGE: "/tmp/x.png"},
        )
        assert result.succeeded
        cer = result.steps[1].junction_metrics["text"]["cer"]
        assert cer == 0.0

    def test_junction_metrics_for_returns_last_text(self) -> None:
        doc = _make_doc("hello world")
        spec = PipelineSpec(
            name="fork",
            steps=[
                PipelineStep("ocr", MockOCR("hello world")),
                PipelineStep(
                    "fix",
                    TextFixer("fix", {}),
                    inputs_from={ArtifactType.TEXT: "ocr"},
                ),
            ],
        )
        result = PipelineRunner.run(
            spec, doc, {ArtifactType.IMAGE: "/tmp/x.png"},
        )
        # La derniรจre รฉtape rรฉussie ayant produit TEXT est "fix"
        final = result.junction_metrics_for(ArtifactType.TEXT)
        assert final is not None
        assert final["cer"] == 0.0