Last active
September 13, 2025 22:57
-
-
Save lardratboy/a07a64b2243cc492a8cb21bc23aa76b0 to your computer and use it in GitHub Desktop.
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 Information Dump</title> | |
| <style> | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Courier New', monospace; | |
| margin: 0; | |
| padding: 20px; | |
| background: #1a1a1a; | |
| color: #e0e0e0; | |
| line-height: 1.4; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| } | |
| header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| padding: 20px; | |
| background: #2d2d2d; | |
| border-radius: 8px; | |
| border: 2px solid #444; | |
| } | |
| h1 { | |
| color: #4CAF50; | |
| margin: 0; | |
| font-size: 2em; | |
| } | |
| .file-input-section { | |
| margin-bottom: 30px; | |
| padding: 20px; | |
| background: #2d2d2d; | |
| border-radius: 8px; | |
| border: 2px solid #444; | |
| } | |
| .file-input { | |
| width: 100%; | |
| padding: 10px; | |
| background: #1a1a1a; | |
| color: #e0e0e0; | |
| border: 2px solid #666; | |
| border-radius: 4px; | |
| font-family: inherit; | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 20px; | |
| align-items: center; | |
| margin: 20px 0; | |
| flex-wrap: wrap; | |
| } | |
| .control-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| label { | |
| color: #4CAF50; | |
| font-weight: bold; | |
| } | |
| input[type="number"], select { | |
| padding: 5px; | |
| background: #1a1a1a; | |
| color: #e0e0e0; | |
| border: 1px solid #666; | |
| border-radius: 4px; | |
| font-family: inherit; | |
| } | |
| .info-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| } | |
| .info-panel { | |
| background: #2d2d2d; | |
| padding: 20px; | |
| border-radius: 8px; | |
| border: 2px solid #444; | |
| } | |
| .info-panel h3 { | |
| color: #4CAF50; | |
| margin-top: 0; | |
| border-bottom: 1px solid #666; | |
| padding-bottom: 10px; | |
| } | |
| .tensor-list { | |
| max-height: 300px; | |
| overflow-y: auto; | |
| background: #1a1a1a; | |
| padding: 10px; | |
| border-radius: 4px; | |
| border: 1px solid #666; | |
| } | |
| .tensor-item { | |
| padding: 8px; | |
| margin: 4px 0; | |
| background: #333; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| } | |
| .tensor-item:hover { | |
| background: #444; | |
| } | |
| .tensor-item.selected { | |
| background: #4CAF50; | |
| color: #000; | |
| } | |
| .tensor-name { | |
| font-weight: bold; | |
| color: #4CAF50; | |
| } | |
| .tensor-details { | |
| font-size: 0.9em; | |
| color: #ccc; | |
| margin-top: 4px; | |
| } | |
| .hexdump-container { | |
| background: #2d2d2d; | |
| padding: 20px; | |
| border-radius: 8px; | |
| border: 2px solid #444; | |
| } | |
| .hexdump { | |
| background: #1a1a1a; | |
| padding: 15px; | |
| border-radius: 4px; | |
| border: 1px solid #666; | |
| font-family: 'Courier New', monospace; | |
| font-size: 14px; | |
| white-space: pre; | |
| overflow-x: auto; | |
| max-height: 500px; | |
| overflow-y: auto; | |
| } | |
| .hex-offset { | |
| color: #888; | |
| } | |
| .hex-bytes { | |
| color: #4CAF50; | |
| } | |
| .hex-ascii { | |
| color: #FFA500; | |
| } | |
| .status { | |
| padding: 10px; | |
| margin: 10px 0; | |
| border-radius: 4px; | |
| border-left: 4px solid #4CAF50; | |
| background: #2d2d2d; | |
| } | |
| .error { | |
| border-left-color: #f44336; | |
| background: #3d2d2d; | |
| color: #ffcccb; | |
| } | |
| .export-button, .rgba-button, .quantized-button { | |
| background: #4CAF50; | |
| color: #000; | |
| border: none; | |
| padding: 10px 20px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-family: inherit; | |
| font-weight: bold; | |
| margin: 5px 5px 5px 0; | |
| transition: background 0.2s; | |
| } | |
| .rgba-button { | |
| background: #FF9800; | |
| } | |
| .quantized-button { | |
| background: #9C27B0; | |
| } | |
| .export-button:hover { | |
| background: #45a049; | |
| } | |
| .rgba-button:hover { | |
| background: #F57C00; | |
| } | |
| .quantized-button:hover { | |
| background: #7B1FA2; | |
| } | |
| .export-button:disabled, .rgba-button:disabled, .quantized-button:disabled { | |
| background: #666; | |
| color: #999; | |
| cursor: not-allowed; | |
| } | |
| .metadata-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 2fr; | |
| gap: 10px; | |
| font-size: 0.9em; | |
| } | |
| .metadata-key { | |
| color: #4CAF50; | |
| font-weight: bold; | |
| } | |
| .metadata-value { | |
| color: #e0e0e0; | |
| word-break: break-all; | |
| } | |
| .rgba-viewer { | |
| background: #2d2d2d; | |
| padding: 20px; | |
| border-radius: 8px; | |
| border: 2px solid #444; | |
| margin-top: 20px; | |
| display: none; | |
| } | |
| .rgba-viewer h3 { | |
| color: #FF9800; | |
| margin-top: 0; | |
| border-bottom: 1px solid #666; | |
| padding-bottom: 10px; | |
| } | |
| .quantized-viewer { | |
| background: #2d2d2d; | |
| padding: 20px; | |
| border-radius: 8px; | |
| border: 2px solid #444; | |
| margin-top: 20px; | |
| display: none; | |
| } | |
| .quantized-viewer h3 { | |
| color: #9C27B0; | |
| margin-top: 0; | |
| border-bottom: 1px solid #666; | |
| padding-bottom: 10px; | |
| } | |
| .rgba-canvas-container, .quantized-canvas-container { | |
| text-align: center; | |
| background: #1a1a1a; | |
| padding: 20px; | |
| border-radius: 4px; | |
| border: 1px solid #666; | |
| } | |
| .rgba-canvas, .quantized-canvas { | |
| max-width: 100%; | |
| height: auto; | |
| border: 2px solid #666; | |
| image-rendering: pixelated; | |
| } | |
| .rgba-info, .quantized-info { | |
| margin-top: 15px; | |
| padding: 10px; | |
| background: #333; | |
| border-radius: 4px; | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 20px; | |
| background: #1a1a1a; | |
| border-radius: 10px; | |
| margin: 10px 0; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #4CAF50, #45a049); | |
| width: 0%; | |
| transition: width 0.3s ease; | |
| } | |
| .stratification-controls { | |
| margin: 15px 0; | |
| padding: 15px; | |
| background: #1a1a1a; | |
| border-radius: 4px; | |
| border: 1px solid #666; | |
| } | |
| .stratification-controls h4 { | |
| color: #9C27B0; | |
| margin: 0 0 10px 0; | |
| font-size: 14px; | |
| } | |
| .radio-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .radio-option { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .radio-option input[type="radio"] { | |
| accent-color: #9C27B0; | |
| } | |
| .radio-option label { | |
| font-size: 12px; | |
| color: #ccc; | |
| font-weight: normal; | |
| } | |
| @media (max-width: 768px) { | |
| .info-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .controls { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| .metadata-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>🔬 GGUF Information Dump</h1> | |
| <p>Analyze GGUF model files with metadata, tensor information, hexdump visualization, and stratified quantized block expansion</p> | |
| </header> | |
| <div class="file-input-section"> | |
| <input type="file" id="fileInput" class="file-input" accept=".gguf,.bin"> | |
| <div class="controls"> | |
| <div class="control-group"> | |
| <label for="hexdumpSize">Hexdump Size:</label> | |
| <input type="number" id="hexdumpSize" min="64" max="4096" value="512" step="64"> | |
| <span>bytes</span> | |
| </div> | |
| <div class="control-group"> | |
| <label for="hexdumpOffset">Offset:</label> | |
| <input type="number" id="hexdumpOffset" min="0" value="0" step="16"> | |
| </div> | |
| <div class="control-group"> | |
| <label for="bytesPerLine">Bytes per line:</label> | |
| <select id="bytesPerLine"> | |
| <option value="8">8</option> | |
| <option value="16" selected>16</option> | |
| <option value="32">32</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="statusContainer"></div> | |
| <div class="info-grid" id="infoGrid" style="display: none;"> | |
| <div class="info-panel"> | |
| <h3>📋 File Information</h3> | |
| <div id="fileInfo"></div> | |
| </div> | |
| <div class="info-panel"> | |
| <h3>🏷️ GGUF Header</h3> | |
| <div id="ggufHeader"></div> | |
| </div> | |
| </div> | |
| <div class="info-grid" id="metadataGrid" style="display: none;"> | |
| <div class="info-panel"> | |
| <h3>📊 Metadata</h3> | |
| <div id="metadataList" class="tensor-list"></div> | |
| </div> | |
| <div class="info-panel"> | |
| <h3>🧮 Tensors</h3> | |
| <div class="tensor-list" id="tensorList"></div> | |
| </div> | |
| </div> | |
| <div class="info-grid" id="tensorGrid" style="display: none;"> | |
| <div class="info-panel"> | |
| <h3>🔍 Selected Tensor Details</h3> | |
| <div id="selectedTensorInfo">Select a tensor to view details</div> | |
| <button id="exportButton" class="export-button" style="display: none;" disabled> | |
| 💾 Export Tensor as .bin | |
| </button> | |
| <button id="rgbaButton" class="rgba-button" style="display: none;" disabled> | |
| 🎨 View as RGBA Image | |
| </button> | |
| <button id="quantizedButton" class="quantized-button" style="display: none;" disabled> | |
| 🔢 View Stratified Quantized RGBA | |
| </button> | |
| </div> | |
| <div class="info-panel"> | |
| <h3>📈 Tensor Statistics</h3> | |
| <div id="tensorStats">Select a tensor to view statistics</div> | |
| </div> | |
| </div> | |
| <div class="rgba-viewer" id="rgbaViewer"> | |
| <h3>🎨 RGBA Image Viewer</h3> | |
| <div class="rgba-canvas-container"> | |
| <canvas id="rgbaCanvas" class="rgba-canvas"></canvas> | |
| <div class="rgba-info" id="rgbaInfo"></div> | |
| </div> | |
| </div> | |
| <div class="quantized-viewer" id="quantizedViewer"> | |
| <h3>🔢 Stratified Quantized RGBA Viewer</h3> | |
| <div class="stratification-controls"> | |
| <h4>Channel Stratification Method:</h4> | |
| <div class="radio-group"> | |
| <div class="radio-option"> | |
| <input type="radio" id="stratifySequential" name="stratification" value="sequential" checked> | |
| <label for="stratifySequential">Sequential RGBA Cycling (R,G,B,A...R,G,B,A...)</label> | |
| </div> | |
| <div class="radio-option"> | |
| <input type="radio" id="stratifyChunked" name="stratification" value="chunked"> | |
| <label for="stratifyChunked">Chunked RGBA Stratification (RRR...R,GGG...G,BBB...B,AAA...A)</label> | |
| </div> | |
| <div class="radio-option"> | |
| <input type="radio" id="stratifyBands" name="stratification" value="bands"> | |
| <label for="stratifyBands">Horizontal Band Stratification (R|G|B|A as separate strips)</label> | |
| </div> | |
| <div class="radio-option"> | |
| <input type="radio" id="stratifyGrayscale" name="stratification" value="grayscale"> | |
| <label for="stratifyGrayscale">Normalized Grayscale (traditional)</label> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="progress-bar" id="progressBar" style="display: none;"> | |
| <div class="progress-fill" id="progressFill"></div> | |
| </div> | |
| <div class="quantized-canvas-container"> | |
| <canvas id="quantizedCanvas" class="quantized-canvas"></canvas> | |
| <div class="quantized-info" id="quantizedInfo"></div> | |
| </div> | |
| </div> | |
| <div class="hexdump-container" id="hexdumpContainer" style="display: none;"> | |
| <h3>🔢 Hexdump</h3> | |
| <div class="hexdump" id="hexdump"></div> | |
| </div> | |
| </div> | |
| <script> | |
| class GGUFDump { | |
| constructor() { | |
| this.fileData = null; | |
| this.header = null; | |
| this.metadata = null; | |
| this.tensors = null; | |
| this.selectedTensor = null; | |
| this.tensorDataOffset = 0; | |
| this.lastExpandedData = null; // Cache for re-rendering with different stratification | |
| this.initializeEventListeners(); | |
| } | |
| initializeEventListeners() { | |
| document.getElementById('fileInput').addEventListener('change', (e) => { | |
| this.handleFileInput(e); | |
| }); | |
| document.getElementById('hexdumpSize').addEventListener('input', () => { | |
| this.updateHexdump(); | |
| }); | |
| document.getElementById('hexdumpOffset').addEventListener('input', () => { | |
| this.updateHexdump(); | |
| }); | |
| document.getElementById('bytesPerLine').addEventListener('change', () => { | |
| this.updateHexdump(); | |
| }); | |
| document.getElementById('exportButton').addEventListener('click', () => { | |
| this.exportSelectedTensor(); | |
| }); | |
| document.getElementById('rgbaButton').addEventListener('click', () => { | |
| this.viewTensorAsRGBA(); | |
| }); | |
| document.getElementById('quantizedButton').addEventListener('click', () => { | |
| this.viewQuantizedAsRGBA(); | |
| }); | |
| // Add stratification method change listeners | |
| document.querySelectorAll('input[name="stratification"]').forEach(radio => { | |
| radio.addEventListener('change', () => { | |
| if (this.lastExpandedData) { | |
| this.renderStratifiedRGBA(this.lastExpandedData); | |
| } | |
| }); | |
| }); | |
| } | |
| showStatus(message, isError = false) { | |
| const container = document.getElementById('statusContainer'); | |
| container.innerHTML = `<div class="status ${isError ? 'error' : ''}">${message}</div>`; | |
| } | |
| updateProgress(percent) { | |
| const progressBar = document.getElementById('progressBar'); | |
| const progressFill = document.getElementById('progressFill'); | |
| if (percent >= 0) { | |
| progressBar.style.display = 'block'; | |
| progressFill.style.width = percent + '%'; | |
| } else { | |
| progressBar.style.display = 'none'; | |
| } | |
| } | |
| async handleFileInput(event) { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| this.showStatus('📖 Reading file...'); | |
| try { | |
| const arrayBuffer = await file.arrayBuffer(); | |
| this.fileData = new Uint8Array(arrayBuffer); | |
| this.showStatus('🔍 Analyzing GGUF structure...'); | |
| await this.analyzeGGUF(file); | |
| } catch (error) { | |
| console.error('Error reading file:', error); | |
| this.showStatus('❌ Error reading file: ' + error.message, true); | |
| } | |
| } | |
| async analyzeGGUF(file) { | |
| try { | |
| if (!this.parseGGUFHeader()) { | |
| throw new Error('Not a valid GGUF file'); | |
| } | |
| this.parseMetadata(); | |
| this.parseTensorInfo(); | |
| this.showStatus('✅ GGUF file parsed successfully'); | |
| this.displayFileInfo(file); | |
| this.displayMetadata(); | |
| this.displayTensors(); | |
| this.updateHexdump(); | |
| } catch (error) { | |
| console.error('Error analyzing GGUF file:', error); | |
| this.showStatus('❌ Error analyzing GGUF file: ' + error.message, true); | |
| } | |
| } | |
| parseGGUFHeader() { | |
| if (this.fileData.length < 16) return false; | |
| const view = new DataView(this.fileData.buffer); | |
| // Check magic number "GGUF" | |
| const magic = new TextDecoder().decode(this.fileData.slice(0, 4)); | |
| if (magic !== 'GGUF') return false; | |
| // Parse header | |
| this.header = { | |
| magic: magic, | |
| version: view.getUint32(4, true), | |
| tensorCount: view.getBigUint64(8, true), | |
| metadataKVCount: view.getBigUint64(16, true) | |
| }; | |
| this.offset = 24; // Start after header | |
| return true; | |
| } | |
| parseMetadata() { | |
| this.metadata = {}; | |
| const metadataCount = Number(this.header.metadataKVCount); | |
| for (let i = 0; i < metadataCount; i++) { | |
| const key = this.readString(); | |
| const valueType = this.readUint32(); | |
| const value = this.readValue(valueType); | |
| this.metadata[key] = { type: valueType, value: value }; | |
| } | |
| } | |
| parseTensorInfo() { | |
| this.tensors = []; | |
| const tensorCount = Number(this.header.tensorCount); | |
| for (let i = 0; i < tensorCount; i++) { | |
| const name = this.readString(); | |
| const nDims = this.readUint32(); | |
| const shape = []; | |
| for (let j = 0; j < nDims; j++) { | |
| shape.push(Number(this.readUint64())); | |
| } | |
| const type = this.readUint32(); | |
| const offset = this.readUint64(); | |
| this.tensors.push({ | |
| name: name, | |
| shape: shape, | |
| type: type, | |
| typeString: this.getTypeString(type), | |
| offset: Number(offset), | |
| nDims: nDims | |
| }); | |
| } | |
| // Calculate tensor data offset (aligned to 32 bytes) | |
| this.tensorDataOffset = Math.ceil(this.offset / 32) * 32; | |
| } | |
| readString() { | |
| const length = this.readUint64(); | |
| const str = new TextDecoder().decode( | |
| this.fileData.slice(this.offset, this.offset + Number(length)) | |
| ); | |
| this.offset += Number(length); | |
| return str; | |
| } | |
| readUint32() { | |
| const value = new DataView(this.fileData.buffer).getUint32(this.offset, true); | |
| this.offset += 4; | |
| return value; | |
| } | |
| readUint64() { | |
| const value = new DataView(this.fileData.buffer).getBigUint64(this.offset, true); | |
| this.offset += 8; | |
| return value; | |
| } | |
| readValue(type) { | |
| switch (type) { | |
| case 0: // UINT8 | |
| return this.fileData[this.offset++]; | |
| case 1: // INT8 | |
| return new DataView(this.fileData.buffer).getInt8(this.offset++); | |
| case 2: // UINT16 | |
| const uint16 = new DataView(this.fileData.buffer).getUint16(this.offset, true); | |
| this.offset += 2; | |
| return uint16; | |
| case 3: // INT16 | |
| const int16 = new DataView(this.fileData.buffer).getInt16(this.offset, true); | |
| this.offset += 2; | |
| return int16; | |
| case 4: // UINT32 | |
| return this.readUint32(); | |
| case 5: // INT32 | |
| const int32 = new DataView(this.fileData.buffer).getInt32(this.offset, true); | |
| this.offset += 4; | |
| return int32; | |
| case 6: // FLOAT32 | |
| const float32 = new DataView(this.fileData.buffer).getFloat32(this.offset, true); | |
| this.offset += 4; | |
| return float32; | |
| case 7: // BOOL | |
| return Boolean(this.fileData[this.offset++]); | |
| case 8: // STRING | |
| return this.readString(); | |
| case 9: // ARRAY | |
| const arrayType = this.readUint32(); | |
| const arrayLength = Number(this.readUint64()); | |
| const array = []; | |
| for (let i = 0; i < arrayLength; i++) { | |
| array.push(this.readValue(arrayType)); | |
| } | |
| return array; | |
| case 10: // UINT64 | |
| return Number(this.readUint64()); | |
| case 11: // INT64 | |
| const int64 = new DataView(this.fileData.buffer).getBigInt64(this.offset, true); | |
| this.offset += 8; | |
| return Number(int64); | |
| case 12: // FLOAT64 | |
| const float64 = new DataView(this.fileData.buffer).getFloat64(this.offset, true); | |
| this.offset += 8; | |
| return float64; | |
| default: | |
| throw new Error(`Unknown value type: ${type}`); | |
| } | |
| } | |
| getTypeString(type) { | |
| const types = { | |
| 0: 'F32', 1: 'F16', 2: 'Q4_0', 3: 'Q4_1', 4: 'Q5_0', 5: 'Q5_1', | |
| 6: 'Q8_0', 7: 'Q8_1', 8: 'Q2_K', 9: 'Q3_K', 10: 'Q4_K', | |
| 11: 'Q5_K', 12: 'Q6_K', 13: 'Q8_K', 14: 'IQ2_XXS', 15: 'IQ2_XS', | |
| 16: 'IQ3_XXS', 17: 'IQ1_S', 18: 'IQ4_NL', 19: 'IQ3_S', 20: 'IQ2_S', | |
| 21: 'IQ4_XS', 22: 'I8', 23: 'I16', 24: 'I32', 25: 'I64', | |
| 26: 'F64', 27: 'IQ1_M' | |
| }; | |
| return types[type] || `UNKNOWN(${type})`; | |
| } | |
| displayFileInfo(file) { | |
| const fileInfo = document.getElementById('fileInfo'); | |
| const ggufHeader = document.getElementById('ggufHeader'); | |
| const fileSizeKB = (file.size / 1024).toFixed(2); | |
| const fileSizeMB = (file.size / (1024 * 1024)).toFixed(2); | |
| fileInfo.innerHTML = ` | |
| <div><strong>Name:</strong> ${file.name}</div> | |
| <div><strong>Size:</strong> ${fileSizeKB} KB (${fileSizeMB} MB)</div> | |
| <div><strong>Type:</strong> ${file.type || 'application/octet-stream'}</div> | |
| <div><strong>Last Modified:</strong> ${new Date(file.lastModified).toLocaleString()}</div> | |
| <div><strong>Tensor Data Offset:</strong> ${this.tensorDataOffset} bytes</div> | |
| `; | |
| ggufHeader.innerHTML = ` | |
| <div><strong>Magic:</strong> ${this.header.magic}</div> | |
| <div><strong>Version:</strong> ${this.header.version}</div> | |
| <div><strong>Tensor Count:</strong> ${this.header.tensorCount}</div> | |
| <div><strong>Metadata KV Count:</strong> ${this.header.metadataKVCount}</div> | |
| `; | |
| document.getElementById('infoGrid').style.display = 'grid'; | |
| } | |
| displayMetadata() { | |
| const metadataList = document.getElementById('metadataList'); | |
| metadataList.innerHTML = ''; | |
| if (Object.keys(this.metadata).length === 0) { | |
| metadataList.innerHTML = '<div>No metadata found</div>'; | |
| return; | |
| } | |
| for (const [key, data] of Object.entries(this.metadata)) { | |
| const metadataItem = document.createElement('div'); | |
| metadataItem.className = 'tensor-item'; | |
| let valueStr = ''; | |
| if (Array.isArray(data.value)) { | |
| valueStr = `[${data.value.slice(0, 5).join(', ')}${data.value.length > 5 ? '...' : ''}]`; | |
| } else if (typeof data.value === 'string' && data.value.length > 50) { | |
| valueStr = data.value.substring(0, 50) + '...'; | |
| } else { | |
| valueStr = String(data.value); | |
| } | |
| metadataItem.innerHTML = ` | |
| <div class="tensor-name">${key}</div> | |
| <div class="tensor-details"> | |
| Type: ${this.getValueTypeString(data.type)}<br> | |
| Value: ${valueStr} | |
| </div> | |
| `; | |
| metadataList.appendChild(metadataItem); | |
| } | |
| document.getElementById('metadataGrid').style.display = 'grid'; | |
| } | |
| getValueTypeString(type) { | |
| const types = { | |
| 0: 'UINT8', 1: 'INT8', 2: 'UINT16', 3: 'INT16', 4: 'UINT32', | |
| 5: 'INT32', 6: 'FLOAT32', 7: 'BOOL', 8: 'STRING', 9: 'ARRAY', | |
| 10: 'UINT64', 11: 'INT64', 12: 'FLOAT64' | |
| }; | |
| return types[type] || `UNKNOWN(${type})`; | |
| } | |
| displayTensors() { | |
| const tensorList = document.getElementById('tensorList'); | |
| tensorList.innerHTML = ''; | |
| if (this.tensors.length === 0) { | |
| tensorList.innerHTML = '<div>No tensors found</div>'; | |
| return; | |
| } | |
| this.tensors.forEach((tensor, index) => { | |
| const tensorItem = document.createElement('div'); | |
| tensorItem.className = 'tensor-item'; | |
| const totalElements = tensor.shape.reduce((a, b) => a * b, 1); | |
| const sizeBytes = this.calculateTensorSize(tensor); | |
| // Add indicators for compatible tensors | |
| const rgbaCompatible = this.isRGBACompatible(tensor); | |
| const quantizedCompatible = this.isQuantizedCompatible(tensor); | |
| let indicators = ''; | |
| if (rgbaCompatible) indicators += ' 🎨'; | |
| if (quantizedCompatible) indicators += ' 🔢'; | |
| tensorItem.innerHTML = ` | |
| <div class="tensor-name">${tensor.name}${indicators}</div> | |
| <div class="tensor-details"> | |
| Shape: [${tensor.shape.join(', ')}]<br> | |
| Type: ${tensor.typeString}<br> | |
| Elements: ${totalElements.toLocaleString()}<br> | |
| Size: ${(sizeBytes / 1024).toFixed(2)} KB | |
| </div> | |
| `; | |
| tensorItem.addEventListener('click', () => { | |
| this.selectTensor(tensor, tensorItem); | |
| }); | |
| tensorList.appendChild(tensorItem); | |
| }); | |
| } | |
| isRGBACompatible(tensor) { | |
| // Check if tensor is 2D and has 4 bytes per element | |
| return tensor.nDims === 2 && this.getBytesPerElement(tensor.type) === 4; | |
| } | |
| isQuantizedCompatible(tensor) { | |
| // Check if tensor is quantized (supports dequantization) | |
| const supportedTypes = [2, 3, 6, 8, 9, 10]; // Q4_0, Q4_1, Q8_0, Q2_K, Q3_K, Q4_K | |
| return supportedTypes.includes(tensor.type); | |
| } | |
| calculateTensorSize(tensor) { | |
| const totalElements = tensor.shape.reduce((a, b) => a * b, 1); | |
| const bytesPerElement = this.getBytesPerElement(tensor.type); | |
| return totalElements * bytesPerElement; | |
| } | |
| getBytesPerElement(type) { | |
| // Approximate bytes per element for quantized types | |
| const sizes = { | |
| 0: 4, 1: 2, 2: 0.5, 3: 0.5625, 4: 0.5, 5: 0.5625, | |
| 6: 1, 7: 1, 8: 0.25, 9: 0.375, 10: 0.5, | |
| 11: 0.5625, 12: 0.75, 13: 1, 22: 1, 23: 2, | |
| 24: 4, 25: 8, 26: 8 | |
| }; | |
| return sizes[type] || 4; | |
| } | |
| selectTensor(tensor, element) { | |
| document.querySelectorAll('.tensor-item').forEach(item => { | |
| item.classList.remove('selected'); | |
| }); | |
| element.classList.add('selected'); | |
| this.selectedTensor = tensor; | |
| this.lastExpandedData = null; // Clear cache when selecting new tensor | |
| const selectedInfo = document.getElementById('selectedTensorInfo'); | |
| const tensorStats = document.getElementById('tensorStats'); | |
| const totalElements = tensor.shape.reduce((a, b) => a * b, 1); | |
| const sizeBytes = this.calculateTensorSize(tensor); | |
| const actualOffset = this.tensorDataOffset + tensor.offset; | |
| const rgbaCompatible = this.isRGBACompatible(tensor); | |
| const quantizedCompatible = this.isQuantizedCompatible(tensor); | |
| selectedInfo.innerHTML = ` | |
| <div><strong>Name:</strong> ${tensor.name}</div> | |
| <div><strong>Shape:</strong> [${tensor.shape.join(', ')}]</div> | |
| <div><strong>Dimensions:</strong> ${tensor.nDims}D</div> | |
| <div><strong>Data Type:</strong> ${tensor.typeString}</div> | |
| <div><strong>Elements:</strong> ${totalElements.toLocaleString()}</div> | |
| <div><strong>Size:</strong> ${(sizeBytes / 1024).toFixed(2)} KB</div> | |
| <div><strong>File Offset:</strong> ${actualOffset}</div> | |
| ${rgbaCompatible ? '<div><strong>RGBA Compatible:</strong> ✅ Yes</div>' : ''} | |
| ${quantizedCompatible ? '<div><strong>Quantized Expandable:</strong> ✅ Yes</div>' : ''} | |
| `; | |
| tensorStats.innerHTML = ` | |
| <div><strong>Memory Layout:</strong> Row-major</div> | |
| <div><strong>Bytes per Element:</strong> ${this.getBytesPerElement(tensor.type)}</div> | |
| <div><strong>Quantization:</strong> ${this.isQuantized(tensor.type) ? 'Yes' : 'No'}</div> | |
| <div><strong>Compression Ratio:</strong> ${this.getCompressionRatio(tensor.type)}x</div> | |
| ${quantizedCompatible ? `<div><strong>Block Size:</strong> ${this.getBlockSize(tensor.type)} elements</div>` : ''} | |
| `; | |
| // Update hexdump to show tensor data | |
| document.getElementById('hexdumpOffset').value = actualOffset; | |
| this.updateHexdump(); | |
| // Show export button | |
| const exportButton = document.getElementById('exportButton'); | |
| exportButton.style.display = 'block'; | |
| exportButton.disabled = false; | |
| // Show RGBA button if compatible | |
| const rgbaButton = document.getElementById('rgbaButton'); | |
| if (rgbaCompatible) { | |
| rgbaButton.style.display = 'block'; | |
| rgbaButton.disabled = false; | |
| } else { | |
| rgbaButton.style.display = 'none'; | |
| document.getElementById('rgbaViewer').style.display = 'none'; | |
| } | |
| // Show quantized button if compatible | |
| const quantizedButton = document.getElementById('quantizedButton'); | |
| if (quantizedCompatible) { | |
| quantizedButton.style.display = 'block'; | |
| quantizedButton.disabled = false; | |
| } else { | |
| quantizedButton.style.display = 'none'; | |
| document.getElementById('quantizedViewer').style.display = 'none'; | |
| } | |
| document.getElementById('tensorGrid').style.display = 'grid'; | |
| } | |
| isQuantized(type) { | |
| return type >= 2 && type <= 21; // Q4_0 through IQ1_M | |
| } | |
| getCompressionRatio(type) { | |
| const bytesPerElement = this.getBytesPerElement(type); | |
| return (4 / bytesPerElement).toFixed(1); // Compared to F32 | |
| } | |
| getBlockSize(type) { | |
| const blockSizes = { | |
| 2: 32, // Q4_0 | |
| 3: 32, // Q4_1 | |
| 6: 32, // Q8_0 | |
| 8: 256, // Q2_K | |
| 9: 256, // Q3_K | |
| 10: 256, // Q4_K | |
| }; | |
| return blockSizes[type] || 32; | |
| } | |
| async viewQuantizedAsRGBA() { | |
| if (!this.selectedTensor || !this.fileData) { | |
| this.showStatus('❌ No tensor selected or no file loaded', true); | |
| return; | |
| } | |
| const tensor = this.selectedTensor; | |
| if (!this.isQuantizedCompatible(tensor)) { | |
| this.showStatus('❌ Selected tensor is not quantized or not supported for expansion', true); | |
| return; | |
| } | |
| try { | |
| this.showStatus('🔢 Expanding quantized tensor...'); | |
| this.updateProgress(10); | |
| const actualOffset = this.tensorDataOffset + tensor.offset; | |
| // Dequantize the tensor if not cached | |
| if (!this.lastExpandedData) { | |
| this.lastExpandedData = await this.dequantizeTensor(tensor, actualOffset); | |
| this.updateProgress(70); | |
| } | |
| // Render with current stratification method | |
| this.renderStratifiedRGBA(this.lastExpandedData); | |
| this.updateProgress(-1); | |
| } catch (error) { | |
| console.error('Error viewing quantized tensor as RGBA:', error); | |
| this.updateProgress(-1); | |
| this.showStatus('❌ Error viewing quantized tensor as RGBA: ' + error.message, true); | |
| } | |
| } | |
| renderStratifiedRGBA(expandedData) { | |
| const stratificationMethod = document.querySelector('input[name="stratification"]:checked').value; | |
| const totalElements = expandedData.length; | |
| // Calculate optimal dimensions for visualization | |
| const { width, height } = this.calculateOptimalDimensions(totalElements); | |
| // Convert based on stratification method | |
| let rgbaData; | |
| switch (stratificationMethod) { | |
| case 'sequential': | |
| rgbaData = this.convertToSequentialRGBA(expandedData, width, height); | |
| break; | |
| case 'chunked': | |
| rgbaData = this.convertToChunkedRGBA(expandedData, width, height); | |
| break; | |
| case 'grayscale': | |
| default: | |
| rgbaData = this.convertToGrayscaleRGBA(expandedData, width, height); | |
| break; | |
| } | |
| // Render to canvas | |
| const canvas = document.getElementById('quantizedCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = width; | |
| canvas.height = height; | |
| const imageData = ctx.createImageData(width, height); | |
| for (let i = 0; i < rgbaData.length; i++) { | |
| imageData.data[i] = rgbaData[i]; | |
| } | |
| ctx.putImageData(imageData, 0, 0); | |
| // Show viewer and update info | |
| const viewer = document.getElementById('quantizedViewer'); | |
| const info = document.getElementById('quantizedInfo'); | |
| const stratificationDesc = this.getStratificationDescription(stratificationMethod); | |
| info.innerHTML = ` | |
| <div><strong>Original Tensor:</strong> ${this.selectedTensor.name}</div> | |
| <div><strong>Original Type:</strong> ${this.selectedTensor.typeString}</div> | |
| <div><strong>Expanded Elements:</strong> ${totalElements.toLocaleString()}</div> | |
| <div><strong>Display Dimensions:</strong> ${width} × ${height}</div> | |
| <div><strong>Stratification Method:</strong> ${stratificationDesc}</div> | |
| <div><strong>Block Size:</strong> ${this.getBlockSize(this.selectedTensor.type)} elements</div> | |
| `; | |
| viewer.style.display = 'block'; | |
| this.showStatus(`✅ Displayed stratified "${this.selectedTensor.name}" as RGBA (${width}×${height})`); | |
| } | |
| getStratificationDescription(method) { | |
| switch (method) { | |
| case 'sequential': | |
| return 'Sequential RGBA cycling - values cycle through R→G→B→A channels'; | |
| case 'chunked': | |
| return 'Chunked RGBA stratification - data split into R, G, B, A quarters'; | |
| case 'grayscale': | |
| return 'Normalized grayscale - traditional min-max normalization'; | |
| default: | |
| return 'Unknown method'; | |
| } | |
| } | |
| convertToSequentialRGBA(floatData, width, height) { | |
| const pixelCount = width * height; | |
| const rgbaData = new Uint8Array(pixelCount * 4); | |
| // Find min/max for normalization | |
| let min = Infinity, max = -Infinity; | |
| for (let i = 0; i < floatData.length; i++) { | |
| if (isFinite(floatData[i])) { | |
| min = Math.min(min, floatData[i]); | |
| max = Math.max(max, floatData[i]); | |
| } | |
| } | |
| const range = max - min; | |
| const scale = range > 0 ? 255 / range : 0; | |
| // Sequential assignment: R,G,B,A,R,G,B,A... | |
| for (let i = 0; i < pixelCount; i++) { | |
| const pixelIndex = i * 4; | |
| // Fill RGBA channels with sequential data | |
| for (let channel = 0; channel < 4; channel++) { | |
| const dataIndex = i * 4 + channel; | |
| if (dataIndex < floatData.length) { | |
| const normalized = isFinite(floatData[dataIndex]) | |
| ? Math.round((floatData[dataIndex] - min) * scale) | |
| : 0; | |
| const clamped = Math.max(0, Math.min(255, normalized)); | |
| rgbaData[pixelIndex + channel] = clamped; | |
| } else { | |
| rgbaData[pixelIndex + channel] = channel === 3 ? 255 : 0; // Alpha = 255, others = 0 | |
| } | |
| } | |
| } | |
| return rgbaData; | |
| } | |
| convertToChunkedRGBA(floatData, width, height) { | |
| const pixelCount = width * height; | |
| const rgbaData = new Uint8Array(pixelCount * 4); | |
| // Find min/max for normalization | |
| let min = Infinity, max = -Infinity; | |
| for (let i = 0; i < floatData.length; i++) { | |
| if (isFinite(floatData[i])) { | |
| min = Math.min(min, floatData[i]); | |
| max = Math.max(max, floatData[i]); | |
| } | |
| } | |
| const range = max - min; | |
| const scale = range > 0 ? 255 / range : 0; | |
| // Split data into 4 chunks for R, G, B, A | |
| const chunkSize = Math.ceil(floatData.length / 4); | |
| const chunks = [ | |
| floatData.slice(0, chunkSize), // R chunk | |
| floatData.slice(chunkSize, chunkSize * 2), // G chunk | |
| floatData.slice(chunkSize * 2, chunkSize * 3), // B chunk | |
| floatData.slice(chunkSize * 3, chunkSize * 4) // A chunk | |
| ]; | |
| for (let i = 0; i < pixelCount; i++) { | |
| const pixelIndex = i * 4; | |
| for (let channel = 0; channel < 4; channel++) { | |
| const chunk = chunks[channel]; | |
| if (i < chunk.length) { | |
| const normalized = isFinite(chunk[i]) | |
| ? Math.round((chunk[i] - min) * scale) | |
| : 0; | |
| const clamped = Math.max(0, Math.min(255, normalized)); | |
| rgbaData[pixelIndex + channel] = clamped; | |
| } else { | |
| rgbaData[pixelIndex + channel] = channel === 3 ? 255 : 0; // Alpha = 255, others = 0 | |
| } | |
| } | |
| } | |
| return rgbaData; | |
| } | |
| convertToBandedRGBA(floatData, width, height) { | |
| const pixelCount = width * height; | |
| const rgbaData = new Uint8Array(pixelCount * 4); | |
| // Find min/max for normalization | |
| let min = Infinity, max = -Infinity; | |
| for (let i = 0; i < floatData.length; i++) { | |
| if (isFinite(floatData[i])) { | |
| min = Math.min(min, floatData[i]); | |
| max = Math.max(max, floatData[i]); | |
| } | |
| } | |
| const range = max - min; | |
| const scale = range > 0 ? 255 / range : 0; | |
| // Split data into 4 chunks for R, G, B, A bands | |
| const chunkSize = Math.ceil(floatData.length / 4); | |
| const chunks = [ | |
| floatData.slice(0, chunkSize), // R chunk | |
| floatData.slice(chunkSize, chunkSize * 2), // G chunk | |
| floatData.slice(chunkSize * 2, chunkSize * 3), // B chunk | |
| floatData.slice(chunkSize * 3, chunkSize * 4) // A chunk | |
| ]; | |
| // Create horizontal bands - each quarter of height gets one channel | |
| const bandHeight = Math.floor(height / 4); | |
| const bandWidth = width; | |
| for (let y = 0; y < height; y++) { | |
| // Determine which band we're in | |
| const bandIndex = Math.min(3, Math.floor(y / bandHeight)); | |
| const chunk = chunks[bandIndex]; | |
| for (let x = 0; x < width; x++) { | |
| const pixelIndex = (y * width + x) * 4; | |
| // Calculate position within this band | |
| const yInBand = y - (bandIndex * bandHeight); | |
| const dataIndex = yInBand * bandWidth + x; | |
| if (dataIndex < chunk.length) { | |
| const normalized = isFinite(chunk[dataIndex]) | |
| ? Math.round((chunk[dataIndex] - min) * scale) | |
| : 0; | |
| const clamped = Math.max(0, Math.min(255, normalized)); | |
| // Color each band with its respective channel color | |
| switch (bandIndex) { | |
| case 0: // Red band | |
| rgbaData[pixelIndex] = clamped; // R | |
| rgbaData[pixelIndex + 1] = 0; // G | |
| rgbaData[pixelIndex + 2] = 0; // B | |
| rgbaData[pixelIndex + 3] = 255; // A | |
| break; | |
| case 1: // Green band | |
| rgbaData[pixelIndex] = 0; // R | |
| rgbaData[pixelIndex + 1] = clamped; // G | |
| rgbaData[pixelIndex + 2] = 0; // B | |
| rgbaData[pixelIndex + 3] = 255; // A | |
| break; | |
| case 2: // Blue band | |
| rgbaData[pixelIndex] = 0; // R | |
| rgbaData[pixelIndex + 1] = 0; // G | |
| rgbaData[pixelIndex + 2] = clamped; // B | |
| rgbaData[pixelIndex + 3] = 255; // A | |
| break; | |
| case 3: // Alpha band (shown as yellow/gold) | |
| rgbaData[pixelIndex] = clamped; // R | |
| rgbaData[pixelIndex + 1] = clamped; // G | |
| rgbaData[pixelIndex + 2] = 0; // B | |
| rgbaData[pixelIndex + 3] = 255; // A | |
| break; | |
| } | |
| } else { | |
| // Fill remaining pixels with black | |
| rgbaData[pixelIndex] = 0; | |
| rgbaData[pixelIndex + 1] = 0; | |
| rgbaData[pixelIndex + 2] = 0; | |
| rgbaData[pixelIndex + 3] = 255; | |
| } | |
| } | |
| } | |
| return rgbaData; | |
| } | |
| convertToGrayscaleRGBA(floatData, width, height) { | |
| const pixelCount = width * height; | |
| const rgbaData = new Uint8Array(pixelCount * 4); | |
| // Find min/max for normalization | |
| let min = Infinity, max = -Infinity; | |
| for (let i = 0; i < floatData.length; i++) { | |
| if (isFinite(floatData[i])) { | |
| min = Math.min(min, floatData[i]); | |
| max = Math.max(max, floatData[i]); | |
| } | |
| } | |
| const range = max - min; | |
| const scale = range > 0 ? 255 / range : 0; | |
| for (let i = 0; i < pixelCount; i++) { | |
| const pixelIndex = i * 4; | |
| if (i < floatData.length) { | |
| const normalized = isFinite(floatData[i]) | |
| ? Math.round((floatData[i] - min) * scale) | |
| : 0; | |
| const clamped = Math.max(0, Math.min(255, normalized)); | |
| rgbaData[pixelIndex] = clamped; // R | |
| rgbaData[pixelIndex + 1] = clamped; // G | |
| rgbaData[pixelIndex + 2] = clamped; // B | |
| rgbaData[pixelIndex + 3] = 255; // A (full opacity) | |
| } else { | |
| // Fill remaining pixels with black | |
| rgbaData[pixelIndex] = 0; | |
| rgbaData[pixelIndex + 1] = 0; | |
| rgbaData[pixelIndex + 2] = 0; | |
| rgbaData[pixelIndex + 3] = 255; | |
| } | |
| } | |
| return rgbaData; | |
| } | |
| calculateOptimalDimensions(totalElements) { | |
| // Try to make a roughly square image, or use a reasonable aspect ratio | |
| const sqrt = Math.sqrt(totalElements); | |
| let width = Math.ceil(sqrt); | |
| let height = Math.ceil(totalElements / width); | |
| // Ensure we don't go over the total elements | |
| while (width * height < totalElements) { | |
| width++; | |
| height = Math.ceil(totalElements / width); | |
| } | |
| // Limit maximum dimensions for performance | |
| const maxDim = 2048; | |
| if (width > maxDim) { | |
| width = maxDim; | |
| height = Math.ceil(totalElements / width); | |
| } | |
| if (height > maxDim) { | |
| height = maxDim; | |
| width = Math.ceil(totalElements / height); | |
| } | |
| return { width, height }; | |
| } | |
| async dequantizeTensor(tensor, offset) { | |
| const totalElements = tensor.shape.reduce((a, b) => a * b, 1); | |
| const result = new Float32Array(totalElements); | |
| switch (tensor.type) { | |
| case 2: // Q4_0 | |
| return this.dequantizeQ4_0(offset, totalElements); | |
| case 3: // Q4_1 | |
| return this.dequantizeQ4_1(offset, totalElements); | |
| case 6: // Q8_0 | |
| return this.dequantizeQ8_0(offset, totalElements); | |
| case 8: // Q2_K | |
| return this.dequantizeQ2_K(offset, totalElements); | |
| case 9: // Q3_K | |
| return this.dequantizeQ3_K(offset, totalElements); | |
| case 10: // Q4_K | |
| return this.dequantizeQ4_K(offset, totalElements); | |
| default: | |
| throw new Error(`Unsupported quantization type: ${tensor.typeString}`); | |
| } | |
| } | |
| dequantizeQ4_0(offset, totalElements) { | |
| const blockSize = 32; | |
| const numBlocks = Math.ceil(totalElements / blockSize); | |
| const result = new Float32Array(totalElements); | |
| const view = new DataView(this.fileData.buffer); | |
| let resultIndex = 0; | |
| let byteOffset = offset; | |
| for (let block = 0; block < numBlocks; block++) { | |
| // Read scale (16-bit float) | |
| const scaleBytes = view.getUint16(byteOffset, true); | |
| const scale = this.float16ToFloat32(scaleBytes); | |
| byteOffset += 2; | |
| // Read 16 bytes of 4-bit values (32 values total) | |
| for (let i = 0; i < 16 && resultIndex < totalElements; i++) { | |
| const byte = this.fileData[byteOffset + i]; | |
| // Extract two 4-bit values from each byte | |
| const val1 = (byte & 0x0F) - 8; // Convert to signed -8 to +7 | |
| const val2 = ((byte >> 4) & 0x0F) - 8; | |
| if (resultIndex < totalElements) { | |
| result[resultIndex++] = val1 * scale; | |
| } | |
| if (resultIndex < totalElements) { | |
| result[resultIndex++] = val2 * scale; | |
| } | |
| } | |
| byteOffset += 16; | |
| } | |
| return result; | |
| } | |
| dequantizeQ4_1(offset, totalElements) { | |
| const blockSize = 32; | |
| const numBlocks = Math.ceil(totalElements / blockSize); | |
| const result = new Float32Array(totalElements); | |
| const view = new DataView(this.fileData.buffer); | |
| let resultIndex = 0; | |
| let byteOffset = offset; | |
| for (let block = 0; block < numBlocks; block++) { | |
| // Read scale and bias (16-bit floats) | |
| const scaleBytes = view.getUint16(byteOffset, true); | |
| const biasBytes = view.getUint16(byteOffset + 2, true); | |
| const scale = this.float16ToFloat32(scaleBytes); | |
| const bias = this.float16ToFloat32(biasBytes); | |
| byteOffset += 4; | |
| // Read 16 bytes of 4-bit values | |
| for (let i = 0; i < 16 && resultIndex < totalElements; i++) { | |
| const byte = this.fileData[byteOffset + i]; | |
| const val1 = byte & 0x0F; // 0 to 15 | |
| const val2 = (byte >> 4) & 0x0F; | |
| if (resultIndex < totalElements) { | |
| result[resultIndex++] = val1 * scale + bias; | |
| } | |
| if (resultIndex < totalElements) { | |
| result[resultIndex++] = val2 * scale + bias; | |
| } | |
| } | |
| byteOffset += 16; | |
| } | |
| return result; | |
| } | |
| dequantizeQ8_0(offset, totalElements) { | |
| const blockSize = 32; | |
| const numBlocks = Math.ceil(totalElements / blockSize); | |
| const result = new Float32Array(totalElements); | |
| const view = new DataView(this.fileData.buffer); | |
| let resultIndex = 0; | |
| let byteOffset = offset; | |
| for (let block = 0; block < numBlocks; block++) { | |
| // Read scale (16-bit float) | |
| const scaleBytes = view.getUint16(byteOffset, true); | |
| const scale = this.float16ToFloat32(scaleBytes); | |
| byteOffset += 2; | |
| // Read 32 bytes of 8-bit values | |
| for (let i = 0; i < 32 && resultIndex < totalElements; i++) { | |
| const val = view.getInt8(byteOffset + i); // Signed -128 to +127 | |
| result[resultIndex++] = val * scale; | |
| } | |
| byteOffset += 32; | |
| } | |
| return result; | |
| } | |
| dequantizeQ2_K(offset, totalElements) { | |
| // Simplified Q2_K implementation | |
| const blockSize = 256; | |
| const numBlocks = Math.ceil(totalElements / blockSize); | |
| const result = new Float32Array(totalElements); | |
| // This is a simplified implementation - real Q2_K is more complex | |
| let resultIndex = 0; | |
| let byteOffset = offset; | |
| for (let block = 0; block < numBlocks && resultIndex < totalElements; block++) { | |
| // Skip complex structure, just use basic dequantization | |
| const scale = 1.0; // Simplified | |
| for (let i = 0; i < blockSize / 4 && resultIndex < totalElements; i++) { | |
| const byte = this.fileData[byteOffset + i] || 0; | |
| // Extract four 2-bit values | |
| for (let j = 0; j < 4 && resultIndex < totalElements; j++) { | |
| const val = ((byte >> (j * 2)) & 0x03) - 1; // -1 to +2 | |
| result[resultIndex++] = val * scale; | |
| } | |
| } | |
| byteOffset += 80; // Approximate block size for Q2_K | |
| } | |
| return result; | |
| } | |
| dequantizeQ3_K(offset, totalElements) { | |
| // Simplified Q3_K implementation | |
| const blockSize = 256; | |
| const numBlocks = Math.ceil(totalElements / blockSize); | |
| const result = new Float32Array(totalElements); | |
| let resultIndex = 0; | |
| let byteOffset = offset; | |
| for (let block = 0; block < numBlocks && resultIndex < totalElements; block++) { | |
| const scale = 1.0; // Simplified | |
| // Process 3-bit values (simplified) | |
| for (let i = 0; i < blockSize / 2 && resultIndex < totalElements; i++) { | |
| const byte = this.fileData[byteOffset + i] || 0; | |
| const val1 = (byte & 0x07) - 4; // 3 bits: -4 to +3 | |
| const val2 = ((byte >> 3) & 0x07) - 4; | |
| if (resultIndex < totalElements) { | |
| result[resultIndex++] = val1 * scale; | |
| } | |
| if (resultIndex < totalElements) { | |
| result[resultIndex++] = val2 * scale; | |
| } | |
| } | |
| byteOffset += 96; // Approximate block size for Q3_K | |
| } | |
| return result; | |
| } | |
| dequantizeQ4_K(offset, totalElements) { | |
| // Simplified Q4_K implementation | |
| const blockSize = 256; | |
| const numBlocks = Math.ceil(totalElements / blockSize); | |
| const result = new Float32Array(totalElements); | |
| let resultIndex = 0; | |
| let byteOffset = offset; | |
| for (let block = 0; block < numBlocks && resultIndex < totalElements; block++) { | |
| const scale = 1.0; // Simplified | |
| for (let i = 0; i < blockSize / 2 && resultIndex < totalElements; i++) { | |
| const byte = this.fileData[byteOffset + i] || 0; | |
| const val1 = (byte & 0x0F) - 8; // 4 bits: -8 to +7 | |
| const val2 = ((byte >> 4) & 0x0F) - 8; | |
| if (resultIndex < totalElements) { | |
| result[resultIndex++] = val1 * scale; | |
| } | |
| if (resultIndex < totalElements) { | |
| result[resultIndex++] = val2 * scale; | |
| } | |
| } | |
| byteOffset += 144; // Approximate block size for Q4_K | |
| } | |
| return result; | |
| } | |
| float16ToFloat32(bytes) { | |
| // Convert 16-bit float to 32-bit float | |
| const sign = (bytes & 0x8000) >> 15; | |
| const exponent = (bytes & 0x7C00) >> 10; | |
| const mantissa = bytes & 0x03FF; | |
| if (exponent === 0) { | |
| return (sign ? -1 : 1) * Math.pow(2, -14) * (mantissa / 1024); | |
| } else if (exponent === 0x1F) { | |
| return mantissa ? NaN : (sign ? -Infinity : Infinity); | |
| } else { | |
| return (sign ? -1 : 1) * Math.pow(2, exponent - 15) * (1 + mantissa / 1024); | |
| } | |
| } | |
| viewTensorAsRGBA() { | |
| if (!this.selectedTensor || !this.fileData) { | |
| this.showStatus('❌ No tensor selected or no file loaded', true); | |
| return; | |
| } | |
| const tensor = this.selectedTensor; | |
| if (!this.isRGBACompatible(tensor)) { | |
| this.showStatus('❌ Selected tensor is not RGBA compatible', true); | |
| return; | |
| } | |
| try { | |
| const actualOffset = this.tensorDataOffset + tensor.offset; | |
| const width = tensor.shape[1]; | |
| const height = tensor.shape[0]; | |
| const bytesNeeded = width * height * 4; | |
| // Get tensor data | |
| const tensorData = this.fileData.slice(actualOffset, actualOffset + bytesNeeded); | |
| // Create canvas and render image | |
| const canvas = document.getElementById('rgbaCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = width; | |
| canvas.height = height; | |
| // Create ImageData | |
| const imageData = ctx.createImageData(width, height); | |
| // Copy tensor data to ImageData | |
| // Handle different data types | |
| if (tensor.type === 0) { // F32 | |
| this.convertF32ToRGBA(tensorData, imageData.data); | |
| } else if (tensor.type === 24) { // I32 | |
| this.convertI32ToRGBA(tensorData, imageData.data); | |
| } else { | |
| // Default: treat as raw bytes | |
| for (let i = 0; i < tensorData.length && i < imageData.data.length; i++) { | |
| imageData.data[i] = tensorData[i]; | |
| } | |
| } | |
| // Draw image to canvas | |
| ctx.putImageData(imageData, 0, 0); | |
| // Show viewer and update info | |
| const viewer = document.getElementById('rgbaViewer'); | |
| const info = document.getElementById('rgbaInfo'); | |
| info.innerHTML = ` | |
| <div><strong>Tensor:</strong> ${tensor.name}</div> | |
| <div><strong>Dimensions:</strong> ${width} × ${height}</div> | |
| <div><strong>Data Type:</strong> ${tensor.typeString}</div> | |
| <div><strong>Bytes Used:</strong> ${bytesNeeded.toLocaleString()}</div> | |
| <div><strong>Interpretation:</strong> ${this.getRGBAInterpretation(tensor.type)}</div> | |
| `; | |
| viewer.style.display = 'block'; | |
| this.showStatus(`✅ Displayed "${tensor.name}" as RGBA image (${width}×${height})`); | |
| } catch (error) { | |
| console.error('Error viewing tensor as RGBA:', error); | |
| this.showStatus('❌ Error viewing tensor as RGBA: ' + error.message, true); | |
| } | |
| } | |
| convertF32ToRGBA(tensorData, rgbaData) { | |
| const view = new DataView(tensorData.buffer, tensorData.byteOffset); | |
| for (let i = 0; i < tensorData.length; i += 4) { | |
| const float32 = view.getFloat32(i, true); | |
| // Normalize float to 0-255 range (assuming values are in 0-1 range) | |
| const normalized = Math.max(0, Math.min(255, Math.round(float32 * 255))); | |
| rgbaData[i] = normalized; // R | |
| rgbaData[i + 1] = normalized; // G | |
| rgbaData[i + 2] = normalized; // B | |
| rgbaData[i + 3] = 255; // A (full opacity) | |
| } | |
| } | |
| convertI32ToRGBA(tensorData, rgbaData) { | |
| const view = new DataView(tensorData.buffer, tensorData.byteOffset); | |
| for (let i = 0; i < tensorData.length; i += 4) { | |
| const int32 = view.getInt32(i, true); | |
| // Extract RGBA bytes from int32 (assuming RGBA packed format) | |
| rgbaData[i] = (int32 >>> 0) & 0xFF; // R | |
| rgbaData[i + 1] = (int32 >>> 8) & 0xFF; // G | |
| rgbaData[i + 2] = (int32 >>> 16) & 0xFF; // B | |
| rgbaData[i + 3] = (int32 >>> 24) & 0xFF; // A | |
| } | |
| } | |
| getRGBAInterpretation(type) { | |
| switch (type) { | |
| case 0: return 'F32 values normalized to 0-255 (grayscale with alpha)'; | |
| case 24: return 'I32 values as packed RGBA bytes'; | |
| default: return 'Raw bytes as RGBA'; | |
| } | |
| } | |
| exportSelectedTensor() { | |
| if (!this.selectedTensor || !this.fileData) { | |
| this.showStatus('❌ No tensor selected or no file loaded', true); | |
| return; | |
| } | |
| try { | |
| const tensor = this.selectedTensor; | |
| const actualOffset = this.tensorDataOffset + tensor.offset; | |
| const sizeBytes = this.calculateTensorSize(tensor); | |
| // Find the next tensor to determine end offset | |
| const currentIndex = this.tensors.indexOf(tensor); | |
| let endOffset; | |
| if (currentIndex < this.tensors.length - 1) { | |
| const nextTensor = this.tensors[currentIndex + 1]; | |
| endOffset = this.tensorDataOffset + nextTensor.offset; | |
| } else { | |
| endOffset = this.fileData.length; | |
| } | |
| const actualSize = Math.min(sizeBytes, endOffset - actualOffset); | |
| const tensorData = this.fileData.slice(actualOffset, actualOffset + actualSize); | |
| // Create blob and download | |
| const blob = new Blob([tensorData], { type: 'application/octet-stream' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `${tensor.name.replace(/[/\\?%*:|"<>]/g, '_')}.bin`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| const sizeKB = (actualSize / 1024).toFixed(2); | |
| this.showStatus(`✅ Exported "${tensor.name}" (${sizeKB} KB)`); | |
| } catch (error) { | |
| console.error('Error exporting tensor:', error); | |
| this.showStatus('❌ Error exporting tensor: ' + error.message, true); | |
| } | |
| } | |
| updateHexdump() { | |
| if (!this.fileData) return; | |
| const size = parseInt(document.getElementById('hexdumpSize').value); | |
| const offset = parseInt(document.getElementById('hexdumpOffset').value); | |
| const bytesPerLine = parseInt(document.getElementById('bytesPerLine').value); | |
| const hexdump = this.generateHexdump(this.fileData, offset, size, bytesPerLine); | |
| document.getElementById('hexdump').textContent = hexdump; | |
| document.getElementById('hexdumpContainer').style.display = 'block'; | |
| } | |
| generateHexdump(data, offset, size, bytesPerLine) { | |
| const end = Math.min(offset + size, data.length); | |
| let result = ''; | |
| for (let i = offset; i < end; i += bytesPerLine) { | |
| // Offset | |
| const addr = i.toString(16).padStart(8, '0').toUpperCase(); | |
| result += addr + ' '; | |
| // Hex bytes | |
| let hexLine = ''; | |
| let asciiLine = ''; | |
| for (let j = 0; j < bytesPerLine; j++) { | |
| if (i + j < end) { | |
| const byte = data[i + j]; | |
| hexLine += byte.toString(16).padStart(2, '0').toUpperCase() + ' '; | |
| asciiLine += (byte >= 32 && byte <= 126) ? String.fromCharCode(byte) : '.'; | |
| } else { | |
| hexLine += ' '; | |
| asciiLine += ' '; | |
| } | |
| // Add extra space in the middle for 16-byte lines | |
| if (bytesPerLine === 16 && j === 7) { | |
| hexLine += ' '; | |
| } | |
| } | |
| result += hexLine + ' |' + asciiLine + '|\n'; | |
| } | |
| return result; | |
| } | |
| } | |
| // Initialize the application | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new GGUFDump(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment