feat(app): scan-mode sync, confirmation-gated documents, single-pass product classification
Fixes reported from APK field testing: DO/Product scan mode was inconsistent between the camera drawer and documents screen (now one shared provider, with an orange/green color cue); unconfirmed scans leaked into history with placeholder data before the user tapped confirm (backend now gates GET /documents on a new `confirmed` column, flipped only by PUT); and Product Scan ran the GPU classifier twice, once at upload and again on review (now a single pass at upload, persisted and read directly by the editor). Also removes the unused "Hubungkan ke PO" field and fabricated PO/SO/DO placeholder values from the Product Scan flow, closes out the per-document-polling and save-recovery tasks (6.1/6.3), and splits several touched files to stay under the repo's 256-line guideline. Full detail in docs/iteration-log.md and backend/docs/iteration-log.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2febe0c886
commit
ada6488592
67 files changed
+5705
-1142
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import base64
|
||||
import io
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
@@ -471,6 +472,45 @@ async def classify_ocr(payload: ScanRequest):
|
||||
raw_image = Image.open(io.BytesIO(img_data))
|
||||
image = ImageOps.exif_transpose(raw_image).convert("RGB")
|
||||
|
||||
# First-pass PaddleOCR to check orientation based on Expiry Date
|
||||
rotated_image_used = False
|
||||
res_list = []
|
||||
text_lines = []
|
||||
text_polys = []
|
||||
expired_date = None
|
||||
expired_idx = None
|
||||
expired_source_line = None
|
||||
|
||||
if ocr:
|
||||
try:
|
||||
img_arr = np.array(image)
|
||||
res_list = list(ocr.predict(img_arr))
|
||||
if res_list and len(res_list) > 0:
|
||||
res_entry = res_list[0]
|
||||
text_lines = res_entry.get("rec_texts", [])
|
||||
text_polys = ocr_text_polys(res_entry)
|
||||
expired_date, expired_idx, expired_source_line = extract_expired_date(text_lines)
|
||||
|
||||
if expired_idx is not None and expired_idx < len(text_polys):
|
||||
poly = text_polys[expired_idx]
|
||||
if len(poly) >= 2:
|
||||
p0 = poly[0]
|
||||
p1 = poly[1]
|
||||
dx = float(p1[0]) - float(p0[0])
|
||||
dy = float(p1[1]) - float(p0[1])
|
||||
|
||||
angle_rad = math.atan2(dy, dx)
|
||||
angle_deg = math.degrees(angle_rad)
|
||||
|
||||
# Standardize tilt rotation
|
||||
if abs(angle_deg) > 3.0:
|
||||
print(f"[Auto-Rotate] Detected Expiry Date text line angle: {angle_deg:.2f} degrees. Rotating image...")
|
||||
image = image.rotate(angle_deg, resample=Image.BICUBIC, expand=True)
|
||||
rotated_image_used = True
|
||||
except Exception as pre_ocr_err:
|
||||
print(f"Error in pre-pass OCR: {pre_ocr_err}")
|
||||
traceback.print_exc()
|
||||
|
||||
# 1. Run DINOv2 Similarity Search or YOLO Classification
|
||||
classification_result = {}
|
||||
top1_name = None
|
||||
@@ -561,24 +601,33 @@ async def classify_ocr(payload: ScanRequest):
|
||||
# 2. Run PaddleOCR
|
||||
ocr_result = {}
|
||||
if ocr:
|
||||
img_arr = np.array(image)
|
||||
# Use predict method and convert generator to list
|
||||
res_list = list(ocr.predict(img_arr))
|
||||
|
||||
text_lines = []
|
||||
if res_list and len(res_list) > 0:
|
||||
text_lines = res_list[0].get("rec_texts", [])
|
||||
|
||||
sku = extract_sku(text_lines)
|
||||
expired_date, expired_idx, expired_source_line = extract_expired_date(text_lines)
|
||||
product_name = extract_product_name(text_lines, top1_name)
|
||||
|
||||
res_entry = res_list[0] if res_list else {}
|
||||
coord_image = ocr_coordinate_image(res_entry, image)
|
||||
text_polys = ocr_text_polys(res_entry)
|
||||
crop_idx = find_expired_crop_index(
|
||||
text_lines, expired_idx, expired_date, len(text_polys)
|
||||
)
|
||||
if rotated_image_used:
|
||||
img_arr = np.array(image)
|
||||
# Use predict method and convert generator to list
|
||||
res_list = list(ocr.predict(img_arr))
|
||||
|
||||
text_lines = []
|
||||
if res_list and len(res_list) > 0:
|
||||
text_lines = res_list[0].get("rec_texts", [])
|
||||
|
||||
sku = extract_sku(text_lines)
|
||||
expired_date, expired_idx, expired_source_line = extract_expired_date(text_lines)
|
||||
product_name = extract_product_name(text_lines, top1_name)
|
||||
|
||||
res_entry = res_list[0] if res_list else {}
|
||||
coord_image = ocr_coordinate_image(res_entry, image)
|
||||
text_polys = ocr_text_polys(res_entry)
|
||||
crop_idx = find_expired_crop_index(
|
||||
text_lines, expired_idx, expired_date, len(text_polys)
|
||||
)
|
||||
else:
|
||||
sku = extract_sku(text_lines)
|
||||
product_name = extract_product_name(text_lines, top1_name)
|
||||
res_entry = res_list[0] if res_list else {}
|
||||
coord_image = ocr_coordinate_image(res_entry, image)
|
||||
crop_idx = find_expired_crop_index(
|
||||
text_lines, expired_idx, expired_date, len(text_polys)
|
||||
)
|
||||
|
||||
# Create visual OCR image with bounding boxes
|
||||
vis_image_b64 = None
|
||||
@@ -633,7 +682,13 @@ async def classify_ocr(payload: ScanRequest):
|
||||
# 3. Call Spotting API
|
||||
spotting_image_b64 = None
|
||||
try:
|
||||
img_b64_only = payload.image_base64.split(",")[-1]
|
||||
if rotated_image_used:
|
||||
buffered = io.BytesIO()
|
||||
image.save(buffered, format="JPEG")
|
||||
img_b64_only = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||||
else:
|
||||
img_b64_only = payload.image_base64.split(",")[-1]
|
||||
|
||||
spotting_payload = {
|
||||
"file": img_b64_only,
|
||||
"matchHistoryJob": False,
|
||||
|
||||
Reference in new issue
Block a user