Merge pull request 'fix: improve OCR CIP extraction to filter common TSI card text' (#10) from fix/ocr-barcode-extraction into main
Build & Push Docker Images / test-backend (push) Successful in 35s
Build & Push Docker Images / test-frontend (push) Successful in 32s
Build & Push Docker Images / deploy (push) Successful in 6s
Build & Push Docker Images / build-backend (push) Successful in 17s
Build & Push Docker Images / build-frontend (push) Successful in 16s

Reviewed-on: #10
This commit was merged in pull request #10.
This commit is contained in:
2026-07-06 13:23:11 +00:00
+62 -5
View File
@@ -531,12 +531,63 @@ const upload = multer({
// Extract CIP from OCR text — looks for a long alphanumeric token (16 chars typical for CIP) // Extract CIP from OCR text — looks for a long alphanumeric token (16 chars typical for CIP)
function extractCip(text) { function extractCip(text) {
const cleaned = text.replace(/\s+/g, ''); const cleaned = text.replace(/\s+/g, '');
// Try exact 16-char alphanumeric match first
// Common Spanish text on TSI cards that should NOT be treated as CIP
const CARD_TEXT_PATTERNS = [
/TARJETASANITARIA/i,
/TARJETASANIT/i,
/TARJETASEGURIDAD/i,
/SISTEMASALUD/i,
/SERVICIOANDALUZ/i,
/SALUD/i,
/NUMERO/i,
/NUM/i,
/CIP/i,
/CODIGO/i,
/IDENTIFICACION/i,
/PERSONAL/i,
/TITULAR/i,
/FECHA/i,
/NACIMIENTO/i,
/CADUCIDAD/i,
/VALIDA/i,
/ESPANA/i,
/ESPAÑA/i,
];
// Check if OCR result is just common card text
const isCardText = (candidate) => {
const upper = candidate.toUpperCase();
return CARD_TEXT_PATTERNS.some(pattern => pattern.test(upper));
};
// Try to find barcodes first (typically numeric-heavy)
// TSI barcodes are usually numeric or alphanumeric with mostly numbers
const numericMatches = cleaned.match(/\d{10,30}/g) || [];
for (const match of numericMatches) {
if (match.length >= 10 && match.length <= 30) {
return match.toUpperCase();
}
}
// Try exact 16-char alphanumeric match, but filter card text
const exact = cleaned.match(/[A-Z0-9]{16}/i); const exact = cleaned.match(/[A-Z0-9]{16}/i);
if (exact) return exact[0].toUpperCase(); if (exact) {
// Fallback: 830 alphanumeric chars const candidate = exact[0].toUpperCase();
const loose = cleaned.match(/[A-Z0-9]{8,30}/i); if (!isCardText(candidate)) {
if (loose) return loose[0].toUpperCase(); return candidate;
}
}
// Fallback: 830 alphanumeric chars, filter card text
const looseMatches = cleaned.match(/[A-Z0-9]{8,30}/gi) || [];
for (const match of looseMatches) {
const candidate = match.toUpperCase();
if (!isCardText(candidate)) {
return candidate;
}
}
return null; return null;
} }
@@ -546,11 +597,17 @@ app.post('/api/tsi/ocr', upload.single('photo'), async (req, res) => {
return res.status(400).json({ error: 'No image file provided' }); return res.status(400).json({ error: 'No image file provided' });
} }
console.log('[OCR] Processing image, size:', req.file.size, 'bytes');
const worker = await createWorker('spa+eng'); const worker = await createWorker('spa+eng');
const { data } = await worker.recognize(req.file.buffer); const { data } = await worker.recognize(req.file.buffer);
await worker.terminate(); await worker.terminate();
console.log('[OCR] Raw text:', data.text); console.log('[OCR] Raw text:', data.text);
console.log('[OCR] Words:', data.words?.length || 0, 'words detected');
if (data.words && data.words.length > 0) {
console.log('[OCR] First 10 words:', data.words.slice(0, 10).map(w => `${w.text}(${w.confidence?.toFixed(2)})`).join(', '));
}
const cip = extractCip(data.text); const cip = extractCip(data.text);
console.log('[OCR] Extracted CIP:', cip); console.log('[OCR] Extracted CIP:', cip);