CoolFace
Apppublic

dea01/halalergy-backend

sourceHugging Faceapache-2.0updated 3mo agoView on Hugging Face
0likes
app.py408 linesDownload Raw Back to root
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)