feat: add normalized matching for label photos → asset DB lookup

- normalize_identifier() strips dots/dashes/prefixes, keeps alphanumeric
- find_asset_by_normalized_id() searches serial_number, connect_id,
  equipment_id, barcode with normalized comparison
- /api/ocr now returns matched_assets in addition to legacy machine_id
- New /api/match-text endpoint for client-side text matching
- scripts/match_label_photo.py CLI tool for OCR + DB matching
- Vision model fixed (mimo-v2-omni at opencode.ai, was using
  truncated placeholder key)
This commit is contained in:
2026-05-29 08:37:06 -04:00
parent e10e226743
commit 8022c77b70
3 changed files with 457 additions and 20 deletions
+107
View File
@@ -23,6 +23,113 @@ from typing import Optional, Tuple
DB_PATH = str(Path(__file__).parent / "assets.db")
# ─── Universal identifier normalization (for photo→DB matching) ───────────
def normalize_identifier(raw: str) -> str:
"""
Normalize any asset identifier (serial number, barcode, equipment ID,
connect ID, machine ID) for comparison.
- Strips leading label prefixes (S/N:, ID#, Machine ID:, Monyx ID, etc.)
- Removes dots, dashes, spaces, slashes, colons
- Uppercases
- Returns just the alphanumeric core for matching
Examples:
'2500.0100.0025534''2500010000255534'
'201037BA00039''201037BA00039'
'S/N: 2500.0100.0025534''2500010000255534'
'ID# 4434331624226353''4434331624226353'
'Monyx ID 48602143''48602143'
'RY10006338''RY10006338'
'201037BA00039''201037BA00039'
"""
if not raw:
return ''
s = raw.strip().upper()
# Strip common label prefixes
s = re.sub(
r'^(S/N|SN|SERIAL|SERIAL\s*NO|ID|UID|MACHINE\s*ID|MACHINE|'
r'EQUIPMENT\s*ID|EQ\s*ID|ASSET\s*ID|ITEM|MODEL|PART\s*NO|'
r'MONYX\s*ID|PROPERTY\s*OF|BARCODE)\s*[:=#]\s*',
'', s, flags=re.IGNORECASE
)
# Strip leading non-alphanumeric (leftover label debris)
s = re.sub(r'^[^A-Z0-9]+', '', s)
# Remove all non-alphanumeric (dots, dashes, spaces, etc.)
s = re.sub(r'[^A-Z0-9]', '', s)
return s
def find_asset_by_normalized_id(db_path: str, normalized: str) -> list:
"""
Search assets.db for any asset whose serial_number, machine_id, connect_id,
equipment_id, or barcode matches the given normalized identifier.
Returns a list of matching rows (dicts).
"""
if not normalized or len(normalized) < 3:
return []
import sqlite3
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
rows = conn.execute("""
SELECT id, machine_id, name, serial_number, connect_id, equipment_id,
barcode, make, model, category
FROM assets
WHERE replace(replace(replace(replace(upper(serial_number), '-', ''), '.', ''), ' ', ''), '/', '') = ?
OR replace(replace(replace(replace(upper(connect_id), '-', ''), '.', ''), ' ', ''), '/', '') = ?
OR replace(replace(replace(replace(upper(equipment_id), '-', ''), '.', ''), ' ', ''), '/', '') = ?
OR replace(replace(replace(replace(upper(machine_id), '-', ''), '.', ''), ' ', ''), '/', '') = ?
OR replace(replace(replace(replace(upper(barcode), '-', ''), '.', ''), ' ', ''), '/', '') = ?
-- Connect-ID suffix match (last 7+ digits → equipment_id suffix)
""", (normalized, normalized, normalized, normalized, normalized)).fetchall()
conn.close()
return [dict(r) for r in rows]
def find_assets_by_scanned_text(db_path: str, raw_text: str) -> list:
"""
Given raw OCR text from a label photo, extract all plausible identifiers
and search the DB for matches. Returns list of (normalized, field, asset) tuples.
"""
if not raw_text:
return []
results = []
# 1. Try each line as a potential identifier
lines = raw_text.strip().split('\n')
for line in lines:
line = line.strip()
if not line or len(line) < 4:
continue
norm = normalize_identifier(line)
if len(norm) >= 4:
matches = find_asset_by_normalized_id(db_path, norm)
for m in matches:
results.append((norm, line.strip(), m))
# 2. Also try individual number-like tokens on each line (space-separated values on a line)
for line in lines:
tokens = re.findall(r'[A-Z0-9]{4,}', line.upper())
for token in tokens:
if len(token) >= 4:
matches = find_asset_by_normalized_id(db_path, token)
for m in matches:
results.append((token, line.strip(), m))
# Deduplicate by asset id
seen = set()
unique = []
for norm, src, asset in results:
if asset['id'] not in seen:
seen.add(asset['id'])
unique.append({'normalized': norm, 'source_text': src, 'asset': asset})
return unique
# ─── Character substitution (human entry errors) ──────────────────────────
def normalise_serial(sn: str) -> str: