File size: 9,780 Bytes
c14ceee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9371818
 
 
ea7b176
 
 
 
 
c14ceee
 
 
 
 
 
 
 
 
 
9371818
c14ceee
 
9371818
c14ceee
 
 
 
 
9371818
 
 
ea7b176
c14ceee
ea7b176
 
9371818
 
 
 
 
 
 
 
 
 
 
 
c14ceee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9371818
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ea7b176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c14ceee
 
ea7b176
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
"""core/assets.py β€” the product-image ASSET SOURCE (wave 18, C2-ASSET).

WHY THIS FILE EXISTS AT ALL, given the route is three functions long: the API process is
HOST-AGNOSTIC by contract (`ops/verify_portability.py` checks B1/B2 β€” the HuggingFace SDK may
appear ONLY in its declared homes, all of which live under `platform/`, so the FastAPI
service can be lifted to Hetzner/EC2 without dragging a hub client with it). The first version
of `routes_assets.py` imported `huggingface_hub` directly and the gate caught it. The fetch is
a STORE-shaped concern anyway, so it belongs beside `core/store.py`.

Two sources, checked by the caller in order:
  * a local master directory (`AIOS_ASSET_DIR`) β€” dev, and the fastest path on the owner's box;
  * the private HF dataset `AIOS_ASSET_REPO` (default `royal-imports/product-assets`), laid out
    by `ingest_product_assets.py`:
        assets/products/orig/<CODE><ext>   masters, archived verbatim (owner ruling R9)
        assets/products/web/<CODE>.png     ~800px derivatives (what the app displays)
        manifest.json                      {"codes": {"<CODE>": {"ext": ".png"}}, ...}

Every failure returns None/{} rather than raising: a missing image is "no picture for that SKU",
never a broken catalog page.
"""
import os
import time
from pathlib import Path

DEFAULT_REPO = 'royal-imports/product-assets'
_MANIFEST_TTL = 300
#: (fetched_at, {"CODE": {...}}, {"slug": {...}}) β€” one manifest read per process per TTL.
#: DEBT-5 (2026-08-04): the same manifest.json now carries a second, NON-SKU half under
#: "editorial" β€” full-bleed lifestyle imagery for catalog gallery pages, keyed by slug.
#: ⭐ WAVE 19 R7 / C5: and a THIRD, under "records" β€” pictures uploaded through an `image` FIELD,
#: namespaced BY TENANT (`<tenant>/<id>`). The other two namespaces are Royal's catalogue assets
#: and are tenant-#0 material by nature; this one is written by any signed-in user of any tenant,
#: so the tenant is part of the address and comes from the SESSION on both write and read.
_MANIFEST = {'at': 0.0, 'codes': None, 'editorial': None, 'records': None}


def repo_id():
    return os.environ.get('AIOS_ASSET_REPO', '').strip() or DEFAULT_REPO


def _token():
    return os.environ.get('HF_TOKEN') or None


def _load():
    now = time.time()
    if _MANIFEST['codes'] is not None and now - _MANIFEST['at'] < _MANIFEST_TTL:
        return
    try:
        import json

        from huggingface_hub import hf_hub_download
        p = hf_hub_download(repo_id(), 'manifest.json', repo_type='dataset', token=_token())
        man = json.loads(Path(p).read_text(encoding='utf-8')) or {}
        codes = man.get('codes') or {}
        editorial_ = man.get('editorial') or {}
        records_ = man.get('records') or {}
    except Exception:
        codes, editorial_, records_ = {}, {}, {}
    _MANIFEST.update(at=now, codes=codes, editorial=editorial_, records=records_)


def manifest():
    """{CODE: {'ext': '.png'}} for the configured asset repo, or {} when unreachable."""
    _load()
    return _MANIFEST['codes']


def editorial():
    """{slug: {'ext': ...}} β€” the editorial (non-SKU) half of the manifest; {} when unreachable."""
    _load()
    return _MANIFEST['editorial'] or {}


def fetch(code, quality='web'):
    """A local Path to one asset, or None. The hub cache dedupes across requests, so a container
    downloads each file at most once."""
    meta = manifest().get(code)
    if not meta:
        return None
    rel = (f'assets/products/web/{code}.png' if quality == 'web'
           else f"assets/products/orig/{code}{meta.get('ext', '.png')}")
    try:
        from huggingface_hub import hf_hub_download
        return Path(hf_hub_download(repo_id(), rel, repo_type='dataset', token=_token()))
    except Exception:
        return None


def fetch_editorial(slug, quality='web'):
    """A local Path to one EDITORIAL asset, or None β€” same shape as fetch()."""
    meta = editorial().get(slug)
    if not meta:
        return None
    rel = (f'assets/editorial/web/{slug}.png' if quality == 'web'
           else f"assets/editorial/orig/{slug}{meta.get('ext', '.png')}")
    try:
        from huggingface_hub import hf_hub_download
        return Path(hf_hub_download(repo_id(), rel, repo_type='dataset', token=_token()))
    except Exception:
        return None


