Spaces:
Running on Zero
Running on Zero
File size: 10,774 Bytes
fed954e | 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 | """
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
|