AbstractPhil's picture
Deploy: 12-task vision extraction + fusion ZeroGPU showcase
fed954e verified
Raw
History Blame Contribute Delete
10.8 kB
"""
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.
# ──────────────────────────────────────────────────────────────────────────────
@dataclass(frozen=True)
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