YourAIEngineer commited on
Commit
27ef3cd
·
verified ·
1 Parent(s): ac27d7e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +253 -181
app.py CHANGED
@@ -1,6 +1,3 @@
1
- # ==============================================================================
2
- # BAGIAN 1: IMPORT LIBRARY & KONFIGURASI AWAL
3
- # ==============================================================================
4
  import streamlit as st
5
  import cv2
6
  import numpy as np
@@ -10,240 +7,315 @@ import pandas as pd
10
  from PIL import Image
11
  import time
12
  import requests
 
13
  from paddleocr import PaddleOCR, draw_ocr
14
 
 
 
15
  # Konfigurasi halaman Streamlit
16
  st.set_page_config(
17
  page_title="Nutri-Grade Label Detection",
18
  page_icon="🥗",
19
  layout="wide",
20
- initial_sidebar_state="auto"
21
  )
22
 
23
- # --- Memuat API Key dengan aman menggunakan st.secrets ---
 
24
  try:
25
  OPENROUTER_API_KEY = st.secrets["OPENROUTER_API_KEY"]
26
- except KeyError:
27
- st.error("API Key (OPENROUTER_API_KEY) tidak ditemukan. Harap buat file .streamlit/secrets.toml.")
 
28
  st.stop()
29
-
30
  OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
31
 
32
- # ==============================================================================
33
- # BAGIAN 2: FUNGSI-FUNGSI BANTUAN (HELPER FUNCTIONS)
34
- # ==============================================================================
35
 
36
  @st.cache_resource
37
  def initialize_ocr():
38
- """Memuat model PaddleOCR ke dalam cache agar tidak dimuat berulang kali."""
39
- st.info("Pertama kali dijalankan: Memuat model OCR. Mohon tunggu sebentar...")
40
- # Langsung gunakan CPU mode (use_gpu=False) untuk stabilitas maksimum di server gratis.
41
  try:
42
- ocr = PaddleOCR(use_gpu=False, lang='en', show_log=False)
 
43
  return ocr
44
  except Exception as e:
45
- st.error(f"Gagal total memuat model OCR: {e}")
46
  return None
47
 
48
  def parse_numeric_value(text: str) -> float:
49
- """Membersihkan string dan mengubahnya menjadi angka float yang valid."""
50
  if not text:
51
  return 0.0
52
- # Mengambil hanya digit dan titik desimal
53
- cleaned = re.sub(r"[^\d\.]", "", str(text))
54
- if not cleaned or cleaned == ".":
55
  return 0.0
56
  try:
57
  return float(cleaned)
58
  except (ValueError, TypeError):
59
  return 0.0
60
 
61
- def extract_nutrition_data(ocr_list: list) -> dict:
62
- """Mengekstrak data Gula, Lemak Jenuh, dan Takaran Saji dari hasil OCR."""
63
- target_keys = {
64
- "Takaran saji": ["takaran saji", "serving size", "per serving", "sajian"],
65
- "Gula": ["gula", "sugar", "sugars", "total sugar"],
66
- "Lemak jenuh": ["lemak jenuh", "saturated fat", "saturated", "sat fat"]
67
- }
68
- extracted = {}
69
-
70
- # Mencari nilai berdasarkan kata kunci yang paling relevan
71
- for item in ocr_list:
72
- txt_lower = item["text"].lower()
73
- for canonical, variants in target_keys.items():
74
- if canonical not in extracted:
75
- for variant in variants:
76
- if variant in txt_lower:
77
- # Mencari angka dalam baris teks yang sama
78
- found_numbers = re.findall(r'(\d+\.?\d*)', txt_lower)
79
- if found_numbers:
80
- extracted[canonical] = found_numbers[0]
81
- break # Lanjut ke kata kunci berikutnya
82
- if canonical in extracted:
83
- break # Lanjut ke baris teks berikutnya
84
- return extracted
85
-
86
- def calculate_grades(sugar_norm: float, fat_norm: float) -> tuple[str, str, str]:
87
- """Menghitung grade A, B, C, atau D berdasarkan nilai ternormalisasi."""
88
- thresholds_sugar = {"A": 1.0, "B": 5.0, "C": 10.0}
89
- thresholds_fat = {"A": 0.7, "B": 1.2, "C": 2.8}
90
-
91
- def get_grade(value, thresholds):
92
- if value <= thresholds["A"]: return "Grade A"
93
- if value <= thresholds["B"]: return "Grade B"
94
- if value <= thresholds["C"]: return "Grade C"
95
- return "Grade D"
96
-
97
- sugar_grade = get_grade(sugar_norm, thresholds_sugar)
98
- fat_grade = get_grade(fat_norm, thresholds_fat)
99
-
100
- grade_scores = {"Grade A": 1, "Grade B": 2, "Grade C": 3, "Grade D": 4}
101
- worst_score = max(grade_scores[sugar_grade], grade_scores[fat_grade])
102
- final_grade = next(grade for grade, score in grade_scores.items() if score == worst_score)
103
-
104
- return sugar_grade, fat_grade, final_grade
105
-
106
  def get_nutrition_advice(serving_size, sugar_norm, fat_norm, sugar_grade, fat_grade, final_grade):
107
- """Mengirim request ke OpenRouter API untuk mendapatkan saran nutrisi dari AI."""
108
- nutrition_prompt = f"""
109
- Anda adalah ahli gizi dari Indonesia yang ramah dan suportif.
110
- Data nutrisi produk:
111
- - Kandungan Gula (per 100g): {sugar_norm:.1f}g (Grade: {sugar_grade})
112
- - Kandungan Lemak Jenuh (per 100g): {fat_norm:.1f}g (Grade: {fat_grade})
113
- - Grade Akhir Produk: {final_grade}
114
-
115
- Tugas: Berikan saran nutrisi yang singkat, positif, dan actionable dalam 1-2 kalimat.
116
- Fokus pada bagaimana cara menikmati produk ini secara bijak dalam pola makan seimbang.
117
- Gunakan bahasa yang mudah dimengerti.
 
118
  """
119
- headers = {"Authorization": f"Bearer {OPENROUTER_API_KEY}"}
 
 
 
120
  payload = {
121
- "model": "qwen/qwen2.5-vl-72b-instruct:free",
122
- "messages": [{"role": "user", "content": nutrition_prompt}],
123
- "max_tokens": 150, "temperature": 0.7
 
 
124
  }
125
  try:
126
  response = requests.post(f"{OPENROUTER_BASE_URL}/chat/completions", headers=headers, json=payload, timeout=30)
127
- response.raise_for_status() # Ini akan memicu error jika status code bukan 2xx
128
- return response.json()["choices"][0]["message"]["content"]
 
 
 
129
  except requests.exceptions.RequestException as e:
130
- return f"Error API: Gagal terhubung ke server. {e}"
131
  except Exception as e:
132
- return f"Error: Terjadi kesalahan saat memproses respons AI. {e}"
133
-
134
- # ==============================================================================
135
- # BAGIAN 3: TAMPILAN UTAMA APLIKASI (USER INTERFACE)
136
- # ==============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
- # --- Judul dan Deskripsi ---
139
- st.title("🥗 Kalkulator Nutri-Grade")
140
- st.caption("Unggah foto label gizi untuk menganalisis kandungan gula & lemak jenuhnya.")
141
 
142
- # --- Petunjuk & Informasi ---
143
- with st.expander("📋 Lihat Petunjuk Penggunaan"):
144
  st.markdown("""
145
- 1. **Unggah Gambar**: Pilih atau ambil foto label gizi produk.
146
- 2. **Analisis Otomatis**: Sistem akan langsung mendeteksi teks dan angka.
147
- 3. **Koreksi Data**: Periksa angka yang diekstrak. Perbaiki jika ada yang salah di formulir.
148
- 4. **Hitung & Lihat Hasil**: Klik tombol "Hitung Grade" untuk melihat hasilnya.
 
 
 
 
 
 
149
  """)
150
 
151
- # --- Inisialisasi Model OCR ---
152
- ocr_model = initialize_ocr()
153
- if ocr_model is None:
154
- st.error("Model OCR tidak dapat dimuat. Aplikasi tidak bisa melanjutkan.")
155
- st.stop()
156
-
157
- # --- STEP 1: Upload Gambar ---
158
- st.header("1. Unggah Gambar Label Gizi")
159
  uploaded_file = st.file_uploader(
160
- "Pilih gambar (JPG/PNG/JPEG)",
161
- type=["jpg", "jpeg", "png"]
 
 
162
  )
163
 
164
- # Alur akan berjalan jika ada file yang diunggah
165
  if uploaded_file is not None:
166
- # Baca dan tampilkan gambar
167
  file_bytes = np.asarray(bytearray(uploaded_file.read()), dtype=np.uint8)
168
  img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
169
- st.image(cv2.cvtColor(img, cv2.COLOR_BGR2RGB), caption="Gambar yang diunggah", use_column_width=True)
170
-
171
- # --- STEP 2: Analisis OCR Otomatis ---
172
- with st.spinner("Menganalisis teks pada gambar..."):
173
- temp_img_path = "temp_uploaded_image.jpg"
174
- cv2.imwrite(temp_img_path, img)
175
- ocr_result = ocr_model.ocr(temp_img_path, cls=True)
176
-
177
- if not ocr_result or not ocr_result[0]:
178
- st.error("Tidak ada teks yang dapat dideteksi. Coba gunakan gambar yang lebih jelas.")
179
  st.stop()
180
 
181
- # Proses dan ekstrak data dari hasil OCR
182
- ocr_list = [{"text": line[1][0], "box": line[0]} for line in ocr_result[0]]
183
- extracted_data = extract_nutrition_data(ocr_list)
184
-
185
- st.success("Analisis OCR selesai! Data berikut berhasil diekstrak:")
186
- st.json(extracted_data) # Tampilkan data yang diekstrak dalam format JSON yang rapi
187
-
188
- # --- STEP 3: Formulir Koreksi dan Perhitungan ---
189
- st.header("2. Verifikasi Data & Hitung Grade")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  with st.form("correction_form"):
191
- st.caption("Periksa kembali angka di bawah ini. Koreksi jika perlu, lalu klik hitung.")
 
