Spaces:
Build error
Build error
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 | |
# ============================== | |
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"<div style='background:{bg};padding:10px;border-radius:8px;text-align:center;color:{text_color};'>" | |
f"<strong>{title}</strong><p>{value:.2f} {unit}</p><h4>{grade}</h4></div>", | |
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("<p style='text-align:center;'>Nutri-Grade App v2.1 © 2024</p>", unsafe_allow_html=True) | |