/** * QUIXZOOM AI Result Logger * * Sparar ALLA AI-resultat för att möjliggöra: * - Återträning utan om-annotering * - Analys av träffsäkerhet * - Kostnadsuppföljning * - A/B-testning av modeller */ const fs = require('fs'); const path = require('path'); class AIResultLogger { constructor(baseDir = '/data/ai-results') { this.baseDir = baseDir; this.ensureDirectory(); // Schema för varje observation this.schema = { observationId: 'string', // UUID timestamp: 'string', // ISO 8601 imagePath: 'string', // Sökväg till originalbild imageHash: 'string', // SHA-256 hash // Modell-info model: 'string', // cloud_vision, yolo, grounding_dino, custom modelVersion: 'string', // t.ex. "v1.2.3" prompt: 'string', // Om relevant (text-prompt) // Detektioner detections: [{ label: 'string', // AI-föreslagen etikett quixzoomLabel: 'string', // Mappad till vårt vokabulär category: 'string', // lighting, road, utility, etc. confidence: 'number', // 0.0 - 1.0 bbox: { // Bounding box x: 'number', y: 'number', width: 'number', height: 'number' } }], // Manuell korrigering manualCorrection: { correctedBy: 'string', // Zoomer-ID eller admin correctedAt: 'string', // Timestamp changes: [{ // Vad som ändrades original: 'string', corrected: 'string', reason: 'string' }] }, // Gold label (slutgiltig sanning) goldLabel: { label: 'string', category: 'string', bbox: 'object', verifiedBy: 'string', verifiedAt: 'string' }, // Metadata region: 'string', // bangkok, torrevieja, stockholm routeId: 'string', // Gold Route ID missionId: 'string', // Mission ID zoomerId: 'string', // Zoomer ID (om tillämpligt) // Kostnad cost: 'number', // USD latency: 'number', // ms // Kvalitet quality: { aiAccuracy: 'number', // Jämfört med gold label needsReview: 'boolean', // Kräver manuell granskning reviewStatus: 'string' // pending, approved, rejected } }; } ensureDirectory() { const dirs = [ this.baseDir, path.join(this.baseDir, 'raw'), path.join(this.baseDir, 'corrected'), path.join(this.baseDir, 'gold'), path.join(this.baseDir, 'models') ]; for (const dir of dirs) { fs.mkdirSync(dir, { recursive: true }); } } /** * ============================================================ * HUVUDMETOD: Logga AI-resultat * ============================================================ */ async logResult(observation) { const observationId = observation.observationId || this.generateId(); const timestamp = new Date().toISOString(); const record = { ...observation, observationId, timestamp, loggedAt: timestamp }; // Spara rådata const rawPath = path.join(this.baseDir, 'raw', `${observationId}.json`); fs.writeFileSync(rawPath, JSON.stringify(record, null, 2)); // Spara i modell-specifik logg const modelLogPath = path.join( this.baseDir, 'models', `${record.model || 'unknown'}.jsonl` ); fs.appendFileSync(modelLogPath, JSON.stringify(record) + '\n'); // Uppdatera index this.updateIndex(record); console.log(`[AI Logger] Sparade: ${observationId}`); return observationId; } /** * ============================================================ * LOGGA MANUELL KORRIGERING * ============================================================ */ async logCorrection(observationId, correction) { const rawPath = path.join(this.baseDir, 'raw', `${observationId}.json`); if (!fs.existsSync(rawPath)) { throw new Error(`Observation ${observationId} hittades inte`); } const record = JSON.parse(fs.readFileSync(rawPath, 'utf8')); record.manualCorrection = { ...correction, correctedAt: new Date().toISOString() }; // Spara korrigerad version const correctedPath = path.join(this.baseDir, 'corrected', `${observationId}.json`); fs.writeFileSync(correctedPath, JSON.stringify(record, null, 2)); console.log(`[AI Logger] Korrigerade: ${observationId}`); return record; } /** * ============================================================ * LOGGA GOLD LABEL * ============================================================ */ async logGoldLabel(observationId, goldLabel) { const correctedPath = path.join(this.baseDir, 'corrected', `${observationId}.json`); const rawPath = path.join(this.baseDir, 'raw', `${observationId}.json`); const sourcePath = fs.existsSync(correctedPath) ? correctedPath : rawPath; const record = JSON.parse(fs.readFileSync(sourcePath, 'utf8')); record.goldLabel = { ...goldLabel, verifiedAt: new Date().toISOString() }; // Beräkna accuracy if (record.detections && record.detections.length > 0) { const aiLabel = record.detections[0].quixzoomLabel; record.quality = { aiAccuracy: aiLabel === goldLabel.label ? 1.0 : 0.0, needsReview: false, reviewStatus: 'approved' }; } // Spara gold version const goldPath = path.join(this.baseDir, 'gold', `${observationId}.json`); fs.writeFileSync(goldPath, JSON.stringify(record, null, 2)); console.log(`[AI Logger] Gold label: ${observationId} (${record.quality?.aiAccuracy === 1.0 ? '✅ Korrekt' : '❌ Fel'})`); return record; } /** * ============================================================ * INDEX & STATISTIK * ============================================================ */ updateIndex(record) { const indexPath = path.join(this.baseDir, 'index.json'); let index = { observations: [], stats: {} }; if (fs.existsSync(indexPath)) { index = JSON.parse(fs.readFileSync(indexPath, 'utf8')); } index.observations.push({ id: record.observationId, timestamp: record.timestamp, model: record.model, region: record.region, hasCorrection: !!record.manualCorrection, hasGoldLabel: !!record.goldLabel }); // Uppdatera stats index.stats = this.calculateStats(index.observations); fs.writeFileSync(indexPath, JSON.stringify(index, null, 2)); } calculateStats(observations) { const stats = { total: observations.length, byModel: {}, byRegion: {}, corrected: 0, goldLabeled: 0 }; for (const obs of observations) { stats.byModel[obs.model] = (stats.byModel[obs.model] || 0) + 1; stats.byRegion[obs.region] = (stats.byRegion[obs.region] || 0) + 1; if (obs.hasCorrection) stats.corrected++; if (obs.hasGoldLabel) stats.goldLabeled++; } return stats; } /** * ============================================================ * EXPORT FÖR TRÄNING * ============================================================ */ exportForTraining(outputDir, options = {}) { fs.mkdirSync(outputDir, { recursive: true }); // Hämta alla gold labels const goldDir = path.join(this.baseDir, 'gold'); const goldFiles = fs.readdirSync(goldDir).filter(f => f.endsWith('.json')); const trainingData = []; for (const file of goldFiles) { const record = JSON.parse(fs.readFileSync(path.join(goldDir, file), 'utf8')); if (options.model && record.model !== options.model) continue; if (options.region && record.region !== options.region) continue; trainingData.push({ imagePath: record.imagePath, imageHash: record.imageHash, label: record.goldLabel.label, category: record.goldLabel.category, bbox: record.goldLabel.bbox, originalDetections: record.detections }); } // Spara const outputPath = path.join(outputDir, `training-data-${Date.now()}.json`); fs.writeFileSync(outputPath, JSON.stringify(trainingData, null, 2)); console.log(`[AI Logger] Exporterade ${trainingData.length} gold labels till: ${outputPath}`); return trainingData; } /** * ============================================================ * STATISTIK * ============================================================ */ getStats() { const indexPath = path.join(this.baseDir, 'index.json'); if (!fs.existsSync(indexPath)) { return { total: 0 }; } return JSON.parse(fs.readFileSync(indexPath, 'utf8')).stats; } generateId() { return `obs-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } } module.exports = AIResultLogger; // Demo if (require.main === module) { const logger = new AIResultLogger('/tmp/ai-results-demo'); console.log('╔════════════════════════════════════════════════════════════╗'); console.log('║ AI RESULT LOGGER — DEMO ║'); console.log('╚════════════════════════════════════════════════════════════╝\n'); console.log('Schema för varje observation:'); console.log(' - observationId (UUID)'); console.log(' - imagePath + imageHash'); console.log(' - model + modelVersion'); console.log(' - detections (label, confidence, bbox)'); console.log(' - manualCorrection'); console.log(' - goldLabel'); console.log(' - region, routeId, missionId'); console.log(' - cost, latency'); console.log(' - quality (accuracy, needsReview)'); console.log(''); console.log('Användning:'); console.log(' const logger = new AIResultLogger("/data/ai-results");'); console.log(' await logger.logResult(observation);'); console.log(' await logger.logCorrection(id, correction);'); console.log(' await logger.logGoldLabel(id, goldLabel);'); console.log(' logger.exportForTraining("/data/training/");'); console.log(''); console.log('✅ AI Result Logger redo!'); }