Franko Fišter commited on
Commit
460ecbc
·
1 Parent(s): ee22cda

Backend first commit

Browse files
.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
- test1
 
 
 
 
 
 
 
 
 
 
 
 
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