SicheateR99 commited on
Commit
fcf59c8
·
1 Parent(s): bcb42d8

Model Integration

Browse files
Files changed (4) hide show
  1. Dockerfile +9 -10
  2. app.py +236 -35
  3. packages.txt +0 -2
  4. requirements.txt +1 -4
Dockerfile CHANGED
@@ -1,24 +1,23 @@
1
- # Gunakan dasar sistem operasi Linux dengan Python 3.9
2
- FROM python:3.9
3
 
4
- # Instal dependensi sistem yang dibutuhkan oleh OpenCV
5
  RUN apt-get update && apt-get install -y \
6
  libgl1 \
7
- libglib2.0-0
 
 
 
 
8
 
9
- # Buat user baru (Syarat keamanan dari Hugging Face)
10
  RUN useradd -m -u 1000 user
11
  USER user
12
  ENV PATH="/home/user/.local/bin:$PATH"
13
 
14
  WORKDIR /app
15
 
16
- # Salin file requirements dan instal
17
- COPY --chown=user requirements.txt requirements.txt
18
  RUN pip install --no-cache-dir --upgrade -r requirements.txt
19
 
20
- # Salin semua kode dan model kamu ke dalam kontainer
21
- COPY --chown=user . /app
22
 
23
- # Jalankan server FastAPI di port 7860 (Port standar Hugging Face)
24
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
+ FROM python:3.9-slim
 
2
 
 
3
  RUN apt-get update && apt-get install -y \
4
  libgl1 \