192
  col1, col2, col3 = st.columns(3)
193
-
194
- takaran_saji = col1.text_input("Takaran Saji (g/ml)", value=extracted_data.get("Takaran saji", "100"))
195
- gula = col2.text_input("Gula (g)", value=extracted_data.get("Gula", "0"))
196
- lemak_jenuh = col3.text_input("Lemak Jenuh (g)", value=extracted_data.get("Lemak jenuh", "0"))
197
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  submit_button = st.form_submit_button("🧮 Hitung Grade", type="primary", use_container_width=True)
199
 
200
- if submit_button:
201
- # Konversi input ke angka
202
- serving_size_val = parse_numeric_value(takaran_saji)
203
- sugar_val = parse_numeric_value(gula)
204
- fat_val = parse_numeric_value(lemak_jenuh)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
- if serving_size_val <= 0:
207
- st.error("Nilai 'Takaran Saji' harus lebih dari nol.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  else:
209
- # Normalisasi nilai nutrisi ke per 100g/ml
210
- sugar_norm = (sugar_val / serving_size_val) * 100
211
- fat_norm = (fat_val / serving_size_val) * 100
212
-
213
- # Hitung grade
214
- sugar_grade, fat_grade, final_grade = calculate_grades(sugar_norm, fat_norm)
215
-
216
- # --- STEP 4: Tampilkan Hasil ---
217
- st.header("🏆 Hasil Analisis Anda")
218
- col1, col2 = st.columns([1, 2]) # Buat kolom dengan rasio lebar 1:2
219
-
220
- with col1: # Kolom Kiri untuk Grade Akhir
221
- color_map = {"A": "#2ecc71", "B": "#f1c40f", "C": "#e67e22", "D": "#e74c3c"}
222
- grade_letter = final_grade.split(" ")[-1]
223
- bg_color = color_map.get(grade_letter, "#7f8c8d")
224
- st.markdown(f"""
225
- <div style="background-color: {bg_color}; padding: 20px; border-radius: 10px; text-align: center; color: white;">
226
- <p style="font-size: 1.2em; margin:0;">Grade Akhir</p>
227
- <h1 style="font-size: 4em; margin:0; color: white;">{grade_letter}</h1>
228
- </div>
229
- """, unsafe_allow_html=True)
230
-
231
- with col2: # Kolom Kanan untuk detail
232
- st.metric(label=f"Kandungan Gula (per 100g)", value=f"{sugar_norm:.1f} g", delta=sugar_grade)
233
- st.metric(label=f"Kandungan Lemak Jenuh (per 100g)", value=f"{fat_norm:.1f} g", delta=fat_grade)
234
-
235
- # --- STEP 5: Tampilkan Saran AI ---
236
- with st.spinner("Meminta saran dari Ahli Gizi AI..."):
237
- advice = get_nutrition_advice(serving_size_val, sugar_norm, fat_norm, sugar_grade, fat_grade, final_grade)
238
- if "Error" in advice:
239
- st.warning(advice)
240
- else:
241
- st.success(f"💡 **Saran Ahli Gizi AI**: {advice}")
242
-
243
- # Hapus file gambar sementara setelah selesai
244
- if os.path.exists(temp_img_path):
245
- os.remove(temp_img_path)
246
-
247
- # --- Footer dan Informasi Tambahan ---
248
  st.markdown("---")
249
- st.markdown("Dibuat oleh Tim Nutri-Grade dengan Streamlit.")
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
  import cv2
3
  import numpy as np
 
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 ---
14
+
15
  # Konfigurasi halaman Streamlit
16
  st.set_page_config(
17
  page_title="Nutri-Grade Label Detection",
18
  page_icon="🥗",
19
  layout="wide",
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 ---
 
 
35
 
36
  @st.cache_resource
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
55
  try:
56
  return float(cleaned)
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"""
63
+ Anda adalah ahli gizi dari Indonesia yang ramah dan komunikatif.
64
+ Data nutrisi produk ini adalah:
65
+ - Takaran Saji: {serving_size} g/ml
66
+ - Gula (per 100 g/ml): {sugar_norm:.2f} g (Grade {sugar_grade.replace('Grade ', '')})
67
+ - Lemak Jenuh (per 100 g/ml): {fat_norm:.2f} g (Grade {fat_grade.replace('Grade ', '')})
68
+ - Grade Akhir Produk: {final_grade.replace('Grade ', '')}
69
+
70
+ Berdasarkan data ini, berikan saran nutrisi singkat (sekitar 50-80 kata) dalam Bahasa Indonesia.
71
+ Fokus pada dampak kesehatan dari kandungan gula dan lemak jenuhnya, serta berikan tips praktis terkait konsumsi produk ini.
72
+ Gunakan bahasa yang mudah dimengerti dan bersahabat.
73
  """
74
+ headers = {
75
+ "Authorization": f"Bearer {OPENROUTER_API_KEY}",
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,
83
+ "temperature": 0.7
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)