rdsarjito commited on
Commit
3003577
·
1 Parent(s): f52c616
Files changed (1) hide show
  1. app.py +53 -303
app.py CHANGED
@@ -2,335 +2,85 @@ import streamlit as st
2
  import pickle
3
  import requests
4
  from bs4 import BeautifulSoup
5
- import time
6
 
7
- # === Configuration ===
8
- st.set_page_config(
9
- page_title="AllergenGuard - Deteksi Alergen Makanan",
10
- page_icon="🛡️",
11
- layout="wide",
12
- initial_sidebar_state="expanded"
13
- )
14
-
15
- # === Custom CSS ===
16
- st.markdown("""
17
- <style>
18
- .main-header {
19
- text-align: center;
20
- background: linear-gradient(90deg, #FF6B6B, #4ECDC4);
21
- -webkit-background-clip: text;
22
- -webkit-text-fill-color: transparent;
23
- font-size: 3rem;
24
- font-weight: bold;
25
- margin-bottom: 0.5rem;
26
- }
27
-
28
- .subtitle {
29
- text-align: center;
30
- color: #666;
31
- font-size: 1.2rem;
32
- margin-bottom: 2rem;
33
- }
34
-
35
- .allergen-card {
36
- background: white;
37
- padding: 1.5rem;
38
- border-radius: 15px;
39
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
40
- margin: 1rem 0;
41
- border-left: 5px solid;
42
- }
43
-
44
- .allergen-positive {
45
- border-left-color: #FF4444;
46
- background: linear-gradient(135deg, #FFE5E5, #FFF0F0);
47
- }
48
-
49
- .allergen-negative {
50
- border-left-color: #44AA44;
51
- background: linear-gradient(135deg, #E5F5E5, #F0FAF0);
52
- }
53
-
54
- .ingredient-list {
55
- background: #F8F9FA;
56
- padding: 1rem;
57
- border-radius: 10px;
58
- margin: 1rem 0;
59
- }
60
-
61
- .model-info {
62
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
63
- color: white;
64
- padding: 1rem;
65
- border-radius: 10px;
66
- margin: 1rem 0;
67
- }
68
-
69
- .stats-container {
70
- display: flex;
71
- justify-content: space-around;
72
- margin: 2rem 0;
73
- }
74
-
75
- .stat-box {
76
- text-align: center;
77
- padding: 1rem;
78
- background: white;
79
- border-radius: 10px;
80
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
81
- min-width: 120px;
82
- }
83
-
84
- .stat-number {
85
- font-size: 2rem;
86
- font-weight: bold;
87
- color: #4ECDC4;
88
- }
89
-
90
- .stat-label {
91
- color: #666;
92
- font-size: 0.9rem;
93
- }
94
- </style>
95
- """, unsafe_allow_html=True)
96
-
97
- # === Load Functions ===
98
  @st.cache_resource
99
  def load_vectorizer():
100
- try:
101
- with open("saved_models/tfidf_vectorizer.pkl", "rb") as f:
102
- return pickle.load(f)
103
- except FileNotFoundError:
104
- st.error("⚠️ File vectorizer tidak ditemukan. Pastikan file ada di folder saved_models/")
105
- return None
106
 
 
107
  @st.cache_resource
108
  def load_model(model_name):
109
- try:
110
- model_path = f"saved_models/{model_name}_model.pkl"
111
- with open(model_path, "rb") as f:
112
- return pickle.load(f)
113
- except FileNotFoundError:
114
- st.error(f"⚠️ Model {model_name} tidak ditemukan. Pastikan file ada di folder saved_models/")
115
- return None
116
 
 
117
  def predict_allergen(model, vectorizer, input_text):
118
- if model is None or vectorizer is None:
119
- return None
120
  X_input = vectorizer.transform([input_text])
121
  prediction = model.predict(X_input)
122
  return prediction[0]
123
 
 
124
  def get_ingredients_from_cookpad(url):
125
- headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
126
  try:
127
- response = requests.get(url, headers=headers, timeout=10)
128
  if response.status_code != 200:
129
- return None, f"Gagal mengambil halaman. Status code: {response.status_code}"
130
-
131
  soup = BeautifulSoup(response.text, "html.parser")
132
  ingredient_div = soup.find("div", class_="ingredient-list")
133
-
134
  if not ingredient_div:
135
- return None, "Tidak menemukan elemen bahan pada halaman tersebut."
136
-
137
  ingredients = [item.get_text(strip=True) for item in ingredient_div.find_all("li")]
138
  return ingredients, None
139
- except requests.RequestException as e:
140
- return None, f"Terjadi kesalahan koneksi: {str(e)}"
141
  except Exception as e:
142
  return None, f"Terjadi kesalahan: {str(e)}"
143
 
144
- # === Header ===
145
- st.markdown('<h1 class="main-header">🛡️ AllergenGuard</h1>', unsafe_allow_html=True)
146
- st.markdown('<p class="subtitle">Sistem Deteksi Alergen Makanan Cerdas untuk Keamanan Pangan Anda</p>', unsafe_allow_html=True)
147
-
148
- # === Sidebar ===
149
- with st.sidebar:
150
- st.markdown("### 📊 Informasi Aplikasi")
151
- st.info("""
152
- **AllergenGuard** menggunakan teknologi Machine Learning untuk mendeteksi 5 jenis alergen utama:
153
- - 🥛 Susu
154
- - 🥜 Kacang-kacangan
155
- - 🥚 Telur
156
- - 🦐 Makanan Laut
157
- - 🌾 Gandum
158
- """)
159
-
160
- st.markdown("### 🤖 Model yang Tersedia")
161
- model_info = {
162
- "XGBoost": "Model ensemble dengan performa tinggi",
163
- "KNN": "Model berbasis kedekatan data",
164
- "Random Forest": "Model ensemble berbasis pohon keputusan"
165
- }
166
-
167
- for model, desc in model_info.items():
168
- st.markdown(f"**{model}**: {desc}")
169
-
170
- # === Main Content ===
171
- col1, col2 = st.columns([2, 1])
172
-
173
- with col1:
174
- st.markdown("### 📝 Input Bahan Makanan")
175
-
176
- # Mode selection with better styling
177
- input_mode = st.radio(
178
- "Pilih metode input:",
179
- ["✏️ Input Manual", "🔗 URL Cookpad"],
180
- horizontal=True
181
- )
182
-
183
- input_text = ""
184
- ingredients_list = []
185
-
186
- if input_mode == "✏️ Input Manual":
187
- input_text = st.text_area(
188
- "Masukkan bahan makanan:",
189
- height=120,
190
- placeholder="Contoh: tepung gandum, telur ayam, susu sapi, udang segar, kacang tanah...",
191
- help="Pisahkan setiap bahan dengan koma atau enter"
192
- )
193
-
194
- elif input_mode == "🔗 URL Cookpad":
195
- url = st.text_input(
196
- "Masukkan URL resep Cookpad:",
197
- placeholder="https://cookpad.com/id/resep/...",
198
- help="Salin dan tempel URL resep dari Cookpad"
199
- )
200
-
201
- if url and st.button("🔍 Ambil Bahan dari URL", type="secondary"):
202
- with st.spinner("🔄 Mengambil data dari Cookpad..."):
203
- time.sleep(1) # Simulasi loading
204
- ingredients_list, error = get_ingredients_from_cookpad(url)
205
-
206
- if error:
207
- st.error(f"❌ {error}")
208
- else:
209
- st.success("✅ Berhasil mengambil bahan-bahan!")
210
- st.markdown('<div class="ingredient-list">', unsafe_allow_html=True)
211
- st.markdown("**Bahan yang ditemukan:**")
212
- for i, item in enumerate(ingredients_list, 1):
213
- st.markdown(f"{i}. {item}")
214
- st.markdown('</div>', unsafe_allow_html=True)
215
- input_text = " ".join(ingredients_list)
216
-
217
- with col2:
218
- st.markdown("### ⚙️ Pengaturan Model")
219
-
220
- model_name = st.selectbox(
221
- "Pilih model prediksi:",
222
- ("XGBoost", "KNN", "Random Forest"),
223
- help="Setiap model memiliki karakteristik prediksi yang berbeda"
224
- )
225
-
226
- # Model info display
227
- st.markdown(f'<div class="model-info">', unsafe_allow_html=True)
228
- st.markdown(f"**Model Aktif:** {model_name}")
229
- st.markdown(f"**Status:** Ready to predict")
230
- st.markdown('</div>', unsafe_allow_html=True)
231
-
232
- # === Prediction Section ===
233
- st.markdown("---")
234
- st.markdown("### 🎯 Hasil Prediksi")
235
-
236
- if st.button("🚀 Mulai Analisis Alergen", type="primary", use_container_width=True):
237
  if not input_text.strip():
