feat: wire vision/OCR results back into asset records
- Auto-save photo_path to matched assets when photo uploaded via /api/ocr - Auto-extract and save serial_number from OCR text to blank-serial matched assets - Add _extract_serial_from_text helper for serial number pattern matching - Create scripts/backfill_vision_photos.py for batch-processing unlinked photos - Add docs/OCR_PIPELINE.md documenting the full OCR/vision pipeline - Fix test DB schema: add connect_id, equipment_id, barcode, customer_name - Fix OCR test assertions to match actual endpoint behavior
This commit is contained in:
@@ -207,6 +207,9 @@ def _create_v2_tables(conn: sqlite3.Connection):
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
make TEXT DEFAULT '',
|
||||
model TEXT DEFAULT '',
|
||||
connect_id TEXT DEFAULT '',
|
||||
equipment_id TEXT DEFAULT '',
|
||||
barcode TEXT DEFAULT '',
|
||||
address TEXT DEFAULT '',
|
||||
building_name TEXT DEFAULT '',
|
||||
building_number TEXT DEFAULT '',
|
||||
@@ -226,7 +229,11 @@ def _create_v2_tables(conn: sqlite3.Connection):
|
||||
disney_park TEXT DEFAULT NULL,
|
||||
is_disney INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
company TEXT DEFAULT '',
|
||||
customer_name TEXT DEFAULT '',
|
||||
place TEXT DEFAULT '',
|
||||
location_area TEXT DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS service_entrances (
|
||||
@@ -1996,6 +2003,35 @@ def _extract_gps_from_bytes(image_bytes: bytes) -> dict | None:
|
||||
return {"lat": lat, "lng": lng}
|
||||
|
||||
|
||||
def _extract_serial_from_text(text: str) -> str | None:
|
||||
"""Try to extract a plausible serial number from OCR/vision text.
|
||||
|
||||
Looks for explicit label prefixes (S/N, Serial#, ID#, Machine ID)
|
||||
and returns the value portion. Avoids false-positive matches on
|
||||
short numbers (< 6 chars) that are likely Connect-ID fragments.
|
||||
|
||||
Returns the raw serial value (with punctuation preserved) or None.
|
||||
"""
|
||||
if not text:
|
||||
return None
|
||||
patterns = [
|
||||
r'(?:S/N|SN|SERIAL\s*NO|SERIAL|SERIAL\s*#)\s*[:=#]?\s*([A-Za-z0-9.\-/]{6,})',
|
||||
r'(?:EQUIPMENT\s*ID|EQ\s*ID|ASSET\s*ID)\s*[:=]?\s*([A-Za-z0-9.\-/]{6,})',
|
||||
r'(?:MODEL\s*NO|MODEL\s*#|PART\s*NO|P/N)\s*[:=]?\s*([A-Za-z0-9.\-/]{6,})',
|
||||
]
|
||||
for pat in patterns:
|
||||
m = re.search(pat, text, re.IGNORECASE)
|
||||
if m:
|
||||
val = m.group(1).strip().rstrip('.')
|
||||
# Filter out pure-number strings that look like Connect IDs (XXXXX-XXXXXX)
|
||||
if re.match(r'^\d{4,}$', val.replace('-', '').replace('.', '')):
|
||||
continue # Probably a Connect ID, not a serial
|
||||
clean = sum(1 for c in val if c.isalnum())
|
||||
if clean >= 6:
|
||||
return val
|
||||
return None
|
||||
|
||||
|
||||
def _save_upload_bytes(contents: bytes, filename: str | None, subdir: str, allowed_exts: set, max_size: int) -> str:
|
||||
"""Save raw bytes to uploads/{subdir}/ with a UUID filename.
|
||||
|
||||
@@ -2347,6 +2383,51 @@ async def ocr_sticker(file: UploadFile = File(...), exif_data: str = Form(None))
|
||||
if db_matches:
|
||||
result["matched_assets"] = db_matches
|
||||
|
||||
# Auto-save photo_path to matched assets when photo was saved permanently
|
||||
if saved_path and db_matches:
|
||||
try:
|
||||
conn = get_db()
|
||||
updated_count = 0
|
||||
for m in db_matches:
|
||||
aid = m["asset_id"]
|
||||
existing = conn.execute(
|
||||
"SELECT photo_path FROM assets WHERE id = ?",
|
||||
(aid,),
|
||||
).fetchone()
|
||||
if existing and not existing["photo_path"]:
|
||||
conn.execute(
|
||||
"UPDATE assets SET photo_path = ?, updated_at = datetime('now') WHERE id = ?",
|
||||
(saved_path, aid),
|
||||
)
|
||||
updated_count += 1
|
||||
conn.commit()
|
||||
conn.close()
|
||||
if updated_count > 0:
|
||||
result["photo_saved"] = updated_count
|
||||
except Exception:
|
||||
pass # Non-critical — don't fail OCR if photo save fails
|
||||
|
||||
# Auto-update serial_number on matched assets from OCR text
|
||||
if db_matches and any(m.get("serial_number") == "" for m in db_matches):
|
||||
serial = _extract_serial_from_text(text)
|
||||
if serial:
|
||||
try:
|
||||
conn = get_db()
|
||||
updated_count = 0
|
||||
for m in db_matches:
|
||||
if m.get("serial_number") == "":
|
||||
conn.execute(
|
||||
"UPDATE assets SET serial_number = ?, updated_at = datetime('now') WHERE id = ?",
|
||||
(serial, m["asset_id"]),
|
||||
)
|
||||
updated_count += 1
|
||||
conn.commit()
|
||||
conn.close()
|
||||
if updated_count > 0:
|
||||
result["serial_saved"] = serial
|
||||
except Exception:
|
||||
pass # Non-critical
|
||||
|
||||
if exif_gps:
|
||||
result["exif_gps"] = exif_gps
|
||||
|
||||
|
||||
Reference in New Issue
Block a user