Spaces:
Running on Zero
Running on Zero
| """ | |
| registry.py — The slot registry. | |
| This is the source of truth for the caption schema. Every slot the system | |
| knows about lives here as a SlotSpec entry. The Pydantic Caption model, the | |
| JSON Schema export, the GBNF grammar, and the evaluator's grounding rules | |
| are all derived from this registry at import time. | |
| Adding a slot is one dict entry. Adding a category is one Literal expansion. | |
| No code outside this file should hardcode slot names or category logic. | |
| Slot taxonomy (the three categories that came out of the baseline analysis): | |
| - descriptive : grounded in the input caption. Hallucination forbidden. | |
| Examples: subjects, actions, setting. | |
| - aesthetic : how the scene should look. Often empty in input; | |
| legitimate inference (or null) in enhancement mode. | |
| Examples: style, lighting, palette. | |
| - semantic : interpretive meaning. Inferential by definition. | |
| Examples: mood, implication, narrative_function. | |
| Groundedness rules (drive the evaluator): | |
| - must_ground : every leaf MUST trace to the input caption. | |
| - may_infer : leaf may be grounded OR inferred; both are acceptable. | |
| - derived_only : leaf is expected to be inferred. Grounding check skipped. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| from typing import Literal, Optional, Type | |
| from pydantic import BaseModel, Field | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Slot-level enums. Adding a value here is a registry-only change; no | |
| # code outside this file matches on these strings directly (the helpers below | |
| # encapsulate all behavior). | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| Category = Literal["descriptive", "aesthetic", "semantic"] | |
| Cardinality = Literal["single", "list"] | |
| Vocabulary = Literal["closed", "open"] | |
| Groundedness = Literal["must_ground", "may_infer", "derived_only"] | |
| # value_kind selects the leaf's primitive type for code generation. "string" is | |
| # the caption default (every existing slot). The numeric kinds exist so the SAME | |
| # registry→Pydantic→JSON-Schema→GBNF machinery can describe vision outputs | |
| # (bounding boxes, confidences, depths) without a second codegen path. | |
| # string → str | |
| # number → float (optionally bounded via number_range) | |
| # integer → int | |
| # bbox → list[float] of length 4 (x1,y1,x2,y2 or x,y,w,h — see coords.py) | |
| # point → list[float] of length 2 | |
| ValueKind = Literal["string", "number", "integer", "bbox", "point"] | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Nested value models. Used by slots whose value is structured (e.g. subjects | |
| # have a name and a list of attributes). New nested types go here and are | |
| # referenced from the SlotSpec via `nested_model=`. | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| class SubjectValue(BaseModel): | |
| """A single entity in the caption.""" | |
| name: str = Field(..., min_length=1, max_length=64) | |
| # No max_length on attributes: rich captions (JoyCaption prose, booru tag | |
| # strings) legitimately carry >8 per subject, and the cap was rejecting 44% | |
| # of otherwise-valid structs in the 100-row bench (2026-07). | |
| attributes: list[str] = Field(default_factory=list) | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # SlotSpec — the unit of the registry. | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| class SlotSpec: | |
| """Declarative description of one schema slot. | |
| The structural axes (cardinality, vocabulary, value_kind, nested_fields, | |
| nested_model) are what drive code generation in schema.py — these apply | |
| equally to caption slots and to vision-task fields. The caption-only axes | |
| (category, groundedness) drive the text evaluator and default to neutral | |
| values so vision per-category registries can omit them. | |
| cardinality — single value vs list | |
| vocabulary — open (any string) vs closed (one of `closed_values`) | |
| value_kind — leaf primitive type (string / number / integer / bbox / point) | |
| nested_model — BaseModel subclass for a structured value (caption: SubjectValue) | |
| nested_fields — declarative nested-object fields; the generalized form of | |
| nested_model — schema.py builds both the Pydantic model and | |
| the GBNF object rule recursively from these | |
| category — taxonomy bucket (caption prompts); ignored by vision | |
| groundedness — strict / soft / never (text evaluator); ignored by vision | |
| optional — may the model emit null/[] when empty | |
| number_range — (min, max) bound for numeric value_kinds (Pydantic ge/le) | |
| """ | |
| name: str | |
| cardinality: Cardinality | |
| vocabulary: Vocabulary | |
| category: Category = "descriptive" | |
| groundedness: Groundedness = "may_infer" | |
| value_kind: ValueKind = "string" | |
| closed_values: tuple[str, ...] = () | |
| nested_model: Optional[Type[BaseModel]] = None | |
| nested_fields: tuple["SlotSpec", ...] = () | |
| optional: bool = True | |
| max_items: int = 8 # only for cardinality == "list" | |
| max_str_length: int = 64 # for open-vocab strings | |
| number_range: Optional[tuple[float, float]] = None | |
| def __post_init__(self): | |
| # Lightweight validation — catch registry mistakes at import time | |
| if self.vocabulary == "closed" and not self.closed_values: | |
| raise ValueError(f"slot {self.name!r}: closed vocab requires closed_values") | |
| if self.vocabulary == "open" and self.closed_values: | |
| raise ValueError(f"slot {self.name!r}: open vocab cannot have closed_values") | |
| if self.nested_model is not None and self.vocabulary == "closed": | |
| raise ValueError(f"slot {self.name!r}: nested_model is incompatible with closed vocab") | |
| if self.nested_fields and self.nested_model is not None: | |
| raise ValueError(f"slot {self.name!r}: nested_fields and nested_model are mutually exclusive") | |
| if self.nested_fields and self.vocabulary == "closed": | |
| raise ValueError(f"slot {self.name!r}: nested_fields is incompatible with closed vocab") | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # THE REGISTRY. | |
| # | |
| # Starter set: 5 slots that exercise all three categories and both | |
| # groundedness extremes. Adding a slot is a single entry below. | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| SLOT_REGISTRY: dict[str, SlotSpec] = { | |
| "subjects": SlotSpec( | |
| name="subjects", | |
| category="descriptive", | |
| cardinality="list", | |
| vocabulary="open", | |
| groundedness="must_ground", | |
| nested_model=SubjectValue, | |
| max_items=8, | |
| ), | |
| "actions": SlotSpec( | |
| name="actions", | |
| category="descriptive", | |
| cardinality="list", | |
| vocabulary="open", | |
| groundedness="must_ground", | |
| max_items=8, | |
| ), | |
| "setting": SlotSpec( | |
| name="setting", | |
| category="descriptive", | |
| cardinality="single", | |
| vocabulary="closed", | |
| # `may_infer` because Qwen reliably guesses indoor/outdoor from cues | |
| # even when the caption doesn't say. The grammar pins the value to | |
| # the enum anyway. | |
| groundedness="may_infer", | |
| closed_values=("indoor", "outdoor", "unknown"), | |
| optional=False, # always required; the enum includes "unknown" as escape | |
| ), | |
| "style": SlotSpec( | |
| name="style", | |
| category="aesthetic", | |
| cardinality="single", | |
| vocabulary="open", | |
| groundedness="may_infer", | |
| ), | |
| "mood": SlotSpec( | |
| name="mood", | |
| category="semantic", | |
| cardinality="single", | |
| vocabulary="open", | |
| # Baseline finding: mood is 73% of all hallucinations under the old | |
| # rule. Reclassifying it as derived_only stops penalizing the model | |
| # for inferring; it's correct behavior now, not error. | |
| groundedness="derived_only", | |
| ), | |
| } | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Query helpers. Use these instead of poking SLOT_REGISTRY directly so behavior | |
| # stays centralized. | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| def slots_by_category(category: Category) -> list[SlotSpec]: | |
| return [s for s in SLOT_REGISTRY.values() if s.category == category] | |
| def slot_names() -> list[str]: | |
| """Slot names in registry-declaration order. JSON output uses this order.""" | |
| return list(SLOT_REGISTRY.keys()) | |
| def get_slot(name: str) -> SlotSpec: | |
| if name not in SLOT_REGISTRY: | |
| raise KeyError(f"unknown slot: {name!r}") | |
| return SLOT_REGISTRY[name] | |
| # Set of closed-vocab values across all slots — used by the evaluator as the | |
| # "always grounded" allowlist for the `may_infer` closed-vocab case. | |
| def all_closed_vocab() -> set[str]: | |
| out: set[str] = set() | |
| for s in SLOT_REGISTRY.values(): | |
| out.update(s.closed_values) | |
| return out | |