| name | gguf-hf-browser-extractor |
| description | Extract full GGUF metadata, tensor tables, and quantization recipes from any HuggingFace GGUF repo using Chrome MCP. Use when you need per-file tensor layouts, tensor-type overrides, imatrix-driven quantization, or named JSON exports, especially when HF does not expose the full tensor table through an API.
|
GGUF HuggingFace Browser Extractor
When to use this skill
Use this skill whenever:
- A HuggingFace repo contains
.gguf files or a GGUF release bundle
- You need the full tensor list (name, shape, precision) per file
- You need to turn extracted JSON recipes into tensor-type override files
- You need to quantize a source F16/BF16 model with llama.cpp and an imatrix
- You need the metadata/chat template when the HF API does not expose it
- The user wants a repeatable end-to-end GGUF extraction and quantization flow
Do not use this skill when:
- Only high-level model card metadata is needed (use the HF API instead)
- The repo has a documented tensor API that returns the full table
Extraction modes
This skill has three extraction paths:
- Binary path (
extract_gguf_binary.py) — downloads the GGUF file and
parses the header directly. Works for any GGUF file regardless of HF's page
rendering. Best for multi-shard / Xet-stored repos where the blob page
does not render tensor tables. Supports --include-tensors for tensor maps
or --metadata-only (default) for just the metadata.
- Browser path — use Chrome MCP plus the local sink server when you need
the full rendered metadata/table view from the HF blob page. Works well for
single-file GGUFs where HF renders both metadata and tensor tables.
- Legacy API path — use
/home/op/extract_gguf.py when you only need
GGUF header metadata and tensor maps from a Hugging Face repo via the API.
Decision tree:
Is the GGUF split into multiple shards (e.g. *-00001-of-00003.gguf)?
├── YES, metadata only (no tensor maps needed)
│ └── Use binary path: extract_gguf_binary.py --file <shard1> (default)
│ → Downloads shard 1 (~11 MB), extracts all metadata.
│ → Shard 1 always holds metadata; tensors are in later shards.
│
├── YES, metadata + tensor maps needed (for re-quantization)
│ └── Use binary path with --include-tensors on EVERY shard
│ → Downloads all shards (can be 30+ GB total).
│ → Only needed if you plan to re-quantize the model.
│
├── NO (single-file GGUF), metadata + tensors from page
│ └── Use browser path (extract.js + sink server)
│ → Fastest for single-file GGUFs; HF renders everything.
│
└── NO, API is sufficient
└── Use legacy API path: extract_gguf.py <repo-id>
Architecture
Binary extractor (extract_gguf_binary.py)
│
├─► downloads GGUF shard(s) from HF
├─► parses GGUF v2/v3 header directly from bytes
├─► extracts all metadata keys (architecture, chat template, tokenizer)
├─► optional: extracts tensor name/shape/dtype map (--include-tensors)
└─► writes JSON to Downloads/<file>.gguf.json
Browser (Chrome MCP tab)
│
│ navigate to the Hugging Face blob page
│ wait for metadata + tensor tables
│ detect sharded/Xet files (empty tensor table)
│ ungroup if the page exposes that control
│ extract metadata + tensors
│ POST JSON ──────────────────────────────► Local Sink (127.0.0.1:8123)
│
├─► Downloads/<file>.gguf.json
└─► ud_results.json (aggregate)
Why browser-first?
HuggingFace renders GGUF metadata client-side by fetching and partially
parsing the binary GGUF file. There is no stable public API that returns the
full tensor table. The blob page at /blob/main/<file>.gguf is the only
reliable source for single-file GGUFs.
However, for multi-shard GGUFs stored with Xet, HF shows only the metadata
table and leaves the tensor table empty. In this case the binary path
(extract_gguf_binary.py) is the only reliable extraction method.
Why local sink?
Chrome blocks repeated scripted blob-URL downloads after a few attempts.
A local HTTP server on 127.0.0.1 receives a fetch() POST from the browser
page — Chrome's localhost-as-secure-origin policy allows HTTPS pages to
reach http://127.0.0.1 without mixed-content errors, and the server writes
files directly to disk with no download-count limit.
Prerequisites
| Requirement | Notes |
|---|
| Chrome MCP | Must be available (chrome_javascript, chrome_navigate, chrome_computer tools) |
| Python 3.8+ | For the API extractor, sink server, and recipe generation helper |
| requests / huggingface-hub | For the API extractor path |
| llama.cpp | Must provide a build of llama-quantize that supports the target architecture |
| HF token | Required only if publishing the final bundle to Hugging Face |
| Port 8123 | Must be free on 127.0.0.1 (configurable) |
Setup
1. Start the sink server
nohup python3 sink_server.py >> ~/gguf_local_sink.log 2>&1 &
curl -s http://127.0.0.1:8123/health
Keep the server running for the entire extraction session.
2. Identify target files
Inspect the HF repo file list to collect the GGUF filenames you want.
Filter as needed (e.g. only files whose names contain UD).
HF file list URL pattern:
https://huggingface.co/<org>/<repo>/tree/main
Check for sharded files: If you see filenames like
model-00001-of-00003.gguf, the model is split. Use the binary path.
Binary extraction path
Use extract_gguf_binary.py for any GGUF file, especially sharded/Xet-stored
repos. No browser or sink server required.
Metadata only (default)
For sharded models, only shard 1 (split.no == 0) contains metadata.
This is sufficient for inference, reference, and chat template extraction.
python3 extract_gguf_binary.py unsloth/Qwen3.5-122B-A10B-GGUF \
--file UD-Q4_K_XL/Qwen3.5-122B-A10B-UD-Q4_K_XL-00001-of-00003.gguf
Output: ~/Downloads/<filename>.gguf.json with all metadata keys.
Metadata + tensor maps
Only needed if you plan to re-quantize the model. You must extract from
every shard since tensors are distributed across them.
for shard in 00001 00002 00003; do
python3 extract_gguf_binary.py unsloth/Qwen3.5-122B-A10B-GGUF \
--file UD-Q4_K_XL/Qwen3.5-122B-A10B-UD-Q4_K_XL-${shard}-of-00003.gguf \
--include-tensors
done
Local file parsing
If you already have the GGUF file downloaded:
python3 extract_gguf_binary.py dummy/repo --file model.gguf \
--no-download --local-path /path/to/model.gguf
Binary path options
| Flag | Default | Meaning |
|---|
--file | (required) | Path to GGUF file within the repo |
--output-dir | ~/Downloads | Where to write the JSON |
--include-tensors | off | Also extract tensor name/shape/dtype |
--no-download | off | Parse a local file instead of downloading |
--local-path | (none) | Path to local GGUF file |
--hf-base | https://huggingface.co | HF base URL (for mirrors) |
Browser extraction loop
Note: For multi-shard GGUFs, prefer the binary path above. The browser
path works best for single-file GGUFs where HF renders both tables.
For each GGUF file, execute these steps sequentially (never in parallel):
Step A — navigate
chrome_navigate(url=f"https://huggingface.co/{org}/{repo}/blob/main/{filename}")
Step B — wait for render
chrome_computer(action="wait", text="Tensors", timeout=35000)
If the tensor table never appears, the page may have returned a 4xx/5xx or the
repo may use a different layout. Skip the file and inspect the page before
retrying.
Step C — run extract.js
Copy the full content of extract.js into a chrome_javascript call.
The script now detects sharded/Xet files automatically and returns
status: "metadata_only" when the tensor table is empty.
For single-file GGUFs, it returns:
{
"status": "done",
"filename": "model.gguf",
"tensor_count": 658,
"metadata_count": 63,
"is_sharded": false,
"sink": {"ok": true, "path": "...", "tensor_count": 658, "metadata_count": 63}
}
For sharded/Xet files where the tensor table is empty:
{
"status": "metadata_only",
"filename": "model-00001-of-00003.gguf",
"tensor_count": 0,
"metadata_count": 55,
"is_sharded": true,
"sink": {"ok": true, "path": "...", "tensor_count": 0, "metadata_count": 55}
}
When is_sharded is true, use extract_gguf_binary.py for complete
metadata extraction (tokenizer, chat template may not be on the rendered page).
Step D — human-like delay
Wait 5–9 seconds (random) between files:
import random, time
time.sleep(random.uniform(5, 9))
Recipe generation
Use the extracted JSONs to generate per-file tensor override recipes:
python3 build_tensor_type_files.py \
--recipes-dir /path/to/Downloads/<repo-slug> \
--output-dir /path/to/quant-recipes
The helper expects *.gguf.json files and writes matching
*.gguf.tensor-types.txt files. Keep the stems aligned exactly.
Quantization flow
Quantize one GGUF at a time with the matching recipe and imatrix:
LD_LIBRARY_PATH=/path/to/llama.cpp/build/bin \
/path/to/llama-quantize \
--imatrix /path/to/imatrix.gguf \
--tensor-type-file /path/to/quant-recipes/<file>.tensor-types.txt \
/path/to/source-model.gguf \
/path/to/output/<source-stem>-UD-Q4_K_M \
Q4_K_M 16
Notes:
llama-quantize wants an output prefix, not a full .gguf filename.
- Use the llama.cpp build that actually understands the target architecture.
- Keep the output naming consistent:
<source-stem>-UD-<quant>.gguf.
- If a quantized file is suspiciously small or missing expected content, treat
it as partial and rebuild it.
extract.js quick-reference
The browser script does the following:
- Polls until
document.querySelectorAll('table').length >= 2
- Clicks the ungroup button (if present) and waits for row count to stabilise
- Extracts metadata while skipping header rows
- Extracts tensors while skipping group headers and deduping on
name|shape
- Normalises shape strings from Hugging Face rendering quirks
- POSTs
{filename, payload} as JSON to http://127.0.0.1:8123/save
- Returns a summary with counts
Config constants at the top of extract.js:
| Constant | Default | Meaning |
|---|
SINK_URL | http://127.0.0.1:8123/save | Sink server endpoint |
POLL_MS | 400 | DOM polling interval |
TABLE_TIMEOUT | 45000 | Max wait for tables (ms) |
EXPAND_TIMEOUT | 30000 | Max wait for ungroup to settle (ms) |
SETTLE_TICKS | 3 | Consecutive polls with same row count to consider settled |
Output format
Per-file JSON (Downloads/<filename>.gguf.json)
{
"filename": "model-UD-Q4_K_M.gguf",
"source_url": "https://huggingface.co/.../blob/main/model-UD-Q4_K_M.gguf",
"extracted_at": "2026-04-12T03:47:00.000Z",
"tensor_count": 658,
"metadata_count": 63,
"metadata": {
"version": "3",
"tensor_count": "658",
"general.architecture": "gemma4",
"general.name": "...",
"tokenizer.chat_template": "...",
...
},
"tensors": [
{ "name": "token_embd.weight", "shape": "[2816, 262144]", "precision": "Q4_K" },
{ "name": "blk.0.attn_k.weight", "shape": "[2816, 2048]", "precision": "Q4_K" },
...
]
}
Aggregate index (ud_results.json)
{
"model-UD-Q4_K_M.gguf": { ...same as per-file... },
"model-UD-Q4_K_XL.gguf": { ... },
...
}
Updated atomically by the sink server after each file.
Rebuilding the aggregate index
If ud_results.json gets corrupted or has stale entries, rebuild it cleanly.
For multi-repo work, point the index at a repo-specific filename instead:
import json
from pathlib import Path
downloads = Path("~/Downloads").expanduser()
index = Path("~/ud_results.json").expanduser()
merged = {}
for p in sorted(downloads.glob("*.gguf.json")):
data = json.loads(p.read_text())
key = p.stem
merged[key] = data
print(f"{key} t={data.get('tensor_count')} m={data.get('metadata_count')}")
index.write_text(json.dumps(merged, indent=2))
print(f"\n{len(merged)} files written to {index}")
Troubleshooting
| Symptom | Cause | Fix |
|---|
Tensors text never appears | Page returned 5xx or the repo layout changed | Retry after a few minutes |
tensor_count < TARGET_TENSORS | Ungroup control not found or DOM changed | Inspect the page with snapshot and adjust the browser step |
status: "metadata_only" | Sharded/Xet-stored file — HF doesn't render tensor table | Use extract_gguf_binary.py --include-tensors instead |
Sink fetch() fails with CORS error | Sink server not running or wrong port | curl http://127.0.0.1:8123/health; restart if needed |
| Sink returns 400 | filename or payload missing in body | Check the chrome_javascript call builds the body correctly |
| Shape strings contain non-ASCII spaces | HF inserts \u00a0 in large numbers | Already handled in extract.js via .replace(/[\u00a0\u2009\u202f]/g, ' ') |
| HF blocks requests (429 / CAPTCHA) | Too fast / too many requests | Increase delay to 10-15 s; switch to a different IP |
| Binary path: "EOF reading string length" | Download was partial (range request too small) | Download the full file (no -r range header) or increase the range |
Anti-detection best practices
- Reuse one browser tab — navigate inside it rather than opening new tabs
- Sequential only — never process two files in parallel
- 5–9 s random jitter between every file navigation
- Do not mix browser scraping with HF API scraping in the same pass
- Scroll naturally — if adding more human-like behaviour, scroll the page before extraction
- Treat partial quant outputs as failures and rerun them before publish
Adapting to other repos
This skill generalises to any HF GGUF repo. Adjust:
| Parameter | Where |
|---|
| API repo ID | /home/op/extract_gguf.py <repo-id-or-url> |
TARGET_TENSORS | extract.js constant — derive it from the target model |
| File filter | Your orchestration code — change the filename pattern |
--index path | sink_server.py --index /path/to/myrepo_results.json |
--downloads path | sink_server.py --downloads /path/to/output/ |
| Recipe output dir | build_tensor_type_files.py --output-dir ... |
The blob page structure (button[title="Ungroup by layer name"], two <table> elements)
is consistent across HF GGUF repos as of April 2026.
Files in this project
gguf-hf-extractor/
├── SKILL.md ← this file
├── extract_gguf_binary.py ← binary GGUF header parser (no browser needed)
├── extract.js ← browser-side extraction script (Chrome MCP)
├── sink_server.py ← local HTTP sink (start once, leave running)
├── build_tensor_type_files.py ← turns JSON recipes into tensor overrides
├── patches/
│ ├── opencode.md ← how to register this skill with OpenCode
│ ├── copilot-cli.md ← how to use with GitHub Copilot CLI
│ └── antigravity.md ← how to use with Antigravity agent framework
└── output/ ← symlink or copy of extraction results