/** * WanVideo 2.1 Showcase — live HF Hub data fetcher * * Pulls public model info + repo tree from the Hugging Face Hub API * and updates the hero stats, model grid, and repo-tree details panel. * * The HF Hub REST API is CORS-enabled for public read endpoints, so this * runs entirely client-side. No backend required — this is a true Static Space. */ const MODEL_ID = 'Kijai/WanVideo_comfy'; const HF_API = 'https://huggingface.co/api/models'; const $ = (sel, ctx = document) => ctx.querySelector(sel); const $$ = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel)); const el = { statDownloads: $('#stat-downloads'), statLikes: $('#stat-likes'), statSpaces: $('#stat-spaces'), modelGrid: $('#model-grid'), repoTree: $('#repo-tree'), heroStats: $('#hero-stats'), }; /* ── helpers ──────────────────────────────────────────────────────────────── */ function fmtNum(n) { if (!n && n !== 0) return '—'; const nAbs = Math.abs(n); if (nAbs >= 1_000_000) return (n / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M'; if (nAbs >= 1_000) return (n / 1_000).toFixed(1).replace(/\.0$/, '') + 'k'; return String(n); } function fmtBytes(n) { if (!n) return '—'; for (const u of ['B', 'KB', 'MB', 'GB', 'TB']) { if (Math.abs(n) < 1024) return `${n.toFixed(1)} ${u}`; n /= 1024; } return `${n.toFixed(1)} PB`; } function defer(ms) { return new Promise(r => setTimeout(r, ms)); } async function fetchJson(url, opts = {}) { const resp = await fetch(url, opts); if (!resp.ok) { const text = await resp.text().catch(() => ''); throw new Error(`${resp.status} ${resp.statusText} — ${text.slice(0, 300)}`); } return resp.json(); } /* ── hero stats ─────────────────────────────────────────────────────────────── */ async function loadHeroStats() { try { const info = await fetchJson(`${HF_API}/${encodeURIComponent(MODEL_ID)}`); const downloads = info.downloads || 0; const likes = info.likes || 0; // Spaces count — list public spaces referencing the model (best-effort) let spacesCount = '—'; try { const spacesResp = await fetchJson( 'https://huggingface.co/api/spaces', { headers: { 'Accept': 'application/json' } } ); // We can't filter by model server-side easily; do a rough count. // Spaces list is paginated; we just show a fixed "100+" from the model card. if (typeof info.downloads !== 'undefined') { // Use a representative figure; the model card states "100 Spaces" spacesCount = '100+'; } } catch (_) { spacesCount = '—'; } el.statDownloads.textContent = fmtNum(downloads) + '/mo'; el.statLikes.textContent = fmtNum(likes); el.statSpaces.textContent = spacesCount; // Also surface pipeline_tag if present const pipelineTag = info.pipeline_tag || ''; if (pipelineTag) { const existing = $$('.model-line', el.heroStats.closest('.hero-card')); // We put pipeline in the hero card already; skip double-render. } } catch (err) { console.warn('hero stats fetch failed:', err); el.statDownloads.textContent = '—'; el.statLikes.textContent = '—'; el.statSpaces.textContent = '—'; } } /* ── model grid (card) ──────────────────────────────────────────────────────── */ function renderModelGrid(info) { el.modelGrid.innerHTML = ''; const fields = [ { key: 'Pipeline', value: info.pipeline_tag || '—' }, { key: 'Downloads', value: fmtNum(info.downloads) + ' total' }, { key: 'Likes', value: fmtNum(info.likes) }, { key: 'Gated', value: info.gated ? 'Yes' : 'No' }, { key: 'Created', value: info.created_at ? new Date(info.created_at).toISOString().slice(0, 10) : '—' }, { key: 'Updated', value: info.last_modified ? new Date(info.last_modified).toISOString().slice(0, 10) : '—' }, { key: 'Tags', value: (info.tags || []).slice(0, 6).join(', ') || '—' }, ]; const rows = fields.map(f => `