feat: enhance OCR matching with fuzzy logic and refined AI prompt
AI Prompt Improvements: - Standardized Item name format: [size] type vendor connector part_number - Clear examples: '5m Patchcord LC-LC', '256GB SSD Samsung SAS', '128GB DDR4 Hynix' - OCR key format: uppercase, space-separated tokens, includes variations/abbreviations - Excludes diameter/mm measurements from Item field - Allows 'HP'/'HPE', 'DDR'/'DDR4', 'NVMe'/'NVME' variations in OCR keys OCR Matching Logic: - Implemented Levenshtein distance fuzzy matching (allows 1-2 char differences) - Priority 0: Exact OCR key match (1000pts) + fuzzy match fallback (800pts) - Priority 2: Part number exact (200pts) + fuzzy match (150pts) - Priority 3: Token-based with fuzzy tolerance (50pts exact, 30pts fuzzy) - Removes whitespace for comparison (e.g., 'LCLC' matches 'LC-LC') Resolves OCR identification failures from minor text variations.
This commit is contained in:
@@ -41,6 +41,34 @@ function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// Fuzzy string matching with Levenshtein distance
|
||||
// Returns true if strings are similar enough (allowing 1-2 character differences)
|
||||
function fuzzyMatch(str1: string, str2: string, maxDistance: number = 2): boolean {
|
||||
const s1 = str1.toLowerCase().replace(/\s+/g, '');
|
||||
const s2 = str2.toLowerCase().replace(/\s+/g, '');
|
||||
|
||||
if (s1 === s2) return true;
|
||||
if (Math.abs(s1.length - s2.length) > maxDistance) return false;
|
||||
|
||||
// Levenshtein distance
|
||||
const matrix: number[][] = Array(s2.length + 1).fill(null).map(() => Array(s1.length + 1).fill(0));
|
||||
for (let i = 0; i <= s1.length; i++) matrix[0][i] = i;
|
||||
for (let j = 0; j <= s2.length; j++) matrix[j][0] = j;
|
||||
|
||||
for (let j = 1; j <= s2.length; j++) {
|
||||
for (let i = 1; i <= s1.length; i++) {
|
||||
const indicator = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
||||
matrix[j][i] = Math.min(
|
||||
matrix[j][i - 1] + 1,
|
||||
matrix[j - 1][i] + 1,
|
||||
matrix[j - 1][i - 1] + indicator
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return matrix[s2.length][s1.length] <= maxDistance;
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [isOnline, setIsOnline] = useState(true);
|
||||
@@ -266,19 +294,51 @@ export default function Home() {
|
||||
const category = item.category.toLowerCase();
|
||||
const ocrKey = (item.ocr_text || '').toLowerCase().replace(/[^a-z0-9\s/+-]/g, ' ');
|
||||
|
||||
// Priority 0: Exact OCR Key match (Heuristic provided by AI)
|
||||
if (ocrKey && cleanText.includes(ocrKey)) score += 1000;
|
||||
// Priority 0: OCR Key match (Heuristic provided by AI) with fuzzy tolerance
|
||||
if (ocrKey) {
|
||||
if (cleanText.includes(ocrKey)) {
|
||||
score += 1000; // Exact match
|
||||
} else {
|
||||
// Fuzzy match for OCR keys (allows 2-char differences for robustness)
|
||||
const ocrTokens = ocrKey.split(/[\s/+-]+/).filter(t => t.length >= 2);
|
||||
const matchedTokens = ocrTokens.filter(token => {
|
||||
return tokens.some(t => fuzzyMatch(t, token, 2));
|
||||
});
|
||||
if (matchedTokens.length >= Math.ceil(ocrTokens.length * 0.7)) {
|
||||
score += 800; // Fuzzy match (70%+ token coverage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 1: Serial Number (Absolute match)
|
||||
if (sn && cleanText.includes(sn)) score += 500;
|
||||
|
||||
// Priority 2: Part Number (High confidence)
|
||||
if (pn && cleanText.includes(pn)) score += 200;
|
||||
// Priority 2: Part Number (High confidence, with fuzzy tolerance)
|
||||
if (pn) {
|
||||
if (cleanText.includes(pn)) {
|
||||
score += 200; // Exact match
|
||||
} else {
|
||||
// Fuzzy match for part numbers (allows 1-char differences)
|
||||
const pnTokens = pn.split(/[\s/+-]/).filter(t => t.length >= 2);
|
||||
const matchedPnTokens = pnTokens.filter(pnToken =>
|
||||
tokens.some(t => fuzzyMatch(t, pnToken, 1))
|
||||
);
|
||||
if (matchedPnTokens.length >= Math.max(2, Math.ceil(pnTokens.length * 0.6))) {
|
||||
score += 150; // Fuzzy match (60%+ token coverage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: Token based matching for PN (LC/UPC etc)
|
||||
if (pn) {
|
||||
const pnTokens = pn.split(/[\s/+-]/).filter(t => t.length >= 3);
|
||||
pnTokens.forEach(t => { if (cleanText.includes(t)) score += 50; });
|
||||
pnTokens.forEach(t => {
|
||||
if (cleanText.includes(t)) {
|
||||
score += 50;
|
||||
} else if (tokens.some(scanToken => fuzzyMatch(scanToken, t, 1))) {
|
||||
score += 30; // Fuzzy token match
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Priority 4: Name & Category
|
||||
|
||||
Reference in New Issue
Block a user