Created
March 17, 2026 22:28
-
-
Save Interpause/f63b9e4786987697d6d83125d80dc876 to your computer and use it in GitHub Desktop.
Thanks Gemini for this.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>GGUF Metadata & Tensor Comparator</title> | |
| <style> | |
| :root { | |
| --bg-color: #f9fafb; | |
| --card-bg: #ffffff; | |
| --border: #e5e7eb; | |
| --primary: #2563eb; | |
| --primary-hover: #1d4ed8; | |
| --text-main: #111827; | |
| --text-muted: #6b7280; | |
| --diff-add-bg: #dcfce7; | |
| --diff-rm-bg: #fee2e2; | |
| --diff-chg-bg: #fef08a; | |
| --diff-rm-text: #991b1b; | |
| } | |
| body { | |
| font-family: system-ui, -apple-system, sans-serif; | |
| background-color: var(--bg-color); | |
| color: var(--text-main); | |
| margin: 0; | |
| padding: 2rem; | |
| line-height: 1.5; | |
| } | |
| .container { max-width: 1400px; margin: 0 auto; } | |
| h1, h2 { margin-top: 0; } | |
| p.subtitle { color: var(--text-muted); margin-bottom: 2rem; } | |
| .card { | |
| background: var(--card-bg); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 1.5rem; | |
| margin-bottom: 1.5rem; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.05); | |
| } | |
| .form-group { margin-bottom: 1.25rem; } | |
| label { display: block; font-weight: 600; margin-bottom: 0.5rem; } | |
| input[type="url"], textarea { | |
| width: 100%; padding: 0.75rem; border: 1px solid var(--border); | |
| border-radius: 6px; box-sizing: border-box; font-family: monospace; | |
| } | |
| textarea { resize: vertical; min-height: 100px; } | |
| .controls { display: flex; gap: 1rem; align-items: center; } | |
| button { | |
| background: var(--primary); color: white; border: none; | |
| padding: 0.75rem 1.5rem; border-radius: 6px; cursor: pointer; | |
| font-weight: 600; font-size: 1rem; transition: background 0.2s; | |
| } | |
| button:hover { background: var(--primary-hover); } | |
| button:disabled { background: #9ca3af; cursor: not-allowed; } | |
| .status-container { margin-top: 1rem; font-weight: 500; } | |
| .spinner { display: inline-block; width: 1rem; height: 1rem; border: 2px solid #ccc; border-top-color: var(--primary); border-radius: 50%; animation: spin 1s linear infinite; margin-right: 0.5rem; vertical-align: middle; } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| /* Tables */ | |
| .table-responsive { width: 100%; overflow-x: auto; margin-top: 1rem; border: 1px solid var(--border); border-radius: 6px; } | |
| table { width: 100%; border-collapse: collapse; min-width: 800px; } | |
| th, td { text-align: left; padding: 1rem; border-bottom: 1px solid var(--border); border-right: 1px solid var(--border); vertical-align: top; } | |
| th { background: #f3f4f6; font-weight: 600; position: sticky; top: 0; z-index: 10; } | |
| th:first-child { min-width: 250px; background: #e5e7eb; left: 0; z-index: 20; position: sticky; } | |
| td:first-child { background: #f9fafb; font-weight: 600; position: sticky; left: 0; z-index: 10; font-size: 0.85rem; word-break: break-all; } | |
| .model-header-repo { font-size: 0.75rem; color: var(--primary); font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; } | |
| .model-header-file { font-size: 0.95rem; word-break: break-all; margin-top: 0.25rem; } | |
| .val-block { | |
| white-space: pre-wrap; | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; | |
| font-size: 0.85rem; | |
| background: rgba(0,0,0,0.03); | |
| padding: 0.75rem; | |
| border-radius: 4px; | |
| max-height: 250px; | |
| overflow-y: auto; | |
| word-break: break-all; | |
| } | |
| .cell-changed { background-color: var(--diff-chg-bg) !important; } | |
| .cell-added { background-color: var(--diff-add-bg) !important; } | |
| .cell-missing { background-color: var(--diff-rm-bg) !important; color: var(--diff-rm-text); font-style: italic; } | |
| .cell-error { background-color: #fef2f2 !important; color: #b91c1c; } | |
| .tensor-shape { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.75rem; color: #4b5563; margin-bottom: 0.4rem; font-weight: 600; } | |
| .badge-quant { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.8rem; font-weight: 700; background: #e0e7ff; color: #3730a3; border: 1px solid #c7d2fe; font-family: monospace; } | |
| .badge-unknown { background: #fee2e2; color: #991b1b; border-color: #fecaca; } | |
| .error-msg { color: var(--diff-rm-text); font-weight: 600; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>GGUF Multi-Model Comparator</h1> | |
| <p class="subtitle">Side-by-side comparison of metadata, tensor shapes, and layer quantization levels.</p> | |
| <div class="card"> | |
| <div class="form-group"> | |
| <label for="refUrl">Reference GGUF URL</label> | |
| <input type="url" id="refUrl" placeholder="https://huggingface.co/username/repo/resolve/main/model.gguf" value="https://huggingface.co/bartowski/Tesslate_OmniCoder-9B-GGUF/resolve/main/Tesslate_OmniCoder-9B-Q8_0.gguf"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="targetUrls">Target GGUF URLs (One per line)</label> | |
| <textarea id="targetUrls" placeholder="https://huggingface.co/username/repo/resolve/main/model-v2.gguf"></textarea> | |
| </div> | |
| <div class="controls"> | |
| <button id="compareBtn">Compare Models</button> | |
| <label style="display:inline-flex; align-items:center; font-weight:normal; margin:0; cursor:pointer;"> | |
| <input type="checkbox" id="showUnchanged" style="margin-right:0.5rem;"> | |
| Show Unchanged Rows | |
| </label> | |
| </div> | |
| <div class="status-container" id="statusArea"></div> | |
| </div> | |
| <div id="resultsArea"></div> | |
| </div> | |
| <script type="module"> | |
| import { gguf } from "https://cdn.jsdelivr.net/npm/@huggingface/gguf@0.1.5/+esm"; | |
| const GGML_QUANT_TYPES = { | |
| 0: "F32", 1: "F16", 2: "Q4_0", 3: "Q4_1", 6: "Q5_0", 7: "Q5_1", 8: "Q8_0", 9: "Q8_1", | |
| 10: "Q2_K", 11: "Q3_K", 12: "Q4_K", 13: "Q5_K", 14: "Q6_K", 15: "Q8_K", 16: "IQ2_XXS", | |
| 17: "IQ2_XS", 18: "IQ3_XXS", 19: "IQ1_S", 20: "IQ4_NL", 21: "IQ3_S", 22: "IQ2_S", | |
| 23: "IQ4_XS", 24: "I8", 25: "I16", 26: "I32", 27: "I64", 28: "F64", 29: "IQ1_M", | |
| 30: "BF16", 31: "Q4_0_4_4", 32: "Q4_0_4_8", 33: "Q4_0_8_8", 34: "TQ1_0", 35: "TQ2_0" | |
| }; | |
| const btn = document.getElementById('compareBtn'); | |
| const statusArea = document.getElementById('statusArea'); | |
| const resultsArea = document.getElementById('resultsArea'); | |
| const showUnchangedCheckbox = document.getElementById('showUnchanged'); | |
| showUnchangedCheckbox.addEventListener('change', (e) => { | |
| const rows = document.querySelectorAll('.row-unchanged'); | |
| rows.forEach(row => { | |
| row.style.display = e.target.checked ? 'table-row' : 'none'; | |
| }); | |
| }); | |
| function cleanUrl(url) { | |
| url = url.trim(); | |
| if (url.includes('huggingface.co') && url.includes('/blob/')) { | |
| return url.replace('/blob/', '/resolve/'); | |
| } | |
| return url; | |
| } | |
| function extractModelInfo(url) { | |
| let repo = 'Unknown Repo'; | |
| let file = url.split('/').pop() || 'Unknown File'; | |
| try { | |
| const u = new URL(url); | |
| if (u.hostname === 'huggingface.co') { | |
| const parts = u.pathname.split('/').filter(Boolean); | |
| if (parts.length >= 2) repo = `${parts[0]}/${parts[1]}`; | |
| } | |
| } catch(e) {} | |
| return { repo, file }; | |
| } | |
| function escapeHTML(str) { | |
| return String(str).replace(/[&<>'"]/g, tag => | |
| ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[tag] || tag) | |
| ); | |
| } | |
| // UTILS FOR FILE SIZE | |
| function formatBytes(bytes) { | |
| if (!+bytes) return '0 Bytes'; | |
| const k = 1024; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; | |
| } | |
| function formatSizeDiff(refBytes, targetBytes) { | |
| if (!refBytes || !targetBytes) return ''; | |
| const diff = targetBytes - refBytes; | |
| if (diff === 0) return `<span style="color: #6b7280; font-weight: normal;">(Same size)</span>`; | |
| const sign = diff > 0 ? '+' : '-'; | |
| const color = diff > 0 ? '#991b1b' : '#166534'; // Red if larger, Green if smaller | |
| return `<span style="color: ${color}; font-weight: bold;">(${sign}${formatBytes(Math.abs(diff))})</span>`; | |
| } | |
| // Helper: JSON Replacer to prevent BigInt crash | |
| const bigIntReplacer = (k, v) => typeof v === 'bigint' ? v.toString() : v; | |
| function formatValue(val) { | |
| if (val === undefined || val === null) return '<i>null</i>'; | |
| if (Array.isArray(val) || ArrayBuffer.isView(val)) { | |
| const arr = Array.isArray(val) ? val : Array.from(val); | |
| if (arr.length > 25) { | |
| // FIXED: Included bigIntReplacer here to prevent the crash on array truncation | |
| const head = arr.slice(0, 10).map(v => JSON.stringify(v, bigIntReplacer)).join(',\n '); | |
| const tail = arr.slice(-3).map(v => JSON.stringify(v, bigIntReplacer)).join(',\n '); | |
| return `<div class="val-block">[ \n ${escapeHTML(head)},\n ...\n // ${arr.length - 13} items truncated\n ...\n ${escapeHTML(tail)}\n]</div>`; | |
| } | |
| } | |
| let str = JSON.stringify(val, bigIntReplacer, 2); | |
| if (typeof val === 'string') str = val; | |
| return `<div class="val-block">${escapeHTML(str)}</div>`; | |
| } | |
| function getComparableString(val) { | |
| return JSON.stringify(val, bigIntReplacer); | |
| } | |
| async function fetchGGUFData(url) { | |
| try { | |
| // Attempt to get file size via HEAD request (No download footprint) | |
| let fileSize = null; | |
| try { | |
| const headRes = await fetch(url, { method: 'HEAD' }); | |
| const cl = headRes.headers.get('content-length'); | |
| if (cl) fileSize = Number(cl); | |
| } catch(e) { console.warn("Could not fetch HEAD for size", e); } | |
| const { metadata, tensorInfos } = await gguf(url); | |
| return { metadata, tensorInfos, fileSize, success: true }; | |
| } catch (err) { | |
| return { error: err.message, success: false }; | |
| } | |
| } | |
| btn.addEventListener('click', async () => { | |
| const refUrlRaw = document.getElementById('refUrl').value; | |
| const targetUrlsRaw = document.getElementById('targetUrls').value; | |
| const urls = [refUrlRaw, ...targetUrlsRaw.split('\n')].map(cleanUrl).filter(u => u); | |
| if (urls.length < 2) { | |
| alert('Please provide a Reference URL and at least one Target URL.'); | |
| return; | |
| } | |
| btn.disabled = true; | |
| resultsArea.innerHTML = ''; | |
| const modelsData = []; | |
| for (let i = 0; i < urls.length; i++) { | |
| const label = i === 0 ? "Reference" : `Target ${i}`; | |
| statusArea.innerHTML = `<div class="spinner"></div> Fetching ${label} GGUF...`; | |
| const info = extractModelInfo(urls[i]); | |
| const data = await fetchGGUFData(urls[i]); | |
| modelsData.push({ url: urls[i], info, ...data, isRef: i === 0 }); | |
| } | |
| statusArea.innerHTML = `<span style="color: #166534;">✓ Data loaded. Rendering comparison...</span>`; | |
| setTimeout(() => { | |
| renderUnifiedTables(modelsData); | |
| statusArea.innerHTML = ''; | |
| btn.disabled = false; | |
| }, 100); | |
| }); | |
| function generateHeaderHTML(modelsData) { | |
| let html = `<tr><th>Attribute / Layer</th>`; | |
| modelsData.forEach(model => { | |
| let sizeBadge = ''; | |
| if (model.success && model.fileSize) { | |
| const sizeStr = formatBytes(model.fileSize); | |
| let diffHtml = ''; | |
| if (!model.isRef && modelsData[0].success && modelsData[0].fileSize) { | |
| diffHtml = ` ${formatSizeDiff(modelsData[0].fileSize, model.fileSize)}`; | |
| } | |
| sizeBadge = `<div style="margin-top: 0.5rem; font-size: 0.8rem; color: #4b5563;">📦 ${sizeStr}${diffHtml}</div>`; | |
| } | |
| html += ` | |
| <th> | |
| <div class="model-header-repo">${model.isRef ? '🛡️ REF: ' : ''}${escapeHTML(model.info.repo)}</div> | |
| <div class="model-header-file">${escapeHTML(model.info.file)}</div> | |
| ${sizeBadge} | |
| </th>`; | |
| }); | |
| html += `</tr>`; | |
| return html; | |
| } | |
| function renderUnifiedTables(modelsData) { | |
| const successfulModels = modelsData.filter(m => m.success); | |
| if (successfulModels.length === 0) { | |
| resultsArea.innerHTML = `<div class="card error-msg">Failed to load any models. Check URLs and CORS.</div>`; | |
| return; | |
| } | |
| const displayToggle = showUnchangedCheckbox.checked ? "table-row" : "none"; | |
| // --- 1. METADATA COMPARISON --- | |
| const allMetaKeys = new Set(); | |
| successfulModels.forEach(m => Object.keys(m.metadata).forEach(k => allMetaKeys.add(k))); | |
| const sortedMetaKeys = Array.from(allMetaKeys).sort(); | |
| let metaHtml = `<div class="card"><h2>Metadata Comparison</h2><div class="table-responsive"><table><thead>${generateHeaderHTML(modelsData)}</thead><tbody>`; | |
| sortedMetaKeys.forEach(key => { | |
| let isUnchanged = true; | |
| let rowCellsHtml = `<td><code>${escapeHTML(key)}</code></td>`; | |
| const refModel = modelsData[0]; | |
| const inRef = refModel.success && key in refModel.metadata; | |
| const refValStr = inRef ? getComparableString(refModel.metadata[key]) : null; | |
| modelsData.forEach((model) => { | |
| if (!model.success) { | |
| rowCellsHtml += `<td class="cell-error">Failed to load</td>`; | |
| isUnchanged = false; | |
| return; | |
| } | |
| const inModel = key in model.metadata; | |
| if (inRef !== inModel) isUnchanged = false; | |
| if (!inModel) { | |
| rowCellsHtml += `<td class="cell-missing">Missing</td>`; | |
| } else { | |
| const valStr = getComparableString(model.metadata[key]); | |
| if (inRef && valStr !== refValStr) isUnchanged = false; | |
| let cellClass = ""; | |
| if (!model.isRef) { | |
| if (!inRef) cellClass = "cell-added"; | |
| else if (valStr !== refValStr) cellClass = "cell-changed"; | |
| } | |
| rowCellsHtml += `<td class="${cellClass}">${formatValue(model.metadata[key])}</td>`; | |
| } | |
| }); | |
| const rowClass = isUnchanged ? "row-unchanged" : ""; | |
| const rowStyle = isUnchanged ? `display: ${displayToggle};` : ""; | |
| metaHtml += `<tr class="${rowClass}" style="${rowStyle}">${rowCellsHtml}</tr>`; | |
| }); | |
| metaHtml += `</tbody></table></div></div>`; | |
| // --- 2. TENSOR COMPARISON --- | |
| const allTensorNames = new Set(); | |
| const tensorMaps = modelsData.map(m => { | |
| const map = {}; | |
| if (m.success && m.tensorInfos) { | |
| m.tensorInfos.forEach(t => { | |
| const quantInt = t.dtype ?? t.type ?? t.ggmlType ?? t.ggml_type; | |
| const rawShape = t.shape || t.dimensions || []; | |
| const shapeArr = (Array.isArray(rawShape) || ArrayBuffer.isView(rawShape)) ? Array.from(rawShape).map(Number) : []; | |
| map[t.name] = { quantInt, shapeArr, raw: t }; | |
| allTensorNames.add(t.name); | |
| }); | |
| } | |
| return map; | |
| }); | |
| const sortedTensorNames = Array.from(allTensorNames).sort(); | |
| let tensorHtml = `<div class="card"><h2>Tensor Info (Shape & Quantization)</h2><div class="table-responsive"><table><thead>${generateHeaderHTML(modelsData)}</thead><tbody>`; | |
| sortedTensorNames.forEach(tName => { | |
| let isUnchanged = true; | |
| let rowCellsHtml = `<td><code>${escapeHTML(tName)}</code></td>`; | |
| const refMap = tensorMaps[0]; | |
| const inRef = tName in refMap; | |
| const refData = inRef ? refMap[tName] : null; | |
| const refShapeStr = inRef ? refData.shapeArr.join(' × ') : null; | |
| modelsData.forEach((model, idx) => { | |
| if (!model.success) { | |
| rowCellsHtml += `<td class="cell-error">N/A</td>`; | |
| isUnchanged = false; | |
| return; | |
| } | |
| const tMap = tensorMaps[idx]; | |
| const inModel = tName in tMap; | |
| if (inRef !== inModel) isUnchanged = false; | |
| if (!inModel) { | |
| rowCellsHtml += `<td class="cell-missing">Missing Layer</td>`; | |
| } else { | |
| const tData = tMap[tName]; | |
| const shapeStr = tData.shapeArr.length ? tData.shapeArr.join(' × ') : "Unknown Shape"; | |
| const isUnknownType = tData.quantInt === undefined; | |
| const quantName = isUnknownType ? "Unknown" : (GGML_QUANT_TYPES[tData.quantInt] || `TYPE_${tData.quantInt}`); | |
| if (inRef && tData.quantInt !== refData.quantInt) isUnchanged = false; | |
| if (inRef && shapeStr !== refShapeStr) isUnchanged = false; | |
| let cellClass = ""; | |
| if (!model.isRef) { | |
| if (!inRef) cellClass = "cell-added"; | |
| else if (tData.quantInt !== refData.quantInt || shapeStr !== refShapeStr) cellClass = "cell-changed"; | |
| } | |
| rowCellsHtml += `<td class="${cellClass}"> | |
| <div class="tensor-shape">[ ${escapeHTML(shapeStr)} ]</div> | |
| <span class="badge-quant ${isUnknownType ? 'badge-unknown' : ''}">${escapeHTML(quantName)}</span> | |
| ${isUnknownType ? `<div style="font-size: 0.7rem; color: #991b1b; margin-top:0.4rem; word-break:break-all;"><strong>Keys found:</strong> ${escapeHTML(Object.keys(tData.raw).join(', '))}</div>` : ''} | |
| </td>`; | |
| } | |
| }); | |
| const rowClass = isUnchanged ? "row-unchanged" : ""; | |
| const rowStyle = isUnchanged ? `display: ${displayToggle};` : ""; | |
| tensorHtml += `<tr class="${rowClass}" style="${rowStyle}">${rowCellsHtml}</tr>`; | |
| }); | |
| tensorHtml += `</tbody></table></div></div>`; | |
| resultsArea.innerHTML = metaHtml + tensorHtml; | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment