Files
boc/quixzoom-capture-pipeline/video-to-observation/pipeline.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

186 lines
5.8 KiB
JavaScript

/**
* QUIXZOOM Video-to-Observation Pipeline
*
* Konverterar video till strukturerade observationer
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
class VideoToObservationPipeline {
constructor(config = {}) {
this.config = {
frameInterval: config.frameInterval || 5,
outputDir: config.outputDir || '/tmp/video-frames',
...config,
};
this.observations = [];
}
async processVideo(videoPath, metadata = {}) {
console.log(`[VIDEO] Processing: ${videoPath}`);
const videoInfo = this.extractMetadata(videoPath);
console.log(`[VIDEO] Duration: ${videoInfo.duration}s`);
const frames = await this.extractFrames(videoPath);
console.log(`[VIDEO] Extracted ${frames.length} frames`);
for (let i = 0; i < frames.length; i++) {
const frame = frames[i];
const timestamp = metadata.startTime
? new Date(metadata.startTime.getTime() + i * this.config.frameInterval * 1000)
: new Date();
const detectedObjects = this.mockObjectDetection(frame, metadata, i);
for (const obj of detectedObjects) {
this.observations.push({
id: `obs_${path.basename(videoPath)}_${i}_${obj.type}`,
objectType: obj.type,
location: { lat: obj.lat, lng: obj.lng },
gpsAccuracy: obj.accuracy,
timestamp: timestamp,
attributes: obj.attributes,
source: {
type: 'video',
videoId: metadata.videoId || path.basename(videoPath),
frameNumber: i,
timestamp: i * this.config.frameInterval,
},
quality: {
confidence: obj.confidence,
blur: 0.1,
exposure: 0.8,
},
});
}
}
console.log(`[VIDEO] Created ${this.observations.length} observations`);
return this.observations;
}
extractMetadata(videoPath) {
try {
const output = execSync(
`ffprobe -v quiet -print_format json -show_format -show_streams "${videoPath}"`,
{ encoding: 'utf-8', timeout: 10000 }
);
const info = JSON.parse(output);
const stream = info.streams.find(s => s.codec_type === 'video');
return {
duration: parseFloat(info.format.duration) || 0,
width: stream?.width || 0,
height: stream?.height || 0,
};
} catch (error) {
return { duration: 0, width: 0, height: 0 };
}
}
async extractFrames(videoPath) {
const outputDir = path.join(this.config.outputDir, path.basename(videoPath, path.extname(videoPath)));
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const fps = 1 / this.config.frameInterval;
const outputPattern = path.join(outputDir, 'frame_%04d.jpg');
try {
execSync(
`ffmpeg -i "${videoPath}" -vf "fps=${fps}" -q:v 2 "${outputPattern}"`,
{ timeout: 60000, stdio: 'pipe' }
);
} catch (error) {
console.warn(`[VIDEO] ffmpeg failed: ${error.message}`);
return [];
}
return fs.readdirSync(outputDir)
.filter(f => f.endsWith('.jpg'))
.map(f => path.join(outputDir, f))
.sort();
}
mockObjectDetection(framePath, metadata = {}, frameIndex) {
const objects = [];
const bangkokObjects = [
{ type: 'street_lamp', probability: 0.7 },
{ type: 'traffic_sign', probability: 0.4 },
{ type: 'tree', probability: 0.6 },
{ type: 'utility_box', probability: 0.2 },
{ type: 'manhole', probability: 0.3 },
];
const numObjects = 1 + Math.floor(Math.random() * 3);
for (let i = 0; i < numObjects; i++) {
const template = bangkokObjects[Math.floor(Math.random() * bangkokObjects.length)];
if (Math.random() < template.probability) {
const baseLat = metadata.baseLat || 13.7563;
const baseLng = metadata.baseLng || 100.5018;
const drift = frameIndex * 0.0001;
objects.push({
type: template.type,
lat: baseLat + (Math.random() - 0.5) * 0.001 + drift,
lng: baseLng + (Math.random() - 0.5) * 0.001 + drift * 0.5,
accuracy: 2 + Math.random() * 3,
confidence: 0.6 + Math.random() * 0.35,
attributes: this.generateAttributes(template.type),
});
}
}
return objects;
}
generateAttributes(objectType) {
const attributes = {
street_lamp: {
height: 7 + Math.random() * 3,
material: ['steel', 'aluminum'][Math.floor(Math.random() * 2)],
paint: ['grey', 'black', 'green'][Math.floor(Math.random() * 3)],
light: Math.random() > 0.9 ? 'broken' : 'working',
},
traffic_sign: {
signType: ['speed_limit', 'stop', 'pedestrian'][Math.floor(Math.random() * 3)],
height: 2 + Math.random() * 1,
reflective: Math.random() > 0.1,
},
tree: {
species: ['palm', 'banyan', 'eucalyptus'][Math.floor(Math.random() * 3)],
height: 5 + Math.random() * 10,
health: Math.random() > 0.9 ? 'poor' : 'good',
},
utility_box: {
type: ['electric', 'telecom'][Math.floor(Math.random() * 2)],
condition: Math.random() > 0.85 ? 'damaged' : 'good',
},
manhole: {
diameter: 0.5 + Math.random() * 0.3,
material: ['cast_iron', 'concrete'][Math.floor(Math.random() * 2)],
},
};
return attributes[objectType] || {};
}
saveToFile(outputPath) {
const data = {
version: '1.0',
generatedAt: new Date().toISOString(),
observationCount: this.observations.length,
observations: this.observations,
};
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2));
console.log(`[VIDEO] Saved to ${outputPath}`);
}
}
module.exports = VideoToObservationPipeline;