238
- st.warning("⚠️ Mohon masukkan bahan makanan terlebih dahulu.")
239
  else:
240
- # Progress bar
241
- progress_bar = st.progress(0)
242
- status_text = st.empty()
243
-
244
- # Loading simulation
245
- status_text.text("🔄 Memuat vectorizer...")
246
- progress_bar.progress(25)
247
- vectorizer = load_vectorizer()
248
-
249
- status_text.text("🔄 Memuat model...")
250
- progress_bar.progress(50)
251
- model = load_model(model_name)
252
-
253
- status_text.text("🔄 Menganalisis bahan...")
254
- progress_bar.progress(75)
255
-
256
- if vectorizer is not None and model is not None:
257
  pred = predict_allergen(model, vectorizer, input_text)
258
- progress_bar.progress(100)
259
- status_text.text("✅ Analisis selesai!")
260
-
261
- time.sleep(0.5)
262
- progress_bar.empty()
263
- status_text.empty()
264
-
265
- # Results display
266
  labels = ['Susu', 'Kacang', 'Telur', 'Makanan Laut', 'Gandum']
267
- allergen_icons = ['🥛', '🥜', '🥚', '🦐', '🌾']
268
  results = dict(zip(labels, pred))
269
-
270
- # Statistics
271
- total_allergens = len(labels)
272
- detected_allergens = sum(pred)
273
- safe_allergens = total_allergens - detected_allergens
274
-
275
- col1, col2, col3 = st.columns(3)
276
- with col1:
277
- st.markdown(f"""
278
- <div class="stat-box">
279
- <div class="stat-number">{total_allergens}</div>
280
- <div class="stat-label">Total Dianalisis</div>
281
- </div>
282
- """, unsafe_allow_html=True)
283
-
284
- with col2:
285
- st.markdown(f"""
286
- <div class="stat-box">
287
- <div class="stat-number" style="color: #FF4444;">{detected_allergens}</div>
288
- <div class="stat-label">Terdeteksi</div>
289
- </div>
290
- """, unsafe_allow_html=True)
291
-
292
- with col3:
293
- st.markdown(f"""
294
- <div class="stat-box">
295
- <div class="stat-number" style="color: #44AA44;">{safe_allergens}</div>
296
- <div class="stat-label">Aman</div>
297
- </div>
298
- """, unsafe_allow_html=True)
299
-
300
- st.markdown("#### 📋 Detail Hasil:")
301
-
302
- # Results cards
303
- for i, (allergen, status) in enumerate(results.items()):
304
- icon = allergen_icons[i]
305
- if status == 1:
306
- st.markdown(f"""
307
- <div class="allergen-card allergen-positive">
308
- <h4>⚠️ {icon} {allergen}</h4>
309
- <p><strong>Status:</strong> <span style="color: #FF4444;">TERDETEKSI</span></p>
310
- <p>Makanan ini kemungkinan mengandung alergen {allergen.lower()}. Harap berhati-hati jika Anda memiliki alergi terhadap {allergen.lower()}.</p>
311
- </div>
312
- """, unsafe_allow_html=True)
313
- else:
314
- st.markdown(f"""
315
- <div class="allergen-card allergen-negative">
316
- <h4>✅ {icon} {allergen}</h4>
317
- <p><strong>Status:</strong> <span style="color: #44AA44;">AMAN</span></p>
318
- <p>Tidak terdeteksi kandungan alergen {allergen.lower()} dalam bahan yang dianalisis.</p>
319
- </div>
320
- """, unsafe_allow_html=True)
321
-
322
- # Disclaimer
323
- st.markdown("---")
324
- st.warning("""
325
- ⚠️ **Disclaimer**: Hasil prediksi ini bersifat estimasi berdasarkan model machine learning.
326
- Untuk kepastian yang akurat, selalu konsultasikan dengan ahli gizi atau baca label produk dengan teliti.
327
- """)
328
 
329
- # === Footer ===
330
- st.markdown("---")
331
- st.markdown("""
332
- <div style="text-align: center; color: #666; padding: 2rem;">
333
- <p>🛡️ <strong>AllergenGuard</strong> - Melindungi Anda dari Alergen Makanan</p>
334
- <p>Dikembangkan dengan ❤️ menggunakan Streamlit dan Machine Learning</p>
335
- </div>
336
- """, unsafe_allow_html=True)
 
