"""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)