Download hf_space/gallery_preview.js from UniLat3D/Supra-Tex: direct link, hf CLI and curl.
- Browser
- Download file 21.7 kB
-
https://huggingface.co/spaces/UniLat3D/Supra-Tex/resolve/main/hf_space/gallery_preview.js
- Command line
-
hf download hf://spaces/UniLat3D/Supra-Tex/hf_space/gallery_preview.js
-
curl -L -o gallery_preview.js https://huggingface.co/spaces/UniLat3D/Supra-Tex/resolve/main/hf_space/gallery_preview.js
21.7 kB
| (() => { | |
| if (window.__stGalleryInstalled) return; | |
| window.__stGalleryInstalled = true; | |
| const config = window.SupraSpaceGalleryConfig; | |
| window.SupraGalleryLightBase = config.lights; | |
| const models = new Map(); | |
| const englishMessage = (value, fallback) => typeof value === 'string' && value.length <= 500 && | |
| !/\p{Script=Han}/u.test(value) ? value : fallback; | |
| // Cache original bytes only. Private entries live solely in this page and | |
| // are keyed by the verified account; no browser storage or GPU scene is kept. | |
| const cache = new Map(), cacheBudget = 128 * 1024 * 1024; | |
| let cacheBytes = 0; | |
| const privateCache = new Map(), transfers = new Map(), objectURLs = new Set(); | |
| const privateBudget = (navigator.deviceMemory && navigator.deviceMemory <= 4 ? 64 : 128) * 1024 * 1024; | |
| let privateBytes = 0, account = '', accountEpoch = 0; | |
| const aborted = () => new DOMException('Model request cancelled', 'AbortError'); | |
| function remember(url, bytes) { | |
| if (bytes.byteLength > cacheBudget) return; | |
| if (cache.has(url)) { cacheBytes -= cache.get(url).byteLength; cache.delete(url); } | |
| while (cache.size >= 2 || cacheBytes + bytes.byteLength > cacheBudget) { | |
| const oldest = cache.keys().next().value; | |
| cacheBytes -= cache.get(oldest).byteLength; cache.delete(oldest); | |
| } | |
| cache.set(url, bytes); cacheBytes += bytes.byteLength; | |
| } | |
| function validate(bytes, expected) { | |
| if (expected && bytes.byteLength !== expected) throw new Error('Incomplete 3D model. Please retry.'); | |
| if (bytes.byteLength < 12) throw new Error('Invalid 3D model.'); | |
| const header = new DataView(bytes); | |
| if (header.getUint32(0, true) !== 0x46546c67 || header.getUint32(4, true) !== 2 || | |
| header.getUint32(8, true) !== bytes.byteLength) throw new Error('Invalid 3D model.'); | |
| return bytes; | |
| } | |
| function clearPrivate() { | |
| ++accountEpoch; | |
| transfers.forEach(transfer => transfer.controller.abort()); transfers.clear(); | |
| privateCache.clear(); privateBytes = 0; | |
| objectURLs.forEach(url => URL.revokeObjectURL(url)); objectURLs.clear(); | |
| } | |
| function rememberPrivate(key, bytes) { | |
| if (!account || bytes.byteLength > privateBudget) return; | |
| if (privateCache.has(key)) { privateBytes -= privateCache.get(key).byteLength; privateCache.delete(key); } | |
| while (privateCache.size >= 2 || privateBytes + bytes.byteLength > privateBudget) { | |
| const oldest = privateCache.keys().next().value; | |
| privateBytes -= privateCache.get(oldest).byteLength; privateCache.delete(oldest); | |
| } | |
| privateCache.set(key, bytes); privateBytes += bytes.byteLength; | |
| } | |
| function privatePath(value) { | |
| try { const url = new URL(value, location.href); return url.origin === location.origin && | |
| /^\/api\/history\/jobs\/[A-Za-z0-9_-]+\/files\/asset\.glb$/.test(url.pathname) ? url : null; } | |
| catch { return null; } | |
| } | |
| async function delivery(url, signal) { | |
| const path = privatePath(url); | |
| if (!path) return null; | |
| try { | |
| const response = await fetch(path.pathname.replace('/files/asset.glb', '/delivery'), { | |
| credentials: 'same-origin', signal: AbortSignal.any([signal, AbortSignal.timeout(10000)])}); | |
| if (!response.ok) return null; | |
| const value = await response.json(); | |
| const target = new URL(value.url, location.href); | |
| // Only the authenticated server may supply a short-lived HTTPS location. | |
| if ((value.direct && target.protocol === 'https:') || target.origin === location.origin) return {...value, url: target.href}; | |
| } catch (error) { if (signal.aborted) throw aborted(); } | |
| return null; | |
| } | |
| async function download(model, url, gzip, expected, decodedBytes, signal, publicAsset) { | |
| if (signal.aborted) throw aborted(); | |
| if (gzip && typeof DecompressionStream === 'undefined') throw new Error('This browser cannot decode the compressed 3D model.'); | |
| let received = 0, chunks = [], total = expected || 0, validator = '', resumable = false; | |
| for (let attempt = 0; attempt < 3; attempt++) { | |
| const controller = new AbortController(); | |
| const cancel = () => controller.abort(); signal.addEventListener('abort', cancel, {once: true}); | |
| const attemptDeadline = setTimeout(() => controller.abort(), 600000); | |
| let timer, reader, invalid = false; | |
| const arm = () => { clearTimeout(timer); timer = setTimeout(() => controller.abort(), 20000); }; | |
| try { | |
| arm(); | |
| const offset = resumable && validator ? received : 0; | |
| if (!offset) { received = 0; chunks = []; } | |
| model.progress(received, total, attempt ? 'Reconnecting to model storage…' : 'Downloading 3D model'); | |
| const response = await fetch(url, {signal: controller.signal, | |
| credentials: publicAsset ? 'omit' : 'same-origin', ...(publicAsset ? {mode: 'cors'} : {}), | |
| headers: offset ? {'Range': `bytes=${offset}-`, 'If-Range': validator} : {}}); | |
| if (!response.ok) { invalid = response.status < 500 && ![408, 429].includes(response.status); throw new Error('The 3D model could not be downloaded.'); } | |
| const encoding = response.headers.get('content-encoding'); | |
| const etag = response.headers.get('etag'); | |
| const nextValidator = etag && !etag.startsWith('W/') ? etag : response.headers.get('last-modified') || ''; | |
| if (offset && response.status === 206) { | |
| const range = /^bytes (\d+)-(\d+)\/(\d+)$/.exec(response.headers.get('content-range') || ''); | |
| if (!range || Number(range[1]) !== offset || (total && Number(range[3]) !== total) || | |
| (nextValidator && validator !== nextValidator) || encoding) { | |
| invalid = true; throw new Error('The model changed during transfer. Please reopen it.'); | |
| } | |
| total = Number(range[3]); | |
| } else { | |
| if (response.status === 206) { invalid = true; throw new Error('Unexpected partial model response.'); } | |
| received = 0; chunks = []; | |
| total = encoding ? decodedBytes || 0 : expected || Number(response.headers.get('content-length')) || 0; | |
| } | |
| validator = nextValidator; | |
| resumable = !encoding && !!validator; | |
| // A raw .gz asset can resume its compressed bytes. Browser-decoded HTTP | |
| // gzip has no reliable raw offset and is restarted instead. | |
| const countedExpected = encoding ? 0 : expected; | |
| reader = (response.body || new Blob([await response.arrayBuffer()]).stream()).getReader(); | |
| while (true) { | |
| arm(); const {done, value} = await reader.read(); if (done) break; | |
| received += value.byteLength; | |
| if ((countedExpected && received > countedExpected) || received > 1024 * 1024 * 1024) { | |
| invalid = true; throw new Error('The 3D model has an unexpected size.'); | |
| } | |
| chunks.push(value); model.progress(received, total); | |
| } | |
| if ((countedExpected && received !== countedExpected) || (total && received !== total && !encoding)) | |
| throw new Error('Incomplete 3D model. Please retry.'); | |
| clearTimeout(timer); | |
| let blob = new Blob(chunks); chunks = []; | |
| const isGzip = gzip && !(encoding && /gzip/i.test(encoding)); | |
| const bytes = isGzip ? await new Response(blob.stream().pipeThrough(new DecompressionStream('gzip'))).arrayBuffer() : await blob.arrayBuffer(); | |
| blob = null; | |
| if (signal.aborted) throw aborted(); | |
| validate(bytes, decodedBytes || (!gzip && expected)); model.progress(received, received); | |
| return bytes; | |
| } catch (error) { | |
| if (signal.aborted) throw aborted(); | |
| if (invalid || attempt === 2) throw new Error(controller.signal.aborted ? | |
| 'The model transfer stopped responding. Select it again to retry.' : error.message); | |
| model.progress(received, total, 'Connection interrupted · Retrying download…'); | |
| await new Promise((resolve, reject) => { | |
| const onAbort = () => { clearTimeout(wait); reject(aborted()); }; | |
| const wait = setTimeout(() => { signal.removeEventListener('abort', onAbort); resolve(); }, (attempt + 1) * 500); | |
| signal.addEventListener('abort', onAbort, {once: true}); | |
| }); | |
| } finally { | |
| clearTimeout(timer); clearTimeout(attemptDeadline); signal.removeEventListener('abort', cancel); | |
| await reader?.cancel().catch(() => {}); | |
| } | |
| } | |
| } | |
| function acquirePrivate(model) { | |
| const key = account + '\0' + new URL(model.url, location.href).href; | |
| const epoch = accountEpoch; | |
| if (!account || !privatePath(model.url)) return {promise: Promise.reject(new Error('Please sign in to load this model.')), cancel() {}}; | |
| if (privateCache.has(key)) { | |
| const bytes = privateCache.get(key); privateCache.delete(key); privateCache.set(key, bytes); | |
| model.progress(bytes.byteLength, bytes.byteLength); | |
| return {promise: Promise.resolve(bytes), cancel() {}}; | |
| } | |
| let transfer = transfers.get(key); | |
| if (!transfer) { | |
| const controller = new AbortController(), listeners = new Set(); | |
| transfer = {controller, listeners, promise: null}; transfers.set(key, transfer); | |
| const report = (...args) => listeners.forEach(listener => listener(...args)); | |
| transfer.promise = Promise.resolve().then(async () => { | |
| const signed = await delivery(model.url, controller.signal); | |
| let bytes; | |
| try { bytes = await download({progress: report}, signed?.url || model.url, false, | |
| Number(signed?.bytes) || model.bytes, 0, controller.signal, !!signed?.direct); } | |
| catch (error) { | |
| if (controller.signal.aborted || !signed?.direct) throw error; | |
| report(0, model.bytes, 'Reconnecting to model storage…'); | |
| bytes = await download({progress: report}, model.url, false, model.bytes, 0, controller.signal, false); | |
| } | |
| if (epoch !== accountEpoch || controller.signal.aborted) throw aborted(); | |
| rememberPrivate(key, bytes); return bytes; | |
| }).finally(() => { if (transfers.get(key) === transfer) transfers.delete(key); }); | |
| } | |
| transfer.listeners.add(model.progress); | |
| let cancelled = false; | |
| return {promise: transfer.promise.then(bytes => { if (cancelled || epoch !== accountEpoch) throw aborted(); return bytes; }), | |
| cancel() { cancelled = true; transfer.listeners.delete(model.progress); if (!transfer.listeners.size) transfer.controller.abort(); }}; | |
| } | |
| window.SupraPrivateAssets = { | |
| setAccount(value) { const next = String(value || ''); if (next !== account) { clearPrivate(); account = next; } }, | |
| clear: clearPrivate, | |
| async download(url, filename = 'supra-tex.glb') { | |
| const path = privatePath(url); if (!path || !account) throw new Error('Please sign in to download this model.'); | |
| const epoch = accountEpoch, key = account + '\0' + path.href; | |
| let bytes = privateCache.get(key); | |
| // Reuse an already active viewer transfer instead of sending a second GET. | |
| const activeTransfer = transfers.get(key); | |
| const listener = () => {}; | |
| if (!bytes && activeTransfer) { | |
| activeTransfer.listeners.add(listener); | |
| try { bytes = await activeTransfer.promise; } finally { activeTransfer.listeners.delete(listener); } | |
| } | |
| if (!bytes && !activeTransfer) { | |
| const selected = [...document.querySelectorAll('.st-gallery-preview[data-model]')].find(node => | |
| new URL(node.dataset.model, location.href).href === path.href); | |
| const expected = Number(selected?.dataset.modelBytes) || 0; | |
| // A click can precede the lazy viewer bundle finishing its load. Start | |
| // the same shared transfer now so the viewer joins it a moment later. | |
| if (expected > 0 && expected <= privateBudget) { | |
| const request = acquirePrivate({url:path.href, bytes:expected, progress:listener}); | |
| try { bytes = await request.promise; } finally { request.cancel(); } | |
| } | |
| } | |
| if (epoch !== accountEpoch) throw aborted(); | |
| const anchor = document.createElement('a'); anchor.download = filename; | |
| if (bytes) { | |
| const blobURL = URL.createObjectURL(new Blob([bytes], {type: 'model/gltf-binary'})); | |
| objectURLs.add(blobURL); anchor.href = blobURL; | |
| setTimeout(() => { URL.revokeObjectURL(blobURL); objectURLs.delete(blobURL); }, 60000); | |
| } else anchor.href = path.href; // Oversized models do not consume unbounded page memory. | |
| anchor.hidden = true; document.body.append(anchor); anchor.click(); anchor.remove(); | |
| return !!bytes; | |
| } | |
| }; | |
| window.SupraPrivateAssets.setAccount(window.SupraPrivateAssetAccount?.() || ''); | |
| window.SupraAssetLoader = { | |
| acquire(path) { | |
| const id = path.split('/').pop().replace(/\.js$/, ''); | |
| const controller = new AbortController(); | |
| const model = models.get(id); | |
| const signal = controller.signal; | |
| return { | |
| promise: Promise.resolve().then(async () => { | |
| if (!model) throw new Error('Unknown 3D model.'); | |
| if (signal.aborted) throw aborted(); | |
| const publicAsset = model.publicAsset && model.encoding === 'gzip'; | |
| if (!publicAsset && privatePath(model.url)) { | |
| const request = acquirePrivate(model); | |
| signal.addEventListener('abort', request.cancel, {once: true}); | |
| try { return await request.promise; } finally { signal.removeEventListener('abort', request.cancel); } | |
| } | |
| if (publicAsset && cache.has(model.url)) { | |
| const bytes = cache.get(model.url); | |
| cache.delete(model.url); cache.set(model.url, bytes); | |
| model.progress(model.bytes, model.bytes); | |
| return bytes; | |
| } | |
| let bytes; | |
| try { | |
| bytes = await download(model, model.url, publicAsset, model.bytes, model.decodedBytes, signal, publicAsset); | |
| } catch (error) { | |
| if (signal.aborted) throw aborted(); | |
| if (error.name === 'AbortError') throw error; | |
| // Old browsers or a CDN outage can still use the verified local example. | |
| if (!publicAsset || !model.fallback) throw error; | |
| const fallback = new URL(model.fallback, location.href); | |
| if (fallback.origin !== location.origin) throw error; | |
| bytes = await download(model, fallback.href, false, model.decodedBytes, model.decodedBytes, signal, false); | |
| } | |
| if (signal.aborted) throw aborted(); | |
| if (publicAsset) remember(model.url, bytes); | |
| return bytes; | |
| }).catch(error => { | |
| if (signal.aborted) throw aborted(); | |
| throw error; | |
| }), | |
| cancel: () => controller.abort() | |
| }; | |
| } | |
| }; | |
| // The website bundle is loaded once. Models and HDRs remain lazy. | |
| let library; | |
| function loadLibrary() { | |
| if (!library) library = new Promise((resolve, reject) => { | |
| const script = document.createElement('script'); | |
| script.src = config.bundle; | |
| script.onload = resolve; | |
| script.onerror = () => { library = null; script.remove(); reject(new Error('The 3D viewer could not load.')); }; | |
| document.head.append(script); | |
| }); | |
| return library; | |
| } | |
| let active = null; | |
| async function mount(root) { | |
| const record = {root, url: root.dataset.model, mount: root.querySelector('.st-gallery-canvas'), viewer: null}; | |
| active = record; | |
| const status = root.querySelector('.st-gallery-status'); | |
| const overlay = document.createElement('div'); | |
| overlay.className = 'st-gallery-loading'; | |
| overlay.setAttribute('role', 'status'); overlay.setAttribute('aria-live', 'polite'); | |
| overlay.innerHTML = '<span>Loading 3D model…</span><progress max="100" aria-label="3D model download progress"></progress>'; | |
| root.append(overlay); | |
| const label = overlay.querySelector('span'), progress = overlay.querySelector('progress'); | |
| try { | |
| await loadLibrary(); | |
| if (active !== record || !root.isConnected) return; | |
| const id = root.dataset.case; | |
| let lastProgress = 0, lastReceived = 0, progressStart = performance.now(); | |
| models.set(id, {url: root.dataset.model, bytes: Number(root.dataset.modelBytes) || 0, | |
| encoding: root.dataset.modelEncoding, decodedBytes: Number(root.dataset.modelDecodedBytes) || 0, | |
| publicAsset: root.dataset.modelPublic === 'true', fallback: root.dataset.modelFallback, | |
| progress(received, total, phase = '') { | |
| if (active !== record) return; | |
| const now = performance.now(); | |
| if (!phase && received !== total && lastReceived > 0 && now - lastProgress < 120) return; | |
| lastProgress = now; lastReceived = received; | |
| const mb = (received / 1048576).toFixed(1); | |
| const speed = now - progressStart > 1000 && received > 0 ? ` · ${(received / 1048576 / ((now - progressStart) / 1000)).toFixed(1)} MB/s` : ''; | |
| if (total > 0) { | |
| const percent = Math.min(100, Math.floor(received / total * 100)); | |
| progress.value = percent; | |
| label.textContent = phase || (percent === 100 ? 'Download complete · Preparing model and lighting…' : | |
| `Loading 3D model · ${percent}% · ${mb} / ${(total / 1048576).toFixed(1)} MB${speed}`); | |
| } else { | |
| progress.removeAttribute('value'); | |
| label.textContent = phase || `Loading 3D model · ${mb} MB`; | |
| } | |
| }}); | |
| const viewer = record.viewer = window.SupraGallery3D.create(root.querySelector('.st-gallery-canvas'), { | |
| onStatus: text => { | |
| status.textContent = englishMessage(text, 'Preparing the 3D model…'); | |
| if (text.includes('Preparing')) label.textContent = 'Download complete · Parsing the model and preparing the surface…'; | |
| } | |
| }); | |
| let selectedMode = 'pbr', appliedMode = 'pbr', modeVersion = 0; | |
| function sync() { | |
| const state = viewer.state(); | |
| root.querySelectorAll('[data-mode]').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.mode === selectedMode))); | |
| } | |
| async function selectMode(mode) { | |
| const version = ++modeVersion; | |
| const environment = mode.startsWith('shaded_') ? 'preview_' + mode.slice(7) : null; | |
| selectedMode = mode; sync(); | |
| root.setAttribute('aria-busy', 'true'); | |
| try { | |
| // The lighting request owns cancellation; a newer click always wins. | |
| const light = environment || 'daylight'; | |
| if (environment || mode === 'pbr' || mode === 'clay') await viewer.setLighting(light); | |
| if (version !== modeVersion || active !== record) return; | |
| viewer.setMode(environment ? 'pbr' : mode === 'base_color' ? 'albedo' : mode); | |
| appliedMode = mode; | |
| status.textContent = 'Drag to rotate · Scroll to zoom'; | |
| } catch (error) { | |
| if (version !== modeVersion || active !== record) return; | |
| selectedMode = appliedMode; | |
| status.textContent = englishMessage(error.message, 'This render mode could not load. Please try again.'); | |
| } finally { | |
| if (version === modeVersion && active === record) { root.removeAttribute('aria-busy'); sync(); } | |
| } | |
| } | |
| root.querySelectorAll('[data-mode]').forEach(button => button.addEventListener('click', () => void selectMode(button.dataset.mode))); | |
| root.querySelector('[data-reset]').addEventListener('click', () => viewer.reset()); | |
| await viewer.load(id); | |
| if (active === record) { overlay.remove(); sync(); } | |
| } catch (error) { | |
| if (active === record) { | |
| const message = englishMessage(error.message, 'The 3D model could not load. Please try again.'); | |
| status.textContent = message; label.textContent = message; progress.hidden = true; | |
| } | |
| } | |
| } | |
| function scan() { | |
| const root = document.querySelector('#st-model .st-gallery-preview, #st-ovoxel .st-gallery-preview'); | |
| // Gradio can reuse the HTML root while changing its URL or replacing its | |
| // children. Root identity alone does not identify the currently loaded model. | |
| if (active?.root === root && active.url === root?.dataset.model && | |
| active.mount === root?.querySelector('.st-gallery-canvas') && | |
| (!active.viewer || active.mount.querySelector('canvas'))) return; | |
| active?.viewer?.dispose(); active = null; models.clear(); | |
| root?.querySelectorAll('.st-gallery-loading').forEach(element => element.remove()); | |
| if (root) mount(root); | |
| } | |
| const app = document.getElementById('st-app'); | |
| if (app) { | |
| new MutationObserver(scan).observe(app, {childList: true, subtree: true, attributes: true, | |
| attributeFilter: ['data-model', 'data-model-bytes', 'data-model-encoding', 'data-model-decoded-bytes', 'data-model-public', 'data-model-fallback']}); | |
| // Show feedback as soon as a case is clicked, including the server lookup | |
| // before Gradio delivers the next model URL. | |
| app.addEventListener('click', event => { | |
| const thumbnail = event.target.closest('#st-examples .thumbnail-item'); | |
| if (!thumbnail || (thumbnail.classList.contains('selected') && active?.viewer?.state().ready)) return; | |
| const root = document.querySelector('#st-model .st-gallery-preview, #st-ovoxel .st-gallery-preview'); | |
| if (!root || root.querySelector('.st-gallery-loading')) return; | |
| const pending = document.createElement('div'); pending.className = 'st-gallery-loading'; | |
| pending.innerHTML = '<span>Loading 3D model…</span><progress max="100" aria-label="3D model download progress"></progress>'; | |
| root.append(pending); | |
| }, true); | |
| scan(); | |
| } | |
| window.addEventListener('pagehide', () => { active?.viewer?.dispose(); clearPrivate(); }); | |
| })(); | |