macbook-llm-compat / scripts /fetch-models.mjs
mishig's picture
mishig HF Staff
Upload folder using huggingface_hub
8dcb261 verified
#!/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);
});