katanaml commited on
Commit
d94af2f
·
1 Parent(s): 872c3f7
Dockerfile CHANGED
@@ -1,9 +1,13 @@
1
- FROM python:3.7-slim
2
 
3
  WORKDIR /code
4
 
5
  COPY requirements-fastapi.txt ./
6
 
 
 
 
 
7
  RUN pip install --no-cache-dir --upgrade -r /code/requirements-fastapi.txt
8
 
9
  RUN useradd -m -u 1000 user
 
1
+ FROM python:3.7
2
 
3
  WORKDIR /code
4
 
5
  COPY requirements-fastapi.txt ./
6
 
7
+ # Installing libGL
8
+ RUN apt-get update && apt-get install -y \
9
+ libgl1-mesa-dev
10
+
11
  RUN pip install --no-cache-dir --upgrade -r /code/requirements-fastapi.txt
12
 
13
  RUN useradd -m -u 1000 user
config.py CHANGED
@@ -4,7 +4,9 @@ import os
4
 
5
  class Settings(BaseSettings):
6
  huggingface_key: str = os.environ.get("huggingface_key")
 
7
  dataset_name: str = "katanaml-org/invoices-donut-data-v1"
 
8
 
9
 
10
  settings = Settings()
 
4
 
5
  class Settings(BaseSettings):
6
  huggingface_key: str = os.environ.get("huggingface_key")
7
+ sparrow_key: str = os.environ.get("sparrow_key")
8
  dataset_name: str = "katanaml-org/invoices-donut-data-v1"
9
+ ocr_stats_file: str = "data/ocr_stats.json"
10
 
11
 
12
  settings = Settings()
data/ocr_stats.json ADDED
@@ -0,0 +1 @@
 
 
1
+ [[0.0, "receipt_00001.png", "2023-05-23 10:55:43"], [19.22510600090027, "receipt_00001.png", "2023-05-23 11:11:02"], [4.0531158447265625e-06, "receipt_00001.png", "2023-05-23 11:11:10"], [3.0994415283203125e-06, "receipt_00001.png", "2023-05-23 11:11:11"], [3.0994415283203125e-06, "receipt_00001.png", "2023-05-23 11:11:12"], [2.86102294921875e-06, "receipt_00001.png", "2023-05-23 11:11:13"], [3.0994415283203125e-06, "receipt_00001.png", "2023-05-23 11:11:13"], [3.653481960296631, "receipt_00001.png", "2023-05-23 11:32:48"], [8.929341077804565, "receipt_00001.png", "2023-05-23 11:34:52"], [3.5088820457458496, "receipt_00001.png", "2023-05-23 16:32:17"], [2.863774061203003, "receipt_00001.png", "2023-05-23 16:32:56"], [4.174198150634766, "inout-20211211_001.jpg", "2023-05-23 16:38:33"], [4.616858243942261, "inout-20211211_001.jpg", "2023-05-23 16:39:28"], [4.6479880809783936, "inout-20211211_001.jpg", "2023-05-23 16:47:27"], [4.756654262542725, "inout-20211211_001.jpg", "2023-05-23 22:07:14"], [5.704661130905151, "wholefoods-20211211_005.jpg", "2023-05-23 22:16:38"], [6.363792896270752, "wholefoods-20211211_005.jpg", "2023-05-23 22:24:13"], [6.582294940948486, "cvs-20211211_009.jpg", "2023-05-23 22:43:41"], [8.032721757888794, "oldnavy-20211211_015.jpg", "2023-05-23 22:45:58"], [6.35598611831665, "ross-20211211_010.jpg", "2023-05-23 22:47:50"], [7.241703987121582, "ross-20211211_010.jpg", "2023-05-24 11:03:57"], [6.259234189987183, "ross-20211211_010.pdf", "2023-05-24 11:04:26"], [7.275213956832886, "ross-20211211_010.pdf", "2023-05-24 11:05:25"], [5.848371982574463, "invoice_10.jpg", "2023-05-24 11:06:21"], [6.028747081756592, "invoice_10.jpg", "2023-05-24 11:12:14"], [6.5253260135650635, "cvs-20211211_009.jpg", "2023-05-24 11:29:53"], [6.507750988006592, "cvs-20211211_009.jpg", "2023-05-24 12:55:14"], [2.864002227783203, "receipt_00001.png", "2023-05-24 12:55:30"], [2.9030818939208984, "receipt_00001.png", "2023-05-24 12:55:40"], [5.672614097595215, "wholefoods-20211211_005.jpg", "2023-05-24 12:56:13"], [5.712976932525635, "wholefoods-20211211_005.pdf", "2023-05-24 12:56:29"], [5.984729051589966, "invoice_10.jpg", "2023-05-24 13:00:23"], [7.3337507247924805, "bestbuy-20211211_006.pdf", "2023-05-24 13:01:13"], [4.676954984664917, "inout-20211211_001.jpg", "2023-05-24 21:09:53"], [3.9793169498443604, "inout-20211211_001.jpg", "2023-05-24 22:01:12"], [4.716302871704102, "inout-20211211_001.jpg", "2023-05-24 22:07:19"], [4.611649990081787, "inout-20211211_001.jpg", "2023-05-24 22:11:00"], [5.18176007270813, "inout-20211211_001.jpg", "2023-05-24 22:12:26"], [4.76771092414856, "inout-20211211_001.jpg", "2023-05-25 10:00:11"], [4.62838888168335, "inout-20211211_001.jpg", "2023-05-25 10:12:36"], [4.6390650272369385, "inout-20211211_001.jpg", "2023-05-25 10:35:31"], [4.605455160140991, "inout-20211211_001.jpg", "2023-05-25 10:36:59"], [4.541555881500244, "inout-20211211_001.jpg", "2023-05-25 10:37:41"], [4.652244806289673, "inout-20211211_001.jpg", "2023-05-25 10:38:09"], [3.947613000869751, "inout-20211211_001.jpg", "2023-05-25 10:58:54"], [4.597126245498657, "inout-20211211_001.jpg", "2023-05-25 11:00:03"], [4.6871421337127686, "inout-20211211_001.jpg", "2023-05-25 11:02:44"], [4.579195976257324, "inout-20211211_001.jpg", "2023-05-25 11:32:11"], [4.734511137008667, "inout-20211211_001.jpg", "2023-05-25 11:33:03"], [4.602473258972168, "inout-20211211_001.jpg", "2023-05-25 11:44:11"], [4.563000202178955, "inout-20211211_001.jpg", "2023-05-25 11:47:35"], [4.576035022735596, "inout-20211211_001.jpg", "2023-05-25 11:49:55"], [4.860241889953613, "inout-20211211_001.jpg", "2023-05-25 11:53:19"], [4.693282127380371, "inout-20211211_001.jpg", "2023-05-25 11:56:00"], [4.5564610958099365, "inout-20211211_001.jpg", "2023-05-25 16:02:52"], [5.022596836090088, "inout-20211211_001.jpg", "2023-05-25 16:03:47"], [4.650119781494141, "inout-20211211_001.jpg", "2023-05-25 16:27:35"], [6.16159200668335, "inout-20211211_001.jpg", "2023-05-30 22:15:29"], [9.421452045440674, "../docs/models/donut/data/img/test/invoice_2.jpg", "2023-06-07 21:03:34"]]
data/result.json ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ['YOUR GUEST NUMBER IS']
2
+ ['43']
3
+ ['IN-N-OUT BURGER LINQ']
4
+ ['320 6 2166 6301']
5
+ ['Cashier: SERJI0 SA']
6
+ ['Check : 43']
7
+ ['TRANS #: 6301']
8
+ ['1 Db1-Dbl']
9
+ ['5.25']
10
+ ['+ Onion']
11
+ ['1 Fry']
12
+ ['2.35']
13
+ ['1 Med Soft Drink']
14
+ ['2.15']
15
+ ['COUNTER-Eat In']
16
+ ['9.75']
17
+ ['TAX 8.375%']
18
+ ['.82']
19
+ ['Amount Due']
20
+ ['$10.57']
21
+ ['Tender MasterCard']
22
+ ['$10.57']
23
+ ['Change']
24
+ ['$.00']
25
+ ['CHARGE DETAIL']
26
+ ['SALE']
27
+ ['Card Type:']
28
+ ['Mastercard']
29
+ ['Account :']
30
+ ['************5562 R']
31
+ ['Auth Code:']
32
+ ['NDTQU8']
33
+ ['Trans #:']
34
+ ['6301']
35
+ ['Auth Ref :']
36
+ ['2015517078']
37
+ ['AUTH AMT :']
38
+ ['$10.57']
39
+ ['AID:']
40
+ ['A0000000041010']
41
+ ['TVR :']
42
+ ['0000008001']
43
+ ['TSI:']
44
+ ['0000']
45
+ ['App Name:']
46
+ ['Debit MasterCard']
47
+ ['ARQC:']
48
+ ['ADCF5208793B7BD6']
49
+ ['THANK YOU!']
50
+ ['Quest ions/Comments: Cal1 800-786-1 :10']
51
+ ['L1 T6']
52
+ ['9:21 PM']
53
+ ['2021-11-30']
endpoints.py CHANGED
@@ -1,6 +1,7 @@
1
  from fastapi import FastAPI
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from routers import dataset
 
4
 
5
  app = FastAPI(openapi_url="/api/v1/sparrow-data/openapi.json", docs_url="/api/v1/sparrow-data/docs")
6
 
@@ -13,6 +14,7 @@ app.add_middleware(
13
  )
14
 
15
  app.include_router(dataset.router, prefix="/api-dataset/v1/sparrow-data", tags=["Dataset"])
 
16
 
17
 
18
  @app.get("/")
 
1
  from fastapi import FastAPI
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from routers import dataset
4
+ from routers import ocr
5
 
6
  app = FastAPI(openapi_url="/api/v1/sparrow-data/openapi.json", docs_url="/api/v1/sparrow-data/docs")
7
 
 
14
  )
15
 
16
  app.include_router(dataset.router, prefix="/api-dataset/v1/sparrow-data", tags=["Dataset"])
17
+ app.include_router(ocr.router, prefix="/api-ocr/v1/sparrow-data", tags=["OCR"])
18
 
19
 
20
  @app.get("/")
requirements-fastapi.txt CHANGED
@@ -1,4 +1,8 @@
 
1
  datasets==2.10.1
2
  Pillow==9.5.0
3
- fastapi==0.95.2
 
 
 
4
  uvicorn[standard]
 
1
+ pdf2image==1.16.2
2
  datasets==2.10.1
3
  Pillow==9.5.0
4
+ paddlepaddle==2.4.2
5
+ paddleocr==2.6.1.3
6
+ fastapi==0.96.0
7
+ python-multipart
8
  uvicorn[standard]
routers/dataset.py CHANGED
@@ -7,11 +7,11 @@ from io import BytesIO
7
  from PIL import Image
8
  import base64
9
  from config import settings
 
10
 
11
 
12
  router = APIRouter()
13
 
14
- from huggingface_hub import login
15
  login(settings.huggingface_key)
16
 
17
  class ImageResponse(BaseModel):
@@ -24,6 +24,7 @@ def encode_pil_image(image: Image) -> str:
24
  img_data = buffer.getvalue()
25
  return base64.b64encode(img_data).decode('utf-8')
26
 
 
27
  @router.get("/dataset_info")
28
  async def get_dataset_info():
29
  dataset = load_dataset(settings.dataset_name)
 
7
  from PIL import Image
8
  import base64
9
  from config import settings
10
+ from huggingface_hub import login
11
 
12
 
13
  router = APIRouter()
14
 
 
15
  login(settings.huggingface_key)
16
 
17
  class ImageResponse(BaseModel):
 
24
  img_data = buffer.getvalue()
25
  return base64.b64encode(img_data).decode('utf-8')
26
 
27
+
28
  @router.get("/dataset_info")
29
  async def get_dataset_info():
30
  dataset = load_dataset(settings.dataset_name)
routers/ocr.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, File, UploadFile, Form
2
+ from typing import Optional
3
+ from config import settings
4
+ from PIL import Image
5
+ import urllib.request
6
+ from io import BytesIO
7
+ import utils
8
+ import os
9
+ import time
10
+ from functools import lru_cache
11
+ from paddleocr import PaddleOCR
12
+ from pdf2image import convert_from_bytes
13
+ import io
14
+ import json
15
+ from routers.ocr_utils import merge_data
16
+
17
+
18
+ router = APIRouter()
19
+
20
+ @lru_cache(maxsize=1)
21
+ def load_ocr_model():
22
+ model = PaddleOCR(use_angle_cls=True, lang='en')
23
+ return model
24
+
25
+
26
+ def invoke_ocr(doc, content_type):
27
+ worker_pid = os.getpid()
28
+ print(f"Handling OCR request with worker PID: {worker_pid}")
29
+ start_time = time.time()
30
+
31
+ model = load_ocr_model()
32
+
33
+ bytes_img = io.BytesIO()
34
+
35
+ format_img = "JPEG"
36
+ if content_type == "image/png":
37
+ format_img = "PNG"
38
+
39
+ doc.save(bytes_img, format=format_img)
40
+ bytes_data = bytes_img.getvalue()
41
+ bytes_img.close()
42
+
43
+ result = model.ocr(bytes_data, cls=True)
44
+
45
+ values = []
46
+ for idx in range(len(result)):
47
+ res = result[idx]
48
+ for line in res:
49
+ values.append(line)
50
+
51
+ values = merge_data(values)
52
+
53
+ end_time = time.time()
54
+ processing_time = end_time - start_time
55
+ print(f"OCR done, worker PID: {worker_pid}")
56
+
57
+ return values, processing_time
58
+
59
+ @router.post("/ocr")
60
+ async def run_ocr(file: Optional[UploadFile] = File(None), image_url: Optional[str] = Form(None),
61
+ sparrow_key: str = Form(None)):
62
+
63
+ if sparrow_key != settings.sparrow_key:
64
+ return {"error": "Invalid Sparrow key."}
65
+
66
+ result = []
67
+ if file:
68
+ if file.content_type in ["image/jpeg", "image/jpg", "image/png"]:
69
+ doc = Image.open(BytesIO(await file.read()))
70
+ elif file.content_type == "application/pdf":
71
+ pdf_bytes = await file.read()
72
+ pages = convert_from_bytes(pdf_bytes, 300)
73
+ doc = pages[0]
74
+ else:
75
+ return {"error": "Invalid file type. Only JPG/PNG images and PDF are allowed."}
76
+
77
+ result, processing_time = invoke_ocr(doc, file.content_type)
78
+
79
+ utils.log_stats(settings.ocr_stats_file, [processing_time, file.filename])
80
+ print(f"Processing time OCR: {processing_time:.2f} seconds")
81
+ elif image_url:
82
+ # test image url: https://raw.githubusercontent.com/katanaml/sparrow/main/sparrow-data/docs/input/invoices/processed/images/invoice_10.jpg
83
+ # test PDF: https://raw.githubusercontent.com/katanaml/sparrow/main/sparrow-data/docs/input/receipts/2021/us/bestbuy-20211211_006.pdf
84
+ with urllib.request.urlopen(image_url) as response:
85
+ content_type = response.info().get_content_type()
86
+
87
+ if content_type in ["image/jpeg", "image/jpg", "image/png"]:
88
+ doc = Image.open(BytesIO(response.read()))
89
+ elif content_type == "application/octet-stream":
90
+ pdf_bytes = response.read()
91
+ pages = convert_from_bytes(pdf_bytes, 300)
92
+ doc = pages[0]
93
+ else:
94
+ return {"error": "Invalid file type. Only JPG/PNG images and PDF are allowed."}
95
+
96
+ result, processing_time = invoke_ocr(doc, content_type)
97
+
98
+ # parse file name from url
99
+ file_name = image_url.split("/")[-1]
100
+ utils.log_stats(settings.ocr_stats_file, [processing_time, file_name])
101
+ print(f"Processing time OCR: {processing_time:.2f} seconds")
102
+ else:
103
+ result = {"info": "No input provided"}
104
+
105
+ return result
106
+
107
+
108
+ @router.get("/statistics")
109
+ async def get_statistics():
110
+ file_path = settings.ocr_stats_file
111
+
112
+ # Check if the file exists, and read its content
113
+ if os.path.exists(file_path):
114
+ with open(file_path, 'r') as file:
115
+ try:
116
+ content = json.load(file)
117
+ except json.JSONDecodeError:
118
+ content = []
119
+ else:
120
+ content = []
121
+
122
+ return content
routers/ocr_utils.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ def merge_data(values):
2
+ data = []
3
+ for idx in range(len(values)):
4
+ data.append([values[idx][1][0]])
5
+ # print(data[idx])
6
+
7
+ return data
utils.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from datetime import datetime
4
+
5
+
6
+ def log_stats(file_path, new_data):
7
+ # Check if the file exists, and read its content
8
+ if os.path.exists(file_path):
9
+ with open(file_path, 'r') as file:
10
+ try:
11
+ content = json.load(file)
12
+ except json.JSONDecodeError:
13
+ content = []
14
+ else:
15
+ content = []
16
+
17
+ # Get the current date and time
18
+ now = datetime.now()
19
+ # Format the date and time as a string
20
+ date_time_string = now.strftime("%Y-%m-%d %H:%M:%S")
21
+ new_data.append(date_time_string)
22
+
23
+ # Append the new data to the content
24
+ content.append(new_data)
25
+
26
+ # Write the updated content back to the file
27
+ with open(file_path, 'w') as file:
28
+ json.dump(content, file)
29
+