Spaces:
Running
Running
File size: 5,090 Bytes
8dcb261 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | #!/usr/bin/env node
// Fetches top GGUF models from HF, parses quants + file sizes, writes static/models.json.
import { writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const OUT = resolve(__dirname, '..', 'static', 'models.json');
const LIMIT = 100;
const HF_TOKEN = process.env.HF_TOKEN;
const headers = HF_TOKEN ? { Authorization: `Bearer ${HF_TOKEN}` } : {};
const QUANT_RE = /(IQ\d_[A-Z0-9_]+|Q\d+_[A-Z0-9_]+|Q\d+|F16|BF16|F32|FP8|MXFP4(?:_MOE)?)/i;
const SHARD_RE = /-(\d{5})-of-(\d{5})/;
const PARAM_RE = /(\d+(?:\.\d+)?)\s*[bB](?![a-z])/;
const TEXT_GEN_TAGS = new Set(['text-generation', 'conversational', 'image-text-to-text']);
async function fetchJson(url) {
const res = await fetch(url, { headers });
if (!res.ok) throw new Error(`${res.status} ${url}`);
return res.json();
}
function parseQuant(filename) {
const base = filename.split('/').pop();
const m = base.match(QUANT_RE);
return m ? m[1].toUpperCase() : null;
}
function parseParams(modelId, fileNames, ggufMeta) {
// Try gguf metadata first
if (ggufMeta?.total) {
const b = ggufMeta.total / 1e9;
if (b > 0.1 && b < 2000) return Math.round(b * 10) / 10;
}
// Try filename (e.g. "Llama-3.1-8B")
for (const f of [modelId, ...fileNames]) {
const m = f.match(PARAM_RE);
if (m) return parseFloat(m[1]);
}
return null;
}
function groupShards(ggufFiles) {
// Group multi-part shards (e.g. "-00001-of-00003.gguf") into one logical file.
const groups = new Map();
for (const f of ggufFiles) {
const sm = f.path.match(SHARD_RE);
let key;
if (sm) {
key = f.path.replace(SHARD_RE, '');
} else {
key = f.path;
}
if (!groups.has(key)) groups.set(key, { path: key, size: 0, parts: 0 });
const g = groups.get(key);
g.size += f.size || 0;
g.parts += 1;
}
return [...groups.values()];
}
async function processModel(m) {
try {
const tree = await fetchJson(
`https://huggingface.co/api/models/${m.id}/tree/main?recursive=true`
);
const detail = await fetchJson(`https://huggingface.co/api/models/${m.id}`);
const ggufFiles = tree
.filter((t) => {
if (t.type !== 'file' || !t.size) return false;
const p = t.path.toLowerCase();
if (!p.endsWith('.gguf')) return false;
// Skip auxiliary files: multimodal projectors, imatrix calibration, embeddings
if (p.includes('mmproj') || p.includes('projector')) return false;
if (p.includes('imatrix')) return false;
return true;
})
.map((t) => ({ path: t.path, size: t.size }));
if (ggufFiles.length === 0) return null;
const grouped = groupShards(ggufFiles);
const quants = [];
for (const g of grouped) {
const quant = parseQuant(g.path);
if (!quant) continue;
quants.push({
path: g.path,
size: g.size,
sizeGB: +(g.size / 1024 ** 3).toFixed(2),
quant,
sharded: g.parts > 1
});
}
if (quants.length === 0) return null;
const params = parseParams(m.id, ggufFiles.map((f) => f.path), detail.gguf);
const arch = detail.gguf?.architecture || detail.config?.model_type || null;
return {
id: m.id,
author: m.id.split('/')[0],
name: m.id.split('/').slice(1).join('/'),
downloads: m.downloads || 0,
likes: m.likes || 0,
pipeline_tag: m.pipeline_tag || null,
params_b: params,
arch,
n_layers: detail.gguf?.n_layers || null,
n_kv_heads: detail.gguf?.n_kv_heads || detail.gguf?.n_heads || null,
n_embd: detail.gguf?.n_embd || null,
context_length: detail.gguf?.context_length || null,
tags: m.tags || [],
quants: quants.sort((a, b) => a.size - b.size)
};
} catch (err) {
console.warn(` skip ${m.id}: ${err.message}`);
return null;
}
}
async function main() {
console.log(`Fetching top ${LIMIT} GGUF models...`);
const list = await fetchJson(
`https://huggingface.co/api/models?filter=gguf&sort=downloads&direction=-1&limit=${LIMIT}`
);
console.log(`Got ${list.length} models. Filtering to text-generation...`);
const candidates = list.filter(
(m) => !m.pipeline_tag || TEXT_GEN_TAGS.has(m.pipeline_tag)
);
console.log(`${candidates.length} candidates after filter.`);
const results = [];
let i = 0;
for (const m of candidates) {
i++;
process.stdout.write(`[${i}/${candidates.length}] ${m.id}... `);
const out = await processModel(m);
if (out) {
results.push(out);
console.log(`OK (${out.quants.length} quants)`);
} else {
console.log('skip');
}
}
results.sort((a, b) => b.downloads - a.downloads);
writeFileSync(
OUT,
JSON.stringify(
{ generated_at: new Date().toISOString(), count: results.length, models: results },
null,
2
)
);
console.log(`\nWrote ${results.length} models to ${OUT}`);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
|