docs: add quixzoom-auth-core product to AAMOS
- Product documentation in docs/products/ - Updated MEMORY.md with product info - quiXzoom Auth Core as AAMOS Identity product
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* POST /v1/extract — Extrahering av data från bilder
|
||||
* OCR-liknande extraktion via AI + bildanalys
|
||||
*/
|
||||
import { Router } from 'express';
|
||||
import sharp from 'sharp';
|
||||
import { fetchImage, hashInput, saveResult, genReqId, requireAuth } from './utils.mjs';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const OLLAMA_BASE = process.env.OLLAMA_URL || 'http://172.31.40.60:11434';
|
||||
const GROQ_KEY = process.env.GROQ_API_KEY || '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';
|
||||
|
||||
async function extractData(buffer, extractType) {
|
||||
const base64 = buffer.toString('base64');
|
||||
|
||||
// Image analysis for structure
|
||||
const { data, info } = await sharp(buffer).resize(128, 128).raw().toBuffer({ resolveWithObject: true });
|
||||
const w = info.width, h = info.height;
|
||||
|
||||
// Detect text-like regions (high contrast horizontal bands)
|
||||
const textRegions = [];
|
||||
for (let y = 2; y < h - 2; y++) {
|
||||
let rowContrast = 0;
|
||||
for (let x = 1; x < w - 1; x++) {
|
||||
const i = (y * w + x) * 3;
|
||||
const gray = (data[i] + data[i+1] + data[i+2]) / 3;
|
||||
const prevGray = (data[i-3] + data[i-2] + data[i-1]) / 3;
|
||||
rowContrast += Math.abs(gray - prevGray);
|
||||
}
|
||||
if (rowContrast / w > 15) {
|
||||
textRegions.push({ y, contrast: Math.round(rowContrast / w) });
|
||||
}
|
||||
}
|
||||
|
||||
// Try AI extraction
|
||||
let extracted = null;
|
||||
try {
|
||||
const promptMap = {
|
||||
text: 'Extract all visible text from this image. Return as plain text.',
|
||||
faces: 'Count the number of faces in this image. Return only the number.',
|
||||
objects: 'List all objects visible in this image as a comma-separated list.',
|
||||
colors: 'List the dominant colors in this image as hex codes.',
|
||||
metadata: 'Describe the image type, approximate dimensions, and visible content.',
|
||||
};
|
||||
|
||||
const r = await fetch(`${OLLAMA_BASE}/api/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'amos-r2:latest',
|
||||
prompt: promptMap[extractType] || promptMap.text,
|
||||
images: [base64],
|
||||
stream: false,
|
||||
options: { num_predict: 300 }
|
||||
}),
|
||||
signal: AbortSignal.timeout(15000)
|
||||
});
|
||||
if (r.ok) {
|
||||
const d = await r.json();
|
||||
extracted = d.response?.trim();
|
||||
}
|
||||
} catch (e) { console.log('[extract] ollama failed:', e.message); }
|
||||
|
||||
// Fallback extraction
|
||||
if (!extracted) {
|
||||
const fallbacks = {
|
||||
text: `Detected ${textRegions.length} text-like horizontal bands in the image. Text extraction requires OCR engine.`,
|
||||
faces: 'Face count requires face detection model. Use /v1/detect endpoint.',
|
||||
objects: 'Object list requires object detection model. Use /v1/detect endpoint.',
|
||||
colors: extractDominantColors(data, w, h),
|
||||
metadata: `Image analyzed: ${w*4}x${h*4}px estimated, ${textRegions.length > 10 ? 'text-heavy' : 'image-heavy'} content.`,
|
||||
};
|
||||
extracted = fallbacks[extractType] || fallbacks.text;
|
||||
}
|
||||
|
||||
return {
|
||||
extracted,
|
||||
text_regions_detected: textRegions.length,
|
||||
has_text: textRegions.length > 5,
|
||||
};
|
||||
}
|
||||
|
||||
function extractDominantColors(data, w, h) {
|
||||
const colorMap = {};
|
||||
for (let i = 0; i < w * h; i++) {
|
||||
const r = Math.round(data[i*3] / 32) * 32;
|
||||
const g = Math.round(data[i*3+1] / 32) * 32;
|
||||
const b = Math.round(data[i*3+2] / 32) * 32;
|
||||
const key = `#${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${b.toString(16).padStart(2,'0')}`;
|
||||
colorMap[key] = (colorMap[key] || 0) + 1;
|
||||
}
|
||||
return Object.entries(colorMap)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 5)
|
||||
.map(([color, count]) => ({ color, coverage_pct: Math.round(count / (w * h) * 100) }));
|
||||
}
|
||||
|
||||
router.post('/', requireAuth, async (req, res) => {
|
||||
const requestId = genReqId();
|
||||
const start = Date.now();
|
||||
try {
|
||||
const { image_url, image_base64, extract_type = 'text' } = req.body || {};
|
||||
const img = await fetchImage({ image_url, image_base64 });
|
||||
const inputHash = hashInput(img.buffer);
|
||||
|
||||
const { extracted, text_regions_detected, has_text } = await extractData(img.buffer, extract_type);
|
||||
const confidence = has_text ? 0.8 : 0.5;
|
||||
|
||||
const result = {
|
||||
ok: true,
|
||||
endpoint: 'extract',
|
||||
request_id: requestId,
|
||||
extract_type,
|
||||
extracted_data: extracted,
|
||||
text_regions_detected,
|
||||
has_text_content: has_text,
|
||||
confidence,
|
||||
inference_time_ms: Date.now() - start,
|
||||
};
|
||||
|
||||
await saveResult('extract', requestId, inputHash, result, confidence, { extract_type, source: img.source });
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
console.error('[extract]', e);
|
||||
res.status(500).json({ ok: false, error: e.message, request_id: requestId });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user