Spaces:
Build error
Build error
Update app.py
Browse files
app.py
CHANGED
@@ -7,7 +7,6 @@ import pandas as pd
|
|
7 |
from PIL import Image
|
8 |
import time
|
9 |
import requests
|
10 |
-
import json
|
11 |
from paddleocr import PaddleOCR, draw_ocr
|
12 |
|
13 |
# --- KONFIGURASI APLIKASI ---
|
@@ -20,15 +19,8 @@ st.set_page_config(
|
|
20 |
initial_sidebar_state="collapsed"
|
21 |
)
|
22 |
|
23 |
-
# [
|
24 |
-
|
25 |
-
try:
|
26 |
-
OPENROUTER_API_KEY = st.secrets["OPENROUTER_API_KEY"]
|
27 |
-
except (FileNotFoundError, KeyError):
|
28 |
-
st.error("🚨 Harap tambahkan OPENROUTER_API_KEY Anda ke Streamlit Secrets.")
|
29 |
-
st.info("Buat file bernama .streamlit/secrets.toml dan tambahkan baris: OPENROUTER_API_KEY = 'sk-or-v1-...'")
|
30 |
-
st.stop()
|
31 |
-
|
32 |
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
33 |
|
34 |
# --- FUNGSI-FUNGSI UTAMA ---
|
@@ -37,18 +29,17 @@ OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
|
37 |
def initialize_ocr():
|
38 |
"""Inisialisasi model PaddleOCR dan menyimpannya di cache."""
|
39 |
try:
|
40 |
-
# Menggunakan CPU (use_gpu=False) untuk kompatibilitas hosting gratisan yang lebih baik
|
41 |
ocr = PaddleOCR(use_gpu=False, lang='en', use_angle_cls=True, show_log=False)
|
42 |
return ocr
|
43 |
except Exception as e:
|
44 |
st.error(f"Gagal total inisialisasi OCR: {e}")
|
45 |
return None
|
46 |
|
|
|
47 |
def parse_numeric_value(text: str) -> float:
|
48 |
"""Membersihkan dan mengubah string menjadi nilai float."""
|
49 |
if not text:
|
50 |
return 0.0
|
51 |
-
# Hanya menyisakan digit, titik, dan tanda minus
|
52 |
cleaned = re.sub(r"[^\d\.\-]", "", str(text))
|
53 |
if not cleaned or cleaned in [".", "-"]:
|
54 |
return 0.0
|
@@ -57,6 +48,7 @@ def parse_numeric_value(text: str) -> float:
|
|
57 |
except (ValueError, TypeError):
|
58 |
return 0.0
|
59 |
|
|
|
60 |
def get_nutrition_advice(serving_size, sugar_norm, fat_norm, sugar_grade, fat_grade, final_grade):
|
61 |
"""Mendapatkan saran nutrisi dari model AI melalui OpenRouter."""
|
62 |
prompt = f"""
|
@@ -76,7 +68,7 @@ def get_nutrition_advice(serving_size, sugar_norm, fat_norm, sugar_grade, fat_gr
|
|
76 |
"Content-Type": "application/json"
|
77 |
}
|
78 |
payload = {
|
79 |
-
#
|
80 |
"model": "mistralai/mistral-7b-instruct:free",
|
81 |
"messages": [{"role": "user", "content": prompt}],
|
82 |
"max_tokens": 250,
|
@@ -84,238 +76,128 @@ def get_nutrition_advice(serving_size, sugar_norm, fat_norm, sugar_grade, fat_gr
|
|
84 |
}
|
85 |
try:
|
86 |
response = requests.post(f"{OPENROUTER_BASE_URL}/chat/completions", headers=headers, json=payload, timeout=30)
|
87 |
-
response.raise_for_status()
|
88 |
data = response.json()
|
89 |
return data["choices"][0]["message"]["content"].strip()
|
90 |
except requests.exceptions.HTTPError as e:
|
91 |
-
return f"Error: Gagal menghubungi server AI ({e.response.status_code}).
|
92 |
except requests.exceptions.RequestException as e:
|
93 |
-
return f"Error: Gagal terhubung ke API
|
94 |
except Exception as e:
|
95 |
-
return f"Error: Terjadi kesalahan tak terduga
|
|
|
96 |
|
97 |
def get_grade_from_value(value, thresholds):
|
98 |
-
"""Menentukan grade (A, B, C, D) berdasarkan nilai dan ambang batas."""
|
99 |
if value <= thresholds["A"]: return "Grade A"
|
100 |
if value <= thresholds["B"]: return "Grade B"
|
101 |
if value <= thresholds["C"]: return "Grade C"
|
102 |
return "Grade D"
|
103 |
|
|
|
104 |
def get_grade_color(grade_text):
|
105 |
-
"""Mengembalikan kode warna berdasarkan grade."""
|
106 |
colors = {
|
107 |
-
"Grade A": ("#2ecc71", "white"),
|
108 |
-
"Grade B": ("#f1c40f", "black"),
|
109 |
-
"Grade C": ("#e67e22", "white"),
|
110 |
-
"Grade D": ("#e74c3c", "white")
|
111 |
}
|
112 |
-
return colors.get(grade_text, ("#bdc3c7", "black"))
|
|
|
113 |
|
114 |
def reset_analysis_state():
|
115 |
-
|
116 |
-
keys_to_reset = ['ocr_ran', 'extracted_data', 'analysis_done']
|
117 |
-
for key in keys_to_reset:
|
118 |
if key in st.session_state:
|
119 |
del st.session_state[key]
|
120 |
-
|
121 |
# --- UI APLIKASI ---
|
122 |
|
123 |
-
# Inisialisasi model OCR saat aplikasi pertama kali dimuat
|
124 |
ocr_model = initialize_ocr()
|
125 |
if ocr_model is None:
|
126 |
-
st.error("Aplikasi tidak dapat berjalan tanpa model OCR.
|
127 |
st.stop()
|
128 |
|
129 |
-
# Judul dan Deskripsi
|
130 |
st.title("🥗 Nutri-Grade Label Detection & Grade Calculator")
|
131 |
st.caption("Aplikasi ini membantu Anda memahami kandungan gizi produk dengan standar Nutri-Grade Singapura.")
|
132 |
|
133 |
with st.expander("📋 Petunjuk Penggunaan & Info"):
|
134 |
st.markdown("""
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
4. **Hitung Grade:** Klik "Hitung Grade" untuk melihat hasil analisis dan saran nutrisi.
|
140 |
-
|
141 |
-
**⚠️ Tolong Diperhatikan:**
|
142 |
-
- Aplikasi ini adalah prototipe, hasil mungkin tidak 100% akurat.
|
143 |
-
- Kualitas gambar sangat mempengaruhi hasil OCR.
|
144 |
-
- Referensi: [Health Promotion Board Singapura](https://www.hpb.gov.sg/docs/default-source/pdf/nutri-grade-ci-guide_eng-only67e4e36349ad4274bfdb22236872336d.pdf)
|
145 |
""")
|
146 |
|
147 |
-
# --- LANGKAH 1: UPLOAD GAMBAR ---
|
148 |
st.header("1. 📸 Upload Gambar Tabel Gizi")
|
149 |
-
uploaded_file = st.file_uploader(
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
)
|
155 |
-
|
156 |
-
if
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
if
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
# Cari kata kunci nutrisi
|
181 |
-
target_keys = {
|
182 |
-
"gula": "gula|sugar|sugars",
|
183 |
-
"takaran saji": r"takaran saji|serving size|per serving",
|
184 |
-
"lemak jenuh": r"lemak jenuh|saturated fat|sat fat"
|
185 |
-
}
|
186 |
-
|
187 |
-
extracted_values = {}
|
188 |
-
# Gabungkan semua teks untuk pencarian yang lebih mudah
|
189 |
-
full_text = " ".join(extracted_texts).lower()
|
190 |
-
|
191 |
-
for key, pattern in target_keys.items():
|
192 |
-
# Cari nilai setelah kata kunci (misal: "Gula 15g")
|
193 |
-
match = re.search(f"({pattern})[^\d\n]*([\d\.]+)", full_text, re.IGNORECASE)
|
194 |
-
if match:
|
195 |
-
extracted_values[key.replace(" ", "_").capitalize()] = match.group(2)
|
196 |
-
|
197 |
-
st.session_state.extracted_data = extracted_values
|
198 |
-
st.session_state.ocr_ran = True
|
199 |
-
st.success("Analisis OCR selesai!")
|
200 |
-
time.sleep(1) # Beri jeda agar user melihat pesan sukses
|
201 |
-
st.rerun() # Muat ulang state untuk menampilkan form koreksi
|
202 |
-
|
203 |
-
# --- LANGKAH 3: KOREKSI MANUAL & PERHITUNGAN ---
|
204 |
-
if st.session_state.get('ocr_ran', False):
|
205 |
st.header("2. ✏️ Koreksi Data & Hitung Grade")
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
st.
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
if submit_button:
|
236 |
-
serving_size = parse_numeric_value(takaran_saji)
|
237 |
-
sugar_value = parse_numeric_value(gula)
|
238 |
-
fat_value = parse_numeric_value(lemak_jenuh)
|
239 |
-
|
240 |
-
if serving_size <= 0:
|
241 |
-
st.error("Takaran Saji harus lebih besar dari 0!")
|
242 |
-
else:
|
243 |
-
# Normalisasi ke per 100g/ml
|
244 |
-
sugar_norm = (sugar_value / serving_size) * 100
|
245 |
-
fat_norm = (fat_value / serving_size) * 100
|
246 |
-
|
247 |
-
# Simpan hasil ke session state untuk ditampilkan
|
248 |
-
st.session_state.analysis_results = {
|
249 |
-
"serving_size": serving_size,
|
250 |
-
"sugar_norm": sugar_norm,
|
251 |
-
"fat_norm": fat_norm
|
252 |
-
}
|
253 |
-
st.session_state.analysis_done = True
|
254 |
-
|
255 |
-
# --- LANGKAH 4: TAMPILKAN HASIL ---
|
256 |
-
if st.session_state.get('analysis_done', False):
|
257 |
-
results = st.session_state.analysis_results
|
258 |
-
sugar_norm = results['sugar_norm']
|
259 |
-
fat_norm = results['fat_norm']
|
260 |
-
serving_size = results['serving_size']
|
261 |
-
|
262 |
-
st.header("3. 📈 Hasil Analisis")
|
263 |
-
|
264 |
-
# Hitung Grade
|
265 |
-
thresholds_sugar = {"A": 1.0, "B": 5.0, "C": 10.0}
|
266 |
-
thresholds_fat = {"A": 0.7, "B": 1.2, "C": 2.8}
|
267 |
-
sugar_grade = get_grade_from_value(sugar_norm, thresholds_sugar)
|
268 |
-
fat_grade = get_grade_from_value(fat_norm, thresholds_fat)
|
269 |
-
|
270 |
-
# Tentukan grade akhir (yang terburuk)
|
271 |
-
grade_scores = {"Grade A": 1, "Grade B": 2, "Grade C": 3, "Grade D": 4}
|
272 |
-
final_grade = max(sugar_grade, fat_grade, key=lambda g: grade_scores[g])
|
273 |
-
|
274 |
-
# Tampilkan Grade
|
275 |
-
st.subheader("🏆 Hasil Grading Produk")
|
276 |
-
col1, col2, col3 = st.columns(3)
|
277 |
-
|
278 |
-
def display_grade_card(container, title, value, unit, grade):
|
279 |
-
bg_color, text_color = get_grade_color(grade)
|
280 |
-
container.markdown(f"""
|
281 |
-
<div style="background-color: {bg_color}; padding: 15px; border-radius: 10px; text-align: center; color: {text_color}; font-weight: bold; margin: 5px;">
|
282 |
-
<h4 style="margin: 0; color: {text_color};">{title}</h4>
|
283 |
-
<p style="margin: 5px 0; color: {text_color};">{value:.2f} {unit}</p>
|
284 |
-
<h3 style="margin: 0; color: {text_color};">{grade}</h3>
|
285 |
-
</div>
|
286 |
-
""", unsafe_allow_html=True)
|
287 |
-
|
288 |
-
display_grade_card(col1, "Gula", sugar_norm, "g / 100ml", sugar_grade)
|
289 |
-
# BUG FIX: Menggunakan fat_grade, bukan sugar_grade
|
290 |
-
display_grade_card(col2, "Lemak Jenuh", fat_norm, "g / 100ml", fat_grade)
|
291 |
-
|
292 |
-
with col3:
|
293 |
-
bg_color, text_color = get_grade_color(final_grade)
|
294 |
-
st.markdown(f"""
|
295 |
-
<div style="background-color: {bg_color}; padding: 15px; border-radius: 10px; text-align: center; color: {text_color}; font-weight: bold; margin: 5px; border: 3px solid #333;">
|
296 |
-
<h4 style="margin: 0; color: {text_color};">Grade Akhir</h4>
|
297 |
-
<h2 style="margin: 10px 0; color: {text_color};">{final_grade}</h2>
|
298 |
-
</div>
|
299 |
-
""", unsafe_allow_html=True)
|
300 |
-
|
301 |
st.divider()
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
st.success("Saran berhasil didapatkan!")
|
312 |
-
st.info(advice)
|
313 |
-
|
314 |
-
# --- FOOTER ---
|
315 |
st.markdown("---")
|
316 |
-
st.markdown(""
|
317 |
-
<div style="text-align: center; padding: 10px;">
|
318 |
-
<p><strong>Nutri-Grade Detection App v2.1</strong> | Dikembangkan oleh Tim Nutri-Grade © 2024</p>
|
319 |
-
<small>Powered by PaddleOCR, OpenRouter API, and Streamlit</small>
|
320 |
-
</div>
|
321 |
-
""", unsafe_allow_html=True)
|
|
|
7 |
from PIL import Image
|
8 |
import time
|
9 |
import requests
|
|
|
10 |
from paddleocr import PaddleOCR, draw_ocr
|
11 |
|
12 |
# --- KONFIGURASI APLIKASI ---
|
|
|
19 |
initial_sidebar_state="collapsed"
|
20 |
)
|
21 |
|
22 |
+
# [Pengaturan API Key] Gunakan API Key langsung (sesuai permintaan)
|
23 |
+
OPENROUTER_API_KEY = "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"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
25 |
|
26 |
# --- FUNGSI-FUNGSI UTAMA ---
|
|
|
29 |
def initialize_ocr():
|
30 |
"""Inisialisasi model PaddleOCR dan menyimpannya di cache."""
|
31 |
try:
|
|
|
32 |
ocr = PaddleOCR(use_gpu=False, lang='en', use_angle_cls=True, show_log=False)
|
33 |
return ocr
|
34 |
except Exception as e:
|
35 |
st.error(f"Gagal total inisialisasi OCR: {e}")
|
36 |
return None
|
37 |
|
38 |
+
|
39 |
def parse_numeric_value(text: str) -> float:
|
40 |
"""Membersihkan dan mengubah string menjadi nilai float."""
|
41 |
if not text:
|
42 |
return 0.0
|
|
|
43 |
cleaned = re.sub(r"[^\d\.\-]", "", str(text))
|
44 |
if not cleaned or cleaned in [".", "-"]:
|
45 |
return 0.0
|
|
|
48 |
except (ValueError, TypeError):
|
49 |
return 0.0
|
50 |
|
51 |
+
|
52 |
def get_nutrition_advice(serving_size, sugar_norm, fat_norm, sugar_grade, fat_grade, final_grade):
|
53 |
"""Mendapatkan saran nutrisi dari model AI melalui OpenRouter."""
|
54 |
prompt = f"""
|
|
|
68 |
"Content-Type": "application/json"
|
69 |
}
|
70 |
payload = {
|
71 |
+
# Model tetap "mistralai/mistral-7b-instruct:free"
|
72 |
"model": "mistralai/mistral-7b-instruct:free",
|
73 |
"messages": [{"role": "user", "content": prompt}],
|
74 |
"max_tokens": 250,
|
|
|
76 |
}
|
77 |
try:
|
78 |
response = requests.post(f"{OPENROUTER_BASE_URL}/chat/completions", headers=headers, json=payload, timeout=30)
|
79 |
+
response.raise_for_status()
|
80 |
data = response.json()
|
81 |
return data["choices"][0]["message"]["content"].strip()
|
82 |
except requests.exceptions.HTTPError as e:
|
83 |
+
return f"Error: Gagal menghubungi server AI ({e.response.status_code})."
|
84 |
except requests.exceptions.RequestException as e:
|
85 |
+
return f"Error: Gagal terhubung ke API ({e})."
|
86 |
except Exception as e:
|
87 |
+
return f"Error: Terjadi kesalahan tak terduga ({e})."
|
88 |
+
|
89 |
|
90 |
def get_grade_from_value(value, thresholds):
|
|
|
91 |
if value <= thresholds["A"]: return "Grade A"
|
92 |
if value <= thresholds["B"]: return "Grade B"
|
93 |
if value <= thresholds["C"]: return "Grade C"
|
94 |
return "Grade D"
|
95 |
|
96 |
+
|
97 |
def get_grade_color(grade_text):
|
|
|
98 |
colors = {
|
99 |
+
"Grade A": ("#2ecc71", "white"),
|
100 |
+
"Grade B": ("#f1c40f", "black"),
|
101 |
+
"Grade C": ("#e67e22", "white"),
|
102 |
+
"Grade D": ("#e74c3c", "white")
|
103 |
}
|
104 |
+
return colors.get(grade_text, ("#bdc3c7", "black"))
|
105 |
+
|
106 |
|
107 |
def reset_analysis_state():
|
108 |
+
for key in ['ocr_ran', 'extracted_data', 'analysis_done', 'analysis_results']:
|
|
|
|
|
109 |
if key in st.session_state:
|
110 |
del st.session_state[key]
|
111 |
+
|
112 |
# --- UI APLIKASI ---
|
113 |
|
|
|
114 |
ocr_model = initialize_ocr()
|
115 |
if ocr_model is None:
|
116 |
+
st.error("Aplikasi tidak dapat berjalan tanpa model OCR.")
|
117 |
st.stop()
|
118 |
|
|
|
119 |
st.title("🥗 Nutri-Grade Label Detection & Grade Calculator")
|
120 |
st.caption("Aplikasi ini membantu Anda memahami kandungan gizi produk dengan standar Nutri-Grade Singapura.")
|
121 |
|
122 |
with st.expander("📋 Petunjuk Penggunaan & Info"):
|
123 |
st.markdown("""
|
124 |
+
1. Upload gambar tabel gizi.
|
125 |
+
2. Klik "Mulai Analisis OCR".
|
126 |
+
3. Koreksi nilai jika perlu.
|
127 |
+
4. Klik "Hitung Grade" dan lihat saran nutrisi.
|
|
|
|
|
|
|
|
|
|
|
|
|
128 |
""")
|
129 |
|
|
|
130 |
st.header("1. 📸 Upload Gambar Tabel Gizi")
|
131 |
+
uploaded_file = st.file_uploader("Pilih file JPG/PNG", type=["jpg","jpeg","png"], on_change=reset_analysis_state)
|
132 |
+
|
133 |
+
if uploaded_file:
|
134 |
+
img_arr = np.frombuffer(uploaded_file.read(), np.uint8)
|
135 |
+
img = cv2.imdecode(img_arr, cv2.IMREAD_COLOR)
|
136 |
+
st.image(cv2.cvtColor(img, cv2.COLOR_BGR2RGB), width=300)
|
137 |
+
|
138 |
+
if st.button("Mulai Analisis OCR"):
|
139 |
+
with st.spinner("Mendeteksi teks..."):
|
140 |
+
ocr_result = ocr_model.ocr(img, cls=True)
|
141 |
+
if not ocr_result or not ocr_result[0]:
|
142 |
+
st.error("OCR gagal menemukan teks!")
|
143 |
+
else:
|
144 |
+
texts = [line[1][0] for line in ocr_result[0]]
|
145 |
+
full_text = " ".join(texts).lower()
|
146 |
+
patterns = {
|
147 |
+
"Takaran_Saji": r"(takaran saj(i|a)|serving size)[^\d]*(\d+\.?\d*)",
|
148 |
+
"Gula": r"(gula|sugar)[^\d]*(\d+\.?\d*)",
|
149 |
+
"Lemak_Jenuh": r"(lemak jenuh|saturated fat)[^\d]*(\d+\.?\d*)"
|
150 |
+
}
|
151 |
+
extracted = {}
|
152 |
+
for key, pat in patterns.items():
|
153 |
+
m = re.search(pat, full_text)
|
154 |
+
if m:
|
155 |
+
extracted[key] = m.group(2)
|
156 |
+
st.session_state.extracted_data = extracted
|
157 |
+
st.session_state.ocr_ran = True
|
158 |
+
st.success("OCR selesai!")
|
159 |
+
st.rerun()
|
160 |
+
|
161 |
+
if st.session_state.get('ocr_ran'):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
162 |
st.header("2. ✏️ Koreksi Data & Hitung Grade")
|
163 |
+
ext = st.session_state.extracted_data
|
164 |
+
with st.form("form2"):
|
165 |
+
ts = st.text_input("Takaran Saji (g/ml)", value=ext.get('Takaran_Saji','100'))
|
166 |
+
gu = st.text_input("Gula (g)", value=ext.get('Gula','0'))
|
167 |
+
lj = st.text_input("Lemak Jenuh (g)", value=ext.get('Lemak_Jenuh','0'))
|
168 |
+
sb = st.form_submit_button("Hitung Grade")
|
169 |
+
if sb:
|
170 |
+
s_val = parse_numeric_value(ts)
|
171 |
+
g_val = parse_numeric_value(gu)
|
172 |
+
f_val = parse_numeric_value(lj)
|
173 |
+
s_pct = g_val/s_val*100 if s_val>0 else 0
|
174 |
+
f_pct = f_val/s_val*100 if s_val>0 else 0
|
175 |
+
st.session_state.analysis_results = {'s_pct':s_pct,'f_pct':f_pct,'s_val':s_val}
|
176 |
+
st.session_state.analysis_done = True
|
177 |
+
|
178 |
+
if st.session_state.get('analysis_done'):
|
179 |
+
r = st.session_state.analysis_results
|
180 |
+
sg = get_grade_from_value(r['s_pct'],{"A":1.0,"B":5.0,"C":10.0})
|
181 |
+
fg = get_grade_from_value(r['f_pct'],{"A":0.7,"B":1.2,"C":2.8})
|
182 |
+
fg_final = max(sg,fg, key=lambda x: ["Grade A","Grade B","Grade C","Grade D"].index(x))
|
183 |
+
st.header("3. 📈 Hasil Grading")
|
184 |
+
c1,c2,c3 = st.columns(3)
|
185 |
+
def card(c,t,v,u,gr):
|
186 |
+
bc,tc = get_grade_color(gr)
|
187 |
+
c.markdown(f"<div style='background:{bc};padding:10px;border-radius:8px;color:{tc};text-align:center;'>"
|
188 |
+
f"<strong>{t}</strong><p>{v:.2f} {u}</p><h4>{gr}</h4></div>",unsafe_allow_html=True)
|
189 |
+
card(c1,"Gula",r['s_pct'],"g/100ml",sg)
|
190 |
+
card(c2,"Lemak Jenuh",r['f_pct'],"g/100ml",fg)
|
191 |
+
card(c3,"Grade Akhir",0,"",fg_final)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
192 |
st.divider()
|
193 |
+
st.header("4. 🤖 Saran Nutrisi AI")
|
194 |
+
with st.spinner("Meminta saran..."):
|
195 |
+
adv = get_nutrition_advice(r['s_val'],r['s_pct'],r['f_pct'],sg,fg,fg_final)
|
196 |
+
if adv.startswith("Error"):
|
197 |
+
st.error(adv)
|
198 |
+
else:
|
199 |
+
st.info(adv)
|
200 |
+
|
201 |
+
# Footer
|
|
|
|
|
|
|
|
|
202 |
st.markdown("---")
|
203 |
+
st.markdown("<p style='text-align:center;'>Nutri-Grade Detection App v2.1 © 2024</p>", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|