rdsarjito commited on
Commit
a3ac587
Β·
1 Parent(s): d4ad6f4
Files changed (1) hide show
  1. app.py +323 -61
app.py CHANGED
@@ -3,6 +3,134 @@ import pickle
3
  import requests
4
  from bs4 import BeautifulSoup
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  # === Load TF-IDF Vectorizer ===
7
  @st.cache_resource
8
  def load_vectorizer():
@@ -46,69 +174,203 @@ def get_ingredients_from_cookpad(url):
46
  except Exception as e:
47
  return None, f"Terjadi kesalahan: {str(e)}"
48
 
49
- # === UI Streamlit ===
50
- st.set_page_config(page_title="Deteksi Alergen Makanan", layout="centered")
51
- st.title("πŸ₯˜ Deteksi Alergen Makanan dari Resep")
52
- st.markdown("Masukkan bahan makanan secara manual atau hingga 20 link Cookpad untuk mendeteksi kemungkinan kandungan alergen.")
53
-
54
- # === Pilihan input ===
55
- input_mode = st.radio("Pilih metode input:", ["Manual", "Cookpad URL"])
56
- vectorizer = load_vectorizer()
57
- model = load_model()
58
- labels = ['Susu', 'Kacang', 'Telur', 'Makanan Laut', 'Gandum']
59
-
60
- if input_mode == "Manual":
61
- input_text = st.text_area("πŸ“ Masukkan bahan makanan", height=150)
62
- if st.button("πŸ” Prediksi Alergen"):
63
- if not input_text.strip():
64
- st.warning("Mohon masukkan bahan makanan terlebih dahulu.")
65
  else:
66
- with st.spinner("Memproses prediksi..."):
67
- pred = predict_allergen(model, vectorizer, input_text)
68
- results = dict(zip(labels, pred))
69
-
70
- st.success("βœ… Hasil Prediksi:")
71
- for allergen, status in results.items():
72
- if status == 1:
73
- st.error(f"⚠️ Mengandung {allergen}")
74
- else:
75
- st.info(f"Tidak mengandung {allergen}")
76
-
77
- elif input_mode == "Cookpad URL":
78
- urls_input = st.text_area(
79
- "πŸ”— Masukkan hingga 20 URL resep dari Cookpad (satu per baris)",
80
- placeholder="https://cookpad.com/id/resep/...\nhttps://cookpad.com/id/resep/...",
81
- height=200
82
- )
83
-
84
- urls = [url.strip() for url in urls_input.splitlines() if url.strip()]
85
- if len(urls) > 20:
86
- st.warning("⚠️ Maksimal hanya bisa memproses 20 URL.")
87
- urls = urls[:20]
88
-
89
- if st.button("πŸ” Prediksi Alergen dari URL"):
90
- if not urls:
91
- st.warning("Mohon masukkan minimal satu URL.")
92
  else:
93
- for i, url in enumerate(urls):
94
- with st.spinner(f"Memproses URL ke-{i+1}: {url}"):
95
- ingredients, error = get_ingredients_from_cookpad(url)
 
 
 
 
96
 
