YourAIEngineer commited on
Commit
2dc2b62
·
verified ·
1 Parent(s): 85a2634

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +94 -212
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
- # [SANGAT PENTING] Ambil API Key dari Streamlit Secrets untuk keamanan
24
- # JANGAN PERNAH MENULIS API KEY LANGSUNG DI KODE!
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
- # Menggunakan model yang lebih umum dan gratis
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() # Akan error jika status code bukan 2xx
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}). Model mungkin sedang sibuk atau API key tidak valid."
92
  except requests.exceptions.RequestException as e:
93
- return f"Error: Gagal terhubung ke API. Periksa koneksi internet Anda. ({e})"
94
  except Exception as e:
95
- return f"Error: Terjadi kesalahan tak terduga. ({e})"
 
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"), # Hijau
108
- "Grade B": ("#f1c40f", "black"), # Kuning
109
- "Grade C": ("#e67e22", "white"), # Oranye
110
- "Grade D": ("#e74c3c", "white") # Merah
111
  }
112
- return colors.get(grade_text, ("#bdc3c7", "black")) # Default Abu-abu
 
113
 
114
  def reset_analysis_state():
115
- """Mereset state analisis jika gambar baru di-upload."""
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. Harap segarkan halaman.")
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
- **Cara Penggunaan:**
136
- 1. **Upload Gambar:** Unggah foto tabel gizi produk yang jelas.
137
- 2. **Mulai Analisis:** Klik tombol "Mulai Analisis OCR" untuk mengekstrak data.
138
- 3. **Koreksi Data:** Periksa dan perbaiki hasil ekstraksi jika ada yang salah.
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
- "Pilih file gambar (JPG, PNG, JPEG)",
151
- type=["jpg", "jpeg", "png"],
152
- help="Upload gambar tabel gizi untuk dianalisis.",
153
- on_change=reset_analysis_state # Reset state jika file berubah
154
- )
155
-
156
- if uploaded_file is not None:
157
- file_bytes = np.asarray(bytearray(uploaded_file.read()), dtype=np.uint8)
158
- img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
159
-
160
- if img is None:
161
- st.error("Gagal memproses file gambar. Silakan coba file lain.")
162
- st.stop()
163
-
164
- # Tampilkan gambar yang di-upload
165
- st.image(cv2.cvtColor(img, cv2.COLOR_BGR2RGB), caption="Gambar yang diupload", width=300)
166
-
167
- # --- LANGKAH 2: PROSES OCR ---
168
- if st.button("Mulai Analisis OCR", type="primary"):
169
- with st.spinner("🔍 Menganalisis teks pada gambar..."):
170
- try:
171
- ocr_result = ocr_model.ocr(img, cls=True)
172
- if not ocr_result or not ocr_result[0]:
173
- st.error("OCR tidak dapat menemukan teks pada gambar. Coba gambar yang lebih jelas.")
174
- st.stop()
175
-
176
- # Ekstrak teks dan lokasinya
177
- ocr_data = ocr_result[0]
178
- extracted_texts = [line[1][0] for line in ocr_data]
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
- extracted = st.session_state.get('extracted_data', {})
208
-
209
- with st.form("correction_form"):
210
- st.write("Periksa dan koreksi hasil OCR jika diperlukan. Masukkan **hanya angka**.")
211
-
212
- col1, col2, col3 = st.columns(3)
213
-
214
- with col1:
215
- takaran_saji = st.text_input(
216
- "Takaran Saji (g/ml)",
217
- value=str(extracted.get("Takaran_saji", "100")),
218
- help="Ukuran satu porsi sajian dalam gram atau ml."
219
- )
220
- with col2:
221
- gula = st.text_input(
222
- "Gula per Porsi (g)",
223
- value=str(extracted.get("Gula", "0")),
224
- help="Total gula dalam satu takaran saji."
225
- )
226
- with col3:
227
- lemak_jenuh = st.text_input(
228
- "Lemak Jenuh per Porsi (g)",
229
- value=str(extracted.get("Lemak_jenuh", "0")),
230
- help="Total lemak jenuh dalam satu takaran saji."
231
- )
232
-
233
- submit_button = st.form_submit_button("🧮 Hitung Grade", type="primary", use_container_width=True)
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
- # Tampilkan Saran Nutrisi dari AI
304
- st.subheader("🤖 Saran Nutrisi dari AI")
305
- with st.spinner("Meminta saran dari ahli gizi AI..."):
306
- advice = get_nutrition_advice(serving_size, sugar_norm, fat_norm, sugar_grade, fat_grade, final_grade)
307
-
308
- if advice.startswith("Error"):
309
- st.error(advice)
310
- else:
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 &copy; 2024</p>", unsafe_allow_html=True)