WanVideo-Demo / app.js
ponponsongs's picture
Upload app.js with huggingface_hub
445a961 verified
Raw History Blame Contribute Delete
7.83 kB
/**
* 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 => `
<div class="model-card">
<h3>${MODEL_ID.split('/')[1]}</h3>
<div class="model-card-id">${MODEL_ID}</div>
<dl>
${fields.map(f2 => `
<dt>${f2.key}</dt>
<dd>${f2.value}</dd>
`).join('')}
</dl>
<div class="model-card-actions">
<a href="https://huggingface.co/${MODEL_ID}" target="_blank" rel="noopener">View on HF</a>
<a href="https://huggingface.co/api/models/${encodeURIComponent(MODEL_ID)}" target="_blank" rel="noopener">API</a>
</div>
</div>
`).join('');
// One card only (the model itself) + a couple of "related" cards
const extraCards = `
<div class="model-card">
<h3>Wan-AI/Wan2.1-VACE-1.3B</h3>
<div class="model-card-id">base model</div>
<dl>
<dt>Pipeline</dt><dd>VACE Β· Video-conditioned</dd>
<dt>Downloads</dt><dd>β€”</dd>
<dt>Notes</dt><dd>Base model for Kijai/WanVideo_comfy</dd>
</dl>
<div class="model-card-actions">
<a href="https://huggingface.co/Wan-AI/Wan2.1-VACE-1.3B" target="_blank" rel="noopener">View on HF</a>
</div>
</div>
<div class="model-card">
<h3>Kijai/WanVideo_comfy_fp8_scaled</h3>
<div class="model-card-id">quantized variant</div>
<dl>
<dt>Pipeline</dt><dd>Text-to-Video</dd>
<dt>Notes</dt><dd>FP8 scaled β€” lower VRAM, small quality trade-off</dd>
</dl>
<div class="model-card-actions">
<a href="https://huggingface.co/Kijai/WanVideo_comfy_fp8_scaled" target="_blank" rel="noopener">View on HF</a>
</div>
</div>
`;
el.modelGrid.innerHTML = rows + extraCards;
}
/* ── repo tree ─────────────────────────────────────────────────────────────── */
async function loadRepoTree() {
try {
const files = await fetchJson(
`https://huggingface.co/api/models/${encodeURIComponent(MODEL_ID)}/tree/main`,
{ headers: { 'Accept': 'application/json' } }
);
const lines = files.slice(0, 60).map(f => {
const isDir = f.type === 'tree';
const kind = isDir ? 'πŸ“' : 'πŸ“„';
const size = isDir ? '' : ` ${fmtBytes(f.size)}`;
const path = f.path;
const indent = path.split('/').length > 1 ? ' '.repeat(Math.min(path.split('/').length - 1, 3)) : '';
return `${indent}${kind} ${path}${size}`;
});
el.repoTree.textContent = lines.join('\n') || 'β€” (empty tree)';
} catch (err) {
console.warn('repo tree fetch failed:', err);
el.repoTree.textContent = 'β€” (could not load repo tree)';
}
}
/* ── init ──────────────────────────────────────────────────────────────────── */
async function init() {
await loadHeroStats();
// model grid + tree can load in parallel; delay slightly to let hero render
await defer(200);
try {
const info = await fetchJson(`${HF_API}/${encodeURIComponent(MODEL_ID)}`);
renderModelGrid(info);
} catch (err) {
console.warn('model grid fetch failed:', err);
el.modelGrid.innerHTML = `<div class="model-grid-placeholder">Could not load model info from the Hub. Check your network and try again.</div>`;
}
await loadRepoTree();
}
document.addEventListener('DOMContentLoaded', init);