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:
@@ -30,6 +30,9 @@ except ImportError:
|
||||
import piexif
|
||||
from PIL import Image as PILImage
|
||||
|
||||
# ─── Asset matcher (photo OCR → DB lookup) ─────────────────────────────────
|
||||
from classify_makes import normalize_identifier, find_asset_by_normalized_id
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Query, Request, UploadFile, File, Form, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
@@ -2212,38 +2215,77 @@ async def ocr_sticker(file: UploadFile = File(...), exif_data: str = Form(None))
|
||||
if not saved_path:
|
||||
ocr_path.unlink(missing_ok=True)
|
||||
|
||||
# Build response — search for XXXXX-XXXXXX pattern (5 digits - 6 digits or more)
|
||||
match = re.search(r"(\d{5})[-\s]*(\d{6,})", text)
|
||||
result: dict = {}
|
||||
# Build response — search for identifiers in the OCR text
|
||||
result: dict = {
|
||||
"raw_text": text.strip()[:1000],
|
||||
}
|
||||
|
||||
# 1. Legacy pattern: XXXXX-XXXXXX (5 digits - 6+ digits = Connect ID)
|
||||
match = re.search(r"(\d{5})[-\s]*(\d{6,})", text)
|
||||
if match:
|
||||
full_match = match.group(0)
|
||||
digits_only = re.sub(r"\D", "", full_match)
|
||||
machine_id = digits_only[-5:]
|
||||
result = {
|
||||
"machine_id": machine_id,
|
||||
"raw_text": text.strip()[:500],
|
||||
"raw_match": full_match,
|
||||
"confidence": "high",
|
||||
}
|
||||
result["machine_id"] = machine_id
|
||||
result["raw_match"] = full_match
|
||||
result["confidence"] = "high"
|
||||
else:
|
||||
# Try looser: any 5+ digit number, take the last 5 digits
|
||||
loose = re.search(r"(\d{5,})", text)
|
||||
if loose:
|
||||
digits = loose.group(1)
|
||||
machine_id = digits[-5:] if len(digits) > 5 else digits
|
||||
result = {
|
||||
"machine_id": machine_id,
|
||||
"raw_text": text.strip()[:500],
|
||||
"confidence": "low",
|
||||
}
|
||||
result["machine_id"] = machine_id
|
||||
result["confidence"] = "low"
|
||||
else:
|
||||
result = {
|
||||
"machine_id": None,
|
||||
"raw_text": text.strip()[:500],
|
||||
"confidence": "none",
|
||||
"detail": "No machine ID pattern found in image. Try again with better lighting.",
|
||||
}
|
||||
result["machine_id"] = None
|
||||
result["confidence"] = "none"
|
||||
result["detail"] = "No machine ID pattern found in image. Try again with better lighting."
|
||||
|
||||
# 2. Cross-reference OCR text against DB — find matched assets by
|
||||
# serial_number, connect_id, equipment_id, or barcode
|
||||
db_path = DB_PATH
|
||||
db_matches = []
|
||||
seen_ids = set()
|
||||
for line in text.strip().split('\n'):
|
||||
line = line.strip()
|
||||
if not line or len(line) < 4:
|
||||
continue
|
||||
# Try full line
|
||||
norm = normalize_identifier(line)
|
||||
if norm and len(norm) >= 4:
|
||||
assets = find_asset_by_normalized_id(db_path, norm)
|
||||
for a in assets:
|
||||
if a['id'] not in seen_ids:
|
||||
seen_ids.add(a['id'])
|
||||
db_matches.append({
|
||||
"asset_id": a['id'],
|
||||
"machine_id": a['machine_id'],
|
||||
"name": a['name'],
|
||||
"serial_number": a['serial_number'],
|
||||
"matched_on": norm,
|
||||
"source_text": line,
|
||||
})
|
||||
# Try individual tokens on the line
|
||||
tokens = re.findall(r'[A-Za-z0-9]{4,}', line)
|
||||
for token in tokens:
|
||||
norm = normalize_identifier(token)
|
||||
if norm and len(norm) >= 4:
|
||||
assets = find_asset_by_normalized_id(db_path, norm)
|
||||
for a in assets:
|
||||
if a['id'] not in seen_ids:
|
||||
seen_ids.add(a['id'])
|
||||
db_matches.append({
|
||||
"asset_id": a['id'],
|
||||
"machine_id": a['machine_id'],
|
||||
"name": a['name'],
|
||||
"serial_number": a['serial_number'],
|
||||
"matched_on": norm,
|
||||
"source_text": token,
|
||||
})
|
||||
|
||||
if db_matches:
|
||||
result["matched_assets"] = db_matches
|
||||
|
||||
if exif_gps:
|
||||
result["exif_gps"] = exif_gps
|
||||
@@ -2273,6 +2315,75 @@ async def ocr_sticker(file: UploadFile = File(...), exif_data: str = Form(None))
|
||||
return result
|
||||
|
||||
|
||||
# ─── Match raw text against DB (for barcode scanner / client-side OCR) ────
|
||||
|
||||
@app.post("/api/match-text", status_code=200)
|
||||
async def match_text(text: str = Form(...)):
|
||||
"""
|
||||
Accept raw text (from barcode scanner, QR reader, or client-side vision),
|
||||
normalize it, and search the DB for matching assets.
|
||||
|
||||
Returns matched_assets if any found.
|
||||
"""
|
||||
if not text or len(text.strip()) < 4:
|
||||
return {"matched_assets": [], "detail": "Text too short to match"}
|
||||
|
||||
db_path = DB_PATH
|
||||
db_matches = []
|
||||
seen_ids = set()
|
||||
raw = text.strip()
|
||||
|
||||
for line in raw.split('\n'):
|
||||
line = line.strip()
|
||||
if not line or len(line) < 4:
|
||||
continue
|
||||
norm = normalize_identifier(line)
|
||||
if norm and len(norm) >= 4:
|
||||
assets = find_asset_by_normalized_id(db_path, norm)
|
||||
for a in assets:
|
||||
if a['id'] not in seen_ids:
|
||||
seen_ids.add(a['id'])
|
||||
db_matches.append({
|
||||
"asset_id": a['id'],
|
||||
"machine_id": a['machine_id'],
|
||||
"name": a['name'],
|
||||
"serial_number": a['serial_number'],
|
||||
"connect_id": a['connect_id'],
|
||||
"make": a['make'],
|
||||
"model": a['model'],
|
||||
"category": a['category'],
|
||||
"matched_on": norm,
|
||||
"source_text": line,
|
||||
})
|
||||
# Also check individual tokens
|
||||
tokens = re.findall(r'[A-Za-z0-9]{4,}', line)
|
||||
for token in tokens:
|
||||
norm = normalize_identifier(token)
|
||||
if norm and len(norm) >= 4:
|
||||
assets = find_asset_by_normalized_id(db_path, norm)
|
||||
for a in assets:
|
||||
if a['id'] not in seen_ids:
|
||||
seen_ids.add(a['id'])
|
||||
db_matches.append({
|
||||
"asset_id": a['id'],
|
||||
"machine_id": a['machine_id'],
|
||||
"name": a['name'],
|
||||
"serial_number": a['serial_number'],
|
||||
"connect_id": a['connect_id'],
|
||||
"make": a['make'],
|
||||
"model": a['model'],
|
||||
"category": a['category'],
|
||||
"matched_on": norm,
|
||||
"source_text": token,
|
||||
})
|
||||
|
||||
return {
|
||||
"raw_text": raw[:1000],
|
||||
"matched_assets": db_matches,
|
||||
"match_count": len(db_matches),
|
||||
}
|
||||
|
||||
|
||||
# ─── T4: Connect Label — unified photo + OCR + GPS endpoint ─────────────────
|
||||
|
||||
class ConnectLabelRequest(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user