YourAIEngineer commited on
Commit
838c721
·
verified ·
1 Parent(s): 2406121

Update app.py

Browse files

Update Chatbot

Files changed (1) hide show
  1. app.py +180 -130
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import streamlit as st
2
  import cv2
3
  import numpy as np
@@ -6,43 +7,48 @@ import os
6
  import pandas as pd
7
  from PIL import Image
8
  import time
9
- from ultralytics import YOLO
10
  from paddleocr import PaddleOCR, draw_ocr
 
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  st.title("Nutri-Grade Label Detection & Grade Calculator")
 
13
 
14
  # -----------------------------------------------
15
  # Info & Petunjuk Penggunaan
16
  # -----------------------------------------------
17
- with st.expander("Info & Petunjuk Penggunaan"):
18
  st.markdown("""
19
- **Deskripsi Aplikasi:**
20
-
21
- Aplikasi ini membantu Anda mendeteksi dan mengekstrak informasi tabel gizi dari gambar label nutrisi, melakukan normalisasi nilai nutrisi per 100 g/ml, dan menghitung Nutri-Grade sesuai dengan standar resmi (Rev. Juni 2023).
22
-
23
- **Fitur Utama:**
24
- - Deteksi objek label nutrisi dengan YOLO.
25
- - Ekstraksi teks dengan PaddleOCR, mendukung format "key: value".
26
- - Normalisasi nilai nutrisi (Gula dan Lemak Jenuh) per 100 g/ml.
27
- - Perhitungan grade berdasarkan threshold:
28
- • Gula: Grade A ≤ 1g, B: >1-5g, C: >5-10g, D: >10g per 100 ml.
29
- • Lemak Jenuh: Grade A ≤ 0.7g, B: >0.7-1.2g, C: >1.2-2.8g, D: >2.8g per 100 ml.
30
- • **Grade akhir diambil dari nilai terburuk antara gula dan lemak jenuh.**
31
-
32
  **Cara Penggunaan:**
33
  1. Upload gambar label nutrisi (JPG/PNG).
34
- 2. Sistem mendeteksi objek dan mengekstrak nilai nutrisi.
35
  3. Periksa dan koreksi nilai secara manual jika diperlukan.
36
- 4. Klik *Hitung* untuk melihat tabel normalisasi dan grade.
37
  """)
38
-
39
  with st.expander("!! Tolong Diperhatikan !!"):
40
  st.markdown("""
41
- Labelisasi di bawah hanya sebagai gambaran umum. Perlu riset lebih lanjut untuk akurasi.
42
-
43
- **Pengembangan:**
44
- - Konsultasi dengan nutritionist untuk parameter yang lebih tepat.
45
- - Integrasi informasi halal, kalori, dan fitur interaktif (misal: chatbot).
46
  """)
47
 
48
  # Fungsi untuk membersihkan nilai numerik (contoh: "15g" → 15.0)
@@ -53,10 +59,8 @@ def parse_numeric_value(text):
53
  except ValueError:
54
  return 0.0
55
 
56
- # Inisialisasi model YOLO dan PaddleOCR
57
- trained_model_path = "best.pt" # Pastikan file model YOLO ada di working directory
58
- yolo_model = YOLO(trained_model_path)
59
- ocr_model = PaddleOCR(use_gpu=True, lang='en', cls=True)
60
 
61
  # --- STEP 1: Upload Gambar ---
62
  uploaded_file = st.file_uploader("Upload Gambar (JPG/PNG)", type=["jpg", "jpeg", "png"])
@@ -67,24 +71,8 @@ if uploaded_file is not None:
67
  img_path = "uploaded_image.jpg"
68
  cv2.imwrite(img_path, img)
69
 
