File size: 8,091 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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
"""
fuse_schema.py — the FusedScene pydantic schema (fusion tier).

Hand-written nested models (the SubjectValue precedent — the SlotSpec codegen in
schema.py handles one nesting level; FusedScene needs three: entities ->
attributes -> ownership). Deliberately NOT a registered VisionTaskSpec: FusedScene
is a deterministically-produced dataset artifact, not a VLM probe — it needs no
GBNF, no system prompt, no scorer. If a VLM is ever trained to EMIT FusedScene,
register a flattened variant then.

Versioned from day one: this module is a second schema authority next to the
registry, so every instance stamps `fused_scene_version`.

Coordinate policy: all geometry emitted in the declared `coord_space`
(NORM_0_1000 by default, via specialists.box_to_space/poly_to_space); the one
pixel-unit field is `image_size`, documented as such. Scorers convert via
coords.to_canonical.
"""

from __future__ import annotations

from typing import Optional

from pydantic import BaseModel, Field

FUSED_SCENE_VERSION = "1.0"

# Caps (data, referenced by fuse.py — never hardcoded at call sites)
MAX_ENTITIES = 12            # kept by saliency rank
MAX_RELATION_ENTITIES = 8    # pairwise relations among the top-K entities
MASK_POLY_MAX_POINTS = 64    # outline_polygon(max_points=...) per entity

# Basin reasons (closed set, tested)
BASIN_REASONS = ("low_margin", "no_grounding_multi_entity", "ambiguous_binding",
                 "abstract_unbound")

# Ownership methods (closed set, tested)
OWNERSHIP_METHODS = ("single_entity", "mask_containment", "box_containment",
                     "caption_binding")


class GridPosition(BaseModel):
    grid: str                                   # "{upper|middle|lower} {left|center|right}"
    offset_from_center: list[float]             # (centroid-center)/half-extents, in [-1,1]


class DepthInfo(BaseModel):
    nearness: float                             # continuous [0,1], bigger = nearer
    rank: int                                   # 1 = nearest


class SaliencyInfo(BaseModel):
    score: float
    rank: int                                   # 1 = most salient


class MaskInfo(BaseModel):
    polygon: list[float] = Field(default_factory=list)   # flat x,y interleaved, task space
    quality: Optional[float] = None             # SAM predicted-IoU (retained signal)


class CaptionBinding(BaseModel):
    source: str                                 # caption column key
    subject_name: str
    confidence: float


class Ownership(BaseModel):
    confidence: float
    margin: Optional[float] = None              # f1 - f2 (containment methods only)
    method: str                                 # one of OWNERSHIP_METHODS


class RegionOnOwner(BaseModel):
    vertical: str                               # upper | middle | lower
    horizontal: str                             # left | center | right
    offset: list[float]                         # (attr center - owner centroid)/owner half-extents


class AttributeRecord(BaseModel):
    text: str                                   # canonical (longest) form after dedup
    stratum: str
    sources: list[str]                          # caption columns that carried it
    consensus: float                            # len(sources)/n_caption_sources
    grounded: bool = False
    box: Optional[list[float]] = None           # present iff grounded (task space)
    grounding_score: Optional[float] = None     # GDINO phrase score
    ownership: Ownership
    region_on_owner: Optional[RegionOnOwner] = None


class Entity(BaseModel):
    id: str                                     # person_1, person_2, dog, ... (left-to-right)
    label: str
    detection_score: float
    box: list[float]                            # task space
    centroid: list[float]                       # task space
    area_frac: float
    position: GridPosition
    depth: Optional[DepthInfo] = None
    saliency: SaliencyInfo
    is_primary: bool = False
    mask: Optional[MaskInfo] = None
    caption_bindings: list[CaptionBinding] = Field(default_factory=list)
    attributes: list[AttributeRecord] = Field(default_factory=list)


