import streamlit as st
import pickle
import requests
from bs4 import BeautifulSoup
# === Custom CSS for better styling ===
def load_css():
st.markdown("""
""", 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'
๐จ {allergen}
', 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'โ {allergen}
', unsafe_allow_html=True)
# Show summary
if not positive_results:
st.markdown('๐ Tidak ada alergen berbahaya terdeteksi!
', 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("""
๐ฅ Deteksi Alergen Makanan
Analisis kandungan alergen dalam resep makanan dengan teknologi AI
""", 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("""
๐ก Petunjuk: Masukkan daftar bahan makanan yang ingin dianalisis.
Pisahkan setiap bahan dengan koma atau baris baru.
""", 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("""
๐ก Petunjuk: Masukkan hingga 20 URL resep dari Cookpad.
Setiap URL harus dalam baris terpisah.
""", 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'Memproses resep {i+1} dari {len(urls)}
', 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 single nice container
ingredients_text = ", ".join(ingredients)
st.markdown(f'''
๐งพ Daftar Bahan:
{ingredients_text}
''', 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("""
""", unsafe_allow_html=True)
if __name__ == "__main__":
main()