70
- # --- STEP 2: Object Detection & Crop dengan YOLO ---
71
- st.write("Melakukan object detection dengan YOLO dan crop region...")
72
- yolo_results = yolo_model.predict(source=img_path, conf=0.5)
73
- crop_images = []
74
- boxes = yolo_results[0].boxes
75
- for i, box in enumerate(boxes):
76
- x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int)
77
- cropped = img[y1:y2, x1:x2]
78
- crop_filename = f"crop_{i}.jpg"
79
- cv2.imwrite(crop_filename, cropped)
80
- crop_images.append((crop_filename, cropped))
81
- st.success("Proses crop bounding box selesai!")
82
- st.write("Jumlah crop yang ditemukan:", len(crop_images))
83
- for crop_filename, cropped in crop_images:
84
- st.image(cv2.cvtColor(cropped, cv2.COLOR_BGR2RGB), caption=f"Crop: {crop_filename}", use_column_width=True)
85
-
86
- # --- STEP 3: OCR pada Gambar Penuh ---
87
- st.write("Melakukan OCR pada gambar penuh dengan PaddleOCR...")
88
  start_time = time.time()
89
  ocr_result = ocr_model.ocr(img_path, cls=True)
90
  ocr_time = time.time() - start_time
@@ -112,18 +100,16 @@ if uploaded_file is not None:
112
  "center_y": center_y,
113
  "height": max(ys) - min(ys)
114
  })
115
- # Urutkan berdasarkan posisi vertikal
116
  ocr_list = sorted(ocr_list, key=lambda x: x["center_y"])
117
 
118
  # Ekstrak pasangan key-value dengan format "key: value"
119
- # Hanya ekstrak gula, takaran saji, dan lemak jenuh
120
  target_keys = {
121
  "gula": ["gula"],
122
  "takaran saji": ["takaran saji", "serving size"],
123
  "lemak jenuh": ["lemak jenuh"]
124
  }
125
  extracted = {}
126
- # Pass 1: Ekstraksi menggunakan tanda titik dua
127
  for item in ocr_list:
128
  txt_lower = item["text"].lower()
129
  if ":" in txt_lower:
@@ -131,12 +117,13 @@ if uploaded_file is not None:
131
  key_candidate = parts[0].strip()
132
  value_candidate = parts[-1].strip()
133
  for canonical, variants in target_keys.items():
134
- for variant in variants:
135
- if variant in key_candidate and canonical not in extracted:
136
- clean_value = re.sub(r"[^\d\.\-]", "", value_candidate)
137
- if clean_value and clean_value != ".":
138
- extracted[canonical.capitalize()] = clean_value
139
- break
 
140
  # Pass 2: Fallback untuk key yang belum diekstrak
141
  for item in ocr_list:
142
  txt_lower = item["text"].lower()
@@ -178,90 +165,153 @@ if uploaded_file is not None:
178
  im_show = Image.fromarray(im_show)
179
  st.image(im_show, caption="Hasil OCR dengan Bounding Boxes", use_column_width=True)
180
 
181
- # --- Koreksi Manual dengan st.form ---
182
- with st.form("correction_form"):
183
- st.write("Silakan koreksi nilai jika diperlukan (hanya angka, tanpa satuan):")
184
- corrected_data = {}
185
- for key in target_keys.keys():
186
- key_cap = key.capitalize()
187
- current_val = str(parse_numeric_value(extracted.get(key_cap, ""))) if key_cap in extracted else ""
188
- new_val = st.text_input(f"{key_cap}", value=current_val)
189
- corrected_data[key_cap] = new_val
190
- submit_button = st.form_submit_button("Hitung")
191
 
192
- if submit_button:
193
- try:
194
- serving_size = parse_numeric_value(corrected_data.get("Takaran saji", "100"))
195
- except:
196
- serving_size = 0.0
197
 
198
- # Ambil nilai nutrisi (hanya gula dan lemak jenuh)
199
- sugar_value = parse_numeric_value(corrected_data.get("Gula", "0"))
200
- fat_value = parse_numeric_value(corrected_data.get("Lemak jenuh", "0"))
201
 
