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:
@@ -103,18 +103,27 @@ def startup_event():
|
||||
try:
|
||||
# Default AI Prompt from User Request
|
||||
default_prompt = (
|
||||
"identify and summarise the minimal necessary information for a quick description if item. "
|
||||
"I need the following output - <field name> : the result from you.\n"
|
||||
"For any field, do not add comments in parenthesis. \n\n"
|
||||
"Item: in three words type of this item\n"
|
||||
"Type: what type of item is, like \"spare parts\", \"consumables\", \"patch cords\" etc.\n"
|
||||
"Description: description (max 5 words)\n"
|
||||
"Category: category, if any\n"
|
||||
"Connector: connectors\n"
|
||||
"Size: size or length\n"
|
||||
"Color: color if useful\n"
|
||||
"PartNr: part number if any\n"
|
||||
"OCR: identification string for local OCR matching"
|
||||
"Extract hardware specifications with PRECISE formatting.\n"
|
||||
"For any field, do not add comments in parenthesis or measurement units in Item name.\n\n"
|
||||
"ITEM FIELD FORMAT (Critical):\n"
|
||||
"[<size_or_length>] <type> <vendor> <connector> <part_number>\n"
|
||||
" <size_or_length>: GB/TB for storage, meters for cables (e.g., '2m', '256GB', '2TB'). Omit diameter/mm measurements.\n"
|
||||
" <type>: DDR3/DDR4/DDR5/SSD/HDD/NVMe/SAS/SATA/Patchcord/Fiber/Cable/Transceiver etc.\n"
|
||||
" <vendor>: HP/HPE/Dell/Samsung/Cisco/Lenovo/Hynix etc.\n"
|
||||
" <connector>: RJ45/LC-LC/MPO/U.3/SATA/SAS/LC/ST etc. Omit if N/A.\n"
|
||||
" <part_number>: PN only if visible on label. Omit serial numbers.\n"
|
||||
"Examples: '5m Patchcord LC-LC' / '256GB SSD Samsung SAS' / '128GB DDR4 Hynix SK-234' / '2TB NVMe HP U.3'\n\n"
|
||||
"TYPE: Item asset class (DDR3/SSD/NVMe/Patchcord/SFP etc.)\n"
|
||||
"DESCRIPTION: Technical details max 5 words (e.g., 'High speed fiber optic cable'). Omit size/length here.\n"
|
||||
"CATEGORY: Broad ecosystem (Memory, Storage, Network, Cabling, etc.)\n"
|
||||
"CONNECTOR: Physical interface type (e.g., 'LC', 'RJ45', 'U.3')\n"
|
||||
"SIZE: Capacity or length ONLY (e.g., '256GB', '2m')\n"
|
||||
"COLOR: Physical color if distinguishing.\n"
|
||||
"PartNr: Part number only (no serial numbers)\n"
|
||||
"OCR: Robust matching key. Include: core type + vendor + key identifiers + common variations.\n"
|
||||
" Example for '5m Patchcord LC-LC': 'PATCHCORD 5M LC LC CAT6 FIBER'\n"
|
||||
" Include abbreviations: 'DDR4'/'DDR' or 'SSD'/'SSDS' or 'HP'/'HPE' or 'NVMe'/'NVME'\n"
|
||||
" Format: ALL UPPERCASE, space-separated tokens, NO special chars. Skip serial numbers."
|
||||
)
|
||||
|
||||
# Wrap in JSON instructions for reliable parsing
|
||||
|
||||
@@ -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