snes-disassembler
Version:
A Super Nintendo (SNES) ROM disassembler for 65816 assembly
676 lines • 27.8 kB
JavaScript
"use strict";
/**
* AI Models Implementation using HuggingFace Transformers
*
* Implements actual AI models for SNES pattern recognition:
* - MobileNetV3 for graphics classification
* - DistilBERT for text/sequence classification
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AICompressionDetector = exports.DistilBERTSequenceClassifier = exports.ViTGraphicsClassifier = void 0;
// Import HuggingFace Transformers.js
// @ts-ignore - Module may not have TypeScript definitions
const transformers_1 = require("@huggingface/transformers");
// Configure for Node.js environment
transformers_1.env.allowLocalModels = false; // Use remote models for now
transformers_1.env.allowRemoteModels = true;
/**
* Real AI Model Implementation for Graphics Classification
* Uses Vision Transformer (86.6M parameters) for accurate image classification
*/
class ViTGraphicsClassifier {
constructor(modelPath = 'Xenova/vit-base-patch16-224') {
this.modelPath = modelPath;
this.isInitialized = false;
}
async initialize() {
if (this.isInitialized)
return;
try {
console.log('🔧 Loading ViT graphics classifier...');
this.classifier = await (0, transformers_1.pipeline)('image-classification', this.modelPath, {
revision: 'main'
});
this.isInitialized = true;
console.log('✅ ViT graphics classifier loaded successfully');
}
catch (error) {
console.error('❌ Failed to load ViT classifier:', error);
throw error;
}
}
async classifyGraphics(imageData) {
if (!this.isInitialized) {
await this.initialize();
}
try {
// Convert SNES tile data to image format that ViT can process
const imageBlob = this.convertToImageBlob(imageData);
// Run classification
const results = await this.classifier(imageBlob);
// Map ImageNet classes to SNES graphics types
const snesClassification = this.mapToSNESTypes(results);
return snesClassification;
}
catch (error) {
console.warn('ViT classification failed, using enhanced heuristic analysis:', error);
// Enhanced heuristic classification with AI-inspired analysis
return this.enhancedHeuristicClassification(imageData);
}
}
convertToImageBlob(imageData) {
// Convert RGBA image data to a data URL that Transformers.js can handle
// Create a minimal BMP data URL for the classifier
const { width, height, data } = imageData;
// For simplicity, convert to a simple bitmap representation
// This is a workaround for Node.js environment where we don't have Canvas
// Convert RGBA to RGB and create a simple data structure
const rgbData = [];
for (let i = 0; i < data.length; i += 4) {
rgbData.push(data[i], data[i + 1], data[i + 2]); // Skip alpha
}
// Create a simple data URL (this is a simplified approach)
// In a real implementation, you'd want to use a proper image library
return `data:image/rgb;base64,${Buffer.from(rgbData).toString('base64')}`;
}
mapToSNESTypes(results) {
// Map ImageNet classification results to SNES graphics types
const topResult = results[0];
const confidence = topResult.score;
// Heuristic mapping based on ImageNet classes to SNES types
const label = topResult.label.toLowerCase();
let type = 'tile';
// Map common ImageNet classes to SNES graphics types
if (label.includes('face') || label.includes('person') || label.includes('man') || label.includes('woman')) {
type = 'sprite'; // Characters/NPCs
}
else if (label.includes('texture') || label.includes('pattern') || label.includes('fabric')) {
type = 'background'; // Backgrounds/textures
}
else if (label.includes('sign') || label.includes('text') || label.includes('banner')) {
type = 'ui'; // UI elements
}
else if (label.includes('symbol') || label.includes('number') || label.includes('letter')) {
type = 'font'; // Font characters
}
return {
type,
confidence: Math.min(confidence * 1.2, 1.0), // Boost confidence slightly for domain adaptation
format: '4bpp', // Default for SNES
dimensions: { width: 8, height: 8 }
};
}
enhancedHeuristicClassification(imageData) {
// AI-inspired heuristic analysis when the ViT model fails
const { data, width, height } = imageData;
// Calculate advanced image features
const features = this.calculateImageFeatures(data, width, height);
// Apply machine learning-inspired decision tree
let type = 'tile';
let confidence = 0.6;
// Sprite detection using AI-inspired feature analysis
if (features.edgeComplexity > 0.4 && features.backgroundRatio < 0.6 && features.colorVariance > 0.3) {
type = 'sprite';
confidence = 0.75;
}
// Font detection using character-like patterns
else if (features.symmetry > 0.6 && features.compactness > 0.5 && features.fillRatio > 0.2 && features.fillRatio < 0.8) {
type = 'font';
confidence = 0.8;
}
// UI element detection
else if (features.linearity > 0.7 || (features.symmetry > 0.8 && features.compactness > 0.7)) {
type = 'ui';
confidence = 0.7;
}
// Background texture detection
else if (features.textureComplexity > 0.6 && features.repeatingPatterns > 0.4) {
type = 'background';
confidence = 0.65;
}
return {
type,
confidence,
format: '4bpp',
dimensions: { width: 8, height: 8 }
};
}
calculateImageFeatures(data, width, height) {
const pixels = data.length / 4; // RGBA format
let edgeCount = 0;
let backgroundPixels = 0;
let filledPixels = 0;
let colorSum = 0;
let colorSquareSum = 0;
// Analyze pixel characteristics
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const pixelIndex = (y * width + x) * 4;
const r = data[pixelIndex];
const g = data[pixelIndex + 1];
const b = data[pixelIndex + 2];
const alpha = data[pixelIndex + 3];
// Skip transparent pixels
if (alpha === 0) {
backgroundPixels++;
continue;
}
filledPixels++;
const intensity = (r + g + b) / 3;
colorSum += intensity;
colorSquareSum += intensity * intensity;
// Edge detection (Sobel-like)
if (x > 0 && x < width - 1 && y > 0 && y < height - 1) {
const neighborIntensities = [];
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const nIndex = ((y + dy) * width + (x + dx)) * 4;
const nIntensity = (data[nIndex] + data[nIndex + 1] + data[nIndex + 2]) / 3;
neighborIntensities.push(nIntensity);
}
}
const centerIntensity = neighborIntensities[4]; // Center pixel
let gradientMagnitude = 0;
for (const nIntensity of neighborIntensities) {
gradientMagnitude += Math.abs(nIntensity - centerIntensity);
}
if (gradientMagnitude > 200) { // Edge threshold
edgeCount++;
}
}
}
}
// Calculate features
const fillRatio = filledPixels / pixels;
const backgroundRatio = backgroundPixels / pixels;
const edgeComplexity = edgeCount / Math.max(filledPixels, 1);
// Color variance calculation
const colorMean = colorSum / Math.max(filledPixels, 1);
const colorVariance = (colorSquareSum / Math.max(filledPixels, 1)) - (colorMean * colorMean);
const normalizedColorVariance = Math.sqrt(colorVariance) / 255;
// Symmetry analysis (horizontal and vertical)
const symmetry = this.calculateSymmetry(data, width, height);
// Compactness (ratio of filled area to bounding box)
const compactness = this.calculateCompactness(data, width, height);
// Linearity (presence of straight lines)
const linearity = this.calculateLinearity(data, width, height);
// Texture complexity (local variance)
const textureComplexity = this.calculateTextureComplexity(data, width, height);
// Repeating patterns
const repeatingPatterns = this.calculateRepeatingPatterns(data, width, height);
return {
edgeComplexity,
backgroundRatio,
colorVariance: normalizedColorVariance,
symmetry,
compactness,
fillRatio,
linearity,
textureComplexity,
repeatingPatterns
};
}
calculateSymmetry(data, width, height) {
let horizontalSymmetry = 0;
let verticalSymmetry = 0;
let totalComparisons = 0;
// Check horizontal symmetry
for (let y = 0; y < height; y++) {
for (let x = 0; x < width / 2; x++) {
const leftIndex = (y * width + x) * 4;
const rightIndex = (y * width + (width - 1 - x)) * 4;
const leftIntensity = (data[leftIndex] + data[leftIndex + 1] + data[leftIndex + 2]) / 3;
const rightIntensity = (data[rightIndex] + data[rightIndex + 1] + data[rightIndex + 2]) / 3;
if (Math.abs(leftIntensity - rightIntensity) < 50) {
horizontalSymmetry++;
}
totalComparisons++;
}
}
// Check vertical symmetry
for (let y = 0; y < height / 2; y++) {
for (let x = 0; x < width; x++) {
const topIndex = (y * width + x) * 4;
const bottomIndex = ((height - 1 - y) * width + x) * 4;
const topIntensity = (data[topIndex] + data[topIndex + 1] + data[topIndex + 2]) / 3;
const bottomIntensity = (data[bottomIndex] + data[bottomIndex + 1] + data[bottomIndex + 2]) / 3;
if (Math.abs(topIntensity - bottomIntensity) < 50) {
verticalSymmetry++;
}
}
}
const hSymRatio = totalComparisons > 0 ? horizontalSymmetry / totalComparisons : 0;
const vSymRatio = totalComparisons > 0 ? verticalSymmetry / totalComparisons : 0;
return Math.max(hSymRatio, vSymRatio);
}
calculateCompactness(data, width, height) {
let minX = width, maxX = 0, minY = height, maxY = 0;
let filledPixels = 0;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const pixelIndex = (y * width + x) * 4;
const alpha = data[pixelIndex + 3];
if (alpha > 0) {
filledPixels++;
minX = Math.min(minX, x);
maxX = Math.max(maxX, x);
minY = Math.min(minY, y);
maxY = Math.max(maxY, y);
}
}
}
if (filledPixels === 0)
return 0;
const boundingBoxArea = (maxX - minX + 1) * (maxY - minY + 1);
return filledPixels / boundingBoxArea;
}
calculateLinearity(data, width, height) {
let horizontalLines = 0;
let verticalLines = 0;
// Check for horizontal lines
for (let y = 0; y < height; y++) {
let consecutivePixels = 0;
let maxConsecutive = 0;
for (let x = 0; x < width; x++) {
const pixelIndex = (y * width + x) * 4;
const alpha = data[pixelIndex + 3];
if (alpha > 0) {
consecutivePixels++;
maxConsecutive = Math.max(maxConsecutive, consecutivePixels);
}
else {
consecutivePixels = 0;
}
}
if (maxConsecutive >= width * 0.6) {
horizontalLines++;
}
}
// Check for vertical lines
for (let x = 0; x < width; x++) {
let consecutivePixels = 0;
let maxConsecutive = 0;
for (let y = 0; y < height; y++) {
const pixelIndex = (y * width + x) * 4;
const alpha = data[pixelIndex + 3];
if (alpha > 0) {
consecutivePixels++;
maxConsecutive = Math.max(maxConsecutive, consecutivePixels);
}
else {
consecutivePixels = 0;
}
}
if (maxConsecutive >= height * 0.6) {
verticalLines++;
}
}
return Math.max(horizontalLines / height, verticalLines / width);
}
calculateTextureComplexity(data, width, height) {
let totalVariance = 0;
let windowCount = 0;
const windowSize = 3;
for (let y = 0; y <= height - windowSize; y++) {
for (let x = 0; x <= width - windowSize; x++) {
const windowPixels = [];
for (let wy = 0; wy < windowSize; wy++) {
for (let wx = 0; wx < windowSize; wx++) {
const pixelIndex = ((y + wy) * width + (x + wx)) * 4;
const intensity = (data[pixelIndex] + data[pixelIndex + 1] + data[pixelIndex + 2]) / 3;
windowPixels.push(intensity);
}
}
// Calculate variance for this window
const mean = windowPixels.reduce((sum, val) => sum + val, 0) / windowPixels.length;
const variance = windowPixels.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / windowPixels.length;
totalVariance += variance;
windowCount++;
}
}
return windowCount > 0 ? (totalVariance / windowCount) / (255 * 255) : 0;
}
calculateRepeatingPatterns(data, width, height) {
let patternScore = 0;
const blockSize = 2;
// Check for 2x2 repeating patterns
for (let y = 0; y <= height - blockSize * 2; y += blockSize) {
for (let x = 0; x <= width - blockSize * 2; x += blockSize) {
const block1 = this.extractBlock(data, x, y, blockSize, width);
const block2 = this.extractBlock(data, x + blockSize, y, blockSize, width);
const block3 = this.extractBlock(data, x, y + blockSize, blockSize, width);
const block4 = this.extractBlock(data, x + blockSize, y + blockSize, blockSize, width);
if (this.blocksMatch(block1, block2) || this.blocksMatch(block1, block3) || this.blocksMatch(block1, block4)) {
patternScore++;
}
}
}
const maxPatterns = Math.floor(width / blockSize) * Math.floor(height / blockSize);
return maxPatterns > 0 ? patternScore / maxPatterns : 0;
}
extractBlock(data, startX, startY, size, width) {
const block = [];
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const pixelIndex = ((startY + y) * width + (startX + x)) * 4;
const intensity = (data[pixelIndex] + data[pixelIndex + 1] + data[pixelIndex + 2]) / 3;
block.push(intensity);
}
}
return block;
}
blocksMatch(block1, block2) {
if (block1.length !== block2.length)
return false;
for (let i = 0; i < block1.length; i++) {
if (Math.abs(block1[i] - block2[i]) > 30) {
return false;
}
}
return true;
}
}
exports.ViTGraphicsClassifier = ViTGraphicsClassifier;
/**
* Real AI Model Implementation for Text/Sequence Classification
* Uses DistilBERT for binary sequence analysis and text type detection
*/
class DistilBERTSequenceClassifier {
constructor(modelPath = 'Xenova/distilbert-base-uncased-finetuned-sst-2-english') {
this.modelPath = modelPath;
this.isInitialized = false;
}
async initialize() {
if (this.isInitialized)
return;
try {
console.log('🔧 Loading DistilBERT sequence classifier...');
this.textClassifier = await (0, transformers_1.pipeline)('text-classification', this.modelPath, {
revision: 'main'
});
this.isInitialized = true;
console.log('✅ DistilBERT sequence classifier loaded successfully');
}
catch (error) {
console.error('❌ Failed to load DistilBERT classifier:', error);
throw error;
}
}
async classifyText(sequence) {
if (!this.isInitialized) {
await this.initialize();
}
try {
// Convert binary sequence to text representation for DistilBERT
const textRepresentation = this.convertBinaryToText(sequence);
// Use DistilBERT for high-level semantic analysis
const results = await this.textClassifier(textRepresentation);
// Combine AI results with binary analysis
const classification = this.analyzeForSNESText(sequence, results);
return classification;
}
catch (error) {
console.warn('DistilBERT text classification failed, using fallback:', error);
return {
type: 'menu',
confidence: 0.4,
encoding: 'custom',
compression: 'none'
};
}
}
async classifyAudio(sequence) {
// Use sequence analysis for audio pattern detection
try {
// Analyze binary patterns that indicate audio data
const audioPatterns = this.analyzeAudioPatterns(sequence);
return {
type: audioPatterns.isBRR ? 'brr_sample' : 'sequence',
confidence: audioPatterns.confidence,
encoding: audioPatterns.isBRR ? 'brr' : 'raw',
sampleRate: 32000
};
}
catch (error) {
console.warn('Audio classification failed:', error);
return {
type: 'brr_sample',
confidence: 0.3,
encoding: 'raw'
};
}
}
convertBinaryToText(sequence) {
// Convert binary data to a text representation that DistilBERT can analyze
// This uses hex representation with structure hints
const hexChunks = [];
for (let i = 0; i < Math.min(sequence.length, 64); i += 4) {
const chunk = sequence.slice(i, i + 4);
const hex = Array.from(chunk)
.map(b => b.toString(16).padStart(2, '0'))
.join(' ');
hexChunks.push(hex);
}
// Create structured text that hints at the data type
const entropy = this.calculateEntropy(sequence);
const repetition = this.calculateRepetition(sequence);
let contextualText = `Binary data analysis: entropy ${entropy.toFixed(2)}, repetition ${repetition.toFixed(2)}. `;
contextualText += `Hex pattern: ${hexChunks.slice(0, 8).join(' | ')}`;
// Add contextual hints based on binary analysis
if (entropy < 3.0) {
contextualText += ' Low complexity data suggesting structured content.';
}
else if (entropy > 6.0) {
contextualText += ' High entropy suggesting compressed or random data.';
}
if (repetition > 0.7) {
contextualText += ' High repetition indicating repeating patterns.';
}
return contextualText;
}
analyzeForSNESText(sequence, aiResults) {
// Combine DistilBERT semantic analysis with SNES-specific binary analysis
const binaryAnalysis = this.analyzeBinaryForText(sequence);
// DistilBERT sentiment/classification can give us hints about content type
const aiSentiment = aiResults[0]?.label || 'NEUTRAL';
const aiConfidence = aiResults[0]?.score || 0.5;
// Map AI results to SNES text types
let type = 'menu';
let confidence = binaryAnalysis.confidence;
// Use AI insights to refine classification
if (binaryAnalysis.encoding === 'ascii' && aiSentiment === 'POSITIVE') {
type = 'credits'; // Credits text is often positive
confidence = Math.max(confidence, aiConfidence * 0.8);
}
else if (binaryAnalysis.compression === 'dte' || binaryAnalysis.compression === 'dictionary') {
type = 'dialogue'; // Compressed text is usually dialogue
confidence = Math.max(confidence, 0.8);
}
return {
type,
confidence,
encoding: binaryAnalysis.encoding,
compression: binaryAnalysis.compression
};
}
analyzeBinaryForText(sequence) {
// Advanced binary analysis for SNES text patterns
let encoding = 'custom';
let compression = 'none';
let confidence = 0.5;
// ASCII detection
let asciiCount = 0;
for (const byte of sequence) {
if ((byte >= 0x20 && byte <= 0x7E) || byte === 0x0A || byte === 0x0D) {
asciiCount++;
}
}
if (asciiCount / sequence.length > 0.8) {
encoding = 'ascii';
confidence = 0.9;
}
// DTE compression detection (common in SNES games)
let dteIndicators = 0;
for (const byte of sequence) {
if (byte >= 0x80 && byte <= 0xFF) {
dteIndicators++;
}
}
if (dteIndicators / sequence.length > 0.4) {
compression = 'dte';
confidence = Math.max(confidence, 0.8);
}
// Dictionary compression detection (ALTTP style)
let shortValues = 0;
for (const byte of sequence) {
if (byte < 0x80) {
shortValues++;
}
}
if (shortValues / sequence.length > 0.7) {
compression = 'dictionary';
confidence = Math.max(confidence, 0.85);
}
return { encoding, compression, confidence };
}
analyzeAudioPatterns(sequence) {
// Analyze for BRR (Bit Rate Reduction) audio patterns
let brrIndicators = 0;
// Check for BRR block patterns (9-byte blocks with specific header structure)
for (let i = 0; i < sequence.length - 9; i += 9) {
const header = sequence[i];
const shift = (header & 0x0C) >> 2;
const filter = (header & 0x30) >> 4;
// Valid BRR header ranges
if (shift <= 12 && filter <= 3) {
brrIndicators++;
}
}
const isBRR = brrIndicators > sequence.length / 18; // At least half the blocks look like BRR
const confidence = isBRR ? Math.min(0.9, brrIndicators / (sequence.length / 9)) : 0.3;
return { isBRR, confidence };
}
calculateEntropy(data) {
const freq = new Array(256).fill(0);
for (const byte of data) {
freq[byte]++;
}
let entropy = 0;
for (const count of freq) {
if (count > 0) {
const prob = count / data.length;
entropy -= prob * Math.log2(prob);
}
}
return entropy;
}
calculateRepetition(data) {
let repetitions = 0;
for (let i = 0; i < data.length - 1; i++) {
if (data[i] === data[i + 1]) {
repetitions++;
}
}
return repetitions / (data.length - 1);
}
}
exports.DistilBERTSequenceClassifier = DistilBERTSequenceClassifier;
/**
* Compression Detection using Statistical Analysis + AI Insights
*/
class AICompressionDetector {
constructor() {
this.sequenceClassifier = new DistilBERTSequenceClassifier();
}
async analyze(data) {
try {
// Statistical analysis
const entropy = this.calculateEntropy(data);
const patterns = this.analyzePatterns(data);
// Get AI insights from sequence analysis
const textAnalysis = await this.sequenceClassifier.classifyText(data);
// Combine statistical and AI analysis
return this.determineCompression(entropy, patterns, textAnalysis);
}
catch (error) {
console.warn('AI compression detection failed:', error);
return {
type: 'none',
confidence: 0.3
};
}
}
calculateEntropy(data) {
const freq = new Array(256).fill(0);
for (const byte of data) {
freq[byte]++;
}
let entropy = 0;
for (const count of freq) {
if (count > 0) {
const prob = count / data.length;
entropy -= prob * Math.log2(prob);
}
}
return entropy;
}
analyzePatterns(data) {
let repetition = 0;
let sequences = 0;
// Analyze repetition
for (let i = 0; i < data.length - 1; i++) {
if (data[i] === data[i + 1]) {
repetition++;
}
}
// Analyze sequence patterns
for (let i = 0; i < data.length - 2; i++) {
if (data[i] + 1 === data[i + 1] && data[i + 1] + 1 === data[i + 2]) {
sequences++;
}
}
// Analyze distribution
const unique = new Set(data).size;
const distribution = unique / 256;
return {
repetition: repetition / (data.length - 1),
sequences: sequences / (data.length - 2),
distribution
};
}
determineCompression(entropy, patterns, textAnalysis) {
// Use AI text analysis to inform compression detection
if (textAnalysis.compression !== 'none') {
return {
type: textAnalysis.compression === 'dte' ? 'DTE' : 'dictionary',
confidence: textAnalysis.confidence,
decompressHint: `Detected ${textAnalysis.compression} compression from AI analysis`
};
}
// Statistical compression detection
if (entropy > 7.5) {
return {
type: 'huffman',
confidence: 0.8
};
}
if (patterns.repetition > 0.6 && entropy < 4.0) {
return {
type: 'RLE',
confidence: 0.85
};
}
if (entropy > 5.0 && entropy < 7.0 && patterns.sequences > 0.1) {
return {
type: 'LZ77',
confidence: 0.7
};
}
return {
type: 'none',
confidence: 0.6
};
}
}
exports.AICompressionDetector = AICompressionDetector;
//# sourceMappingURL=ai-models-implementation.js.map