VSEARCH / src /assets.js
bep40's picture
fix(build): restore assets.js exports so the Space can rebuild + ship rebuilt dist
ef50ce7
Raw History Blame Contribute Delete
9.81 kB
/**
* Image database: 1,222 Vietnamese interior-design photographs, encoded offline with
* CLIP-ViT-B-32 and stored as raw little-endian float32 in 512-d rows.
*/
export const DATASET = 'https://huggingface.co/datasets/bep40/interior-image-field-assets/resolve/main/';
/**
* Text tower. `sentence-transformers/clip-ViT-B-32-multilingual-v1` maps 50+
* languages (including Vietnamese) into the same 512-d space as CLIP-ViT-B-32,
* so a Vietnamese query retrieves the matching interior photograph. The stock
* repository ships a SentenceTransformer wrapper whose ONNX export stops at
* 768-d `last_hidden_state`, so this fork adds a `text_model.onnx` that
* includes mean pooling and the 768->512 projection, letting Transformers.js
* run it under the usual CLIP file naming.
*/
export const TEXT_MODEL = 'bep40/clip-vit-b32-multilingual-vi-text';
/**
* Pinned to the commit whose `onnx/text_model.onnx` actually loads, instead of
* following the `main` branch.
*
* The repo uploaded a broken export first (commit 68dc949, 136,951,792 bytes).
* Its graph declares `masked` as [batch_size, sequence_length, 768] while an
* attention mask only ever has 1 feature, so ONNX shape inference aborts. A
* later commit (9e1d535, 136,915,522 bytes) re-exported the graph with no
* `value_info` at all, which loads fine -- that is the revision pinned here.
*
* Pinning matters beyond reproducibility: transformers.js requests the
* *unversioned* /resolve/main/ URL unless `revision` is given, and caches under
* that same URL. Naming an explicit commit changes the cache key, so visitors
* who cached the broken graph fetch the known-good file instead.
*/
export const TEXT_MODEL_REVISION = '9e1d53553228b17abfe6986fbe92e22bd6bd6bad';
export const TOKENIZER = 'sentence-transformers/clip-ViT-B-32-multilingual-v1';
export const DIMENSIONS = 512;
/** The URL of an interior photograph in the dataset repo, at a CDN width. */
export function imageURL(image, width = 360) {
return `${DATASET}images/${image.url}?w=${width}&q=80`;
}
/**
* The thumbnail URL for a tile, whichever kind of record it holds.
*
* Interior photographs are files in the dataset repo, so they go through
* imageURL and its CDN resize. A Malloca product points at a third-party shop
* CDN that this Space knows nothing about -- there is no filename to build a
* path from and no query string it would honour -- so its own URL is used
* verbatim.
*
* Dispatch is on `src`, which merge.js sets only for products, rather than on
* the presence of `url`: an interior record always has `url`, and keying on the
* absence of a field would mis-classify a record that is merely malformed.
*/
export function tileURL(image, width) {
return image?.src ? image.src : imageURL(image, width);
}
/** Cache is an optimization: unavailable storage never prevents a search. */
export async function cachedFile(url, onProgress) {
let cache;
try {
cache = await caches.open('interior-image-field-v1');
const cached = await cache.match(url);
if (cached) return cached.arrayBuffer();
} catch { /* Private browsing / storage disabled. */ }
const response = await fetch(url);
if (!response.ok) throw new Error(`Could not download image database (${response.status}).`);
const total = Number(response.headers.get('content-length'));
let buffer;
if (response.body && onProgress) {
const reader = response.body.getReader();
const chunks = [];
let loaded = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
loaded += value.length;
onProgress(loaded, total);
}
const joined = new Uint8Array(loaded);
let offset = 0;
for (const chunk of chunks) { joined.set(chunk, offset); offset += chunk.length; }
buffer = joined.buffer;
} else buffer = await response.arrayBuffer();
try { await cache?.put(url, new Response(buffer)); } catch { /* Cache quota reached. */ }
return buffer;
}
/**
* The V.AI STUDIO share page for whatever is currently open in the viewer.
*
* Both routes live on the vai-avatar Space, which serves them server-side with
* OpenGraph tags so a pasted link becomes a card in Facebook or Zalo rather
* than a bare page:
*
* /share/i/<image_id> interior photos (the gallery that image came from)
* /share/p/<sku> products (name, price and gallery)
*
* The product route did not exist until it was added there; before that the
* only product-ish URL was `?product=<sku>`, which is an OG-tag handler and
* renders nothing but the chat page. `/share/p/` resolves a sku, a model code or
* a slug against the same catalogue the Space already loads, so all 3,966
* identity forms in it resolve.
*
* Returns null when there is no id to share, so the caller can omit the button
* rather than render a link that would 404.
*/
const VAI_STUDIO = 'https://bep40-vai-avatar.hf.space';
/**
* The prefix avatar-interiors.js puts on a vai-avatar photograph's `sku`.
*
* Namespacing it there is deliberate -- it is what stops a 16-hex image_id
* colliding with a baked interior's id in merge.js's de-duplication set, and it
* is what lets a photograph travel the same code path as a catalogue product
* (same viewer, same panel, same neon, same share anchor).
*
* It is NOT a catalogue identity, and /share/p/ resolves against the product
* catalogue -- so a photograph sent through that route answers "Not Found".
* Nothing about that is fixable on vai-avatar's side: no SKU of that shape
* exists there to find. It has to be routed as the interior it is.
*/
const AVATAR_SKU_PREFIX = 'avatarimg:';
/**
* Whether this record is an interior photograph published on vai-avatar.
*
* Either field is conclusive on its own. `avatarImageId` is set on every such
* photograph; the namespaced `sku` covers a record that has lost it.
*/
function isAvatarInterior(photo) {
return Boolean(photo.avatarImageId) || String(photo.sku ?? '').startsWith(AVATAR_SKU_PREFIX);
}
/**
* The interior id behind a record, or '' when it has none.
*
* Two sources, in order, and the second is a fallback rather than an
* alternative: `avatarImageId` is the field avatar-interiors.js sets, present on
* every vai-avatar photograph. But a record can reach shareURL() having lost it
* -- merge.js rewrites `id` into a namespaced tile key, and any path that
* rebuilds a record from its `sku` alone would carry the prefix but not the
* original field. That is exactly the shape that produced
* /share/p/avatarimg%3Ab68dc779370dc70e, which is a "Not Found" page for every
* single photograph in the vai-avatar interior set.
*
* So the namespaced sku is unwrapped as well, and a prefix that is present but
* empty (`avatarimg:` with nothing after it) yields ''.
*/
function interiorIdOf(photo) {
const direct = String(photo.avatarImageId ?? '').trim();
if (direct) return direct;
const sku = String(photo.sku ?? '').trim();
return sku.startsWith(AVATAR_SKU_PREFIX)
? sku.slice(AVATAR_SKU_PREFIX.length).trim()
: '';
}
export function shareURL(photo) {
if (!photo || typeof photo !== 'object') return null;
/**
* A photograph published on vai-avatar carries BOTH `avatarImageId` and a
* namespaced `sku` of `avatarimg:<image_id>`.
*
* `sku` exists so the record travels the same path as a catalogue product --
* same viewer, same detail panel, same neon, same share anchor -- and it has
* to stay for that. But it is not an identity the product catalogue knows,
* and it must not decide the URL: `/share/p/` resolves against the product
* catalogue, so every such photograph sent through it answered "Not Found".
* The whole vai-avatar interior set was affected, on the tile's share button
* and on the viewer's "Xem trang" alike.
*
* The interior identity is therefore resolved first and unconditionally, and
* /share/i/ gets the raw id -- no prefix to strip, and nothing to keep in
* step with whatever namespace merge.js uses for its tile ids.
*/
const interiorId = interiorIdOf(photo);
if (interiorId) return `${VAI_STUDIO}/share/i/${encodeURIComponent(interiorId)}`;
/**
* A record that still says it is an interior but carries no usable id gets NO
* share URL, rather than falling through to the product branch below.
*
* That fall-through is a trap worth naming, because it is exactly how the bug
* comes back. `avatarimg:` with nothing after it has no id to share, and every
* remaining branch here would still produce a URL for it: the product branch
* would encode the prefix itself into `/share/p/avatarimg%3A`, which is the
* dead route all over again, and the id branch would send it to `/share/i/`
* with an empty segment, which 404s in its own right. Returning null lets the
* caller omit the button instead, which is the only outcome that is true.
*/
if (isAvatarInterior(photo)) return null;
if (photo.sku || photo.slug) {
// slug first, then sku, then mod. A shared URL is a permanent, public,
// indexable address and the one thing about a product a reader can see:
// `MH800TC` says nothing about what is linked, while
// `mh800tc-malloca-may-hut-mui-gan-tu-am-tu-80cm` reads as the product's name
// to a person and to a crawler. The slug is already the canonical key this
// Space's catalogue uses, and vai-avatar's /share/p/ matches on it first, so
// this changes how the link reads, not whether it resolves.
const id = String(photo.slug || photo.sku || photo.mod || '').trim();
return id ? `${VAI_STUDIO}/share/p/${encodeURIComponent(id)}` : null;
}
const id = String(photo.id ?? '').trim();
return id ? `${VAI_STUDIO}/share/i/${encodeURIComponent(id)}` : null;
}