import os import re import cv2 import time import numpy as np import pandas as pd import requests import streamlit as st from PIL import Image from paddleocr import PaddleOCR # ============================== # KONFIGURASI APLIKASI # ============================== st.set_page_config( page_title="Nutri-Grade Label Detection", page_icon="🥗", layout="wide", initial_sidebar_state="collapsed" ) # API OpenRouter OPENROUTER_API_KEY = "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" OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" # ============================== # FUNGSI UTAMA # ============================== @st.cache_resource def initialize_ocr(): """Inisialisasi model PaddleOCR (CPU).""" try: return PaddleOCR(lang='en', use_angle_cls=True) except Exception as e: st.error(f"Gagal inisialisasi OCR: {e}") return None def parse_numeric_value(text: str) -> float: """Mengubah string menjadi float, hanya menyisakan angka/desimal.""" cleaned = re.sub(r"[^\d\.\-]", "", str(text)) if cleaned in ['', '.', '-']: return 0.0 try: return float(cleaned) except ValueError: return 0.0 def get_nutrition_advice(serving_size, sugar_norm, fat_norm, sugar_grade, fat_grade, final_grade): """Memanggil API OpenRouter untuk menghasilkan saran nutrisi singkat.""" prompt = f""" Anda adalah ahli gizi dari Indonesia yang ramah. - Takaran Saji: {serving_size} g/ml - Gula (per 100): {sugar_norm:.2f} g (Grade {sugar_grade.replace('Grade ', '')}) - Lemak Jenuh (per 100): {fat_norm:.2f} g (Grade {fat_grade.replace('Grade ', '')}) - Grade Akhir: {final_grade.replace('Grade ', '')} Berikan saran nutrisi singkat 50-80 kata, fokus pada dampak kesehatan dan tips praktis. """ headers = {"Authorization": f"Bearer {OPENROUTER_API_KEY}", "Content-Type": "application/json"} payload = { "model": "mistralai/mistral-7b-instruct:free", "messages": [{"role": "user", "content": prompt}], "max_tokens": 250, "temperature": 0.7, } try: r = requests.post(f"{OPENROUTER_BASE_URL}/chat/completions", headers=headers, json=payload, timeout=30) r.raise_for_status() return r.json()["choices"][0]["message"]["content"].strip() except Exception as e: return f"Error: {e}" def get_grade_from_value(value, thresholds): """Menentukan grade berdasarkan ambang batas.""" if value <= thresholds["A"]: return "Grade A" if value <= thresholds["B"]: return "Grade B" if value <= thresholds["C"]: return "Grade C" return "Grade D" def get_grade_color(grade): """Mengembalikan warna background & teks untuk tiap grade.""" return { "Grade A": ("#2ecc71", "white"), "Grade B": ("#f1c40f", "black"), "Grade C": ("#e67e22", "white"), "Grade D": ("#e74c3c", "white") }.get(grade, ("#bdc3c7", "black")) def reset_state(): """Reset session_state agar analisis bisa diulang.""" for key in ['ocr_done', 'data', 'calculated', 'calc']: st.session_state.pop(key, None) # ============================== # UI APLIKASI # ============================== # Inisialisasi OCR ocr_engine = initialize_ocr() if ocr_engine is None: st.error("Model OCR tidak tersedia.") st.stop() st.title("🥗 Nutri-Grade Detection & Grade Calculator") st.caption("Analisis gizi produk berdasarkan standar Nutri-Grade Singapura.") with st.expander("📋 Petunjuk Penggunaan"): st.markdown(""" 1. Upload gambar (JPG/PNG). 2. Klik **Analisis OCR**. 3. Koreksi hasil jika perlu. 4. Klik **Hitung Grade**. """) # --- Step 1: Upload --- st.header("1. Upload Gambar") file = st.file_uploader("Pilih gambar tabel gizi", type=["jpg", "jpeg", "png"], on_change=reset_state) if file: arr = np.frombuffer(file.read(), np.uint8) img = cv2.imdecode(arr, cv2.IMREAD_COLOR) st.image(cv2.cvtColor(img, cv2.COLOR_BGR2RGB), width=300) if st.button("Analisis OCR"): with st.spinner("Mendeteksi teks..."): res = ocr_engine.ocr(img) texts = [ln[1][0] for ln in (res[0] if res else [])] full_text = " ".join(texts).lower() patterns = { 'serving': r"(takaran saj[i|a]|serving size)[^\d]*(\d+\.?\d*)", 'sugar': r"(gula|sugar)[^\d]*(\d+\.?\d*)", 'fat': r"(lemak jenuh|saturated fat)[^\d]*(\d+\.?\d*)" } data = {} for key, pattern in patterns.items(): match = re.search(pattern, full_text) if match: data[key] = match.group(2) st.session_state.data = data st.session_state.ocr_done = True st.success("OCR selesai!") st.rerun() # --- Step 2: Koreksi & Hitung --- if st.session_state.get('ocr_done'): st.header("2. Koreksi & Hitung Grade") d = st.session_state.data with st.form("form2"): serving = st.text_input("Takaran Saji (g/ml)", value=d.get('serving', '100')) sugar = st.text_input("Gula (g)", value=d.get('sugar', '0')) fat = st.text_input("Lemak Jenuh (g)", value=d.get('fat', '0')) ok = st.form_submit_button("Hitung Grade") if ok: sv = parse_numeric_value(serving) sg = parse_numeric_value(sugar) fg = parse_numeric_value(fat) sugar_per100 = (sg / sv) * 100 if sv > 0 else 0 fat_per100 = (fg / sv) * 100 if sv > 0 else 0 st.session_state.calc = {'sv': sv, 'sp': sugar_per100, 'fp': fat_per100} st.session_state.calculated = True # --- Step 3: Tampilkan Hasil --- if st.session_state.get('calculated'): c = st.session_state.calc gs = get_grade_from_value(c['sp'], {"A": 1.0, "B": 5.0, "C": 10.0}) gf = get_grade_from_value(c['fp'], {"A": 0.7, "B": 1.2, "C": 2.8}) final_grade = max(gs, gf, key=lambda x: ['Grade A', 'Grade B', 'Grade C', 'Grade D'].index(x)) st.header("3. Hasil Grading") cols = st.columns(3) def show(col, title, value, unit, grade): bg, text_color = get_grade_color(grade) col.markdown( f"
" f"{title}

{value:.2f} {unit}

{grade}

", unsafe_allow_html=True ) show(cols[0], "Gula", c['sp'], "g/100ml", gs) show(cols[1], "Lemak Jenuh", c['fp'], "g/100ml", gf) show(cols[2], "Grade Akhir", 0, "", final_grade) st.divider() st.header("4. Saran Nutrisi AI") with st.spinner("Meminta AI..."): advice = get_nutrition_advice(c['sv'], c['sp'], c['fp'], gs, gf, final_grade) st.info(advice) # --- Footer --- st.markdown("---") st.markdown("

Nutri-Grade App v2.1 © 2024

", unsafe_allow_html=True)