dea01/halalergy-backend
0
1from flask import Flask, request, jsonify
2import cv2
3import json
4import pytesseract
5import pandas as pd
6import re
7import os
8from rapidfuzz import process, fuzz
9import numpy as np
10
11# Logika deteksi OS
12if os.name == 'nt': # Windows
13 pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
14else:
15 # Di Linux/Docker, biasanya tesseract sudah ada di PATH.
16 # Jika tidak jalan, pakai path default Linux ini:
17 pytesseract.pytesseract.tesseract_cmd = '/usr/bin/tesseract'
18
19app = Flask(__name__)
20
21# =====================================================
22# LOAD DATASET
23# =====================================================
24URL_DATASET = "https://raw.githubusercontent.com/deayulianis/coba-skripsi/refs/heads/main/komposisi.json"
25
26try:
27 df = pd.read_json(URL_DATASET)
28 df = df.rename(columns={0: "ingredients_text"})
29 print("Dataset berhasil dimuat.")
30except Exception as e:
31 print(f"Gagal memuat dataset online: {e}")
32 df = pd.DataFrame(columns=["ingredients_text"])
33
34# =====================================================
35# CLEAN TEXT
36# =====================================================
37def clean_text(text):
38 if pd.isna(text):
39 return ""
40
41 text = text.lower()
42
43 garbage_phrases = [
44 r'cetak tebal.*',
45 r'komposise',
46 r'that also process',
47 r'mengandung alergen.*',
48 r'tanpa bahan pengawet.*',
49 r'tanpa penguat rasa.*',
50 r'tanpa pemanis buatan.*',
51 r'komposisi',
52 r'komposis',
53 r'mungkin mengandung.*',
54 r'ingredients*'
55 ]
56
57 for phrase in garbage_phrases:
58 text = re.sub(phrase, '', text)
59
60 # hapus persentase
61 text = re.sub(r'\d+(\.\d+)?\s?%?', '', text)
62
63 text = text.replace('(', ',').replace(')', ',')
64 text = text.replace('.', ',')
65
66 text = re.sub(r'[^a-z0-9,\s]', ' ', text)
67 text = re.sub(r'\s+', ' ', text).strip()
68 text = re.sub(r'\s*,\s*', ',', text)
69
70 return text
71
72# =====================================================
73# ALLERGEN DICTIONARY
74# =====================================================
75allergen_dict = {
76 "Susu": [
77 "milk", "whole milk", "skim milk", "milk powder", "milk solids", "milk powder", "milk fat", "buttermilk", "milk protein", "milk concentrate", "skimmed milk",
78 "whey", "whey powder", "whey protein", "casein", "caseinate", "lactose", "butter", "butterfat", "butter oil",
79 "cream", "cheese", "yogurt", "sodium caseinate", "calcium caseinate", "ceddar", "mozarella", "yogurt", "kefir",
80 "susu", "lemak susu", "mentega", "keju", "susu bubuk", "lemak susu", "krim", "yoghurt",
81 ],
82
83 "Telur": [
84 "egg", "egg powder", "egg white", "egg yolk", "ovomucoid", "ovotransferrin", "ovovitellin",
85 "albumin", "lysozyme", "ovalbumin", "ovoglobulin", "egg lecithin", "egg solids",
86 "telur", "putih telur", "kuning telur"
87 ],
88
89 "Ikan": [
90 "fish", "fish oil", "fish extract", "fish protein", "surimi",
91 "fish sauce", "fish gelatin", "fish sauce", "isinglass",
92 "anchovy", "tuna", "salmon", "sardine", "cod", "pollock", "haddock",
93 "ikan", "minyak ikan", "ekstrak ikan"
94 ],
95
96 "Krustasea & Moluska": [
97 "shrimp", "prawn", "crab", "lobster", "crayfish", "krill", "shellfish",
98 "squid", "octopus", "clam", "oyster", "mussel", "scallop",
99 "udang", "kepiting", "cumi", "kerang", "gurita", "lobster"
100 ],
101
102 "Kacang Tanah": [
103 "peanut", "groundnut", "peanut butter", "peanut oil", "peanut flour", "peanut paste", "peanut protein",
104 "kacang tanah", "selai kacang", "minyak kacang"
105 ],
106
107 "Kacang Pohon": [
108 "almond", "walnut", "cashew",
109 "hazelnut", "pistachio", "brazil nut", "pine nut", "tree nut",
110 "pecan", "macadamia", "kacang almond", "kacang mete"
111 ],
112
113 "Gandum & Gluten": [
114 "wheat", "wheat flour", "gluten", "vital gluten"
115 "barley", "rye", "oats", "oat", "spelt", "oat",
116 "malt", "breadcrumb", "triticale", "semolina", "breadcrumb", "bread crumbs",
117 "gandum", "tepung gandum", "malt extract", "malt flavor",
118 "tepung terigu", "durum wheat", "gandum", "tepung gandum"
119 ],
120
121 "Kedelai": [
122 "soy", "soya", "soybean", "soy isolate", "soy concentrate",
123 "soy protein", "soy lecithin", "textured vegetable protein",
124 "tvp", "soy sauce", "miso", "natto", "soy milk", "tofu", "tempe", "kedelai", "susu kedelai"
125 ],
126
127 "Wijen": [
128 "sesame", "sesame seed", "sesame oil", "tahini", "wijen", "minyak wijen"
129 ],
130
131 "Sulfit": [
132 "sulfite", "sulphite", "sodium metabisulfite", "sulfur dioxide", "sodium bisulfite",
133 "sodium sulfite", "sulfit", "potassium metabisulfite",
134 "e220", "e221", "e223", "e224", "e226", "e227", "e228"
135 ]
136}
137
138# =====================================================
139# NON HALAL & CRITICAL
140# =====================================================
141NON_HALAL = [
142 "pork", "pig", "swine", "lard", "bacon", "ham", "prosciutto", "beer extract", "wine extract",
143 "salami", "porcine", "alcohol", "ethanol", "wine", "beer", "rum", "ethyl alcohol", "cooking wine", "rum extract",
144 "whisky", "vodka", "brandy", "liqueur", "blood", "mirin", "sake", "blood plasma", "hemoglobin",
145 "pork fat", "wine extract", "porcine gelatin", "porcine collagen", "porcine enzyme", "dog meat", "carnivore meat"
146]
147
148CRITICAL_HALAL = [
149 "gelatin", "enzyme", "rennet", "pepsin", "lipase", "protease", "food gelatin", "hydrolyzed collagen",
150 "maltodextrin", "mono diglyceride", "e471", "e472", "emulsifier", "e471", "e472", "e473", "e477",
151 "stabilizer", "animal fat", "shortening", "collagen", "flavor", "mono and diglycerides",
152 "flavour", "glycerin", "glycerol", "stearic acid", "magnesium stearate", "emulsifier", "marshmallow",
153 "acidity regulator", "natural flavor", "synthetic flavour", "perisa sintetik", "artificial flavor", "pengatur keasaman", "maltodekstrin", "processing aid"
154]
155
156# =====================================================
157# MASTER INGREDIENTS
158# =====================================================
159MASTER_INGREDIENTS = []
160
161if not df.empty:
162 for t in df['ingredients_text']:
163 cleaned = clean_text(t)
164 ingredients = [i.strip() for i in cleaned.split(',') if i.strip()]
165 MASTER_INGREDIENTS.extend(ingredients)
166
167for values in allergen_dict.values():
168 MASTER_INGREDIENTS.extend(values)
169
170MASTER_INGREDIENTS.extend(NON_HALAL)
171MASTER_INGREDIENTS.extend(CRITICAL_HALAL)
172MASTER_INGREDIENTS = list(set(MASTER_INGREDIENTS))
173
174# =====================================================
175# NORMALIZATION
176# =====================================================
177def normalize_ingredient(ingredient):
178 numbers = re.findall(r'\d+\.?\d*%?', ingredient)
179 clean_name = re.sub(r'\d+\.?\d*%?', '', ingredient).strip()
180
181 if clean_name in MASTER_INGREDIENTS:
182 return ingredient
183
184 match = process.extractOne(
185 clean_name,
186 MASTER_INGREDIENTS,
187 scorer=fuzz.token_sort_ratio
188 )
189
190 if match and match[1] >= 80:
191 result = match[0]
192 if numbers:
193 return f"{result} {' '.join(numbers)}"
194 return result
195
196 return ingredient
197
198# =====================================================
199# DETECT ALLERGEN
200# =====================================================
201def detect_allergen(ingredients):
202 detected = set()
203 for ing in ingredients:
204 ing_lower = ing.lower()
205 words = re.findall(r'\b\w+\b', ing_lower)
206
207 for allergen, keywords in allergen_dict.items():
208 for keyword in keywords:
209 keyword = keyword.lower()
210 if ' ' in keyword:
211 if keyword in ing_lower:
212 detected.add(allergen)
213 else:
214 if keyword in words:
215 detected.add(allergen)
216 return list(detected)
217
218# =====================================================
219# DETECT HALAL STATUS
220# =====================================================
221def detect_halal_status(ingredients):
222 haram_found = set()
223 critical_found = set()
224
225 for ing in ingredients:
226 ing_lower = ing.lower()
227 # Ambil daftar kata individual untuk pengecekan kata tunggal
228 words = re.findall(r'\b\w+\b', ing_lower)
229
230 # 1. Cek Bahan Haram
231 for haram in NON_HALAL:
232 haram = haram.lower()
233 if ' ' in haram: # Jika keyword lebih dari 1 kata (contoh: 'pork fat')
234 if haram in ing_lower:
235 haram_found.add(ing)
236 else: # Jika keyword hanya 1 kata (contoh: 'pork')
237 if haram in words:
238 haram_found.add(ing)
239
240 # 2. Cek Bahan Titik Kritis
241 for critical in CRITICAL_HALAL:
242 critical = critical.lower()
243 if ' ' in critical:
244 if critical in ing_lower:
245 critical_found.add(ing)
246 else:
247 if critical in words:
248 critical_found.add(ing)
249
250 # --- FORMAT OUTPUT UNTUK TAMPIL DI FLUTTER ---
251 if haram_found:
252 # Menampilkan: NON-HALAL (Terdeteksi: lard, bacon)
253 return f"NON-HALAL (Terdeteksi: {', '.join(haram_found)})", list(critical_found), list(haram_found)
254
255 elif critical_found:
256 # Menampilkan: BUTUH PENGECEKAN (Bahan Kritis: gelatin, emulsifier)
257 return f"BUTUH PENGECEKAN ({', '.join(critical_found)})", list(critical_found), []
258
259 return "HALAL", [], []
260
261# =====================================================
262# OCR ACCURACY
263# =====================================================
264def calculate_ocr_accuracy(thresh):
265 ocr_data = pytesseract.image_to_data(
266 thresh,
267 config='--oem 3 --psm 6',
268 lang='ind+eng',
269 output_type=pytesseract.Output.DICT
270 )
271
272 confidences = []
273 for conf in ocr_data['conf']:
274 try:
275 conf = float(conf)
276 if conf > 0:
277 confidences.append(conf)
278 except:
279 pass
280
281 if len(confidences) == 0:
282 return 0
283
284 return round(sum(confidences) / len(confidences), 2)
285
286# =====================================================
287# API ENDPOINT
288# =====================================================
289import numpy as np # Tambahkan ini di bagian paling atas app.py
290
291@app.route('/analyze', methods=['POST'])
292def analyze():
293 if 'image' not in request.files:
294 return jsonify({"error": "No image uploaded"}), 400
295
296 file = request.files['image']
297
298 # Ambil data alergi dari Flutter
299 user_preferences = request.form.get('allergies', '[]')
300 try:
301 user_allergies_list = json.loads(user_preferences)
302 except:
303 user_allergies_list = []
304
305 try:
306 # --- PERBAIKAN: Baca gambar langsung dari memori (Tanpa save file) ---
307 file_bytes = np.frombuffer(file.read(), np.uint8)
308 img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
309
310 if img is None:
311 return jsonify({"error": "Gagal mendekode gambar. Pastikan formatnya benar."}), 400
312
313 # 1. IMAGE PROCESSING & OCR
314 img = cv2.resize(img, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
315 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
316 denoise = cv2.fastNlMeansDenoising(gray, h=10)
317 _, thresh = cv2.threshold(denoise, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
318
319 # Gunakan lang='ind+eng' hanya jika tesseract-ocr-ind sudah terpasang di Render
320 raw_text = pytesseract.image_to_string(thresh, config='--oem 3 --psm 6', lang='ind+eng')
321
322 # 2. HITUNG AKURASI
323 ocr_accuracy = calculate_ocr_accuracy(thresh)
324 ocr_quality = "Tinggi" if ocr_accuracy >= 80 else "Sedang" if ocr_accuracy >= 60 else "Rendah"
325
326 # 3. CLEANING & NORMALISASI
327 cleaned = clean_text(raw_text)
328 ingredients = [i.strip() for i in cleaned.split(',') if i.strip()]
329 normalized = [normalize_ingredient(i) for i in ingredients]
330
331 # 4. DETEKSI
332 allergens = detect_allergen(normalized)
333 status_halal, critical_list, haram_list = detect_halal_status(normalized)
334
335 # 5. LOGIKA PERSONALISASI
336 personal_allergen_alert = [a for a in allergens if a in user_allergies_list]
337 is_personal_allergy = len(personal_allergen_alert) > 0
338
339 is_low_quality = len(cleaned.split(',')) < 3
340 is_haram = "NON-HALAL" in status_halal
341 is_critical = "BUTUH PENGECEKAN" in status_halal
342
343 final_status = "AMAN"
344 final_message = ""
345
346 if is_low_quality:
347 final_status = "SCAN RENDAH"
348 final_message = "Kualitas teks kurang jelas. Silakan scan ulang dengan cahaya yang lebih terang."
349 else:
350 # KONDISI 1: JIKA PRODUK TERBUKTI HARAM (NON-HALAL)
351 if is_haram:
352 final_status = "BAHAYA: NON-HALAL"
353 if is_personal_allergy:
354 # [Kasus A] Haram + Alergi Anda
355 final_message = f"Ditemukan bahan non-halal: {', '.join(haram_list)}. Hindari konsumsi. Hati-hati! Produk ini juga mengandung alergi Anda: {', '.join(personal_allergen_alert)}."
356 else:
357 # [Kasus E REVISI] Hanya Haram (Alergi Aman)
358 final_message = f"TIDAK MENGANDUNG BAHAN YANG BISA MEMBUAT ALERGEN TETAPI Ditemukan bahan non-halal: {', '.join(haram_list)}. Hindari konsumsi."
359
360 # KONDISI 2: JIKA PRODUK MEMILIKI TITIK KRITIS (SYUBHAT)
361 elif is_critical:
362 if is_personal_allergy:
363 # [Kasus B] Titik Kritis + Alergi Anda
364 final_status = "PERINGATAN: ALERGI & TITIK KRITIS"
365 final_message = f"Hati-hati! Produk ini mengandung alergi Anda: {', '.join(personal_allergen_alert)}. Selain itu, terdeteksi kandungan titik kritis: {', '.join(critical_list)}, tolong lihat logo kehalalan resmi pada kemasan."
366 else:
367 # [Kasus D REVISI] Hanya Titik Kritis (Alergi Aman)
368 final_status = "PERLU VERIFIKASI"
369 final_message = f"tidak mengandung bahan yang menyebabkan alergen tetapi HATI HATI Mengandung bahan titik kritis ({', '.join(critical_list)}). Tolong lihat logo kehalalan resmi pada kemasan."
370
371 # KONDISI 3: [Kasus C REVISI] JIKA HANYA ALERGI ANDA TERDETEKSI (Halal Aman)
372 elif is_personal_allergy:
373 final_status = "PERINGATAN: ALERGI ANDA TERDETEKSI"
374 final_message = f"Hati-hati! Produk ini mengandung {', '.join(personal_allergen_alert)} yang terdaftar di profil alergi Anda tetapi produk ini tidak mengandung bahan haram/kritis."
375
376 # KONDISI 4: JIKA HANYA ADA ALERGEN UMUM (YANG TIDAK DICENTANG USER)
377 elif len(allergens) > 0:
378 final_status = "AMAN (DENGAN CATATAN)"
379 final_message = f"Produk terindikasi Halal, namun mengandung alergen umum: {', '.join(allergens)}."
380
381 # KONDISI 5: SEMUANYA BERSIH DAN AMAN
382 else:
383 final_status = "AMAN"
384 final_message = "Tidak ditemukan bahan berbahaya atau alergi. Aman untuk dikonsumsi."
385
386 return jsonify({
387 "ocr_text": cleaned,
388 "ingredients": ingredients,
389 "allergens": allergens,
390 "user_specific_allergies": personal_allergen_alert,
391 "halal_status": status_halal,
392 "ocr_accuracy": ocr_accuracy,
393 "ocr_quality": ocr_quality,
394 "conclusion": {
395 "status": final_status,
396 "message": final_message
397 }
398 })
399
400 except Exception as e:
401 # Agar error detail muncul di Logs Render
402 import traceback
403 traceback.print_exc()
404 return jsonify({"error": str(e)}), 500
405
406if __name__ == "__main__":
407 port = int(os.environ.get("PORT", 5000))
408 app.run(host="0.0.0.0", port=port)