File size: 4,634 Bytes
97b64b9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Deterministic authorized-signer identity registry.

The base generation pipeline has no notion of "the correct signer for this
box" -- it just draws a random genuine signature for whichever box was
picked. Every verification case type (see :mod:`src.verification.case_types`)
hinges on comparing an *authorized* identity against an *actual* one, so this
module derives that ground truth purely from ``(master_seed, template_id,
box_id)`` via :func:`~src.utils.random_utils.derive_seed` -- no persisted
state, fully reproducible, consistent with the rest of the pipeline's
seeding philosophy.
"""

from __future__ import annotations

import random
import re

from src.signatures.signature_dataset import SignatureDataset
from src.templates.template_loader import TemplateLoader
from src.utils.names import generate_name
from src.utils.random_utils import derive_seed

# Signer groups auto-derived by SignatureDataset (CEDAR filenames, or no
# metadata.csv identity override) look like "signer_0014" and carry no real
# name -- those get a generated display name. Groups sourced from
# metadata.csv (e.g. "Priya Sharma") already *are* a real name.
_AUTO_GROUP_PATTERN = re.compile(r"^signer_\d+$")


class SignerIdentityRegistry:
    """Resolves ground-truth authorized signer identities for template boxes.

    Example:
        >>> registry = SignerIdentityRegistry(template_loader, signature_dataset, master_seed=1234)
        >>> registry.authorized_signer_group("bank_doc_03", "sig1")
        'signer_0014'
    """

    def __init__(self, template_loader: TemplateLoader, signature_dataset: SignatureDataset, master_seed: int) -> None:
        self.template_loader = template_loader
        self.signature_dataset = signature_dataset
        self.master_seed = master_seed
        self._signer_groups = signature_dataset.signer_groups()

    def authorized_signer_group(self, template_id: str, box_id: str) -> str:
        """Return the deterministic ground-truth authorized signer for one (template, box)."""
        seed = derive_seed(self.master_seed, template_id, box_id, "authorized_signer")
        idx = seed % len(self._signer_groups)
        return self._signer_groups[idx]

    def foreign_authorized(self, exclude_template_id: str) -> tuple[str, str, str]:
        """Return ``(signer_group, template_id, box_id)`` for a box on a *different* template.

        Used for the "signer from a different form entirely" scenario: the
        identity is a legitimately authorized signer, just not for this
        document.
        """
        templates = self.template_loader.discover()
        candidate_ids = sorted(tid for tid in templates if tid != exclude_template_id)
        if not candidate_ids:
            # Only one template discovered; fall back to a different box on the same one.
            candidate_ids = [exclude_template_id]

        seed = derive_seed(self.master_seed, exclude_template_id, "foreign_template")
        foreign_template_id = candidate_ids[seed % len(candidate_ids)]

        boxes = templates[foreign_template_id].annotation.pages[0].signature_boxes
        box_seed = derive_seed(self.master_seed, exclude_template_id, foreign_template_id, "foreign_box")
        box = boxes[box_seed % len(boxes)]

        return self.authorized_signer_group(foreign_template_id, box.id), foreign_template_id, box.id

    def random_wrong_signer_group(self, rng: random.Random, exclude_group: str) -> str:
        """Return a uniformly random signer group other than ``exclude_group``."""
        pool = [g for g in self._signer_groups if g != exclude_group]
        return rng.choice(pool)

    def display_name(self, signer_group: str) -> str:
        """Return a stable, human-readable display name for a signer group.

        Groups sourced from ``dataset/metadata.csv`` (e.g. ``"Priya Sharma"``)
        already carry a real name and are returned as-is. Anonymous
        auto-derived groups (e.g. ``"signer_0014"``, from the bundled CEDAR
        dataset) get a generated name, stable across an entire run so a
        benchmark built from many samples reads as consistent identities
        rather than a fresh random name every time.
        """
        if not _AUTO_GROUP_PATTERN.match(signer_group):
            return signer_group
        seed = derive_seed(self.master_seed, signer_group, "display_name")
        return generate_name(random.Random(seed))

    def script_for(self, signer_group: str) -> str:
        """Return the handwriting script (e.g. ``"latin"``, ``"hindi"``, ``"bengali"``) for a signer group."""
        return self.signature_dataset.script_for_group(signer_group)