202
- if serving_size > 0:
203
- sugar_norm = (sugar_value / serving_size) * 100
204
- fat_norm = (fat_value / serving_size) * 100
205
- else:
206
- st.error("Takaran saji tidak valid untuk normalisasi.")
207
- sugar_norm, fat_norm = sugar_value, fat_value
208
 
209
- st.write("**Tabel Hasil Normalisasi per 100 g/ml**")
210
- data_tabel = {
211
- "Nutrisi": ["Gula", "Lemak jenuh"],
212
- "Nilai (per 100 g/ml)": [sugar_norm, fat_norm]
213
- }
214
- df_tabel = pd.DataFrame(data_tabel)
215
- st.table(df_tabel)
216
 
217
- # Fungsi untuk menghitung grade berdasarkan threshold
218
- def grade_from_value(value, thresholds):
219
- if value <= thresholds["A"]:
220
- return "Grade A"
221
- elif value <= thresholds["B"]:
222
- return "Grade B"
223
- elif value <= thresholds["C"]:
224
- return "Grade C"
225
- else:
226
- return "Grade D"
227
 
228
- # Threshold sesuai panduan Nutri-Grade (g/100ml)
229
- thresholds_sugar = {"A": 1.0, "B": 5.0, "C": 10.0}
230
- thresholds_fat = {"A": 0.7, "B": 1.2, "C": 2.8}
231
 
232
- sugar_grade = grade_from_value(sugar_norm, thresholds_sugar)
233
- fat_grade = grade_from_value(fat_norm, thresholds_fat)
234
 
235
- # Grade akhir diambil dari nilai terburuk (nilai maksimum skor)
236
- grade_scores = {"Grade A": 1, "Grade B": 2, "Grade C": 3, "Grade D": 4}
237
- worst_score = max(grade_scores[sugar_grade], grade_scores[fat_grade])
238
- inverse_scores = {v: k for k, v in grade_scores.items()}
239
- final_grade = inverse_scores[worst_score]
240
 
241
- st.write(f"**Grade Gula:** {sugar_grade}")
242
- st.write(f"**Grade Lemak Jenuh:** {fat_grade}")
243
- st.write(f"**Grade Akhir:** {final_grade}")
244
 
