model-explorer / js /core /tfliteParser.js
mr4's picture
Upload 71 files
9bd422a verified
/**
* TFLiteParser - Bộ Phân Tích TFLite
* Parses TFLite (.tflite) FlatBuffer binary files using DataView,
* extracting model metadata, operators, tensors, and subgraph info.
* Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6
*/
class TFLiteParser {
constructor() {
/**
* TFLite BuiltinOperator enum lookup table.
* Maps numeric code → operator name string.
* Source: TFLite schema.fbs BuiltinOperator enum
* @type {Record<number, string>}
*/
this.BUILTIN_OPERATORS = {
0: 'ADD',
1: 'AVERAGE_POOL_2D',
2: 'CONCATENATION',
3: 'CONV_2D',
4: 'DEPTHWISE_CONV_2D',
5: 'DEPTH_TO_SPACE',
6: 'DEQUANTIZE',
7: 'EMBEDDING_LOOKUP',
8: 'FLOOR',
9: 'FULLY_CONNECTED',
10: 'HASHTABLE_LOOKUP',
11: 'L2_NORMALIZATION',
12: 'L2_POOL_2D',
13: 'LOCAL_RESPONSE_NORMALIZATION',
14: 'LOGISTIC',
15: 'LSH_PROJECTION',
16: 'LSTM',
17: 'MAX_POOL_2D',
18: 'MUL',
19: 'RELU',
20: 'RELU_N1_TO_1',
21: 'RELU6',
22: 'RESHAPE',
23: 'RESIZE_BILINEAR',
24: 'RNN',
25: 'SOFTMAX',
26: 'SPACE_TO_DEPTH',
27: 'SVDF',
28: 'TANH',
29: 'CONCAT_EMBEDDINGS',
30: 'SKIP_GRAM',
31: 'CALL',
32: 'CUSTOM',
33: 'EMBEDDING_LOOKUP_SPARSE',
34: 'PAD',
35: 'UNIDIRECTIONAL_SEQUENCE_RNN',
36: 'GATHER',
37: 'BATCH_TO_SPACE_ND',
38: 'SPACE_TO_BATCH_ND',
39: 'TRANSPOSE',
40: 'MEAN',
41: 'SUB',
42: 'DIV',
43: 'SQUEEZE',
44: 'UNIDIRECTIONAL_SEQUENCE_LSTM',
45: 'STRIDED_SLICE',
46: 'BIDIRECTIONAL_SEQUENCE_RNN',
47: 'EXP',
48: 'TOPK_V2',
49: 'SPLIT',
50: 'LOG_SOFTMAX',
51: 'DELEGATE',
52: 'BIDIRECTIONAL_SEQUENCE_LSTM',
53: 'CAST',
54: 'PRELU',
55: 'MAXIMUM',
56: 'ARG_MAX',
57: 'MINIMUM',
58: 'LESS',
59: 'NEG',
60: 'PADV2',
61: 'GREATER',
62: 'GREATER_EQUAL',
63: 'LESS_EQUAL',
64: 'SELECT',
65: 'SLICE',
66: 'SIN',
67: 'TRANSPOSE_CONV',
68: 'SPARSE_TO_DENSE',
69: 'TILE',
70: 'EXPAND_DIMS',
71: 'EQUAL',
72: 'NOT_EQUAL',
73: 'LOG',
74: 'SUM',
75: 'SQRT',
76: 'RSQRT',
77: 'SHAPE',
78: 'POW',
79: 'ARG_MIN',
80: 'FAKE_QUANT',
81: 'REDUCE_PROD',
82: 'REDUCE_MAX',
83: 'PACK',
84: 'LOGICAL_OR',
85: 'ONE_HOT',
86: 'LOGICAL_AND',
87: 'LOGICAL_NOT',
88: 'UNPACK',
89: 'REDUCE_MIN',
90: 'FLOOR_DIV',
91: 'REDUCE_ANY',
92: 'SQUARE',
93: 'ZEROS_LIKE',
94: 'FILL',
95: 'FLOOR_MOD',
96: 'RANGE',
97: 'RESIZE_NEAREST_NEIGHBOR',
98: 'LEAKY_RELU',
99: 'SQUARED_DIFFERENCE',
100: 'MIRROR_PAD',
101: 'ABS',
102: 'SPLIT_V',
103: 'UNIQUE',
104: 'CEIL',
105: 'REVERSE_V2',
106: 'ADD_N',
107: 'GATHER_ND',
108: 'COS',
109: 'WHERE',
110: 'RANK',
111: 'ELU',
112: 'REVERSE_SEQUENCE',
113: 'MATRIX_DIAG',
114: 'QUANTIZE',
115: 'MATRIX_SET_DIAG',
116: 'ROUND',
117: 'HARD_SWISH',
118: 'IF',
119: 'WHILE',
120: 'NON_MAX_SUPPRESSION_V4',
121: 'NON_MAX_SUPPRESSION_V5',
122: 'SCATTER_ND',
123: 'SELECT_V2',
124: 'DENSIFY',
125: 'SEGMENT_SUM',
126: 'BATCH_MATMUL',
127: 'PLACEHOLDER_FOR_GREATER_OP_CODES',
128: 'CUMSUM',
129: 'CALL_ONCE',
130: 'BROADCAST_TO',
131: 'RFFT2D',
132: 'CONV_3D',
133: 'IMAG',
134: 'REAL',
135: 'COMPLEX_ABS',
136: 'HASHTABLE',
137: 'HASHTABLE_FIND',
138: 'HASHTABLE_IMPORT',
139: 'HASHTABLE_SIZE',
140: 'REDUCE_ALL',
141: 'CONV_3D_TRANSPOSE',
142: 'VAR_HANDLE',
143: 'READ_VARIABLE',
144: 'ASSIGN_VARIABLE',
145: 'BROADCAST_ARGS',
146: 'RANDOM_STANDARD_NORMAL',
147: 'BUCKETIZE',
148: 'RANDOM_UNIFORM',
149: 'MULTINOMIAL',
150: 'GELU',
151: 'DYNAMIC_UPDATE_SLICE',
152: 'RELU_0_TO_1',
153: 'UNSORTED_SEGMENT_PROD',
154: 'UNSORTED_SEGMENT_MAX',
155: 'UNSORTED_SEGMENT_SUM',
156: 'ATAN2',
157: 'UNSORTED_SEGMENT_MIN',
158: 'SIGN',
159: 'BITCAST',
160: 'BITWISE_XOR',
161: 'RIGHT_SHIFT',
162: 'STABLEHLO_LOGISTIC',
163: 'STABLEHLO_ADD',
164: 'STABLEHLO_DIVIDE',
165: 'STABLEHLO_MULTIPLY',
166: 'STABLEHLO_MAXIMUM',
167: 'STABLEHLO_RESHAPE',
168: 'STABLEHLO_CLAMP',
169: 'STABLEHLO_CONCATENATE',
170: 'STABLEHLO_BROADCAST_IN_DIM',
171: 'STABLEHLO_CONVOLUTION',
172: 'STABLEHLO_SLICE',
173: 'STABLEHLO_CUSTOM_CALL',
174: 'STABLEHLO_REDUCE',
175: 'STABLEHLO_ABS',
176: 'STABLEHLO_AND',
177: 'STABLEHLO_COSINE',
178: 'STABLEHLO_EXPONENTIAL',
179: 'STABLEHLO_FLOOR',
180: 'STABLEHLO_LOG',
181: 'STABLEHLO_MINIMUM',
182: 'STABLEHLO_NEGATE',
183: 'STABLEHLO_OR',
184: 'STABLEHLO_POWER',
185: 'STABLEHLO_REMAINDER',
186: 'STABLEHLO_RSQRT',
187: 'STABLEHLO_SELECT',
188: 'STABLEHLO_SUBTRACT',
189: 'STABLEHLO_TANH',
190: 'STABLEHLO_SCATTER',
191: 'STABLEHLO_COMPARE',
192: 'STABLEHLO_CONVERT',
193: 'STABLEHLO_DYNAMIC_SLICE',
194: 'STABLEHLO_DYNAMIC_UPDATE_SLICE',
195: 'STABLEHLO_PAD',
196: 'STABLEHLO_IOTA',
197: 'STABLEHLO_DOT_GENERAL',
198: 'STABLEHLO_REDUCE_WINDOW',
199: 'STABLEHLO_SORT',
200: 'STABLEHLO_WHILE',
201: 'STABLEHLO_GATHER',
202: 'STABLEHLO_TRANSPOSE',
203: 'DILATE',
204: 'STABLEHLO_RNG_BIT_GENERATOR',
205: 'REDUCE_WINDOW',
206: 'STABLEHLO_COMPOSITE',
207: 'STABLEHLO_SHIFT_LEFT',
208: 'STABLEHLO_CBRT'
};
/**
* TFLite TensorType enum lookup table.
* Maps numeric code → type name string.
* @type {Record<number, string>}
*/
this.TENSOR_TYPES = {
0: 'FLOAT32',
1: 'FLOAT16',
2: 'INT32',
3: 'UINT8',
4: 'INT64',
5: 'STRING',
6: 'BOOL',
7: 'INT16',
8: 'COMPLEX64',
9: 'INT8',
10: 'FLOAT64',
11: 'COMPLEX128',
12: 'UINT64',
13: 'UINT32',
14: 'UINT16',
15: 'INT4',
16: 'BFLOAT16'
};
/**
* Bytes per element for each TFLite tensor type.
* Used for estimating memory footprint.
* @type {Record<string, number>}
*/
this.BYTES_PER_ELEMENT = {
'FLOAT32': 4,
'FLOAT16': 2,
'INT32': 4,
'UINT8': 1,
'INT64': 8,
'STRING': 1,
'BOOL': 1,
'INT16': 2,
'COMPLEX64': 8,
'INT8': 1,
'FLOAT64': 8,
'COMPLEX128': 16,
'UINT64': 8,
'UINT32': 4,
'UINT16': 2,
'INT4': 0.5,
'BFLOAT16': 2
};
}
// ─── Public API ───────────────────────────────────────────────────────────
/**
* Parse an ArrayBuffer containing a .tflite file.
* @param {ArrayBuffer} buffer - The raw file content
* @returns {{ success: boolean, data?: TFLiteModelData, error?: string }}
*/
parse(buffer) {
try {
// Validate buffer
if (!buffer || !(buffer instanceof ArrayBuffer) || buffer.byteLength === 0) {
return { success: false, error: 'File không hợp lệ: buffer rỗng' };
}
if (buffer.byteLength < 8) {
return { success: false, error: 'File không hợp lệ: không đủ dữ liệu' };
}
const view = new DataView(buffer);
// Read root table offset (first 4 bytes, little-endian uint32)
const rootTableOffset = view.getUint32(0, true);
// Validate root table offset
if (rootTableOffset >= buffer.byteLength || rootTableOffset < 4) {
return { success: false, error: 'File không hợp lệ: cấu trúc FlatBuffer lỗi' };
}
// Read Model table
const model = this._readModelTable(view, rootTableOffset, buffer.byteLength);
return { success: true, data: model };
} catch (err) {
return {
success: false,
error: err.message || 'Lỗi không xác định khi parse file TFLite'
};
}
}
// ─── FlatBuffer Helpers ───────────────────────────────────────────────────
/**
* Read a vtable field offset for a given table position and field index.
* Returns 0 if the field is not present.
* @param {DataView} view
* @param {number} tablePos - Absolute position of the table in the buffer
* @param {number} fieldIndex - 0-based field index
* @returns {number} Absolute offset to the field data, or 0 if not present
*/
_getFieldOffset(view, tablePos, fieldIndex) {
// Table starts with soffset_t (int32) pointing back to vtable
const vtableRelOffset = view.getInt32(tablePos, true);
const vtablePos = tablePos - vtableRelOffset;
// vtable: [uint16 vtableSize] [uint16 objectSize] [uint16 field0] [uint16 field1] ...
const vtableSize = view.getUint16(vtablePos, true);
// Each field is at vtable offset 4 + fieldIndex * 2
const fieldVtableOffset = 4 + fieldIndex * 2;
if (fieldVtableOffset >= vtableSize) {
return 0; // Field not present in this version
}
const fieldRelOffset = view.getUint16(vtablePos + fieldVtableOffset, true);
if (fieldRelOffset === 0) {
return 0; // Field not set
}
return tablePos + fieldRelOffset;
}
/**
* Read a scalar uint32 field from a table.
* @param {DataView} view
* @param {number} tablePos
* @param {number} fieldIndex
* @param {number} defaultValue
* @returns {number}
*/
_readUint32Field(view, tablePos, fieldIndex, defaultValue = 0) {
const offset = this._getFieldOffset(view, tablePos, fieldIndex);
if (offset === 0) return defaultValue;
return view.getUint32(offset, true);
}
/**
* Read a scalar uint8 field from a table.
* @param {DataView} view
* @param {number} tablePos
* @param {number} fieldIndex
* @param {number} defaultValue
* @returns {number}
*/
_readUint8Field(view, tablePos, fieldIndex, defaultValue = 0) {
const offset = this._getFieldOffset(view, tablePos, fieldIndex);
if (offset === 0) return defaultValue;
return view.getUint8(offset);
}
/**
* Read a string field from a table.
* FlatBuffer strings: offset → [uint32 length] [bytes...] [null]
* @param {DataView} view
* @param {number} tablePos
* @param {number} fieldIndex
* @returns {string}
*/
_readStringField(view, tablePos, fieldIndex) {
const offset = this._getFieldOffset(view, tablePos, fieldIndex);
if (offset === 0) return '';
// The field contains a uoffset_t to the string
const stringRelOffset = view.getUint32(offset, true);
const stringPos = offset + stringRelOffset;
const strLen = view.getUint32(stringPos, true);
const bytes = new Uint8Array(view.buffer, stringPos + 4, strLen);
return new TextDecoder('utf-8').decode(bytes);
}
/**
* Read a vector field from a table. Returns array of absolute offsets
* to each element (for tables) or the vector position info.
* @param {DataView} view
* @param {number} tablePos
* @param {number} fieldIndex
* @returns {{ pos: number, length: number } | null}
*/
_readVectorField(view, tablePos, fieldIndex) {
const offset = this._getFieldOffset(view, tablePos, fieldIndex);
if (offset === 0) return null;
// Field contains uoffset_t to the vector
const vectorRelOffset = view.getUint32(offset, true);
const vectorPos = offset + vectorRelOffset;
const length = view.getUint32(vectorPos, true);
return { pos: vectorPos + 4, length };
}
/**
* Read a vector of int32 values.
* @param {DataView} view
* @param {number} tablePos
* @param {number} fieldIndex
* @returns {number[]}
*/
_readInt32Vector(view, tablePos, fieldIndex) {
const vec = this._readVectorField(view, tablePos, fieldIndex);
if (!vec) return [];
const result = [];
for (let i = 0; i < vec.length; i++) {
result.push(view.getInt32(vec.pos + i * 4, true));
}
return result;
}
/**
* Dereference a table offset within a vector.
* Vector of tables: each element is a uoffset_t pointing to the table.
* @param {DataView} view
* @param {number} elementPos - Position of the uoffset_t element
* @returns {number} Absolute position of the referenced table
*/
_derefTable(view, elementPos) {
const relOffset = view.getUint32(elementPos, true);
return elementPos + relOffset;
}
// ─── Model Parsing ────────────────────────────────────────────────────────
/**
* Read the root Model table and extract all data.
* Model fields (by vtable index):
* 0: version (uint32)
* 1: operator_codes (vector of OperatorCode tables)
* 2: subgraphs (vector of SubGraph tables)
* 3: description (string)
* @param {DataView} view
* @param {number} rootOffset
* @param {number} bufferSize
* @returns {TFLiteModelData}
*/
_readModelTable(view, rootOffset, bufferSize) {
const modelPos = rootOffset;
// Validate we can read the vtable soffset
if (modelPos + 4 > bufferSize) {
throw new Error('File không hợp lệ: cấu trúc FlatBuffer lỗi');
}
const version = this._readUint32Field(view, modelPos, 0, 0);
const description = this._readStringField(view, modelPos, 3);
// Read operator_codes
const operatorCodes = this._readOperatorCodes(view, modelPos);
// Read subgraphs
const subgraphsVec = this._readVectorField(view, modelPos, 2);
const subgraphCount = subgraphsVec ? subgraphsVec.length : 0;
// Parse first subgraph for tensors, operators, inputs, outputs
let tensors = [];
let operators = [];
let inputIndices = [];
let outputIndices = [];
if (subgraphCount > 0) {
const firstSubgraphPos = this._derefTable(view, subgraphsVec.pos);
const subgraphData = this._readSubGraph(view, firstSubgraphPos, operatorCodes);
tensors = subgraphData.tensors;
operators = subgraphData.operators;
inputIndices = subgraphData.inputIndices;
outputIndices = subgraphData.outputIndices;
}
return {
version,
description,
operators,
operatorCodes,
tensors,
subgraphs: subgraphCount,
inputIndices,
outputIndices
};
}
/**
* Read operator_codes vector from Model table.
* OperatorCode fields:
* 0: builtin_code (byte/uint8)
* 1: custom_code (string)
* 2: version (int32)
* @param {DataView} view
* @param {number} modelPos
* @returns {Array<{ builtinCode: number, customCode: string|null, opcodeName: string }>}
*/
_readOperatorCodes(view, modelPos) {
const vec = this._readVectorField(view, modelPos, 1);
if (!vec) return [];
const codes = [];
for (let i = 0; i < vec.length; i++) {
const opcodeTablePos = this._derefTable(view, vec.pos + i * 4);
const builtinCode = this._readUint8Field(view, opcodeTablePos, 0, 0);
const customCode = this._readStringField(view, opcodeTablePos, 1) || null;
let opcodeName;
if (builtinCode === 32 && customCode) {
// CUSTOM operator — use custom_code as name
opcodeName = customCode;
} else {
opcodeName = this.BUILTIN_OPERATORS[builtinCode] || `UNKNOWN_OP_${builtinCode}`;
}
codes.push({ builtinCode, customCode, opcodeName });
}
return codes;
}
/**
* Read a SubGraph table.
* SubGraph fields:
* 0: tensors (vector of Tensor tables)
* 1: inputs (vector of int32)
* 2: outputs (vector of int32)
* 3: operators (vector of Operator tables)
* 4: name (string)
* @param {DataView} view
* @param {number} subgraphPos
* @param {Array} operatorCodes
* @returns {{ tensors: Array, operators: Array, inputIndices: number[], outputIndices: number[] }}
*/
_readSubGraph(view, subgraphPos, operatorCodes) {
const tensors = this._readTensors(view, subgraphPos);
const inputIndices = this._readInt32Vector(view, subgraphPos, 1);
const outputIndices = this._readInt32Vector(view, subgraphPos, 2);
const operators = this._readOperators(view, subgraphPos, operatorCodes);
return { tensors, operators, inputIndices, outputIndices };
}
/**
* Read tensors vector from a SubGraph table.
* Tensor fields:
* 0: shape (vector of int32)
* 1: type (TensorType byte)
* 2: buffer (uint32)
* 3: name (string)
* @param {DataView} view
* @param {number} subgraphPos
* @returns {Array<{ name: string, shape: number[], dtype: string, byteSize: number }>}
*/
_readTensors(view, subgraphPos) {
const vec = this._readVectorField(view, subgraphPos, 0);
if (!vec) return [];
const tensors = [];
for (let i = 0; i < vec.length; i++) {
const tensorPos = this._derefTable(view, vec.pos + i * 4);
const shape = this._readInt32Vector(view, tensorPos, 0);
const typeCode = this._readUint8Field(view, tensorPos, 1, 0);
const name = this._readStringField(view, tensorPos, 3);
const dtype = this.TENSOR_TYPES[typeCode] || `UNKNOWN_TYPE_${typeCode}`;
const bytesPerEl = this.BYTES_PER_ELEMENT[dtype] || 1;
const elementCount = shape.length > 0
? shape.reduce((acc, dim) => acc * Math.abs(dim), 1)
: 0;
const byteSize = Math.ceil(elementCount * bytesPerEl);
tensors.push({ name, shape, dtype, byteSize });
}
return tensors;
}
/**
* Read operators vector from a SubGraph table.
* Operator fields:
* 0: opcode_index (uint32)
* @param {DataView} view
* @param {number} subgraphPos
* @param {Array} operatorCodes
* @returns {Array<{ opcodeName: string, opcodeIndex: number }>}
*/
_readOperators(view, subgraphPos, operatorCodes) {
const vec = this._readVectorField(view, subgraphPos, 3);
if (!vec) return [];
const operators = [];
for (let i = 0; i < vec.length; i++) {
const opPos = this._derefTable(view, vec.pos + i * 4);
const opcodeIndex = this._readUint32Field(view, opPos, 0, 0);
const opcodeName = (opcodeIndex < operatorCodes.length)
? operatorCodes[opcodeIndex].opcodeName
: `UNKNOWN_OP_${opcodeIndex}`;
operators.push({ opcodeName, opcodeIndex });
}
return operators;
}
}
window.TFLiteParser = TFLiteParser;