class Relation(BaseModel):
    a: str                                      # entity id (smaller entity index)
    b: str
    predicates: list[str]                       # spatial_relations predicate vocab
    dx: float                                   # (centroid_b - centroid_a)/W, task-space-free
    dy: float
    distance: float                             # centroid distance / image diagonal
    iou: float
    depth_delta: Optional[float] = None         # nearness_a - nearness_b (continuous)
    confidence: float                           # min(detection scores)


class BasinCandidate(BaseModel):
    entity_id: str
    likelihood: float


class BasinItem(BaseModel):
    text: str
    stratum: str
    sources: list[str]
    consensus: float
    reason: str                                 # one of BASIN_REASONS
    grounded: bool = False
    box: Optional[list[float]] = None
    candidates: list[BasinCandidate] = Field(default_factory=list)


class VotedValue(BaseModel):
    value: Optional[str] = None
    votes: dict[str, int] = Field(default_factory=dict)


class StyleValue(BaseModel):
    value: Optional[str] = None                 # specialist wins conflicts (it saw the image)
    caption_votes: dict[str, int] = Field(default_factory=dict)
    specialist: Optional[str] = None


class MoodValue(BaseModel):
    value: Optional[str] = None
    per_source: dict[str, str] = Field(default_factory=dict)


class SymmetryInfo(BaseModel):
    axis: str = "none"
    lr: Optional[float] = None                  # continuous correlations (retained)
    tb: Optional[float] = None


class SceneAttribute(BaseModel):
    text: str
    stratum: str = "scene_level"
    sources: list[str] = Field(default_factory=list)


class SceneOCRLine(BaseModel):
    text: str
    box: Optional[list[float]] = None
    conf: Optional[float] = None                # EasyOCR confidence (retained)


class SceneOCR(BaseModel):
    full_text: str = ""
    lines: list[SceneOCRLine] = Field(default_factory=list)


class LabelScore(BaseModel):
    label: str
    score: float


class SceneBlock(BaseModel):
    setting: VotedValue = Field(default_factory=VotedValue)
    style: StyleValue = Field(default_factory=StyleValue)
    mood: MoodValue = Field(default_factory=MoodValue)
    layout: str = "unknown"
    symmetry: SymmetryInfo = Field(default_factory=SymmetryInfo)
    actions: list[SceneAttribute] = Field(default_factory=list)   # unbound caption actions
    scene_attributes: list[SceneAttribute] = Field(default_factory=list)
    ocr: SceneOCR = Field(default_factory=SceneOCR)
    class_top: list[LabelScore] = Field(default_factory=list)


class Counts(BaseModel):
    total_entities: int = 0
    people: int = 0                             # via the person synonym group
    by_label: dict[str, int] = Field(default_factory=dict)


class GroundingStats(BaseModel):
    phrases_total: int = 0
    phrases_grounded: int = 0
    assigned: int = 0
    basin: int = 0
    scene_level: int = 0
    ungroundable: int = 0


class Quality(BaseModel):
    n_caption_sources: int = 0
    detection_score_mean: float = 0.0
    mask_quality_mean: Optional[float] = None
    ocr_conf_mean: Optional[float] = None
    grounding: GroundingStats = Field(default_factory=GroundingStats)
    overall_confidence: float = 0.0             # the fusion_confidence scalar


class FusedScene(BaseModel):
    fused_scene_version: str = FUSED_SCENE_VERSION
    coord_space: str
    image_size: list[int]                       # (W, H) PIXELS — the one pixel field
    counts: Counts = Field(default_factory=Counts)
    entities: list[Entity] = Field(default_factory=list)
    relations: list[Relation] = Field(default_factory=list)
    shared_basin: list[BasinItem] = Field(default_factory=list)
    scene: SceneBlock = Field(default_factory=SceneBlock)
    quality: Quality = Field(default_factory=Quality)


FUSED_SCENE_JSON_SCHEMA = FusedScene.model_json_schema()