Spaces:
Runtime error
Runtime error
Franko Fišter
commited on
Commit
·
460ecbc
1
Parent(s):
ee22cda
Backend first commit
Browse files- .gitattributes +35 -0
- README.md +12 -1
- __pycache__/app.cpython-312.pyc +0 -0
- __pycache__/inference.cpython-312.pyc +0 -0
- app.py +127 -0
- inference.py +113 -0
- model.onnx +3 -0
- receipt-vision-key.json +13 -0
- receipt_processor/__pycache__/google_ocr.cpython-312.pyc +0 -0
- receipt_processor/__pycache__/receipt_parser.cpython-312.pyc +0 -0
- receipt_processor/google_ocr.py +12 -0
- receipt_processor/receipt_parser.py +58 -0
- requirements.txt +10 -0
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1 +1,12 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Endpoint
|
| 3 |
+
emoji: 😻
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 5.23.1
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
__pycache__/app.cpython-312.pyc
ADDED
|
Binary file (4.82 kB). View file
|
|
|
__pycache__/inference.cpython-312.pyc
ADDED
|
Binary file (8.44 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from inference import ObjectDetector
|
| 4 |
+
import numpy as np
|
| 5 |
+
import cv2
|
| 6 |
+
from receipt_processor.google_ocr import GoogleVisionOCR
|
| 7 |
+
from receipt_processor.receipt_parser import ReceiptParser
|
| 8 |
+
|
| 9 |
+
# Configuration
|
| 10 |
+
MODEL_ONNX_PATH = "model.onnx"
|
| 11 |
+
CLASS_NAMES = [
|
| 12 |
+
'Dukat_Maslac_250g_1',
|
| 13 |
+
'Z-Bregov_Maslac_250g_1',
|
| 14 |
+
'Zdenka_Maslac_250g_1',
|
| 15 |
+
'President_Gouda-Sir_250g_1',
|
| 16 |
+
'Cekin_Pileća-Prsa_500g_1',
|
| 17 |
+
'Franck_Crema-Kava_175g_1',
|
| 18 |
+
'Franck_Crema-Kava_250g_1',
|
| 19 |
+
'Franck_Instant-Crema-Kava_80g_1',
|
| 20 |
+
'Franck_Intense-Kava_250g_1',
|
| 21 |
+
'Franck_Original-Kava_250g_1',
|
| 22 |
+
'Franck_Sensual-Kava_250g_1',
|
| 23 |
+
'Coca-Cola_Coca-Cola-Original_1l_1',
|
| 24 |
+
'Mlineta_Oštro-Brašno_1kg_1',
|
| 25 |
+
'Vindi_Naranča-Nektar_1l_1',
|
| 26 |
+
'Zvijezda_Mild-Ketchup_500g_1',
|
| 27 |
+
'Zvijezda_Delicate-Majoneza_400g_1',
|
| 28 |
+
'Z-Bregov_Trajno-Mlijeko-2.8%_1l_1',
|
| 29 |
+
'Dijamant_Suncokretovo-Ulje_1l_1',
|
| 30 |
+
'Zvijezda_Suncokretovo-Ulje_1l_1',
|
| 31 |
+
'Barilla_Fusilli-Tijesto_500g_1',
|
| 32 |
+
'Gallo_Riža_900g_1',
|
| 33 |
+
'Kplus_Riža_1kg_1',
|
| 34 |
+
'Solana-Pag_Sitna-Sol_1kg_1',
|
| 35 |
+
'Pasta-Zara_Spaghettini-Tijesto_500g_1',
|
| 36 |
+
'Rio-Mare_Konzervativna-Tuna_150g_1'
|
| 37 |
+
]
|
| 38 |
+
INPUT_SIZE = 640
|
| 39 |
+
|
| 40 |
+
# Initialize detector
|
| 41 |
+
detector = ObjectDetector(
|
| 42 |
+
model_path=MODEL_ONNX_PATH,
|
| 43 |
+
class_names=CLASS_NAMES,
|
| 44 |
+
input_size=INPUT_SIZE
|
| 45 |
+
)
|
| 46 |
+
ocr_processor = GoogleVisionOCR()
|
| 47 |
+
receipt_parser = ReceiptParser()
|
| 48 |
+
# Initialize FastAPI
|
| 49 |
+
app = FastAPI()
|
| 50 |
+
|
| 51 |
+
# Enhanced CORS configuration
|
| 52 |
+
app.add_middleware(
|
| 53 |
+
CORSMiddleware,
|
| 54 |
+
allow_origins=["*"],
|
| 55 |
+
allow_credentials=True,
|
| 56 |
+
allow_methods=["*"],
|
| 57 |
+
allow_headers=["*"],
|
| 58 |
+
expose_headers=["*"]
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
@app.options("/detect")
|
| 62 |
+
async def detect_options():
|
| 63 |
+
return {"Allow": "POST"}
|
| 64 |
+
|
| 65 |
+
@app.get("/")
|
| 66 |
+
def health_check():
|
| 67 |
+
return {"status": "OK", "model": "Object Detection API"}
|
| 68 |
+
|
| 69 |
+
@app.post("/detect")
|
| 70 |
+
async def detect_objects(file: UploadFile = File(...)):
|
| 71 |
+
try:
|
| 72 |
+
if not file.content_type.startswith("image/"):
|
| 73 |
+
raise HTTPException(400, "File must be an image")
|
| 74 |
+
|
| 75 |
+
image_data = await file.read()
|
| 76 |
+
image = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR)
|
| 77 |
+
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # <<< ADD THIS LINE
|
| 78 |
+
if image is None:
|
| 79 |
+
raise HTTPException(400, "Invalid image data")
|
| 80 |
+
|
| 81 |
+
# Remove RGB conversion - models expect BGR from OpenCV
|
| 82 |
+
# image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # DELETE THIS LINE
|
| 83 |
+
|
| 84 |
+
# Fix variable reference
|
| 85 |
+
detections = detector.predict(image) # Add this line
|
| 86 |
+
|
| 87 |
+
return {
|
| 88 |
+
"status": "success",
|
| 89 |
+
"detections": detections, # Use the variable
|
| 90 |
+
"count": len(detections) # Now properly defined
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
except HTTPException:
|
| 94 |
+
raise
|
| 95 |
+
except Exception as e:
|
| 96 |
+
raise HTTPException(500, f"Processing error: {str(e)}")
|
| 97 |
+
|
| 98 |
+
# Add new endpoint
|
| 99 |
+
@app.post("/receipt-ocr")
|
| 100 |
+
async def process_receipt(file: UploadFile = File(...)):
|
| 101 |
+
try:
|
| 102 |
+
if not file.content_type.startswith("image/"):
|
| 103 |
+
raise HTTPException(400, "File must be an image")
|
| 104 |
+
|
| 105 |
+
content = await file.read()
|
| 106 |
+
extracted_text = ocr_processor.extract_text(content)
|
| 107 |
+
|
| 108 |
+
if not extracted_text:
|
| 109 |
+
raise HTTPException(400, "No text extracted from image")
|
| 110 |
+
|
| 111 |
+
parsed_receipt = receipt_parser.parse_receipt_text(extracted_text)
|
| 112 |
+
|
| 113 |
+
return {
|
| 114 |
+
"status": "success",
|
| 115 |
+
"receipt": parsed_receipt
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
except HTTPException:
|
| 119 |
+
raise
|
| 120 |
+
except Exception as e:
|
| 121 |
+
raise HTTPException(500, f"Receipt processing error: {str(e)}")
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
if __name__ == "__main__":
|
| 126 |
+
import uvicorn
|
| 127 |
+
uvicorn.run(app, host="0.0.0.0", port=7860) # Hugging Face requires port 7860
|
inference.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import cv2
|
| 3 |
+
import onnxruntime as ort
|
| 4 |
+
from typing import List, Dict, Tuple
|
| 5 |
+
|
| 6 |
+
class ObjectDetector:
|
| 7 |
+
def __init__(self, model_path: str, class_names: List[str], input_size: int = 640):
|
| 8 |
+
self.class_names = class_names
|
| 9 |
+
self.input_size = input_size
|
| 10 |
+
self.session = self._load_model(model_path)
|
| 11 |
+
self._warmup()
|
| 12 |
+
|
| 13 |
+
def _load_model(self, model_path: str) -> ort.InferenceSession:
|
| 14 |
+
options = ort.SessionOptions()
|
| 15 |
+
options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
| 16 |
+
return ort.InferenceSession(
|
| 17 |
+
model_path,
|
| 18 |
+
providers=['CUDAExecutionProvider', 'CPUExecutionProvider'],
|
| 19 |
+
sess_options=options
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
def _warmup(self):
|
| 23 |
+
dummy_input = np.random.randn(1, 3, self.input_size, self.input_size).astype(np.float32)
|
| 24 |
+
self.session.run(None, {"images": dummy_input})
|
| 25 |
+
|
| 26 |
+
@staticmethod
|
| 27 |
+
def compute_iou(box: np.ndarray, boxes: np.ndarray) -> np.ndarray:
|
| 28 |
+
xmin = np.maximum(box[0], boxes[:, 0])
|
| 29 |
+
ymin = np.maximum(box[1], boxes[:, 1])
|
| 30 |
+
xmax = np.minimum(box[2], boxes[:, 2])
|
| 31 |
+
ymax = np.minimum(box[3], boxes[:, 3])
|
| 32 |
+
|
| 33 |
+
intersection_area = np.maximum(0, xmax - xmin) * np.maximum(0, ymax - ymin)
|
| 34 |
+
box_area = (box[2] - box[0]) * (box[3] - box[1])
|
| 35 |
+
boxes_area = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])
|
| 36 |
+
|
| 37 |
+
return intersection_area / (box_area + boxes_area - intersection_area + 1e-6)
|
| 38 |
+
|
| 39 |
+
@staticmethod
|
| 40 |
+
def nms(boxes: np.ndarray, scores: np.ndarray, iou_threshold: float) -> List[int]:
|
| 41 |
+
sorted_indices = np.argsort(scores)[::-1]
|
| 42 |
+
keep_boxes = []
|
| 43 |
+
|
| 44 |
+
while sorted_indices.size > 0:
|
| 45 |
+
box_id = sorted_indices[0]
|
| 46 |
+
keep_boxes.append(box_id)
|
| 47 |
+
ious = ObjectDetector.compute_iou(boxes[box_id, :], boxes[sorted_indices[1:], :])
|
| 48 |
+
keep_indices = np.where(ious < iou_threshold)[0]
|
| 49 |
+
sorted_indices = sorted_indices[keep_indices + 1]
|
| 50 |
+
return keep_boxes
|
| 51 |
+
|
| 52 |
+
def preprocess(self, image: np.ndarray) -> Tuple[np.ndarray, float, Tuple[int, int]]:
|
| 53 |
+
h, w = image.shape[:2]
|
| 54 |
+
scale = min(self.input_size / h, self.input_size / w)
|
| 55 |
+
new_h, new_w = int(h * scale), int(w * scale)
|
| 56 |
+
|
| 57 |
+
resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
|
| 58 |
+
canvas = np.full((self.input_size, self.input_size, 3), 114, dtype=np.uint8)
|
| 59 |
+
ph, pw = (self.input_size - new_h) // 2, (self.input_size - new_w) // 2
|
| 60 |
+
canvas[ph:ph+new_h, pw:pw+new_w] = resized
|
| 61 |
+
|
| 62 |
+
blob = canvas.astype(np.float32) / 255.0
|
| 63 |
+
return blob.transpose(2, 0, 1)[None, ...], scale, (pw, ph)
|
| 64 |
+
|
| 65 |
+
def postprocess(
|
| 66 |
+
self,
|
| 67 |
+
predictions: np.ndarray,
|
| 68 |
+
original_shape: Tuple[int, int],
|
| 69 |
+
scale: float,
|
| 70 |
+
padding: Tuple[int, int],
|
| 71 |
+
conf_threshold: float = 0.3,
|
| 72 |
+
iou_threshold: float = 0.45
|
| 73 |
+
) -> List[Dict]:
|
| 74 |
+
predictions = np.squeeze(predictions).T
|
| 75 |
+
scores = np.max(predictions[:, 4:], axis=1)
|
| 76 |
+
valid = scores > conf_threshold
|
| 77 |
+
predictions = predictions[valid]
|
| 78 |
+
|
| 79 |
+
if predictions.size == 0:
|
| 80 |
+
return []
|
| 81 |
+
|
| 82 |
+
boxes = predictions[:, :4]
|
| 83 |
+
boxes[:, [0, 1]] = boxes[:, [0, 1]] - boxes[:, [2, 3]] / 2
|
| 84 |
+
boxes[:, [2, 3]] = boxes[:, [0, 1]] + boxes[:, [2, 3]]
|
| 85 |
+
|
| 86 |
+
pad_w, pad_h = padding
|
| 87 |
+
boxes[:, [0, 2]] = (boxes[:, [0, 2]] - pad_w) / scale
|
| 88 |
+
boxes[:, [1, 3]] = (boxes[:, [1, 3]] - pad_h) / scale
|
| 89 |
+
|
| 90 |
+
h, w = original_shape
|
| 91 |
+
boxes[:, [0, 2]] = boxes[:, [0, 2]].clip(0, w)
|
| 92 |
+
boxes[:, [1, 3]] = boxes[:, [1, 3]].clip(0, h)
|
| 93 |
+
|
| 94 |
+
class_ids = np.argmax(predictions[:, 4:], axis=1)
|
| 95 |
+
indices = self.nms(boxes, scores[valid], iou_threshold)
|
| 96 |
+
|
| 97 |
+
return [{
|
| 98 |
+
"class": self.class_names[int(class_ids[i])],
|
| 99 |
+
"confidence": float(scores[valid][i]),
|
| 100 |
+
"bbox": boxes[i].tolist(),
|
| 101 |
+
"bbox_normalized": [
|
| 102 |
+
float((boxes[i][0] + boxes[i][2])/2 / w),
|
| 103 |
+
float((boxes[i][1] + boxes[i][3])/2 / h),
|
| 104 |
+
float((boxes[i][2] - boxes[i][0]) / w),
|
| 105 |
+
float((boxes[i][3] - boxes[i][1]) / h)
|
| 106 |
+
]
|
| 107 |
+
} for i in indices]
|
| 108 |
+
|
| 109 |
+
def predict(self, image: np.ndarray) -> List[Dict]:
|
| 110 |
+
"""Main prediction method"""
|
| 111 |
+
input_tensor, scale, padding = self.preprocess(image)
|
| 112 |
+
outputs = self.session.run(None, {"images": input_tensor})
|
| 113 |
+
return self.postprocess(outputs[0], image.shape[:2], scale, padding)
|
model.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5b42cdb2b3e5a7e706f8f03d3541324a3d7d3f5da4de71e9b3769e981d2d22b9
|
| 3 |
+
size 103663101
|
receipt-vision-key.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"type": "service_account",
|
| 3 |
+
"project_id": "singular-cache-417619",
|
| 4 |
+
"private_key_id": "654a5fedb5f0b75c770321448ea36510da45e347",
|
| 5 |
+
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDfp8lRIZWfmjvC\nn5GHKlWdplBSdUSLv57J3KWb0yDx1WZ+G3W7/vYeyn1QhSnFXe7p990A2NZD7Lbx\nYTfeUD2+msj3GQeKq1c7EwLvPNiy5HRTxdk33LkE066BD9icvS34ZvGAgMQxdy1f\nH3BebAr+7A5uZeMz+TmN+r+flbsp4oabA++qafUFGVZqn3GG+9+3cnhZlhhna8GR\nQPQgX43GXD9eVLzVtjdVGRg4CVwjSn/rR33+MkEqoxBKAb3t86KNevvRvAqCVRgK\nHw+/VDdJrD4tCDCDxNapIpF9s6ZnrFhKK8yzRUNqgj5FMQX7cTJ2b5lpR1Ucwogq\neXaof3nlAgMBAAECggEAIS1Q3A/lE+SrdkK48gnJ4wWzlxPNvARMIAoy5+NhGPas\nykq1A5L9/BHSFpJ2YJB/WyY5WsGPwUo5ViOzh680BZUM+Di2iW/C1CDNF+OZCqqA\nhhfMkfCUYp6rHXqWCaQ3kEhnDUass+DHsntlriANXoTyXBaRpllTXBgk+l2aAsuQ\nZdGYSaMAM1vpK7yp+TwdBteefrE7BgE+rareh/tU9Dhq6rzJq8S3t5m+lJa8e+sw\nUPW9HjlP+Q9W5MQPf6cXiUmhoo+OXKkCuz9r1BRGyZnlVkgy3jPpjJBNOIg3Jtiy\nO/NnhunPf8mZi9sa4TMud+ekT/HnPmGh4xbmAvIH+QKBgQD0bFK6rbJl+gxqQPo7\nSDwF0ys5Sapr75Bt7Vj8LGnFTQKFvMRBU/N89ISVcB8V7sA/6ApDQ/4CIyg61HkW\nhKcIOqpPQUQgftl6TkHwJ8PtmZlTsfgRKfbnbtRK8o/tdaNgNnjFz3kkSGb8PzQp\njo5VcaNYqwFJ2lsbEx05/Ba4WQKBgQDqP6aShXzz+ToVEr8wrax08JrRRd7RUjoR\nJVmRuFwPn6XxB/J4B4wCUQFdkhN/BUL+f+IzpsQLP9LWxeH3sANppOIYXzihz2nO\nD4uK7JUkpcDTSkLNaZ99eJmG5gCR7OJoxT5z3a+USpwzTEkWiJmJFmlazcPFZ4Xx\n/JDaistcbQKBgQCRX7gYxeScWIt3yuvJxJ3GjSFhMlpFVjgd2ZQacEP8kGAWsP49\nKLRiNoCA7S3f+p+notgvx8nU9Zog22ylowJBl7rh5pyhgzDQWKlJMC2NLNUP/YLg\nmof6gGWNqhVGk7g3Kk7MwCh6FwIBt4nLybkIQs13mEXs6g1T3ht8+F1/CQKBgQDE\nBm3jcXfGRsq3Nc/u8Xc/CNXVyM2Uh2X2UTYqPogzvtrD4G2kylP84ELvRb2w7vtI\nNEZcCPNHoqpSdpgJiS7h6kwWLyEaL5MQEGwq3p5UY60AY8WRVhFUk2aOv8y8UOqK\n2HzRwzMaOnGKcA09oSQy1yFlDooEmQQ7I6soZzuU5QKBgBPeYC/Sre6uGBnGcJ7N\nGQ7Kjiej4aIhne2XHOjkJEAYDwSnm3jEB69wARB+oHiCTY1Us9n1jrhxgjsppcif\nmgG8ynTv4fGB+H15fhdXbm3pZg72w3KcnumV4i1moF7J9zp/+rgcrOn1B1ucXOb9\nVgr1+d15I5+k+QjZ7v10itLx\n-----END PRIVATE KEY-----\n",
|
| 6 |
+
"client_email": "kiki-163@singular-cache-417619.iam.gserviceaccount.com",
|
| 7 |
+
"client_id": "116841134751275341792",
|
| 8 |
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
| 9 |
+
"token_uri": "https://oauth2.googleapis.com/token",
|
| 10 |
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
| 11 |
+
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/kiki-163%40singular-cache-417619.iam.gserviceaccount.com",
|
| 12 |
+
"universe_domain": "googleapis.com"
|
| 13 |
+
}
|
receipt_processor/__pycache__/google_ocr.cpython-312.pyc
ADDED
|
Binary file (1.31 kB). View file
|
|
|
receipt_processor/__pycache__/receipt_parser.cpython-312.pyc
ADDED
|
Binary file (3.37 kB). View file
|
|
|
receipt_processor/google_ocr.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from google.cloud import vision
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
class GoogleVisionOCR:
|
| 5 |
+
def __init__(self):
|
| 6 |
+
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "receipt-vision-key.json"
|
| 7 |
+
self.client = vision.ImageAnnotatorClient()
|
| 8 |
+
|
| 9 |
+
def extract_text(self, image_content: bytes) -> str:
|
| 10 |
+
image = vision.Image(content=image_content)
|
| 11 |
+
response = self.client.text_detection(image=image)
|
| 12 |
+
return response.text_annotations[0].description if response.text_annotations else ""
|
receipt_processor/receipt_parser.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
class ReceiptParser:
|
| 4 |
+
@staticmethod
|
| 5 |
+
def parse_receipt_text(full_text: str) -> dict:
|
| 6 |
+
lines = full_text.splitlines()
|
| 7 |
+
receipt = {"store": None, "date": None, "total": None, "items": []}
|
| 8 |
+
|
| 9 |
+
# Store detection
|
| 10 |
+
for line in lines:
|
| 11 |
+
if any(kw in line.lower() for kw in ["konzum", "plodine", "studenac"]):
|
| 12 |
+
receipt["store"] = line.strip()
|
| 13 |
+
break
|
| 14 |
+
|
| 15 |
+
# Date detection
|
| 16 |
+
for line in lines:
|
| 17 |
+
if match := re.search(r'\b(\d{2}\.\d{2}\.\d{4})\b', line):
|
| 18 |
+
receipt["date"] = match.group(1)
|
| 19 |
+
break
|
| 20 |
+
|
| 21 |
+
# Total detection
|
| 22 |
+
for line in reversed(lines):
|
| 23 |
+
if any(word in line.lower() for word in ["ukupno", "za platiti"]):
|
| 24 |
+
if match := re.search(r'(\d+,\d{2})', line):
|
| 25 |
+
receipt["total"] = f"{match.group(1).replace(',', '.')} EUR"
|
| 26 |
+
break
|
| 27 |
+
|
| 28 |
+
# Item parsing logic
|
| 29 |
+
merged_lines = []
|
| 30 |
+
skip_next = False
|
| 31 |
+
for i, line in enumerate(lines):
|
| 32 |
+
if skip_next:
|
| 33 |
+
skip_next = False
|
| 34 |
+
continue
|
| 35 |
+
if re.search(r'\d+,\d{2}$', line):
|
| 36 |
+
if i+1 < len(lines) and re.match(r'^\d+,\d{2}', lines[i+1]):
|
| 37 |
+
merged_lines.append(f"{line} {lines[i+1]}")
|
| 38 |
+
skip_next = True
|
| 39 |
+
continue
|
| 40 |
+
merged_lines.append(line)
|
| 41 |
+
|
| 42 |
+
item_patterns = [
|
| 43 |
+
re.compile(r'(.+?)\s+(\d+)\s+(\d+,\d{2})\s+(\d+,\d{2})'),
|
| 44 |
+
re.compile(r'(.+?)\s+(\d+)\s+x\s+(\d+,\d{2})\s+(\d+,\d{2})'),
|
| 45 |
+
re.compile(r'(.+?)\s+(\d+)\s+(\d+)\s+(\d+,\d{2})'),
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
for line in merged_lines:
|
| 49 |
+
for pattern in item_patterns:
|
| 50 |
+
if match := pattern.match(line):
|
| 51 |
+
receipt["items"].append({
|
| 52 |
+
"name": match.group(1).strip().title(),
|
| 53 |
+
"qty": int(match.group(2)),
|
| 54 |
+
"price": match.group(4).replace(",", ".")
|
| 55 |
+
})
|
| 56 |
+
break
|
| 57 |
+
|
| 58 |
+
return receipt
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
numpy
|
| 4 |
+
onnxruntime-gpu
|
| 5 |
+
opencv-python
|
| 6 |
+
Pillow
|
| 7 |
+
torch
|
| 8 |
+
ultralytics
|
| 9 |
+
python-multipart
|
| 10 |
+
google-cloud-vision
|