Files
boc/quixzoom-capture-pipeline/pilot/ai-result-logger.js
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

336 lines
10 KiB
JavaScript

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