Spaces:
Sleeping
Sleeping
rdsarjito
commited on
Commit
Β·
16d78bc
1
Parent(s):
39e7123
Update kode untuk fitur baru
Browse files
app.py
CHANGED
|
@@ -2,6 +2,10 @@ import streamlit as st
|
|
| 2 |
import pickle
|
| 3 |
import requests
|
| 4 |
from bs4 import BeautifulSoup
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
# === Custom CSS for better styling ===
|
| 7 |
def load_css():
|
|
@@ -40,6 +44,16 @@ def load_css():
|
|
| 40 |
margin: 1rem 0;
|
| 41 |
}
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
/* Results styling - matching the image design */
|
| 44 |
.allergen-result {
|
| 45 |
padding: 1rem 1.5rem;
|
|
@@ -75,6 +89,23 @@ def load_css():
|
|
| 75 |
text-align: center;
|
| 76 |
}
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
/* Button styling */
|
| 79 |
.stButton > button {
|
| 80 |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
@@ -92,6 +123,11 @@ def load_css():
|
|
| 92 |
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
|
| 93 |
}
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
/* Radio button styling */
|
| 96 |
.stRadio > div {
|
| 97 |
background: white;
|
|
@@ -155,6 +191,17 @@ def load_css():
|
|
| 155 |
</style>
|
| 156 |
""", unsafe_allow_html=True)
|
| 157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
# === Load TF-IDF Vectorizer ===
|
| 159 |
@st.cache_resource
|
| 160 |
def load_vectorizer():
|
|
@@ -167,6 +214,37 @@ def load_model():
|
|
| 167 |
with open("saved_models/XGBoost_model.pkl", "rb") as f:
|
| 168 |
return pickle.load(f)
|
| 169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
# === Prediksi ===
|
| 171 |
def predict_allergen(model, vectorizer, input_text):
|
| 172 |
X_input = vectorizer.transform([input_text])
|
|
@@ -228,6 +306,27 @@ def get_ingredients_from_cookpad(url):
|
|
| 228 |
except Exception as e:
|
| 229 |
return None, f"Terjadi kesalahan: {str(e)}"
|
| 230 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
# === Display results with custom styling matching the image ===
|
| 232 |
def display_results(results, probabilities, labels):
|
| 233 |
st.markdown("### π― Hasil Analisis Alergen")
|
|
@@ -307,7 +406,7 @@ def main():
|
|
| 307 |
st.markdown("""
|
| 308 |
<div class="main-header">
|
| 309 |
<h1>π₯ Deteksi Alergen Makanan</h1>
|
| 310 |
-
<p>Analisis kandungan alergen dalam resep makanan dengan teknologi AI</p>
|
| 311 |
</div>
|
| 312 |
""", unsafe_allow_html=True)
|
| 313 |
|
|
@@ -325,9 +424,17 @@ def main():
|
|
| 325 |
|
| 326 |
st.markdown("### π‘ Tips Penggunaan")
|
| 327 |
st.markdown("""
|
|
|
|
| 328 |
- Masukkan bahan dengan detail
|
| 329 |
- Gunakan nama bahan dalam bahasa Indonesia
|
| 330 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
- Maksimal 20 URL per analisis
|
| 332 |
""")
|
| 333 |
|
|
@@ -339,7 +446,7 @@ def main():
|
|
| 339 |
st.markdown("### π§ Pilih Metode Input")
|
| 340 |
input_mode = st.radio(
|
| 341 |
"",
|
| 342 |
-
["π Input Manual", "π URL Cookpad"],
|
| 343 |
horizontal=True
|
| 344 |
)
|
| 345 |
|
|
@@ -384,6 +491,52 @@ def main():
|
|
| 384 |
st.success("β
Analisis selesai!")
|
| 385 |
display_results(results, probs, labels)
|
| 386 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
elif input_mode == "π URL Cookpad":
|
| 388 |
st.markdown("### π Analisis dari URL Cookpad")
|
| 389 |
|
|
@@ -458,7 +611,7 @@ def main():
|
|
| 458 |
# Footer
|
| 459 |
st.markdown("""
|
| 460 |
<div class="footer">
|
| 461 |
-
<p>π¬ Powered by XGBoost
|
| 462 |
</div>
|
| 463 |
""", unsafe_allow_html=True)
|
| 464 |
|
|
|
|
| 2 |
import pickle
|
| 3 |
import requests
|
| 4 |
from bs4 import BeautifulSoup
|
| 5 |
+
import easyocr
|
| 6 |
+
import numpy as np
|
| 7 |
+
from PIL import Image
|
| 8 |
+
import cv2
|
| 9 |
|
| 10 |
# === Custom CSS for better styling ===
|
| 11 |
def load_css():
|
|
|
|
| 44 |
margin: 1rem 0;
|
| 45 |
}
|
| 46 |
|
| 47 |
+
/* Camera card styling */
|
| 48 |
+
.camera-card {
|
| 49 |
+
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
|
| 50 |
+
padding: 1.5rem;
|
| 51 |
+
border-radius: 10px;
|
| 52 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 53 |
+
border-left: 4px solid #28a745;
|
| 54 |
+
margin: 1rem 0;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
/* Results styling - matching the image design */
|
| 58 |
.allergen-result {
|
| 59 |
padding: 1rem 1.5rem;
|
|
|
|
| 89 |
text-align: center;
|
| 90 |
}
|
| 91 |
|
| 92 |
+
/* OCR result styling */
|
| 93 |
+
.ocr-result {
|
| 94 |
+
background: #f8f9fa;
|
| 95 |
+
padding: 1rem 1.5rem;
|
| 96 |
+
margin: 1rem 0;
|
| 97 |
+
border-radius: 10px;
|
| 98 |
+
border-left: 4px solid #17a2b8;
|
| 99 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
| 100 |
+
line-height: 1.6;
|
| 101 |
+
font-size: 1rem;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.ocr-result strong {
|
| 105 |
+
color: #495057;
|
| 106 |
+
font-weight: 600;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
/* Button styling */
|
| 110 |
.stButton > button {
|
| 111 |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
| 123 |
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
|
| 124 |
}
|
| 125 |
|
| 126 |
+
/* Camera button styling */
|
| 127 |
+
.camera-button {
|
| 128 |
+
background: linear-gradient(135deg, #28a745 0%, #20c997 100%) !important;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
/* Radio button styling */
|
| 132 |
.stRadio > div {
|
| 133 |
background: white;
|
|
|
|
| 191 |
</style>
|
| 192 |
""", unsafe_allow_html=True)
|
| 193 |
|
| 194 |
+
# === Load EasyOCR Reader ===
|
| 195 |
+
@st.cache_resource
|
| 196 |
+
def load_ocr_reader():
|
| 197 |
+
"""Load EasyOCR reader with Indonesian and English language support"""
|
| 198 |
+
try:
|
| 199 |
+
reader = easyocr.Reader(['id', 'en'], gpu=False) # Indonesian and English
|
| 200 |
+
return reader
|
| 201 |
+
except Exception as e:
|
| 202 |
+
st.error(f"β Gagal memuat EasyOCR: {str(e)}")
|
| 203 |
+
return None
|
| 204 |
+
|
| 205 |
# === Load TF-IDF Vectorizer ===
|
| 206 |
@st.cache_resource
|
| 207 |
def load_vectorizer():
|
|
|
|
| 214 |
with open("saved_models/XGBoost_model.pkl", "rb") as f:
|
| 215 |
return pickle.load(f)
|
| 216 |
|
| 217 |
+
# === OCR Text Extraction ===
|
| 218 |
+
def extract_text_from_image(image, reader):
|
| 219 |
+
"""Extract text from image using EasyOCR"""
|
| 220 |
+
try:
|
| 221 |
+
# Convert PIL image to numpy array
|
| 222 |
+
if isinstance(image, Image.Image):
|
| 223 |
+
image_array = np.array(image)
|
| 224 |
+
else:
|
| 225 |
+
image_array = image
|
| 226 |
+
|
| 227 |
+
# Perform OCR
|
| 228 |
+
results = reader.readtext(image_array)
|
| 229 |
+
|
| 230 |
+
# Extract text from results
|
| 231 |
+
extracted_texts = []
|
| 232 |
+
confidence_scores = []
|
| 233 |
+
|
| 234 |
+
for (bbox, text, confidence) in results:
|
| 235 |
+
if confidence > 0.3: # Filter out low confidence text
|
| 236 |
+
extracted_texts.append(text)
|
| 237 |
+
confidence_scores.append(confidence)
|
| 238 |
+
|
| 239 |
+
# Join all extracted text
|
| 240 |
+
full_text = " ".join(extracted_texts)
|
| 241 |
+
avg_confidence = np.mean(confidence_scores) if confidence_scores else 0
|
| 242 |
+
|
| 243 |
+
return full_text, extracted_texts, avg_confidence
|
| 244 |
+
|
| 245 |
+
except Exception as e:
|
| 246 |
+
return "", [], 0
|
| 247 |
+
|
| 248 |
# === Prediksi ===
|
| 249 |
def predict_allergen(model, vectorizer, input_text):
|
| 250 |
X_input = vectorizer.transform([input_text])
|
|
|
|
| 306 |
except Exception as e:
|
| 307 |
return None, f"Terjadi kesalahan: {str(e)}"
|
| 308 |
|
| 309 |
+
# === Display OCR Results ===
|
| 310 |
+
def display_ocr_results(extracted_text, text_list, confidence):
|
| 311 |
+
"""Display OCR extraction results"""
|
| 312 |
+
st.markdown("### π Hasil Ekstraksi Teks")
|
| 313 |
+
|
| 314 |
+
if extracted_text.strip():
|
| 315 |
+
st.markdown(f'''
|
| 316 |
+
<div class="ocr-result">
|
| 317 |
+
<strong>π Teks yang Terdeteksi:</strong><br>
|
| 318 |
+
{extracted_text}
|
| 319 |
+
</div>
|
| 320 |
+
''', unsafe_allow_html=True)
|
| 321 |
+
|
| 322 |
+
# Show confidence and individual text elements
|
| 323 |
+
with st.expander(f"π Detail OCR (Confidence: {confidence:.2f})", expanded=False):
|
| 324 |
+
st.markdown("**Teks Individual yang Terdeteksi:**")
|
| 325 |
+
for i, text in enumerate(text_list, 1):
|
| 326 |
+
st.write(f"{i}. {text}")
|
| 327 |
+
else:
|
| 328 |
+
st.warning("β οΈ Tidak ada teks yang dapat diekstrak dari gambar. Pastikan gambar memiliki teks yang jelas dan terbaca.")
|
| 329 |
+
|
| 330 |
# === Display results with custom styling matching the image ===
|
| 331 |
def display_results(results, probabilities, labels):
|
| 332 |
st.markdown("### π― Hasil Analisis Alergen")
|
|
|
|
| 406 |
st.markdown("""
|
| 407 |
<div class="main-header">
|
| 408 |
<h1>π₯ Deteksi Alergen Makanan</h1>
|
| 409 |
+
<p>Analisis kandungan alergen dalam resep makanan dengan teknologi AI & OCR</p>
|
| 410 |
</div>
|
| 411 |
""", unsafe_allow_html=True)
|
| 412 |
|
|
|
|
| 424 |
|
| 425 |
st.markdown("### π‘ Tips Penggunaan")
|
| 426 |
st.markdown("""
|
| 427 |
+
**Input Manual:**
|
| 428 |
- Masukkan bahan dengan detail
|
| 429 |
- Gunakan nama bahan dalam bahasa Indonesia
|
| 430 |
+
|
| 431 |
+
**Kamera OCR:**
|
| 432 |
+
- Pastikan teks terlihat jelas
|
| 433 |
+
- Gunakan pencahayaan yang baik
|
| 434 |
+
- Hindari blur atau teks terpotong
|
| 435 |
+
|
| 436 |
+
**URL Cookpad:**
|
| 437 |
+
- Pastikan link valid
|
| 438 |
- Maksimal 20 URL per analisis
|
| 439 |
""")
|
| 440 |
|
|
|
|
| 446 |
st.markdown("### π§ Pilih Metode Input")
|
| 447 |
input_mode = st.radio(
|
| 448 |
"",
|
| 449 |
+
["π Input Manual", "π· Kamera OCR", "π URL Cookpad"],
|
| 450 |
horizontal=True
|
| 451 |
)
|
| 452 |
|
|
|
|
| 491 |
st.success("β
Analisis selesai!")
|
| 492 |
display_results(results, probs, labels)
|
| 493 |
|
| 494 |
+
elif input_mode == "π· Kamera OCR":
|
| 495 |
+
st.markdown("### π· Deteksi Alergen dari Gambar")
|
| 496 |
+
|
| 497 |
+
# Info card for camera
|
| 498 |
+
st.markdown("""
|
| 499 |
+
<div class="camera-card">
|
| 500 |
+
<strong>π· Petunjuk Kamera:</strong> Ambil foto langsung dari daftar bahan, kemasan makanan,
|
| 501 |
+
atau resep. Pastikan teks terlihat jelas dan pencahayaan memadai untuk hasil OCR terbaik.
|
| 502 |
+
</div>
|
| 503 |
+
""", unsafe_allow_html=True)
|
| 504 |
+
|
| 505 |
+
# Camera input
|
| 506 |
+
camera_image = st.camera_input("πΈ Ambil foto dengan kamera")
|
| 507 |
+
|
| 508 |
+
if camera_image is not None:
|
| 509 |
+
# Display the captured image
|
| 510 |
+
st.image(camera_image, caption="π· Gambar yang diambil", use_column_width=True)
|
| 511 |
+
|
| 512 |
+
# Load OCR reader
|
| 513 |
+
with st.spinner("π Memuat OCR engine..."):
|
| 514 |
+
reader = load_ocr_reader()
|
| 515 |
+
|
| 516 |
+
if reader is None:
|
| 517 |
+
st.error("β Gagal memuat OCR engine. Pastikan EasyOCR telah terinstall.")
|
| 518 |
+
else:
|
| 519 |
+
col_btn1, col_btn2, col_btn3 = st.columns([2, 2, 2])
|
| 520 |
+
with col_btn2:
|
| 521 |
+
if st.button("π Ekstrak Teks & Analisis", use_container_width=True, key="ocr_analyze"):
|
| 522 |
+
# Extract text from image
|
| 523 |
+
with st.spinner("π Mengekstrak teks dari gambar..."):
|
| 524 |
+
extracted_text, text_list, confidence = extract_text_from_image(camera_image, reader)
|
| 525 |
+
|
| 526 |
+
if extracted_text.strip():
|
| 527 |
+
# Display OCR results
|
| 528 |
+
display_ocr_results(extracted_text, text_list, confidence)
|
| 529 |
+
|
| 530 |
+
# Analyze allergens
|
| 531 |
+
with st.spinner("π Menganalisis alergen..."):
|
| 532 |
+
pred, probs = predict_allergen(model, vectorizer, extracted_text)
|
| 533 |
+
results = dict(zip(labels, pred))
|
| 534 |
+
|
| 535 |
+
st.success("β
Analisis selesai!")
|
| 536 |
+
display_results(results, probs, labels)
|
| 537 |
+
else:
|
| 538 |
+
st.warning("β οΈ Tidak dapat mengekstrak teks dari gambar. Coba ambil foto dengan pencahayaan yang lebih baik dan pastikan teks terlihat jelas.")
|
| 539 |
+
|
| 540 |
elif input_mode == "π URL Cookpad":
|
| 541 |
st.markdown("### π Analisis dari URL Cookpad")
|
| 542 |
|
|
|
|
| 611 |
# Footer
|
| 612 |
st.markdown("""
|
| 613 |
<div class="footer">
|
| 614 |
+
<p>π¬ Powered by XGBoost, TF-IDF & EasyOCR | Made with β€οΈ using Streamlit</p>
|
| 615 |
</div>
|
| 616 |
""", unsafe_allow_html=True)
|
| 617 |
|