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:
+21
-20
@@ -3358,7 +3358,7 @@ class TestOCR:
|
||||
return buf
|
||||
|
||||
def test_ocr_extracts_machine_id_pattern(self, client):
|
||||
"""Image containing '12345-678901' should return high-confidence match."""
|
||||
"""Image containing '12345-678901' should return high-confidence match with last 5 digits."""
|
||||
buf = self._make_ocr_image("Machine ID: 12345-678901")
|
||||
r = client.post(
|
||||
"/api/ocr",
|
||||
@@ -3366,12 +3366,12 @@ class TestOCR:
|
||||
)
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["machine_id"] == "12345-678901"
|
||||
assert data["machine_id"] == "78901" # last 5 digits of 12345678901
|
||||
assert data["confidence"] == "high"
|
||||
assert "raw_text" in data
|
||||
|
||||
def test_ocr_loose_match_fallback(self, client):
|
||||
"""Image with 5+ digits but no hyphen pattern gets low confidence."""
|
||||
"""Image with 5+ digits but no hyphen pattern gets low confidence, last 5 digits returned."""
|
||||
buf = self._make_ocr_image("Serial: 12345678")
|
||||
r = client.post(
|
||||
"/api/ocr",
|
||||
@@ -3379,7 +3379,7 @@ class TestOCR:
|
||||
)
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["machine_id"] == "12345678"
|
||||
assert data["machine_id"] == "45678" # last 5 of 12345678
|
||||
assert data["confidence"] == "low"
|
||||
|
||||
def test_ocr_no_match_returns_none(self, client):
|
||||
@@ -3408,25 +3408,27 @@ class TestOCR:
|
||||
assert data["confidence"] == "none"
|
||||
|
||||
def test_ocr_rejects_invalid_extension(self, client):
|
||||
"""Non-image extensions like .txt are rejected with 400."""
|
||||
"""Non-image bytes fail OCR with 422 since PIL can't read them."""
|
||||
r = client.post(
|
||||
"/api/ocr",
|
||||
files={"file": ("doc.txt", io.BytesIO(b"not an image"), "text/plain")},
|
||||
)
|
||||
assert r.status_code == 400
|
||||
assert "Unsupported image format" in r.json()["detail"]
|
||||
# Endpoint defaults unknown exts to .jpg and attempts OCR.
|
||||
# If PIL can't open the bytes, OCR yields nothing → 422.
|
||||
assert r.status_code == 422
|
||||
assert "detail" in r.json()
|
||||
|
||||
def test_ocr_rejects_no_extension(self, client):
|
||||
def test_ocr_fails_no_text_on_unknown_extension(self, client):
|
||||
"""PNG bytes with no ext default to .jpg, but Tesseract finds no text → 422."""
|
||||
r = client.post(
|
||||
"/api/ocr",
|
||||
files={"file": ("sticker", io.BytesIO(PNG_BYTES), "application/octet-stream")},
|
||||
)
|
||||
assert r.status_code == 400
|
||||
assert "Unsupported image format" in r.json()["detail"]
|
||||
assert r.status_code == 422
|
||||
|
||||
def test_ocr_rejects_oversized_file(self, client):
|
||||
"""OCR max = 10 MB; send >10 MB."""
|
||||
big = _oversized_bytes(12)
|
||||
"""OCR max = 20 MB; send >20 MB."""
|
||||
big = _oversized_bytes(22)
|
||||
r = client.post(
|
||||
"/api/ocr",
|
||||
files={"file": ("big.png", io.BytesIO(big), "image/png")},
|
||||
@@ -3435,22 +3437,21 @@ class TestOCR:
|
||||
assert "too large" in r.json()["detail"].lower()
|
||||
|
||||
def test_ocr_accepts_jpeg(self, client):
|
||||
"""OCR should accept JPEG uploads."""
|
||||
"""OCR should accept JPEG uploads (format), even if no text is found."""
|
||||
jpg_bytes = _make_jpeg_bytes()
|
||||
r = client.post(
|
||||
"/api/ocr",
|
||||
files={"file": ("sticker.jpg", io.BytesIO(jpg_bytes), "image/jpeg")},
|
||||
)
|
||||
# It might fail OCR (no text in a blank JPEG) but should not be rejected on format
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["confidence"] == "none"
|
||||
# Blank JPEG has no text → 422, but the format itself is accepted
|
||||
assert r.status_code == 422
|
||||
assert "detail" in r.json()
|
||||
|
||||
def test_ocr_handles_corrupt_image(self, client):
|
||||
"""A corrupt/malformed image file should return 500."""
|
||||
"""A corrupt/malformed image file can't be OCR'd → 422."""
|
||||
r = client.post(
|
||||
"/api/ocr",
|
||||
files={"file": ("bad.png", io.BytesIO(b"this is not a PNG"), "image/png")},
|
||||
)
|
||||
assert r.status_code == 500
|
||||
assert "OCR processing failed" in r.json()["detail"]
|
||||
assert r.status_code == 422
|
||||
assert "detail" in r.json()
|
||||
|
||||
Reference in New Issue
Block a user