5
+ libglib2.0-0 \
6
+ libgomp1 \
7
+ libsm6 \
8
+ libxext6 \
9
+ && rm -rf /var/lib/apt/lists/*
10
 
11
+ # Buat user baru
12
  RUN useradd -m -u 1000 user
13
  USER user
14
  ENV PATH="/home/user/.local/bin:$PATH"
15
 
16
  WORKDIR /app
17
 
18
+ COPY --chown=user requirements.txt .
 
19
  RUN pip install --no-cache-dir --upgrade -r requirements.txt
20
 
21
+ COPY --chown=user . .
 
22
 
 
23
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py CHANGED
@@ -3,22 +3,23 @@ from fastapi.middleware.cors import CORSMiddleware
3
  from fastapi.responses import JSONResponse
4
  from paddleocr import PaddleOCR
5
  from datetime import datetime
 
6
  import cv2
7
  import re
8
  import numpy as np
9
- import random
10
 
11
-
12
- app = FastAPI(title="OCR Service")
13
 
14
  app.add_middleware(
15
  CORSMiddleware,
16
- allow_origins=["*"], # ganti dengan URL Vercel
17
  allow_credentials=True,
18
  allow_methods=["*"],
19
  allow_headers=["*"],
20
  )
21
 
 
22
  paddle_engine = PaddleOCR(
23
  lang='id',
24
  show_log=False,
@@ -26,10 +27,13 @@ paddle_engine = PaddleOCR(
26
  enable_mkldnn=True,
27
  cpu_threads=2,
28
  ocr_version="PP-OCRv4",
29
- det_limit_side_len=960
30
  )
31
 
32
- #FUNGSI EKSTRAKSI TANGGAL
 
 
 
33
  MONTH_MAP = {
34
  "jan": 1, "januari": 1,
35
  "feb": 2, "februari": 2,
@@ -47,10 +51,7 @@ MONTH_MAP = {
47
  }
48
 
49
  def extract_transaction_date(raw_text: str):
50
- # OCR cleanup
51
- raw_text = raw_text.replace("O", "0").replace("I", "1")
52
  raw_text = re.sub(r"\s+", " ", raw_text)
53
-
54
  patterns = [
55
  # dd/mm/yyyy | dd-mm-yyyy | dd.mm.yyyy
56
  r'\d{2}[\/\-.]\d{2}[\/\-.]\d{4}',
@@ -67,55 +68,226 @@ def extract_transaction_date(raw_text: str):
67
  # 31 Feb 25 / 31 Februari 2025
68
  r'\d{1,2}\s[a-zA-Z]+\s\d{2,4}'
69
  ]
70
-
71
  numeric_formats = [
72
  "%d/%m/%Y", "%d-%m-%Y", "%d.%m.%Y",
73
  "%d/%m/%y", "%d-%m-%y", "%d.%m.%y",
74
  "%Y/%m/%d", "%Y-%m-%d", "%Y.%m.%d",
75
  "%Y%m%d"
76
  ]
77
-
78
  candidates = []
79
 
80
  for pattern in patterns:
81
  matches = re.findall(pattern, raw_text, re.IGNORECASE)
82
-
83
  for text in matches:
84
  dt = None
85
- # numeric formats
86
  for fmt in numeric_formats:
87
  try:
88
  dt = datetime.strptime(text, fmt)
89
  break
90
  except:
91
  continue
92
-
93
- # text month format
94
  if not dt:
95
- match = re.match(
96
- r'(\d{1,2})\s([a-zA-Z]+)\s(\d{2,4})',
97
- text,
98
- re.IGNORECASE
99
- )
100
  if match:
101
  day, month_txt, year = match.groups()
102
  month = MONTH_MAP.get(month_txt.lower())
103
-
104
  if month:
105
  year = int(year)
106
- if year < 100:
107
- year += 2000
108
  dt = datetime(year, month, int(day))
109
-
110
  if dt:
111
  candidates.append(dt)
112
 
113
- if not candidates:
114
- return None
115
-
116
  best_date = max(candidates)
117
  return best_date.strftime("%d-%m-%Y")
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  @app.get("/")
120
  def home():
121
  return {"status": "Optima Team - OCR API Aktif :D !"}
@@ -132,15 +304,44 @@ async def proses_gambar_paddle(file: UploadFile = File(...)):
132
  teks_terbaca = []
133
  if result and result[0]:
134
  for baris in result[0]:
135
- teks = baris[1][0]
136
- teks_terbaca.append(teks)
137
 
138
  ocr_mentah = "\n".join(teks_terbaca)
 
139
  date = extract_transaction_date(ocr_mentah)
140
- items = "Ini Daftar Item ✅"
141
- merchant = "Ini Nama Merchant ✅"
142
- total = random.randint(10, 10000)*100
143
- classes = 2
144
- return {"sukses": True, "ocr_mentah": ocr_mentah, "date": date, "items": items, "merchant": merchant, "total": total, "classes": classes}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  except Exception as e:
146
- return JSONResponse(status_code=500, content={"sukses": False, "pesan": str(e)})
 
3
  from fastapi.responses import JSONResponse
4
  from paddleocr import PaddleOCR
5
  from datetime import datetime
6
+ from rapidfuzz import fuzz
7
  import cv2
8
  import re
9
  import numpy as np
10
+ import requests
11
 
12
+ app = FastAPI(title="OCR & Extraction Service")
 
13
 
14
  app.add_middleware(
15
  CORSMiddleware,
16
+ allow_origins=["*"],
17
  allow_credentials=True,
18
  allow_methods=["*"],
19
  allow_headers=["*"],
20
  )
21
 
22
+ # Inisialisasi PaddleOCR
23
  paddle_engine = PaddleOCR(
24
  lang='id',
25
  show_log=False,
 
27
  enable_mkldnn=True,
28
  cpu_threads=2,
29
  ocr_version="PP-OCRv4",
30
+ det_limit_side_len=640
31
  )
32
 
33
+ SPACE_B_URL = "https://struklyai-clasify.hf.space/classify/"
34
+
35
+ # ENGINE EKSTRAKSI (RULE-BASED + REGEX + FUZZY)
36
+
37
  MONTH_MAP = {
38
  "jan": 1, "januari": 1,
39
  "feb": 2, "februari": 2,
 
51
  }
52
 
53
  def extract_transaction_date(raw_text: str):
 
 
54
  raw_text = re.sub(r"\s+", " ", raw_text)
 
55
  patterns = [
56
  # dd/mm/yyyy | dd-mm-yyyy | dd.mm.yyyy
57
  r'\d{2}[\/\-.]\d{2}[\/\-.]\d{4}',
 
68
  # 31 Feb 25 / 31 Februari 2025
69
  r'\d{1,2}\s[a-zA-Z]+\s\d{2,4}'
70
  ]
 
71
  numeric_formats = [
72
  "%d/%m/%Y", "%d-%m-%Y", "%d.%m.%Y",
73
  "%d/%m/%y", "%d-%m-%y", "%d.%m.%y",
74
  "%Y/%m/%d", "%Y-%m-%d", "%Y.%m.%d",
75
  "%Y%m%d"
76
  ]
 
77
  candidates = []
78
 
79
  for pattern in patterns:
80
  matches = re.findall(pattern, raw_text, re.IGNORECASE)
 
81
  for text in matches:
82
  dt = None
 
83
  for fmt in numeric_formats:
84
  try:
85
  dt = datetime.strptime(text, fmt)
86
  break
87
  except:
88
  continue
 
 
89
  if not dt:
90
+ match = re.match(r'(\d{1,2})\s([a-zA-Z]+)\s(\d{2,4})', text, re.IGNORECASE)
 
 
 
 
91
  if match:
92
  day, month_txt, year = match.groups()
93
  month = MONTH_MAP.get(month_txt.lower())
 
94
  if month:
95
  year = int(year)
96
+ if year < 100: year += 2000
 
97
  dt = datetime(year, month, int(day))
 
98
  if dt:
99
  candidates.append(dt)
100
 
101
+ if not candidates: return None
 
 
102
  best_date = max(candidates)
103
  return best_date.strftime("%d-%m-%Y")
104
 
105
+ def clean(text):
106
+ return text.replace(" ", " ")
107
+
108
+ def extract_number(line):
109
+ match = re.search(r'\d{1,3}(?:[.,]\d{3})+', line)
110
+ return match.group() if match else None
111
+
112
+ def has_money(line):
113
+ return bool(extract_number(line))
114
+
115
+ def is_noise(line):
116
+ upper = line.upper().strip()
117
+
118
+ noise_keywords = [
119
+ "TOTAL", "SUBTOTAL", "CASH", "KEMBALI", "CHANGE", "BAYAR", "PAID",
120
+ "ITEM", "QTY", "TERIMA", "KASIH", "THANK", "PELANGGAN",
121
+ "STAFF", "KASSA", "NPWP", "TRANS", "DELIVERY",
122
+ "DISCOUNT", "DISC", "ROUNDED", "DEBIT", "CREDIT",
123
+ "PPN", "DPP", "TUNAI", "HEMAT",
124
+ "UNBOXING", "COMPLAIN", "VIDEO",
125
+ "WWW.", "HTTP",
126
+ "CASHIER", "MOHON"
127
+ ]
128
+
129
+ address_keywords = [
130
+ "JL", "JLN", "JALAN", "RAYA", "KEL", "KEC", "KAB",
131
+ "KOTA", "RT", "RW","BARAT", "TIMUR",
132
+ "SELATAN", "UTARA", "TENGAH"
133
+ ]
134
+
135
+ promo_keywords = [
136
+ "PT ", "/PT", "CV ", "PROMO", "VOUCHER", "MEMBER"
137
+ ]
138
+
139
+ if any(k in upper for k in noise_keywords):
140
+ return True
141
+
142
+ if any(k in upper for k in address_keywords):
143
+ return True
144
+
145
+ if any(k in upper for k in promo_keywords):
146
+ return True
147
+
148
+ if re.fullmatch(r'[\d.,:=-]+', upper):
149
+ return True
150
+
151
+ if re.search(r'\{?\d+\}?X\d+', upper.replace(" ", "")):
152
+ return True
153
+
154
+ if re.search(r'\d+\s*X\s*@?', upper):
155
+ return True
156
+
157
+ if re.search(r'\bRP\b', upper):
158
+ return True
159
+
160
+ return False
161
+
162
+ def is_product_like(line):
163
+ return (
164
+ bool(re.search(r'\d+\s*(PCS|BKS|BTL|@)', line.upper())) or
165
+ bool(re.search(r'\d{1,3}[.,]\d{3}', line))
166
+ )
167
+
168
+ def is_noise_merc(line):
169
+ upper = line.upper()
170
+ noise_keywords = [
171
+ "TOTAL", "CASH", "KEMBALI", "CHANGE", "BAYAR", "PAID", "ITEM", "QTY",
172
+ "TERIMA", "KASIH", "THANK", "PELANGGAN", "STAFF", "KASSA",
173
+ "NPWP", "HP", "TRANS", "BISA DELIVERY"
174
+ ]
175
+ if any(k in upper for k in noise_keywords): return True
176
+ if re.fullmatch(r'[\d.,:]+', line.strip()): return True
177
+ if re.fullmatch(r'\d+\w*@', upper.replace(" ", "")): return True
178
+ if "." in line and len(line.split()) == 1: return True
179
+ if re.search(r'^[A-Z0-9]{8,}$', upper): return True
180
+ return False
181
+
182
+
183
+ def detect_merchant(lines):
184
+ header = lines[:8]
185
+ candidates = []
186
+ for line in header:
187
+ if not line: continue
188
+ line = line.strip("= ").strip()
189
+ if is_noise_merc(line) or is_product_like(line): continue
190
+ if len(line.split()) >= 8: continue
191
+ score = 0
192
+ if line.isupper(): score += 2
193
+ if 5 <= len(line) <= 35: score += 2
194
+ if not any(c.isdigit() for c in line): score += 2
195
+ candidates.append((score, line))
196
+
197
+ if not candidates: return "Tidak Terdeteksi"
198
+ candidates.sort(reverse=True)
199
+ return candidates[0][1]
200
+
201
+ def is_item(line):
202
+ if not line:
203
+ return False
204
+
205
+ if is_noise(line):
206
+ return False
207
+
208
+ if len(line.split()) < 2:
209
+ return False
210
+
211
+ letters = sum(c.isalpha() for c in line)
212
+ alpha_ratio = letters / max(len(line), 1)
213
+
214
+ if letters < 5:
215
+ return False
216
+
217
+ if alpha_ratio < 0.35:
218
+ return False
219
+
220
+ return True
221
+
222
+
223
+ def find_item_zone(lines):
224
+ start = 0
225
+ for i, l in enumerate(lines):
226
+ if re.search(r'\d+\s*X@|@\d|\d{1,3}[.,]\d{3}', l.upper()):
227
+ start = max(0, i - 1)
228
+ break
229
+ return start
230
+
231
+ def extract_items(lines):
232
+ result = []
233
+
234
+ for i, line in enumerate(lines):
235
+ line = line.strip()
236
+
237
+ m = re.search(r'^\{?\d+[.,]?\d*\}?[Xx]@?[\d.,]+(.+)$', line)
238
+ if m:
239
+ item = m.group(1).strip(" =-")
240
+ if is_item(item):
241
+ result.append(item)
242
+ continue
243
+
244
+ if is_item(line):
245
+ next_lines = lines[i+1:i+5]
246
+
247
+ numeric_count = sum(
248
+ bool(re.fullmatch(r'[\d.,X@x=-]+', x.strip()))
249
+ for x in next_lines
250
+ )
251
+
252
+ if numeric_count >= 2:
253
+ result.append(line)
254
+
255
+ return list(dict.fromkeys(result))
256
+
257
+ def extract_receipt(text):
258
+ text = clean(text)
259
+ lines = [l.strip() for l in text.split("\n") if l.strip()]
260
+ return {
261
+ "items": extract_items(lines),
262
+ }
263
+
264
+ def detect_total(lines):
265
+ bad = ["TUNAI", "CASH", "KEMBALI", "CHANGE", "BAYAR", "PAID"]
266
+ for i, line in enumerate(lines):
267
+ upper = line.upper().replace(" ", "")
268
+ if any(b in upper for b in bad): continue
269
+ if "TOTAL" in upper:
270
+ for j in range(i, min(i + 6, len(lines))):
271
+ num = extract_number(lines[j])
272
+ if num: return num
273
+
274
+ for line in reversed(lines):
275
+ if not any(b in line.upper() for b in bad):
276
+ num = extract_number(line)
277
+ if num: return num
278
+ return "0"
279
+
280
+ def extract_receipt(text):
281
+ text = clean(text)
282
+ lines = [l.strip() for l in text.split("\n") if l.strip()]
283
+ return {
284
+ "merchant": detect_merchant(lines),
285
+ "items": extract_items(lines),
286
+ "total": detect_total(lines)
287
+ }
288
+
289
+ # API ENDPOINTS
290
+
291
  @app.get("/")
292
  def home():
293
  return {"status": "Optima Team - OCR API Aktif :D !"}
 
304
  teks_terbaca = []
305
  if result and result[0]:
306
  for baris in result[0]:
307
+ teks_terbaca.append(baris[1][0])
 
308
 
309
  ocr_mentah = "\n".join(teks_terbaca)
310
+
311
  date = extract_transaction_date(ocr_mentah)
312
+ hasil_ekstraksi = extract_receipt(ocr_mentah)
313
+
314
+ merchant = hasil_ekstraksi["merchant"]
315
+ items_list = hasil_ekstraksi["items"]
316
+
317
+ total_str = re.sub(r'[^\d]', '', hasil_ekstraksi["total"])
318
+ total = int(total_str) if total_str else 0
319
+
320
+ class_id = None
321
+ gabungan_items = " ".join(items_list)
322
+
323
+ if gabungan_items.strip():
324
+ try:
325
+ response = requests.post(SPACE_B_URL, json={"items_text": gabungan_items}, timeout=15)
326
+
327
+ if response.status_code == 200:
328
+ data = response.json()
329
+ class_id = data.get("class_id")
330
+ else:
331
+ print(f"Gagal mendapat respons Klasifikasi, status: {response.status_code}")
332
+
333
+ except requests.exceptions.RequestException as e:
334
+ print(f"Koneksi ke Space B gagal: {e}")
335
+
336
+ return {
337
+ "sukses": True,
338
+ "ocr_mentah": ocr_mentah,
339
+ "date": date,
340
+ "items": items_list,
341
+ "merchant": merchant,
342
+ "total": total,
343
+ "classes": class_id
344
+ }
345
+
346
  except Exception as e:
347
+ return JSONResponse(status_code=500, content={"sukses": False, "pesan": str(e)})
packages.txt DELETED
@@ -1,2 +0,0 @@
1
- tesseract-ocr
2
- tesseract-ocr-ind
 
 
 
requirements.txt CHANGED
@@ -1,10 +1,7 @@
1
  fastapi
2
  uvicorn
3
  python-multipart
4
- #pytesseract
5
- #Pillow
6
  opencv-python-headless
7
  paddlepaddle
8
  paddleocr==2.7.3
9
- numpy<2.0.0
10
- onnxruntime
 
1
  fastapi
2
  uvicorn
3
  python-multipart
 
 
4
  opencv-python-headless
5
  paddlepaddle
6
  paddleocr==2.7.3
7
+ numpy<2.0.0