Spaces:
Running on Zero
Running on Zero
File size: 8,635 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 | """
strata.py — the attribute stratification lexicon (fusion tier).
Classifies a caption attribute string into a STRATUM — the "disperse the topics"
layer that replaces the flat attribute list with typed, routable records. Pure
stdlib, deterministic, registry-as-python (same pattern as registry.py /
tasks_vision.py): the lexicon is data to iterate on, not code.
Routing semantics consumed by fuse.py:
- GROUNDABLE strata are sent to the GDINO phrase-grounding pass (they name
visible things a detector can box).
- "scene_level" bypasses entities entirely -> FusedScene.scene.scene_attributes.
- "abstract_quality", "color", and "action" ride the caption-binding-only path
(never grounded: a detector box for "elegant" or bare "red" is noise).
- Everything is classified; "abstract_quality" is the catch-all default.
"""
from __future__ import annotations
import re
from .metrics import _depluralize
# Minimal stopword set for head-noun extraction (articles/preps/conjunctions that
# can trail a phrase). Deliberately tiny — attribute phrases are short.
_STOP = frozenset({
"a", "an", "the", "of", "in", "on", "at", "with", "and", "or", "to",
"her", "his", "its", "their", "very", "slightly",
})
_TOKEN_RE = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*")
STRATA: dict[str, frozenset] = {
"hair": frozenset({
"hair", "hairstyle", "bangs", "fringe", "ponytail", "pigtails", "twintails",
"braid", "braids", "bun", "curls", "updo", "bob", "undercut", "mohawk",
"sidelocks", "ahoge", "afro", "dreadlocks", "cornrows", "mullet", "buzzcut",
}),
"face": frozenset({
"face", "eyes", "eye", "eyebrows", "eyebrow", "eyelashes", "lips", "lip",
"mouth", "nose", "cheeks", "cheekbones", "chin", "jaw", "jawline", "forehead",
"freckles", "dimples", "beard", "mustache", "stubble", "smile", "grin",
"expression", "gaze", "makeup", "lipstick", "eyeliner", "eyeshadow", "blush",
"mascara", "teeth",
}),
"skin": frozenset({
"skin", "complexion", "tan", "tattoo", "tattoos", "scar", "scars", "mole",
"birthmark", "wrinkles", "pores",
}),
"clothing": frozenset({
"dress", "shirt", "t-shirt", "tshirt", "blouse", "top", "skirt", "pants",
"trousers", "jeans", "shorts", "jacket", "coat", "hoodie", "sweater",
"cardigan", "vest", "suit", "uniform", "kimono", "yukata", "robe", "gown",
"leotard", "swimsuit", "bikini", "armor", "cape", "cloak", "apron",
"sleeves", "sleeve", "collar", "neckline", "hem", "outfit", "attire",
"clothes", "clothing", "costume", "sweatshirt", "leggings", "stockings",
"tights", "socks", "corset", "bodysuit", "tunic", "sari", "poncho",
}),
"accessory": frozenset({
"earrings", "earring", "necklace", "pendant", "choker", "bracelet", "ring",
"rings", "watch", "hat", "cap", "beanie", "beret", "crown", "tiara",
"headband", "hairband", "ribbon", "bow", "hairpin", "hairclip", "scrunchie",
"glasses", "sunglasses", "eyepatch", "monocle", "mask", "scarf", "gloves",
"glove", "belt", "bag", "handbag", "backpack", "purse", "umbrella", "fan",
"brooch", "badge", "piercing", "anklet", "shoes", "boots", "sandals",
"heels", "sneakers", "veil", "headphones", "tie", "bowtie",
}),
"body": frozenset({
"build", "figure", "physique", "body", "shoulders", "shoulder", "arms",
"arm", "hands", "hand", "fingers", "legs", "leg", "thighs", "knees",
"feet", "chest", "waist", "hips", "back", "neck", "collarbone", "height",
"frame", "posture", "muscles", "abs", "curves",
}),
"pose": frozenset({
"standing", "sitting", "kneeling", "crouching", "lying", "leaning",
"walking", "running", "jumping", "dancing", "posing", "looking", "facing",
"reaching", "pointing", "waving", "holding", "carrying", "crossed",
"outstretched", "tilted", "turned", "pose", "stance",
}),
"color": frozenset({
"red", "orange", "yellow", "green", "blue", "purple", "violet", "pink",
"brown", "black", "white", "gray", "grey", "silver", "gold", "golden",
"blonde", "blond", "brunette", "auburn", "crimson", "scarlet", "teal",
"turquoise", "cyan", "magenta", "lavender", "beige", "cream", "ivory",
"navy", "maroon", "olive", "platinum", "pastel", "neon", "dark", "light",
"pale", "bright", "vivid", "striped", "plaid", "polka-dot", "checkered",
"floral", "gradient",
}),
"abstract_quality": frozenset({
"beautiful", "pretty", "handsome", "cute", "elegant", "graceful", "stylish",
"fashionable", "detailed", "intricate", "delicate", "soft", "sharp",
"masterpiece", "quality", "aesthetic", "gorgeous", "stunning", "charming",
"youthful", "mature", "young", "old", "confident", "shy", "serene", "calm",
"cheerful", "melancholic", "mysterious", "dramatic", "ethereal", "dreamy",
}),
"scene_level": frozenset({
"background", "foreground", "backdrop", "lighting", "light", "shadow",
"shadows", "sunlight", "moonlight", "sunset", "sunrise", "dusk", "dawn",
"sky", "clouds", "bokeh", "blur", "depth", "wall", "walls", "floor",
"ceiling", "window", "windows", "door", "room", "indoors", "outdoors",
"outdoor", "indoor", "scenery", "landscape", "cityscape", "street",
"forest", "beach", "mountains", "atmosphere", "ambiance", "setting",
"scene", "environment", "composition", "framing",
}),
}
# hyphen/compound-adjective suffixes -> stratum ("silver-haired", "blue-eyed")
SUFFIX_RULES: tuple = (
("haired", "hair"),
("eyed", "face"),
("faced", "face"),
("skinned", "skin"),
("sleeved", "clothing"),
("dressed", "clothing"),
("clad", "clothing"),
("shouldered", "body"),
("legged", "body"),
("armed", "body"),
)
# -ing words that are NOUNS, not gerunds — exempt from the verb-phrase rule
# (data to extend as COCO round-trips surface more)
_NOUN_ING = frozenset({
"wedding", "building", "painting", "lighting", "ceiling", "clothing",
"evening", "morning", "string", "earring", "ring", "king", "wing",
"railing", "awning",
})
# Strata whose phrases go to the GDINO grounding pass (visible, boxable things).
GROUNDABLE = frozenset({"hair", "face", "skin", "clothing", "accessory", "body", "pose"})
# Any-token tie-break order (only reached when the head noun missed the lexicon).
# Concrete/visible strata outrank colors and abstractions.
STRATUM_PRECEDENCE = ("hair", "face", "skin", "accessory", "clothing", "body",
"pose", "scene_level", "color", "abstract_quality")
# The full stratum vocabulary fuse.py may emit ("action" is assigned by fuse.py to
# caption `actions` entries directly — it has no lexicon and is never grounded).
ALL_STRATA = tuple(STRATA.keys()) + ("action",)
def _content_tokens(text: str) -> list:
return [t for t in _TOKEN_RE.findall((text or "").lower()) if t not in _STOP]
def classify_stratum(text: str) -> str:
"""Deterministic stratum for an attribute string.
1. head-noun rule: depluralized LAST content token, exact lexicon lookup
2. suffix rules on the head token ("silver-haired" -> hair)
3. any-token lookup in STRATUM_PRECEDENCE order
4. all tokens are color/pattern terms -> color
5. default -> abstract_quality (nothing is ever unclassified)
"""
toks = _content_tokens(text)
if not toks:
return "abstract_quality"
# _depluralize is crude ("dress"->"dres") — always try the raw form too
head_forms = {toks[-1], _depluralize(toks[-1])}
for stratum, words in STRATA.items():
if head_forms & words:
return stratum
for suffix, stratum in SUFFIX_RULES:
if any(h.endswith(suffix) for h in head_forms):
return stratum
forms = [{t, _depluralize(t)} for t in toks]
for stratum in STRATUM_PRECEDENCE:
words = STRATA[stratum]
if any(f & words for f in forms):
return stratum
if all(f & STRATA["color"] for f in forms):
return "color"
# verb-phrase heuristic: leading gerund ("playing baseball", "taking a photo",
# "running") → pose. Caught live on COCO captions, where the structurer emits
# verb phrases as attributes that otherwise fell to abstract_quality.
first = toks[0]
if first.endswith("ing") and len(first) > 4 and first not in _NOUN_ING:
return "pose"
return "abstract_quality"
def is_groundable(stratum: str) -> bool:
return stratum in GROUNDABLE
|