def put_editorial(slug, orig_bytes, ext, web_png):
    """Store one editorial image: the original, its web derivative and the updated manifest in
    ONE commit (a partial upload can never leave a listed-but-unfetchable slug). Raises on
    failure β€” the route turns that into an honest 503, never a silent success.

    ⚠ manifest.json is read-modify-write with no lock; two admins uploading in the same
    second could drop one entry. Accepted for an admin-rare action; the fix (a server-side
    merge) rides the day uploads become routine."""
    import json

    from huggingface_hub import CommitOperationAdd, HfApi, hf_hub_download
    api = HfApi(token=_token())
    try:
        p = hf_hub_download(repo_id(), 'manifest.json', repo_type='dataset', token=_token(),
                            force_download=True)
        man = json.loads(Path(p).read_text(encoding='utf-8')) or {}
    except Exception:
        man = {}
    man.setdefault('editorial', {})[slug] = {'ext': ext}
    api.create_commit(
        repo_id(), repo_type='dataset',
        operations=[
            CommitOperationAdd(f'assets/editorial/orig/{slug}{ext}', orig_bytes),
            CommitOperationAdd(f'assets/editorial/web/{slug}.png', web_png),
            CommitOperationAdd('manifest.json',
                               json.dumps(man, indent=1, sort_keys=True).encode('utf-8')),
        ],
        commit_message=f'editorial asset: {slug}')
    reset_cache()
    return True


# ───────────────────────────────────────── record images (wave 19, R7 / contract C5)
#
# A picture attached to ONE ROW through an `image` field, by any signed-in user. Two things make
# this namespace different from the two above, and both are about who may write it:
#
#   * it is TENANT-ADDRESSED (`<tenant>/<id>`), because a nurilab user uploading a photo must not
#     land anywhere a royal-imports read can reach. The tenant comes from the SESSION at both
#     ends β€” never from the reference string, which is why a `rec:` ref copied between tenants
#     resolves to a 404 rather than to somebody else's photograph.
#   * the id is SERVER-MINTED hex, so the client never names a path component.


def records():
    """{'<tenant>/<id>': {'ext': ...}} β€” the record-image half of the manifest; {} if unreachable."""
    _load()
    return _MANIFEST['records'] or {}


def record_ref(tenant, asset_id):
    """The manifest key for one tenant's asset. ONE composition point, so read and write cannot
    disagree about where a picture lives."""
    return f"{str(tenant or '').strip().lower()}/{str(asset_id or '').strip().lower()}"


def fetch_record(tenant, asset_id, quality='web'):
    """A local Path to one RECORD image, or None β€” same shape as fetch()/fetch_editorial().

    β›” Returns None for an id this tenant does not own, and that is the cross-tenant wall: the
    lookup key is composed from the caller's own tenant, so an id belonging to another tenant is
    simply absent from the manifest under this address. Indistinguishable from "no such picture",
    which is exactly what the caller is entitled to know.
    """
    ref = record_ref(tenant, asset_id)
    meta = records().get(ref)
    if not meta:
        return None
    rel = (f'assets/records/{ref}/web.png' if quality == 'web'
           else f"assets/records/{ref}/orig{meta.get('ext', '.png')}")
    try:
        from huggingface_hub import hf_hub_download
        return Path(hf_hub_download(repo_id(), rel, repo_type='dataset', token=_token()))
    except Exception:
        return None


def put_record(tenant, asset_id, orig_bytes, ext, web_png):
    """Store one record image: original, web derivative and the updated manifest in ONE commit
    (a partial upload can never leave a listed-but-unfetchable reference). Raises on failure β€”
    the route turns that into an honest 503, never a silent success.

    ⚠ Shares `put_editorial`'s read-modify-write caveat on manifest.json: two uploads landing in
    the same second could drop one entry. Uploads through a field are rarer than a scroll but far
    from admin-rare, so this is the first namespace where that will actually bite β€” booked in the
    wave doc rather than quietly inherited.
    """
    import json

    from huggingface_hub import CommitOperationAdd, HfApi, hf_hub_download
    ref = record_ref(tenant, asset_id)
    api = HfApi(token=_token())
    try:
        p = hf_hub_download(repo_id(), 'manifest.json', repo_type='dataset', token=_token(),
                            force_download=True)
        man = json.loads(Path(p).read_text(encoding='utf-8')) or {}
    except Exception:
        man = {}
    man.setdefault('records', {})[ref] = {'ext': ext}
    api.create_commit(
        repo_id(), repo_type='dataset',
        operations=[
            CommitOperationAdd(f'assets/records/{ref}/orig{ext}', orig_bytes),
            CommitOperationAdd(f'assets/records/{ref}/web.png', web_png),
            CommitOperationAdd('manifest.json',
                               json.dumps(man, indent=1, sort_keys=True).encode('utf-8')),
        ],
        commit_message=f'record image: {ref}')
    reset_cache()
    return True


def reset_cache():
    """Drop the memoized manifest β€” for gates and for a post-ingest refresh."""
    _MANIFEST.update(at=0.0, codes=None, editorial=None, records=None)