/** * 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} */ 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} */ 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} */ 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;