allergen-detector / backup.py
rdsarjito
21
f0c0e0f
import streamlit as st
import pickle
import requests
from bs4 import BeautifulSoup
# === Custom CSS for better styling ===
def load_css():
st.markdown("""
<style>
/* Main app styling */
.main-header {
text-align: center;
padding: 2rem 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
margin-bottom: 2rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.main-header h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.main-header p {
font-size: 1.1rem;
opacity: 0.9;
margin: 0;
}
/* Card styling */
.info-card {
background: white;
padding: 1.5rem;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-left: 4px solid #667eea;
margin: 1rem 0;
}
/* Results styling */
.result-positive {
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
color: white;
padding: 1rem;
border-radius: 8px;
margin: 0.5rem 0;
box-shadow: 0 2px 4px rgba(255, 107, 107, 0.3);
}
.result-negative {
background: linear-gradient(135deg, #51cf66, #69db7c);
color: white;
padding: 1rem;
border-radius: 8px;
margin: 0.5rem 0;
box-shadow: 0 2px 4px rgba(81, 207, 102, 0.3);
}
/* Button styling */
.stButton > button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 25px;
padding: 0.75rem 2rem;
font-weight: bold;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.stButton > button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
}
/* Radio button styling */
.stRadio > div {
background: white;
padding: 1rem;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Text area styling */
.stTextArea > div > div > textarea {
border-radius: 10px;
border: 2px solid #e0e0e0;
transition: border-color 0.3s ease;
}
.stTextArea > div > div > textarea:focus {
border-color: #667eea;
box-shadow: 0 0 10px rgba(102, 126, 234, 0.2);
}
/* Expander styling */
.streamlit-expanderHeader {
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 10px;
border: 1px solid #dee2e6;
}
/* Progress indicator */
.progress-text {
text-align: center;
font-weight: bold;
color: #667eea;
margin: 1rem 0;
}
/* Ingredient list styling */
.ingredient-item {
background: #f8f9fa;
padding: 0.5rem 1rem;
margin: 0.25rem 0;
border-radius: 20px;
border-left: 3px solid #667eea;
}
/* Footer */
.footer {
text-align: center;
padding: 2rem 0;
color: #6c757d;
border-top: 1px solid #e9ecef;
margin-top: 3rem;
}
</style>
""", unsafe_allow_html=True)
# === Load TF-IDF Vectorizer ===
@st.cache_resource
def load_vectorizer():
with open("saved_models/tfidf_vectorizer.pkl", "rb") as f:
return pickle.load(f)
# === Load XGBoost Model ===
@st.cache_resource
def load_model():
with open("saved_models/XGBoost_model.pkl", "rb") as f:
return pickle.load(f)
# === Prediksi ===
def predict_allergen(model, vectorizer, input_text):
X_input = vectorizer.transform([input_text])
prediction = model.predict(X_input)
return prediction[0]
# === Scraping bahan dari Cookpad ===
def get_ingredients_from_cookpad(url):
headers = {"User-Agent": "Mozilla/5.0"}
try:
response = requests.get(url, headers=headers)
if response.status_code != 200:
return None, "Gagal mengambil halaman."
soup = BeautifulSoup(response.text, "html.parser")
ingredient_div = soup.find("div", class_="ingredient-list")
if not ingredient_div:
return None, "Tidak menemukan elemen bahan."
ingredients = []
for item in ingredient_div.find_all("li"):
amount = item.find("bdi")
name = item.find("span")
if amount and name:
ingredients.append(f"{amount.get_text(strip=True)} {name.get_text(strip=True)}")
else:
ingredients.append(item.get_text(strip=True))
return ingredients, None
except Exception as e:
return None, f"Terjadi kesalahan: {str(e)}"
# === Display results with custom styling ===
def display_results(results):
st.markdown("### 🎯 Hasil Analisis Alergen")
positive_results = []
negative_results = []
for allergen, status in results.items():
if status == 1:
positive_results.append(allergen)
else:
negative_results.append(allergen)
# Display positive results (allergens detected)
if positive_results:
st.markdown("#### ⚠️ **Alergen Terdeteksi:**")
for allergen in positive_results:
st.markdown(f'<div class="result-positive">🚨 <strong>{allergen}</strong></div>', unsafe_allow_html=True)
# Display negative results (safe allergens)
if negative_results:
st.markdown("#### βœ… **Aman dari Alergen:**")
for allergen in negative_results:
st.markdown(f'<div class="result-negative">βœ“ {allergen}</div>', unsafe_allow_html=True)
# Show summary
if not positive_results:
st.markdown('<div class="result-negative">πŸŽ‰ <strong>Tidak ada alergen berbahaya terdeteksi!</strong></div>', unsafe_allow_html=True)
# === Main UI ===
def main():
st.set_page_config(
page_title="Deteksi Alergen Makanan",
page_icon="πŸ₯˜",
layout="wide",
initial_sidebar_state="expanded"
)
# Load custom CSS
load_css()
# Header
st.markdown("""
<div class="main-header">
<h1>πŸ₯˜ Deteksi Alergen Makanan</h1>
<p>Analisis kandungan alergen dalam resep makanan dengan teknologi AI</p>
</div>
""", unsafe_allow_html=True)
# Sidebar info
with st.sidebar:
st.markdown("### πŸ“‹ Informasi Alergen")
st.markdown("""
**Alergen yang dapat dideteksi:**
- πŸ₯› Susu
- πŸ₯œ Kacang
- πŸ₯š Telur
- 🦐 Makanan Laut
- 🌾 Gandum
""")
st.markdown("### πŸ’‘ Tips Penggunaan")
st.markdown("""
- Masukkan bahan dengan detail
- Gunakan nama bahan dalam bahasa Indonesia
- Untuk URL Cookpad, pastikan link valid
- Maksimal 20 URL per analisis
""")
# Main content
col1, col2, col3 = st.columns([1, 6, 1])
with col2:
# Input method selection
st.markdown("### πŸ”§ Pilih Metode Input")
input_mode = st.radio(
"",
["πŸ“ Input Manual", "πŸ”— URL Cookpad"],
horizontal=True
)
# Load model components
try:
vectorizer = load_vectorizer()
model = load_model()
labels = ['Susu', 'Kacang', 'Telur', 'Makanan Laut', 'Gandum']
except Exception as e:
st.error(f"❌ Gagal memuat model: {str(e)}")
st.stop()
st.markdown("---")
if input_mode == "πŸ“ Input Manual":
st.markdown("### πŸ“ Masukkan Bahan Makanan")
# Info card
st.markdown("""
<div class="info-card">
<strong>πŸ’‘ Petunjuk:</strong> Masukkan daftar bahan makanan yang ingin dianalisis.
Pisahkan setiap bahan dengan koma atau baris baru.
</div>
""", unsafe_allow_html=True)
input_text = st.text_area(
"",
height=150,
placeholder="Contoh: telur, susu, tepung terigu, garam, mentega..."
)
col_btn1, col_btn2, col_btn3 = st.columns([2, 2, 2])
with col_btn2:
if st.button("πŸ” Analisis Alergen", use_container_width=True):
if not input_text.strip():
st.warning("⚠️ Mohon masukkan bahan makanan terlebih dahulu.")
else:
with st.spinner("πŸ”„ Sedang menganalisis..."):
pred = predict_allergen(model, vectorizer, input_text)
results = dict(zip(labels, pred))
st.success("βœ… Analisis selesai!")
display_results(results)
elif input_mode == "πŸ”— URL Cookpad":
st.markdown("### πŸ”— Analisis dari URL Cookpad")
# Info card
st.markdown("""
<div class="info-card">
<strong>πŸ’‘ Petunjuk:</strong> Masukkan hingga 20 URL resep dari Cookpad.
Setiap URL harus dalam baris terpisah.
</div>
""", unsafe_allow_html=True)
urls_input = st.text_area(
"",
placeholder="https://cookpad.com/id/resep/...\nhttps://cookpad.com/id/resep/...",
height=200
)
urls = [url.strip() for url in urls_input.splitlines() if url.strip()]
if len(urls) > 20:
st.warning("⚠️ Maksimal hanya bisa memproses 20 URL. Menggunakan 20 URL pertama.")
urls = urls[:20]
if urls:
st.info(f"πŸ“Š Siap memproses {len(urls)} URL")
if st.button("πŸ” Analisis dari URL", use_container_width=True):
if not urls:
st.warning("⚠️ Mohon masukkan minimal satu URL.")
else:
# Progress bar
progress_bar = st.progress(0)
status_text = st.empty()
for i, url in enumerate(urls):
# Update progress
progress = (i + 1) / len(urls)
progress_bar.progress(progress)
status_text.markdown(f'<div class="progress-text">Memproses resep {i+1} dari {len(urls)}</div>', unsafe_allow_html=True)
ingredients, error = get_ingredients_from_cookpad(url)
with st.expander(f"πŸ“– Resep #{i+1}", expanded=False):
st.markdown(f"**URL:** {url}")
if error:
st.error(f"❌ {error}")
else:
st.success("βœ… Bahan berhasil diambil!")
# Display ingredients in a nice format
st.markdown("**🧾 Daftar Bahan:**")
for ing in ingredients:
st.markdown(f'<div class="ingredient-item">β€’ {ing}</div>', unsafe_allow_html=True)
# Predict allergens
joined_ingredients = " ".join(ingredients)
pred = predict_allergen(model, vectorizer, joined_ingredients)
results = dict(zip(labels, pred))
st.markdown("---")
display_results(results)
# Clear progress indicators
progress_bar.empty()
status_text.empty()
st.success("πŸŽ‰ Semua resep telah dianalisis!")
# Footer
st.markdown("""
<div class="footer">
<p>πŸ”¬ Powered by XGBoost & TF-IDF | Made with ❀️ using Streamlit</p>
</div>
""", unsafe_allow_html=True)
if __name__ == "__main__":
main()