245
- def color_grade(grade_text):
246
- if grade_text == "Grade A":
247
- bg_color = "#2ecc71"
248
- elif grade_text == "Grade B":
249
- bg_color = "#f1c40f"
250
- elif grade_text == "Grade C":
251
- bg_color = "#e67e22"
252
- else:
253
- bg_color = "#e74c3c"
254
- return f"""
255
- <div style="
256
- background-color: {bg_color};
257
- padding: 10px;
258
- border-radius: 5px;
259
- margin-top: 10px;
260
- font-weight: bold;
261
- color: white;
262
- text-align: center;
263
- ">
264
- {grade_text}
265
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  """
267
- st.markdown(color_grade(final_grade), unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ %%writefile app.py
2
  import streamlit as st
3
  import cv2
4
  import numpy as np
 
7
  import pandas as pd
8
  from PIL import Image
9
  import time
 
10
  from paddleocr import PaddleOCR, draw_ocr
11
+ from openai import OpenAI
12
 
13
+ # # --- Set Background Wallpaper ---
14
+ # st.markdown(
15
+ # """
16
+ # <style>
17
+ # .stApp {
18
+ # background: url("/content/wallpaper.jpg");
19
+ # background-size: cover;
20
+ # background-attachment: fixed;
21
+ # background-position: center;
22
+ # filter: brightness(0.75);
23
+ # }
24
+ # </style>
25
+ # """,
26
+ # unsafe_allow_html=True
27
+ # )
28
+
29
+ # Title dan Deskripsi
30
  st.title("Nutri-Grade Label Detection & Grade Calculator")
31
+ st.caption("Selamat Datang di aplikasi prototype kami. Terinspirasi dari NutriGrade Singapura, kami berharap aplikasi ini dapat membantu teman-teman dalam memilih produk makanan yang lebih sehat.")
32
 
33
  # -----------------------------------------------
34
  # Info & Petunjuk Penggunaan
35
  # -----------------------------------------------
36
+ with st.expander("Petunjuk Penggunaan"):
37
  st.markdown("""
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  **Cara Penggunaan:**
39
  1. Upload gambar label nutrisi (JPG/PNG).
40
+ 2. Sistem mendeteksi teks pada gambar menggunakan OCR.
41
  3. Periksa dan koreksi nilai secara manual jika diperlukan.
42
+ 4. Klik *Hitung* untuk melihat tabel normalisasi, grade, dan saran nutrisi.
43
  """)
44
+
45
  with st.expander("!! Tolong Diperhatikan !!"):
46
  st.markdown("""
47
+ 1. Aplikasi ini masih dalam Pengembangan.
48
+ 2. Hasil ekstraksi hanya sebagai gambaran; silakan koreksi bila diperlukan.
49
+ 3. Hosting gratisan, jadi mungkin ada beberapa kendala.
50
+ 4. Kode dapat diakses di Hugging Face untuk kontribusi atau feedback.
51
+ 5. Referensi kami dari [Health Promotion Board Singapura](https://www.hpb.gov.sg/docs/default-source/pdf/nutri-grade-ci-guide_eng-only67e4e36349ad4274bfdb22236872336d.pdf)
52
  """)
53
 
54
  # Fungsi untuk membersihkan nilai numerik (contoh: "15g" → 15.0)
 
59
  except ValueError:
60
  return 0.0
61
 
62
+ # Inisialisasi model PaddleOCR
63
+ ocr_model = PaddleOCR(use_gpu=True, lang='id', cls=True)
 
 
64
 
65
  # --- STEP 1: Upload Gambar ---
66
  uploaded_file = st.file_uploader("Upload Gambar (JPG/PNG)", type=["jpg", "jpeg", "png"])
 
71
  img_path = "uploaded_image.jpg"
72
  cv2.imwrite(img_path, img)
73
 
74
+ # --- STEP 2: OCR pada Gambar Penuh ---
75
+ st.write("Melakukan OCR pada gambar...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  start_time = time.time()
77
  ocr_result = ocr_model.ocr(img_path, cls=True)
78
  ocr_time = time.time() - start_time
 
100
  "center_y": center_y,
101
  "height": max(ys) - min(ys)
102
  })
 
103
  ocr_list = sorted(ocr_list, key=lambda x: x["center_y"])
104
 
105
  # Ekstrak pasangan key-value dengan format "key: value"
 
106
  target_keys = {
107
  "gula": ["gula"],
108
  "takaran saji": ["takaran saji", "serving size"],
109
  "lemak jenuh": ["lemak jenuh"]
110
  }
111
  extracted = {}
112
+ # Pass 1: Ekstraksi dengan tanda titik dua
113
  for item in ocr_list:
114
  txt_lower = item["text"].lower()
115
  if ":" in txt_lower:
 
117
  key_candidate = parts[0].strip()
118
  value_candidate = parts[-1].strip()
119
  for canonical, variants in target_keys.items():
120
+ if canonical not in extracted:
121
+ for variant in variants:
122
+ if variant in key_candidate:
123
+ clean_value = re.sub(r"[^\d\.\-]", "", value_candidate)
124
+ if clean_value and clean_value != ".":
125
+ extracted[canonical.capitalize()] = clean_value
126
+ break
127
  # Pass 2: Fallback untuk key yang belum diekstrak
128
  for item in ocr_list:
129
  txt_lower = item["text"].lower()
 
165
  im_show = Image.fromarray(im_show)
166
  st.image(im_show, caption="Hasil OCR dengan Bounding Boxes", use_column_width=True)
167
 