97
- with st.expander(f"Resep #{i+1}: {url}"):
98
- if error:
99
- st.error(f"❌ Gagal mengambil data: {error}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  else:
101
- st.success("βœ… Bahan berhasil diambil:")
102
- for ing in ingredients:
103
- st.markdown(f"- {ing}")
104
-
105
- joined_ingredients = " ".join(ingredients)
106
- pred = predict_allergen(model, vectorizer, joined_ingredients)
107
- results = dict(zip(labels, pred))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
- st.subheader("πŸ§ͺ Hasil Prediksi Alergen:")
110
- for allergen, status in results.items():
111
- if status == 1:
112
- st.error(f"⚠️ Mengandung {allergen}")
113
- else:
114
- st.info(f"Tidak mengandung {allergen}")
 
3
  import requests
4
  from bs4 import BeautifulSoup
5
 
6
+ # === Custom CSS for better styling ===
7
+ def load_css():
8
+ st.markdown("""
9
+ <style>
10
+ /* Main app styling */
11
+ .main-header {
12
+ text-align: center;
13
+ padding: 2rem 0;
14
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
15
+ color: white;
16
+ border-radius: 10px;
17
+ margin-bottom: 2rem;
18
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
19
+ }
20
+
21
+ .main-header h1 {
22
+ font-size: 2.5rem;
23
+ margin-bottom: 0.5rem;
24
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
25
+ }
26
+
27
+ .main-header p {
28
+ font-size: 1.1rem;
29
+ opacity: 0.9;
30
+ margin: 0;
31
+ }
32
+
33
+ /* Card styling */
34
+ .info-card {
35
+ background: white;
36
+ padding: 1.5rem;
37
+ border-radius: 10px;
38
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
39
+ border-left: 4px solid #667eea;
40
+ margin: 1rem 0;
41
+ }
42
+
43
+ /* Results styling */
44
+ .result-positive {
45
+ background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
46
+ color: white;
47
+ padding: 1rem;
48
+ border-radius: 8px;
49
+ margin: 0.5rem 0;
50
+ box-shadow: 0 2px 4px rgba(255, 107, 107, 0.3);
51
+ }
52
+
53
+ .result-negative {
54
+ background: linear-gradient(135deg, #51cf66, #69db7c);
55
+ color: white;
56
+ padding: 1rem;
57
+ border-radius: 8px;
58
+ margin: 0.5rem 0;
59
+ box-shadow: 0 2px 4px rgba(81, 207, 102, 0.3);
60
+ }
61
+
62
+ /* Button styling */
63
+ .stButton > button {
64
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
65
+ color: white;
66
+ border: none;
67
+ border-radius: 25px;
68
+ padding: 0.75rem 2rem;
69
+ font-weight: bold;
70
+ transition: all 0.3s ease;
71
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
72
+ }
73
+
74
+ .stButton > button:hover {
75
+ transform: translateY(-2px);
76
+ box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
77
+ }
78
+
79
+ /* Radio button styling */
80
+ .stRadio > div {
81
+ background: white;
82
+ padding: 1rem;
83
+ border-radius: 10px;
84
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
85
+ }
86
+
87
+ /* Text area styling */
88
+ .stTextArea > div > div > textarea {
89
+ border-radius: 10px;
90
+ border: 2px solid #e0e0e0;
91
+ transition: border-color 0.3s ease;
92
+ }
93
+
94
+ .stTextArea > div > div > textarea:focus {
95
+ border-color: #667eea;
96
+ box-shadow: 0 0 10px rgba(102, 126, 234, 0.2);
97
+ }
98
+
99
+ /* Expander styling */
100
+ .streamlit-expanderHeader {
101
+ background: linear-gradient(135deg, #f8f9fa, #e9ecef);
102
+ border-radius: 10px;
103
+ border: 1px solid #dee2e6;
104
+ }
105
+
106
+ /* Progress indicator */
107
+ .progress-text {
108
+ text-align: center;
109
+ font-weight: bold;
110
+ color: #667eea;
111
+ margin: 1rem 0;
112
+ }
113
+
114
+ /* Ingredient list styling */
115
+ .ingredient-item {
116
+ background: #f8f9fa;
117
+ padding: 0.5rem 1rem;
118
+ margin: 0.25rem 0;
119
+ border-radius: 20px;
120
+ border-left: 3px solid #667eea;
121
+ }
122
+
123
+ /* Footer */
124
+ .footer {
125
+ text-align: center;
126
+ padding: 2rem 0;
127
+ color: #6c757d;
128
+ border-top: 1px solid #e9ecef;
129
+ margin-top: 3rem;
130
+ }
131
+ </style>
132
+ """, unsafe_allow_html=True)
133
+
134
  # === Load TF-IDF Vectorizer ===
135
  @st.cache_resource
136
  def load_vectorizer():
 
174
  except Exception as e:
175
  return None, f"Terjadi kesalahan: {str(e)}"
176
 
177
+ # === Display results with custom styling ===
178
+ def display_results(results):
179
+ st.markdown("### 🎯 Hasil Analisis Alergen")
180
+
181
+ col1, col2 = st.columns(2)
182
+
183
+ positive_results = []
184
+ negative_results = []
185
+
186
+ for allergen, status in results.items():
187
+ if status == 1:
188
+ positive_results.append(allergen)
 
 
 
 
189
  else:
190
+ negative_results.append(allergen)
191
+
192
+ with col1:
193
+ if positive_results:
194
+ st.markdown("#### ⚠️ **Mengandung Alergen:**")
195
+ for allergen in positive_results:
196
+ st.markdown(f'<div class="result-positive">🚨 <strong>{allergen}</strong></div>', unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  else:
198
+ st.markdown('<div class="result-negative">βœ… <strong>Tidak ada alergen terdeteksi!</strong></div>', unsafe_allow_html=True)
199
+
200
+ with col2:
201
+ if negative_results:
202
+ st.markdown("#### βœ… **Aman dari:**")
203
+ for allergen in negative_results:
204
+ st.markdown(f'<div class="result-negative">βœ“ {allergen}</div>', unsafe_allow_html=True)
205
 
206
+ # === Main UI ===
207
+ def main():
208
+ st.set_page_config(
209
+ page_title="Deteksi Alergen Makanan",
210
+ page_icon="πŸ₯˜",
211
+ layout="wide",
212
+ initial_sidebar_state="expanded"
213
+ )
214
+
215
+ # Load custom CSS
216
+ load_css()
217
+
218
+ # Header
219
+ st.markdown("""
220
+ <div class="main-header">
221
+ <h1>πŸ₯˜ Deteksi Alergen Makanan</h1>
222
+ <p>Analisis kandungan alergen dalam resep makanan dengan teknologi AI</p>
223
+ </div>
224
+ """, unsafe_allow_html=True)
225
+
226
+ # Sidebar info
227
+ with st.sidebar:
228
+ st.markdown("### πŸ“‹ Informasi Alergen")
229
+ st.markdown("""
230
+ **Alergen yang dapat dideteksi:**
231
+ - πŸ₯› Susu
232
+ - πŸ₯œ Kacang
233
+ - πŸ₯š Telur
234
+ - 🦐 Makanan Laut
235
+ - 🌾 Gandum
236
+ """)
237
+
238
+ st.markdown("### πŸ’‘ Tips Penggunaan")
239
+ st.markdown("""
240
+ - Masukkan bahan dengan detail
241
+ - Gunakan nama bahan dalam bahasa Indonesia
242
+ - Untuk URL Cookpad, pastikan link valid
243
+ - Maksimal 20 URL per analisis
244
+ """)
245
+
246
+ # Main content
247
+ col1, col2, col3 = st.columns([1, 6, 1])
248
+
249
+ with col2:
250
+ # Input method selection
251
+ st.markdown("### πŸ”§ Pilih Metode Input")
252
+ input_mode = st.radio(
253
+ "",
254
+ ["πŸ“ Input Manual", "πŸ”— URL Cookpad"],
255
+ horizontal=True
256
+ )
257
+
258
+ # Load model components
259
+ try:
260
+ vectorizer = load_vectorizer()
261
+ model = load_model()
262
+ labels = ['Susu', 'Kacang', 'Telur', 'Makanan Laut', 'Gandum']
263
+ except Exception as e:
264
+ st.error(f"❌ Gagal memuat model: {str(e)}")
265
+ st.stop()
266
+
267
+ st.markdown("---")
268
+
269
+ if input_mode == "πŸ“ Input Manual":
270
+ st.markdown("### πŸ“ Masukkan Bahan Makanan")
271
+
272
+ # Info card
273
+ st.markdown("""
274
+ <div class="info-card">
275
+ <strong>πŸ’‘ Petunjuk:</strong> Masukkan daftar bahan makanan yang ingin dianalisis.
276
+ Pisahkan setiap bahan dengan koma atau baris baru.
277
+ </div>
278
+ """, unsafe_allow_html=True)
279
+
280
+ input_text = st.text_area(
281
+ "",
282
+ height=150,
283
+ placeholder="Contoh: telur, susu, tepung terigu, garam, mentega..."
284
+ )
285
+
286
+ col_btn1, col_btn2, col_btn3 = st.columns([2, 2, 2])
287
+ with col_btn2:
288
+ if st.button("πŸ” Analisis Alergen", use_container_width=True):
289
+ if not input_text.strip():
290
+ st.warning("⚠️ Mohon masukkan bahan makanan terlebih dahulu.")
291
  else:
292
+ with st.spinner("πŸ”„ Sedang menganalisis..."):
293
+ pred = predict_allergen(model, vectorizer, input_text)
294
+ results = dict(zip(labels, pred))
295
+
296
+ st.success("βœ… Analisis selesai!")
297
+ display_results(results)
298
+
299
+ elif input_mode == "πŸ”— URL Cookpad":
300
+ st.markdown("### πŸ”— Analisis dari URL Cookpad")
301
+
302
+ # Info card
303
+ st.markdown("""
304
+ <div class="info-card">
305
+ <strong>πŸ’‘ Petunjuk:</strong> Masukkan hingga 20 URL resep dari Cookpad.
306
+ Setiap URL harus dalam baris terpisah.
307
+ </div>
308
+ """, unsafe_allow_html=True)
309
+
310
+ urls_input = st.text_area(
311
+ "",
312
+ placeholder="https://cookpad.com/id/resep/...\nhttps://cookpad.com/id/resep/...",
313
+ height=200
314
+ )
315
+
316
+ urls = [url.strip() for url in urls_input.splitlines() if url.strip()]
317
+ if len(urls) > 20:
318
+ st.warning("⚠️ Maksimal hanya bisa memproses 20 URL. Menggunakan 20 URL pertama.")
319
+ urls = urls[:20]
320
+
321
+ if urls:
322
+ st.info(f"πŸ“Š Siap memproses {len(urls)} URL")
323
+
324
+ col_btn1, col_btn2, col_btn3 = st.columns([2, 2, 2])
325
+ with col_btn2:
326
+ if st.button("πŸ” Analisis dari URL", use_container_width=True):
327
+ if not urls:
328
+ st.warning("⚠️ Mohon masukkan minimal satu URL.")
329
+ else:
330
+ # Progress bar
331
+ progress_bar = st.progress(0)
332
+ status_text = st.empty()
333
+
334
+ for i, url in enumerate(urls):
335
+ # Update progress
336
+ progress = (i + 1) / len(urls)
337
+ progress_bar.progress(progress)
338
+ status_text.markdown(f'<div class="progress-text">Memproses resep {i+1} dari {len(urls)}</div>', unsafe_allow_html=True)
339
+
340
+ ingredients, error = get_ingredients_from_cookpad(url)
341
+
342
+ with st.expander(f"πŸ“– Resep #{i+1}", expanded=False):
343
+ st.markdown(f"**URL:** {url}")
344
+
345
+ if error:
346
+ st.error(f"❌ {error}")
347
+ else:
348
+ st.success("βœ… Bahan berhasil diambil!")
349
+
350
+ # Display ingredients in a nice format
351
+ st.markdown("**🧾 Daftar Bahan:**")
352
+ for ing in ingredients:
353
+ st.markdown(f'<div class="ingredient-item">β€’ {ing}</div>', unsafe_allow_html=True)
354
+
355
+ # Predict allergens
356
+ joined_ingredients = " ".join(ingredients)
357
+ pred = predict_allergen(model, vectorizer, joined_ingredients)
358
+ results = dict(zip(labels, pred))
359
+
360
+ st.markdown("---")
361
+ display_results(results)
362
+
363
+ # Clear progress indicators
364
+ progress_bar.empty()
365
+ status_text.empty()
366
+ st.success("πŸŽ‰ Semua resep telah dianalisis!")
367
+
368
+ # Footer
369
+ st.markdown("""
370
+ <div class="footer">
371
+ <p>πŸ”¬ Powered by XGBoost & TF-IDF | Made with ❀️ using Streamlit</p>
372
+ </div>
373
+ """, unsafe_allow_html=True)
374
 
375
+ if __name__ == "__main__":
376
+ main()