Spaces:
Running on Zero
Running on Zero
| """ | |
| schema.py — Registry-parametric schema code generation. | |
| Generates three representations of a *registry* (a `dict[str, SlotSpec]`): | |
| build_model_from_registry(name, registry) → Pydantic model class | |
| build_json_schema(model) → JSON Schema dict | |
| build_gbnf_from_registry(registry) → GBNF grammar string | |
| The caption schema is the canonical instance, exposed under stable names: | |
| Caption — Pydantic model (validation / parsing) | |
| CAPTION_JSON_SCHEMA — JSON Schema dict (Anthropic API, outlines, etc.) | |
| CAPTION_GRAMMAR_GBNF — GBNF grammar string (xgrammar) | |
| The vision subpackage reuses the SAME generators per task category (each | |
| category owns a small `dict[str, SlotSpec]`), so numbers, bounding boxes, and | |
| nested objects are described with the same machinery — see | |
| `qwen_test_runner/vision/`. | |
| All caption artifacts are generated at import time from `registry.SLOT_REGISTRY`. | |
| To add or modify a caption slot, edit `registry.py` only — this file stays | |
| untouched. | |
| """ | |
| from __future__ import annotations | |
| from typing import Any, Literal, Mapping, Optional | |
| from pydantic import BaseModel, Field, create_model | |
| from .registry import SLOT_REGISTRY, SlotSpec, SubjectValue | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Pydantic model — built dynamically from any registry. | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| _OBJECT_MODEL_CACHE: dict[SlotSpec, type[BaseModel]] = {} | |
| def _object_model_name(spec: SlotSpec) -> str: | |
| return "".join(part.capitalize() for part in spec.name.split("_")) + "Obj" | |
| def _build_object_model(spec: SlotSpec) -> type[BaseModel]: | |
| """Build (and cache) a Pydantic model from a spec's `nested_fields`.""" | |
| cached = _OBJECT_MODEL_CACHE.get(spec) | |
| if cached is not None: | |
| return cached | |
| fields: dict[str, Any] = {} | |
| for f in spec.nested_fields: | |
| fields[f.name] = (_python_type_for_slot(f), _field_for_slot(f)) | |
| model = create_model(_object_model_name(spec), **fields) | |
| _OBJECT_MODEL_CACHE[spec] = model | |
| return model | |
| def _item_type_for_slot(spec: SlotSpec) -> Any: | |
| """Python type of a single value of this slot (before list/Optional wrapping).""" | |
| if spec.nested_fields: | |
| return _build_object_model(spec) | |
| if spec.nested_model is not None: | |
| return spec.nested_model | |
| if spec.vocabulary == "closed": | |
| # Literal[("a", "b", "c")] parses identically to Literal["a", "b", "c"]. | |
| return Literal[spec.closed_values] | |
| vk = spec.value_kind | |
| if vk == "number": | |
| return float | |
| if vk == "integer": | |
| return int | |
| if vk in ("bbox", "point"): | |
| return list[float] | |
| return str | |
| def _python_type_for_slot(spec: SlotSpec) -> Any: | |
| """Compute the Python type annotation for a slot's value. | |
| List cardinality wraps the item type in list[...]. | |
| Single + optional wraps in Optional[...]. | |
| """ | |
| item_type = _item_type_for_slot(spec) | |
| if spec.cardinality == "list": | |
| return list[item_type] | |
| if spec.optional: | |
| return Optional[item_type] | |
| return item_type | |
| def _default_for_slot(spec: SlotSpec) -> Any: | |
| if spec.cardinality == "list": | |
| return [] # default_factory handled by Field below | |
| if spec.optional: | |
| return None | |
| # Required single value with no default. For closed vocab, default to | |
| # the last value (usually "unknown") so partial outputs don't blow up. | |
| if spec.vocabulary == "closed": | |
| return spec.closed_values[-1] | |
| return ... # required, no default | |
| def _field_for_slot(spec: SlotSpec): | |
| """Construct a Pydantic Field with the right constraints for this slot.""" | |
| kwargs: dict[str, Any] = {} | |
| if spec.cardinality == "list": | |
| kwargs["default_factory"] = list | |
| kwargs["max_length"] = spec.max_items | |
| return Field(**kwargs) | |
| default = _default_for_slot(spec) | |
| # Fixed-length numeric arrays (bbox/point): exactly 4 / 2 elements. | |
| if spec.value_kind in ("bbox", "point"): | |
| n = 4 if spec.value_kind == "bbox" else 2 | |
| if default is ...: | |
| return Field(..., min_length=n, max_length=n) | |
| return Field(default=default, min_length=n, max_length=n) | |
| # Scalar numerics, optionally bounded. | |
| if spec.value_kind in ("number", "integer"): | |
| rng: dict[str, Any] = {} | |
| if spec.number_range is not None: | |
| rng["ge"] = spec.number_range[0] | |
| rng["le"] = spec.number_range[1] | |
| if default is ...: | |
| return Field(..., **rng) | |
| return Field(default=default, **rng) | |
| # Strings / enums / nested objects. | |
| if default is ...: | |
| # Only plain open strings get a length cap; nested models / enums don't. | |
| if spec.vocabulary == "open" and spec.nested_model is None and not spec.nested_fields: | |
| return Field(..., max_length=spec.max_str_length) | |
| return Field(...) | |
| kwargs["default"] = default | |
| if ( | |
| spec.vocabulary == "open" | |
| and spec.nested_model is None | |
| and not spec.nested_fields | |
| and spec.value_kind == "string" | |
| ): | |
| kwargs["max_length"] = spec.max_str_length | |
| return Field(**kwargs) | |
| def build_model_from_registry(model_name: str, registry: Mapping[str, SlotSpec]) -> type[BaseModel]: | |
| """Build a Pydantic model with one field per registry entry.""" | |
| fields: dict[str, Any] = {} | |
| for name, spec in registry.items(): | |
| fields[name] = (_python_type_for_slot(spec), _field_for_slot(spec)) | |
| return create_model(model_name, **fields) | |
| def build_json_schema(model: type[BaseModel]) -> dict: | |
| """JSON Schema for a generated model (thin wrapper for symmetry).""" | |
| return model.model_json_schema() | |
| Caption = build_model_from_registry("Caption", SLOT_REGISTRY) | |
| # Re-export SubjectValue under the old name "Subject" for callers that | |
| # imported it from schema previously. | |
| Subject = SubjectValue | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # JSON Schema — derived from the Pydantic model. | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| CAPTION_JSON_SCHEMA: dict = build_json_schema(Caption) | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # GBNF grammar — built from the registry. Independent of pydantic. | |
| # | |
| # xgrammar's auto-converter from JSON schema sometimes adds unwanted slack | |
| # (e.g. permissive whitespace patterns that hurt parse rates). Generating GBNF | |
| # by hand from the registry gives tighter control and stays consistent with | |
| # the Pydantic model. | |
| # | |
| # The four base primitives (str_array, string, char, ws) are always emitted, as | |
| # in v0.2. Numeric primitives (number, bbox4, …) are emitted ONLY when a slot | |
| # references them, so the caption grammar is byte-for-byte the v0.2 grammar. | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Primitive rule definitions, emitted on demand. | |
| _PRIMITIVE_DEFS: dict[str, str] = { | |
| "uint": 'uint ::= "0" | [1-9] [0-9]*', | |
| "frac": 'frac ::= "." [0-9]+', | |
| "exp": 'exp ::= ("e" | "E") ("+" | "-")? [0-9]+', | |
| "integer": 'integer ::= "-"? uint', | |
| "number": 'number ::= "-"? uint frac? exp?', | |
| "num_array": 'num_array ::= "[" ws "]" | "[" ws number (ws "," ws number)* ws "]"', | |
| "bbox4": 'bbox4 ::= "[" ws number ws "," ws number ws "," ws number ws "," ws number ws "]"', | |
| "point2": 'point2 ::= "[" ws number ws "," ws number ws "]"', | |
| } | |
| # Transitive dependencies between numeric primitives (the four base primitives | |
| # are always present, so they are never listed here). | |
| _PRIMITIVE_DEPS: dict[str, set[str]] = { | |
| "uint": set(), | |
| "frac": set(), | |
| "exp": set(), | |
| "integer": {"uint"}, | |
| "number": {"uint", "frac", "exp"}, | |
| "num_array": {"number"}, | |
| "bbox4": {"number"}, | |
| "point2": {"number"}, | |
| } | |
| # Stable emission order so the grammar regenerates deterministically. | |
| _PRIMITIVE_ORDER = ["integer", "number", "num_array", "bbox4", "point2", "uint", "frac", "exp"] | |
| def _gbnf_string_alternation(values: tuple[str, ...]) -> str: | |
| """Emit `"\"a\"" | "\"b\"" | ...` for a closed enum.""" | |
| return " | ".join(f'"\\"{v}\\""' for v in values) | |
| def _resolve_primitive_deps(deps: set[str]) -> set[str]: | |
| """Expand a set of primitive names with all transitive dependencies.""" | |
| out: set[str] = set() | |
| stack = list(deps) | |
| while stack: | |
| d = stack.pop() | |
| if d in out: | |
| continue | |
| out.add(d) | |
| stack.extend(_PRIMITIVE_DEPS.get(d, set())) | |
| return out | |
| def _gbnf_object_rule(spec: SlotSpec) -> tuple[str, list[str], set[str]]: | |
| """Build the GBNF object rule for a spec's nested_fields. Returns | |
| (object_rule_name, extra_rules, primitive_deps).""" | |
| rule_name = f"obj_{spec.name}" | |
| parts: list[str] = ['"{"', "ws"] | |
| extras: list[str] = [] | |
| deps: set[str] = set() | |
| for i, f in enumerate(spec.nested_fields): | |
| if i > 0: | |
| parts += ['","', "ws"] | |
| frhs, fextras, fdeps = _gbnf_slot_value_rule(f) | |
| extras += fextras | |
| deps |= fdeps | |
| # Wrap the field value in parens so alternations (e.g. "null" | bbox4) | |
| # compose correctly inside the object. | |
| parts += [f'"\\"{f.name}\\":"', "ws", f"( {frhs} )", "ws"] | |
| parts.append('"}"') | |
| extras.append(f"{rule_name} ::= " + " ".join(parts)) | |
| return rule_name, extras, deps | |
| def _gbnf_slot_value_rule(spec: SlotSpec) -> tuple[str, list[str], set[str]]: | |
| """Return (right-hand-side, extra_rules, primitive_deps) for this slot's value. | |
| The RHS is what appears after `slot_<name> ::=`. It is either a rule name or | |
| a small alternation (e.g. `"null" | number`). Extra rules are helper rules | |
| this slot needs; primitive_deps names numeric primitives to emit globally. | |
| """ | |
| extras: list[str] = [] | |
| deps: set[str] = set() | |
| if spec.cardinality == "list": | |
| if spec.nested_model is SubjectValue: | |
| # SubjectValue is the caption's one hand-written nested type; keep the | |
| # exact v2 rules so the caption grammar is unchanged. | |
| extras.append( | |
| 'subject ::= "{" ws "\\"name\\":" ws string ws "," ws ' | |
| '"\\"attributes\\":" ws str_array ws "}"' | |
| ) | |
| extras.append( | |
| 'subject_list ::= "[" ws "]" | ' | |
| '"[" ws subject (ws "," ws subject)* ws "]"' | |
| ) | |
| return "subject_list", extras, deps | |
| if spec.nested_fields: | |
| obj_name, obj_extras, obj_deps = _gbnf_object_rule(spec) | |
| extras += obj_extras | |
| deps |= obj_deps | |
| list_name = f"{spec.name}_list" | |
| extras.append( | |
| f'{list_name} ::= "[" ws "]" | ' | |
| f'"[" ws {obj_name} (ws "," ws {obj_name})* ws "]"' | |
| ) | |
| return list_name, extras, deps | |
| if spec.value_kind in ("number", "integer"): | |
| deps.add("num_array") | |
| return "num_array", extras, deps | |
| # Primitive open-vocab list — array of strings | |
| return "str_array", extras, deps | |
| # Single value | |
| if spec.nested_fields: | |
| # A single nested object (e.g. subject_fixation.primary_subject). Without | |
| # this, the grammar would fall through to the string rule and force the | |
| # object to serialize as a string — breaking constrained decoding. | |
| obj_name, obj_extras, obj_deps = _gbnf_object_rule(spec) | |
| extras += obj_extras | |
| deps |= obj_deps | |
| if spec.optional: | |
| return f'"null" | {obj_name}', extras, deps | |
| return obj_name, extras, deps | |
| if spec.vocabulary == "closed": | |
| alts = _gbnf_string_alternation(spec.closed_values) | |
| rule_name = f"closed_{spec.name}" | |
| extras.append(f"{rule_name} ::= {alts}") | |
| return rule_name, extras, deps | |
| if spec.value_kind == "bbox": | |
| deps.add("bbox4") | |
| base = "bbox4" | |
| elif spec.value_kind == "point": | |
| deps.add("point2") | |
| base = "point2" | |
| elif spec.value_kind == "number": | |
| deps.add("number") | |
| base = "number" | |
| elif spec.value_kind == "integer": | |
| deps.add("integer") | |
| base = "integer" | |
| else: | |
| base = "string" | |
| # Optional single → allow null literal. | |
| if spec.optional: | |
| return f'"null" | {base}', extras, deps | |
| return base, extras, deps | |
| def build_gbnf_from_registry(registry: Mapping[str, SlotSpec], root_name: str = "root") -> str: | |
| """Generate a GBNF grammar that produces JSON conforming to `registry`.""" | |
| slot_rules: list[str] = [] | |
| helper_rules: list[str] = [] | |
| helper_seen: set[str] = set() | |
| deps: set[str] = set() | |
| for name, spec in registry.items(): | |
| rhs, extras, sdeps = _gbnf_slot_value_rule(spec) | |
| slot_rules.append(f"slot_{name} ::= {rhs}") | |
| deps |= sdeps | |
| for r in extras: | |
| head = r.split("::=", 1)[0].strip() | |
| if head not in helper_seen: | |
| helper_rules.append(r) | |
| helper_seen.add(head) | |
| # Root rule: opening brace, slot1, comma, slot2, ..., closing brace. | |
| parts: list[str] = ['"{"', "ws"] | |
| for i, name in enumerate(registry.keys()): | |
| if i > 0: | |
| parts += ['","', "ws"] | |
| parts += [f'"\\"{name}\\":"', "ws", f"slot_{name}", "ws"] | |
| parts.append('"}"') | |
| root_rule = f"{root_name} ::= " + " ".join(parts) | |
| # Base primitives — always present (str_array is needed by open-vocab lists | |
| # and SubjectValue.attributes). | |
| common = [ | |
| 'str_array ::= "[" ws "]" | "[" ws string (ws "," ws string)* ws "]"', | |
| 'string ::= "\\"" char* "\\""', | |
| 'char ::= [^"\\\\] | "\\\\" ["\\\\/bfnrt]', | |
| 'ws ::= [ \\t\\n]*', | |
| ] | |
| # Numeric primitives — only those actually referenced (keeps caption grammar | |
| # identical to v2 and vision grammars minimal). | |
| resolved = _resolve_primitive_deps(deps) | |
| numeric = [_PRIMITIVE_DEFS[k] for k in _PRIMITIVE_ORDER if k in resolved] | |
| return "\n".join([root_rule] + slot_rules + helper_rules + common + numeric) | |
| def build_gbnf_grammar() -> str: | |
| """Generate the caption GBNF grammar (back-compat wrapper).""" | |
| return build_gbnf_from_registry(SLOT_REGISTRY) | |
| CAPTION_GRAMMAR_GBNF: str = build_gbnf_grammar() | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Smoke test — `python -m qwen_test_runner.schema` validates the three reps. | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| def _smoke_test() -> None: | |
| example = Caption( | |
| subjects=[Subject(name="dog", attributes=["golden"])], | |
| actions=["catching"], | |
| setting="outdoor", | |
| style="photorealistic", | |
| mood="energetic", | |
| ) | |
| as_dict = example.model_dump() | |
| rebuilt = Caption.model_validate(as_dict) | |
| assert rebuilt == example, "pydantic round-trip failed" | |
| as_json = example.model_dump_json() | |
| reparsed = Caption.model_validate_json(as_json) | |
| assert reparsed == example, "JSON round-trip failed" | |
| schema = CAPTION_JSON_SCHEMA | |
| assert "properties" in schema | |
| assert set(schema["properties"].keys()) == set(SLOT_REGISTRY.keys()) | |
| g = CAPTION_GRAMMAR_GBNF | |
| for slot in SLOT_REGISTRY: | |
| assert f'\\"{slot}\\"' in g, f"GBNF missing slot {slot}" | |
| print("schema.py smoke test: OK") | |
| print(f" slots: {list(SLOT_REGISTRY.keys())}") | |
| print(f" example JSON length: {len(as_json)}") | |
| print(f" JSON Schema fields: {list(schema['properties'].keys())}") | |
| print(f" GBNF length: {len(g)} chars") | |
| if __name__ == "__main__": | |
| _smoke_test() | |