File size: 5,820 Bytes
4fca835
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Structured visual attributes for scene windows: shot type, setting, era.

These let footage searches match on *how* a scene is filmed and *when/where*
it is set — "aerial shots of a Kingdom Hall exterior", "modern-day disaster
relief", "Bible-times scenes set indoors" — which a free-text description alone
captures inconsistently.

Single source of truth for the controlled vocabularies, normalization of the
model's free text into those vocabularies, and the search phrase that gets
folded into a window's embedded text so the existing semantic search picks the
attributes up (no query-side changes needed). Both the vision pipeline (going
forward) and the text-backfill pass import from here so the two can't drift.
"""

from __future__ import annotations

# canonical value -> synonym-rich phrase woven into the embedded text. The
# phrase is what makes the attribute searchable: a query like "aerial drone
# shot" embeds near a window whose text contains "aerial drone overhead".
SHOT_TYPES: dict[str, str] = {
    "aerial": "aerial drone overhead bird's-eye shot",
    "establishing": "wide establishing landscape shot",
    "medium": "medium shot",
    "close-up": "close-up tight shot",
    "pov": "point-of-view first-person shot",
    "talking-head": "interview talking-head piece-to-camera shot",
    "unclear": "",
}
SETTINGS: dict[str, str] = {
    "interior": "indoor interior",
    "exterior": "outdoor exterior",
    "mixed": "both indoor and outdoor",
    "unclear": "",
}
ERAS: dict[str, str] = {
    "ancient": "Bible times ancient biblical era",
    "medieval": "medieval middle ages",
    "early-1900s": "early 1900s early twentieth century historical",
    "mid-1900s": "mid 1900s mid twentieth century historical",
    "late-1900s": "1970s 1980s 1990s late twentieth century",
    "modern-day": "modern-day present-day contemporary",
    "unclear": "",
}

# Free-text variants the model may emit -> canonical key. Checked as substrings
# (longest-first) so "close up" / "closeup" / "tight" all map to "close-up".
_SHOT_ALIASES: tuple[tuple[str, str], ...] = (
    ("drone", "aerial"), ("aerial", "aerial"), ("overhead", "aerial"),
    ("bird", "aerial"), ("tilt-shift", "aerial"),
    ("establishing", "establishing"), ("wide", "establishing"),
    ("landscape", "establishing"), ("vista", "establishing"),
    ("close-up", "close-up"), ("close up", "close-up"), ("closeup", "close-up"),
    ("tight", "close-up"), ("portrait", "close-up"), ("macro", "close-up"),
    ("point-of-view", "pov"), ("point of view", "pov"), ("pov", "pov"),
    ("first-person", "pov"),
    ("talking head", "talking-head"), ("talking-head", "talking-head"),
    ("piece to camera", "talking-head"), ("to camera", "talking-head"),
    ("interview", "talking-head"), ("presenter", "talking-head"),
    ("medium", "medium"),
)
_SETTING_ALIASES: tuple[tuple[str, str], ...] = (
    ("interior", "interior"), ("indoor", "interior"), ("inside", "interior"),
    ("exterior", "exterior"), ("outdoor", "exterior"), ("outside", "exterior"),
    ("mixed", "mixed"), ("both", "mixed"),
)
_ERA_ALIASES: tuple[tuple[str, str], ...] = (
    ("biblical", "ancient"), ("bible times", "ancient"), ("ancient", "ancient"),
    ("first century", "ancient"), ("antiquity", "ancient"),
    ("medieval", "medieval"), ("middle ages", "medieval"),
    ("early 1900", "early-1900s"), ("early-1900", "early-1900s"),
    ("early twentieth", "early-1900s"), ("1910", "early-1900s"),
    ("1920", "early-1900s"), ("1930", "early-1900s"),
    ("mid 1900", "mid-1900s"), ("mid-1900", "mid-1900s"),
    ("1940", "mid-1900s"), ("1950", "mid-1900s"), ("1960", "mid-1900s"),
    ("1970", "late-1900s"), ("1980", "late-1900s"), ("1990", "late-1900s"),
    ("late 1900", "late-1900s"), ("late-1900", "late-1900s"),
    ("late twentieth", "late-1900s"),
    ("modern", "modern-day"), ("present", "modern-day"),
    ("contemporary", "modern-day"), ("today", "modern-day"),
    ("2000", "modern-day"), ("2010", "modern-day"), ("2020", "modern-day"),
)


def _normalize(raw: object, aliases: tuple[tuple[str, str], ...], valid: dict[str, str]) -> str:
    """Map the model's free text to a canonical key; 'unclear' when no match."""
    text = str(raw or "").strip().lower()
    if not text:
        return "unclear"
    if text in valid:  # already canonical
        return text
    for needle, canonical in aliases:
        if needle in text:
            return canonical
    return "unclear"


def normalize_shot_type(raw: object) -> str:
    return _normalize(raw, _SHOT_ALIASES, SHOT_TYPES)


def normalize_setting(raw: object) -> str:
    return _normalize(raw, _SETTING_ALIASES, SETTINGS)


def normalize_era(raw: object) -> str:
    return _normalize(raw, _ERA_ALIASES, ERAS)


def attribute_search_phrase(shot_type: str, setting: str, era: str) -> str:
    """Build the phrase appended to a window's description before embedding.

    Empty parts ('unclear') are omitted. Returns '' when nothing is known, so a
    window with no usable attributes embeds exactly as its description did.
    """
    parts: list[str] = []
    shot = SHOT_TYPES.get(shot_type or "unclear", "")
    setting_phrase = SETTINGS.get(setting or "unclear", "")
    era_phrase = ERAS.get(era or "unclear", "")
    if shot:
        parts.append(f"Camera: {shot}.")
    if setting_phrase:
        parts.append(f"Setting: {setting_phrase}.")
    if era_phrase:
        parts.append(f"Era: {era_phrase}.")
    return " ".join(parts)


def embed_text(description: str, shot_type: str, setting: str, era: str) -> str:
    """The full text to embed for a window: description + attribute phrase."""
    phrase = attribute_search_phrase(shot_type, setting, era)
    description = (description or "").strip()
    return f"{description} {phrase}".strip() if phrase else description