2
  import pickle
3
  import requests
4
  from bs4 import BeautifulSoup
 
5
 
6
+ # === Load TF-IDF Vectorizer ===
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  @st.cache_resource
8
  def load_vectorizer():
9
+ with open("saved_models/tfidf_vectorizer.pkl", "rb") as f:
10
+ return pickle.load(f)
 
 
 
 
11
 
12
+ # === Load Model Berdasarkan Nama ===
13
  @st.cache_resource
14
  def load_model(model_name):
15
+ model_path = f"saved_models/{model_name}_model.pkl"
16
+ with open(model_path, "rb") as f:
17
+ return pickle.load(f)
 
 
 
 
18
 
19
+ # === Prediksi ===
20
  def predict_allergen(model, vectorizer, input_text):
 
 
21
  X_input = vectorizer.transform([input_text])
22
  prediction = model.predict(X_input)
23
  return prediction[0]
24
 
25
+ # === Scraping bahan dari Cookpad ===
26
  def get_ingredients_from_cookpad(url):
27
+ headers = {"User-Agent": "Mozilla/5.0"}
28
  try:
29
+ response = requests.get(url, headers=headers)
30
  if response.status_code != 200:
31
+ return None, "Gagal mengambil halaman."
 
32
  soup = BeautifulSoup(response.text, "html.parser")
33
  ingredient_div = soup.find("div", class_="ingredient-list")
 
34
  if not ingredient_div:
35
+ return None, "Tidak menemukan elemen bahan."
 
36
  ingredients = [item.get_text(strip=True) for item in ingredient_div.find_all("li")]
37
  return ingredients, None
 
 
38
  except Exception as e:
39
  return None, f"Terjadi kesalahan: {str(e)}"
40
 
41
+ # === Streamlit UI ===
42
+ st.set_page_config(page_title="Deteksi Alergen Makanan", layout="centered")
43
+ st.title("🥘 Deteksi Alergen Makanan dari Resep")
44
+ st.markdown("Masukkan bahan makanan atau link Cookpad untuk mendeteksi kemungkinan kandungan alergen.")
45
+
46
+ # Pilihan input: manual atau dari URL
47
+ input_mode = st.radio("Pilih metode input:", ["Manual", "Cookpad URL"])
48
+
49
+ # Input berdasarkan mode
50
+ input_text = ""
51
+ if input_mode == "Manual":
52
+ input_text = st.text_area("📝 Masukkan bahan makanan (contoh: bubuk cabai, tepung gandum, telur ayam)", height=150)
53
+ elif input_mode == "Cookpad URL":
54
+ url = st.text_input("🔗 Masukkan URL resep dari Cookpad", placeholder="https://cookpad.com/id/resep/...")
55
+ if url:
56
+ with st.spinner("Mengambil data dari Cookpad..."):
57
+ ingredients, error = get_ingredients_from_cookpad(url)
58
+ if error:
59
+ st.error(error)
60
+ else:
61
+ st.success("Berhasil mengambil bahan-bahan:")
62
+ for item in ingredients:
63
+ st.markdown(f"- {item}")
64
+ input_text = " ".join(ingredients)
65
+
66
+ # Pilih model
67
+ model_name = st.selectbox("🤖 Pilih Model", ("XGBoost", "KNN", "Random Forest"))
68
+
69
+ # Tombol prediksi
70
+ if st.button("🔍 Prediksi Alergen"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  if not input_text.strip():
72
+ st.warning("Mohon masukkan atau ambil bahan makanan terlebih dahulu.")
73
  else:
74
+ with st.spinner("Sedang memuat model dan memproses prediksi..."):
75
+ vectorizer = load_vectorizer()
76
+ model = load_model(model_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  pred = predict_allergen(model, vectorizer, input_text)
 
 
 
 
 
 
 
 
78
  labels = ['Susu', 'Kacang', 'Telur', 'Makanan Laut', 'Gandum']
 
79
  results = dict(zip(labels, pred))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
+ st.success("✅ Hasil Prediksi:")
82
+ for allergen, status in results.items():
83
+ if status == 1:
84
+ st.error(f"⚠️ Mengandung {allergen}")
85
+ else:
86
+ st.info(f"Tidak mengandung {allergen}")