Spaces:
Running
Running
Download src/assets.js from bep40/VSEARCH: direct link, hf CLI and curl.
- Browser
- Download file 9.81 kB
-
https://huggingface.co/spaces/bep40/VSEARCH/resolve/main/src/assets.js
- Command line
-
hf download hf://spaces/bep40/VSEARCH/src/assets.js
-
curl -L -o assets.js https://huggingface.co/spaces/bep40/VSEARCH/resolve/main/src/assets.js
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; | |
| } | |