bae705aa97
- 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
336 lines
10 KiB
JavaScript
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!');
|
|
}
|