168
+ # --- Koreksi Manual dengan st.form ---
169
+ with st.form("correction_form"):
170
+ st.write("Silakan koreksi nilai jika diperlukan (hanya angka, tanpa satuan):")
171
+ corrected_data = {}
172
+ for key in target_keys.keys():
173
+ key_cap = key.capitalize()
174
+ current_val = str(parse_numeric_value(extracted.get(key_cap, ""))) if key_cap in extracted else ""
175
+ new_val = st.text_input(f"{key_cap}", value=current_val)
176
+ corrected_data[key_cap] = new_val
177
+ submit_button = st.form_submit_button("Hitung")
178
 
179
+ if submit_button:
180
+ try:
181
+ serving_size = parse_numeric_value(corrected_data.get("Takaran saji", "100"))
182
+ except:
183
+ serving_size = 0.0
184
 
185
+ sugar_value = parse_numeric_value(corrected_data.get("Gula", "0"))
186
+ fat_value = parse_numeric_value(corrected_data.get("Lemak jenuh", "0"))
 
187
 
188
+ if serving_size > 0:
189
+ sugar_norm = (sugar_value / serving_size) * 100
190
+ fat_norm = (fat_value / serving_size) * 100
191
+ else:
192
+ st.error("Takaran saji tidak valid untuk normalisasi.")
193
+ sugar_norm, fat_norm = sugar_value, fat_value
194
 
195
+ st.write("**Tabel Hasil Normalisasi per 100 g/ml**")
196
+ data_tabel = {
197
+ "Nutrisi": ["Gula", "Lemak jenuh"],
198
+ "Nilai (per 100 g/ml)": [sugar_norm, fat_norm]
199
+ }
200
+ df_tabel = pd.DataFrame(data_tabel)
201
+ st.table(df_tabel)
202
 
203
+ # Hitung Grade
204
+ def grade_from_value(value, thresholds):
205
+ if value <= thresholds["A"]:
206
+ return "Grade A"
207
+ elif value <= thresholds["B"]:
208
+ return "Grade B"
209
+ elif value <= thresholds["C"]:
210
+ return "Grade C"
211
+ else:
212
+ return "Grade D"
213
 
214
+ thresholds_sugar = {"A": 1.0, "B": 5.0, "C": 10.0}
215
+ thresholds_fat = {"A": 0.7, "B": 1.2, "C": 2.8}
 
216
 
217
+ sugar_grade = grade_from_value(sugar_norm, thresholds_sugar)
218
+ fat_grade = grade_from_value(fat_norm, thresholds_fat)
219
 
220
+ grade_scores = {"Grade A": 1, "Grade B": 2, "Grade C": 3, "Grade D": 4}
221
+ worst_score = max(grade_scores[sugar_grade], grade_scores[fat_grade])
222
+ inverse_scores = {v: k for k, v in grade_scores.items()}
223
+ final_grade = inverse_scores[worst_score]
 
224
 
225
+ st.write(f"**Grade Gula:** {sugar_grade}")
226
+ st.write(f"**Grade Lemak Jenuh:** {fat_grade}")
227
+ st.write(f"**Grade Akhir:** {final_grade}")
228
 
