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)
|