229
+ def color_grade(grade_text):
230
+ if grade_text == "Grade A":
231
+ bg_color = "#2ecc71"
232
+ elif grade_text == "Grade B":
233
+ bg_color = "#f1c40f"
234
+ elif grade_text == "Grade C":
235
+ bg_color = "#e67e22"
236
+ else:
237
+ bg_color = "#e74c3c"
238
+ return f"""
239
+ <div style="
240
+ background-color: {bg_color};
241
+ padding: 10px;
242
+ border-radius: 5px;
243
+ margin-top: 10px;
244
+ font-weight: bold;
245
+ color: white;
246
+ text-align: center;
247
+ ">
248
+ {grade_text}
249
+ </div>
250
+ """
251
+ st.markdown(color_grade(final_grade), unsafe_allow_html=True)
252
+
253
+ # --- Integrasi Qwen Satu Kali untuk Saran Nutrisi ---
254
+ # --- Integrasi Qwen Satu Kali untuk Saran Nutrisi ---
255
+ nutrition_prompt = f"""
256
+ Anda adalah ahli gizi yang ramah, komunikatif, dan berpengalaman.
257
+ Data nutrisi:
258
+ - Takaran saji: {serving_size} g/ml
259
+ - Kandungan Gula (per 100 g/ml): {sugar_norm} g
260
+ - Kandungan Lemak Jenuh (per 100 g/ml): {fat_norm} g
261
+ - Grade Gula: {sugar_grade}
262
+ - Grade Lemak Jenuh: {fat_grade}
263
+ - Grade Akhir: {final_grade}
264
+ Berdasarkan data tersebut, berikan saran nutrisi yang informatif dalam satu paragraf pendek (50-100 kata).
265
+ Jelaskan secara ringkas dengan mengulang data nutrisi lalu dampak kesehatan dari nilai-nilai tersebut dan berikan tips praktis untuk menjaga pola makan seimbang dengan bahasa yang bersahabat.
266
  """
267
+ print("\n")
268
+ st.write("Tunggu sebentar, Qwen si AI nutritionist sedang memproses penjelasannya... 🤖")
269
+ client = OpenAI(
270
+ base_url="https://openrouter.ai/api/v1",
271
+ api_key="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"
272
+ )
273
+ try:
274
+ completion = client.chat.completions.create(
275
+ extra_headers={
276
+ "HTTP-Referer": "<YOUR_SITE_URL>",
277
+ "X-Title": "<YOUR_SITE_NAME>"
278
+ },
279
+ extra_body={},
280
+ model="qwen/qwen2.5-vl-72b-instruct:free",
281
+ messages=[
282
+ {
283
+ "role": "user",
284
+ "content": [
285
+ {
286
+ "type": "text",
287
+ "text": nutrition_prompt
288
+ }
289
+ ]
290
+ }
291
+ ]
292
+ )
293
+ nutrition_advice = completion.choices[0].message.content
294
+ st.write("**Saran Nutrisi dari Qwen:**")
295
+ st.write(nutrition_advice)
296
+ except Exception as e:
297
+ st.error(f"Gagal mendapatkan saran dari Qwen: {e}")
298
+
299
+ # --- Tampilan Tim Pengembang ---
300
+ st.markdown("""
301
+ <div style="border: 2px solid #007BFF; padding: 10px; border-radius: 8px; margin-top: 20px;">
302
+ <h4>Tim Pengembang</h4>
303
+ <p><strong>Nicholas Dominic</strong>, Mentor - <a href="https://www.linkedin.com/in/nicholas-dominic">LinkedIn</a></p>
304
+ <p><strong>Tata Aditya Pamungkas</strong>, Machine Learning - <a href="https://www.linkedin.com/in/tata-aditya-pamungkas">LinkedIn</a></p>
305
+ <p><strong>Firzah Marhamah</strong>, Web Dev - <a href="https://www.linkedin.com/in/m-raihan-hafiz-91a368186">LinkedIn</a></p>
306
+ </div> <br>
307
+ """, unsafe_allow_html=True)
308
+
309
+ print("\n")
310
+
311
+ with st.expander("Ide inovasi kami kedepannya untuk pengembangan"):
312
+ st.markdown("""
313
+ 1. Memakai server berbayar agar lebih banyak pengguna yang bisa mengakses.
314
+ 2. Recall asupan berdasarkan makanan real food sehari-hari. Kami sudah berkonsultasi dengan kak Firzah Marhamah [nutritionist](https://www.linkedin.com/in/firza-marhamah)
315
+ dan ini akan sangat membantu masyarakat untuk mengetahui asupan gizi seimbang.
316
+ 3. Penghitung kalori harian yang terpersonalisasi.
317
+ """)