|
|
import streamlit as st |
|
|
from PIL import Image |
|
|
import torch |
|
|
from torchvision import models |
|
|
import torch.nn as nn |
|
|
import torchvision.transforms as transforms |
|
|
import io |
|
|
import os |
|
|
import openai |
|
|
import json |
|
|
import timm |
|
|
|
|
|
|
|
|
st.set_page_config(page_title="EatSmart Pro", page_icon="π½οΈ", layout="wide") |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<div style="display: block; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); |
|
|
padding: 8px 15px; border-radius: 8px; margin-bottom: 15px; text-align: center;"> |
|
|
<p style="color: white; margin: 0; font-size: 14px;"> |
|
|
π± <strong>Mobile Users:</strong> Click the <strong>></strong> arrow (top-left) to open preferences menu |
|
|
</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CLASS_NAMES = [ |
|
|
'apple_pie', 'baby_back_ribs', 'baklava', 'beef_carpaccio', 'beef_tartare', |
|
|
'beet_salad', 'beignets', 'bibimbap', 'bread_pudding', 'breakfast_burrito', |
|
|
'bruschetta', 'caesar_salad', 'cannoli', 'caprese_salad', 'carrot_cake', |
|
|
'ceviche', 'cheesecake', 'cheese_plate', 'chicken_curry', 'chicken_quesadilla', |
|
|
'chicken_wings', 'chocolate_cake', 'chocolate_mousse', 'churros', 'clam_chowder', |
|
|
'club_sandwich', 'crab_cakes', 'creme_brulee', 'croque_madame', 'cup_cakes', |
|
|
'deviled_eggs', 'donuts', 'dumplings', 'edamame', 'eggs_benedict', |
|
|
'escargots', 'falafel', 'filet_mignon', 'fish_and_chips', 'foie_gras', |
|
|
'french_fries', 'french_onion_soup', 'french_toast', 'fried_calamari', 'fried_rice', |
|
|
'frozen_yogurt', 'garlic_bread', 'gnocchi', 'greek_salad', 'grilled_cheese_sandwich', |
|
|
'grilled_salmon', 'guacamole', 'gyoza', 'hamburger', 'hot_and_sour_soup', |
|
|
'hot_dog', 'huevos_rancheros', 'hummus', 'ice_cream', 'lasagna', |
|
|
'lobster_bisque', 'lobster_roll_sandwich', 'macaroni_and_cheese', 'macarons', 'miso_soup', |
|
|
'mussels', 'nachos', 'omelette', 'onion_rings', 'oysters', |
|
|
'pad_thai', 'paella', 'pancakes', 'panna_cotta', 'peking_duck', |
|
|
'pho', 'pizza', 'pork_chop', 'poutine', 'prime_rib', |
|
|
'pulled_pork_sandwich', 'ramen', 'ravioli', 'red_velvet_cake', 'risotto', |
|
|
'samosa', 'sashimi', 'scallops', 'seaweed_salad', 'shrimp_and_grits', |
|
|
'spaghetti_bolognese', 'spaghetti_carbonara', 'spring_rolls', 'steak', 'strawberry_shortcake', |
|
|
'sushi', 'tacos', 'takoyaki', 'tiramisu', 'tuna_tartare', |
|
|
'waffles' |
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_convnext_model(num_classes): |
|
|
""" |
|
|
Creates the ConvNeXt Large model architecture, |
|
|
matching the training script for maximum accuracy. |
|
|
""" |
|
|
print(f"π Loading ConvNeXt Large model for inference...") |
|
|
print(f"π Model: ConvNeXt Large (197M parameters)") |
|
|
|
|
|
|
|
|
model = timm.create_model('convnext_large.fb_in22k_ft_in1k', pretrained=True, num_classes=num_classes) |
|
|
|
|
|
|
|
|
total_params = sum(p.numel() for p in model.parameters()) |
|
|
print(f"β
Model architecture loaded:") |
|
|
print(f" Total parameters: {total_params:,}") |
|
|
print(f" Model size: ~{total_params * 4 / 1024**2:.1f} MB") |
|
|
|
|
|
return model |
|
|
|
|
|
def get_efficientnet_model(num_classes): |
|
|
""" |
|
|
Creates the old EfficientNet-V2-S model architecture for fallback. |
|
|
""" |
|
|
print(f"β οΈ Loading EfficientNet-V2-S model (fallback)...") |
|
|
model = models.efficientnet_v2_s(weights=None) |
|
|
num_features = model.classifier[1].in_features |
|
|
model.classifier = nn.Sequential( |
|
|
nn.Dropout(p=0.2), |
|
|
nn.Linear(num_features, 256), |
|
|
nn.ReLU(), |
|
|
nn.Dropout(p=0.1), |
|
|
nn.Linear(256, num_classes) |
|
|
) |
|
|
return model |
|
|
|
|
|
def load_json_data(path): |
|
|
if not os.path.exists(path): return {} |
|
|
try: |
|
|
with open(path, 'r') as f: return json.load(f) |
|
|
except (FileNotFoundError, json.JSONDecodeError): return {} |
|
|
|
|
|
def get_health_info(food_name, health_data): |
|
|
"""Enhanced health information display with comprehensive nutritional data""" |
|
|
food_name_key = food_name.replace('_', ' ').title() |
|
|
|
|
|
|
|
|
nutrition_info = health_data.get("nutrition_info", {}) |
|
|
info = nutrition_info.get(food_name_key) or nutrition_info.get(food_name.lower()) |
|
|
|
|
|
|
|
|
if not info: |
|
|
info = health_data.get(food_name_key) |
|
|
|
|
|
if not info: |
|
|
return "<p>No specific health information available for this dish.</p>" |
|
|
|
|
|
|
|
|
health_scores = health_data.get("health_scores", {}) |
|
|
health_score = health_scores.get(food_name.lower(), {}) |
|
|
score = health_score.get("score", 75) |
|
|
score_explanation = health_score.get("explanation", "Good") |
|
|
|
|
|
|
|
|
if score >= 80: |
|
|
score_color = "#28a745" |
|
|
score_bg = "#d4edda" |
|
|
elif score >= 60: |
|
|
score_color = "#ffc107" |
|
|
score_bg = "#fff3cd" |
|
|
else: |
|
|
score_color = "#dc3545" |
|
|
score_bg = "#f8d7da" |
|
|
|
|
|
|
|
|
nutrition_html = f""" |
|
|
<div style="background-color: #f8f9fa; padding: 15px; border-radius: 8px; margin: 10px 0;"> |
|
|
<h4 style="color: #495057; margin-top: 0;">π Nutritional Information</h4> |
|
|
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; text-align: center;"> |
|
|
<div> |
|
|
<div style="font-size: 14px; color: #6c757d;">Calories</div> |
|
|
<div style="font-size: 24px; font-weight: bold; color: #495057;">{info.get("calories", "N/A")}</div> |
|
|
<div style="font-size: 12px; color: #6c757d;">kcal</div> |
|
|
</div> |
|
|
<div> |
|
|
<div style="font-size: 14px; color: #6c757d;">Protein</div> |
|
|
<div style="font-size: 24px; font-weight: bold; color: #495057;">{info.get("protein", "N/A")}</div> |
|
|
<div style="font-size: 12px; color: #6c757d;">g</div> |
|
|
</div> |
|
|
<div> |
|
|
<div style="font-size: 14px; color: #6c757d;">Carbs</div> |
|
|
<div style="font-size: 24px; font-weight: bold; color: #495057;">{info.get("carbs", "N/A")}</div> |
|
|
<div style="font-size: 12px; color: #6c757d;">g</div> |
|
|
</div> |
|
|
<div> |
|
|
<div style="font-size: 14px; color: #6c757d;">Fat</div> |
|
|
<div style="font-size: 24px; font-weight: bold; color: #495057;">{info.get("fat", "N/A")}</div> |
|
|
<div style="font-size: 12px; color: #6c757d;">g</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
score_html = f""" |
|
|
<div style="background-color: {score_bg}; border: 1px solid {score_color}; border-radius: 8px; padding: 15px; margin: 10px 0;"> |
|
|
<h4 style="color: {score_color}; margin-top: 0;">π₯ Health Assessment</h4> |
|
|
<div style="background-color: {score_color}; color: white; padding: 10px; border-radius: 5px; text-align: center; font-weight: bold;"> |
|
|
Health Score: {score}/100 ({score_explanation}) |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
benefits_section = "" |
|
|
if info.get("benefits"): |
|
|
|
|
|
benefits_section = "BENEFITS_SECTION" |
|
|
|
|
|
return nutrition_html + score_html + benefits_section |
|
|
|
|
|
def get_allergen_info(food_name, allergen_data): |
|
|
"""Enhanced allergen information using sidebar preferences""" |
|
|
food_name_key = food_name.replace('_', ' ').title() |
|
|
allergens = allergen_data.get(food_name_key, []) |
|
|
|
|
|
|
|
|
if allergens: |
|
|
|
|
|
user_allergen_matches = [a for a in allergens if a in st.session_state.user_allergens] |
|
|
|
|
|
if user_allergen_matches: |
|
|
|
|
|
st.error(f"π¨ **CRITICAL ALLERGEN ALERT**: This dish contains **{', '.join(user_allergen_matches)}** which you've marked as allergens to avoid!") |
|
|
|
|
|
|
|
|
st.markdown("### β οΈ Allergens Detected in This Food") |
|
|
|
|
|
|
|
|
cols = st.columns(min(len(allergens), 4)) |
|
|
for i, allergen in enumerate(allergens): |
|
|
with cols[i % len(cols)]: |
|
|
if allergen in st.session_state.user_allergens: |
|
|
|
|
|
st.error(f"π¨ {allergen}") |
|
|
else: |
|
|
|
|
|
st.info(f"βΉοΈ {allergen}") |
|
|
|
|
|
|
|
|
if user_allergen_matches: |
|
|
st.markdown("---") |
|
|
st.markdown("π΄ **Red alerts** are for allergens you've marked in your preferences") |
|
|
else: |
|
|
st.markdown("---") |
|
|
st.markdown("π **Blue badges** show allergens present in this food") |
|
|
|
|
|
else: |
|
|
st.success("β
No common allergens typically found in this dish.") |
|
|
|
|
|
def get_trans_fat_analysis(food_name, health_data): |
|
|
"""Enhanced trans fat analysis with user preferences""" |
|
|
food_name_lower = food_name.lower().replace('_', ' ') |
|
|
|
|
|
|
|
|
trans_fat_ingredients = health_data.get("trans_fat_ingredients", [ |
|
|
"hydrogenated oil", "partially hydrogenated oil", "margarine", "shortening" |
|
|
]) |
|
|
|
|
|
|
|
|
high_trans_fat_foods = [ |
|
|
"donuts", "french_fries", "fried", "margarine", "shortening", |
|
|
"processed", "packaged", "fast food", "baked goods", "cookies", "crackers" |
|
|
] |
|
|
|
|
|
|
|
|
likely_trans_fat = any(ingredient in food_name_lower for ingredient in high_trans_fat_foods) |
|
|
|
|
|
if likely_trans_fat: |
|
|
alert_level = "HIGH RISK" if st.session_state.avoid_trans_fat else "POTENTIAL RISK" |
|
|
if st.session_state.avoid_trans_fat: |
|
|
st.error(f"π¨ **TRANS FAT ALERT**: You've enabled trans fat warnings and this food may contain trans fats!") |
|
|
|
|
|
trans_fat_html = f""" |
|
|
<div style="background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%); border: 2px solid #dc3545; border-radius: 12px; padding: 20px; margin: 15px 0; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"> |
|
|
<h4 style="color: #721c24; margin-top: 0; text-align: center;">π§ͺ Trans Fat Analysis - {alert_level}</h4> |
|
|
<div style="color: #721c24; text-align: center;"> |
|
|
<p><strong>β οΈ This food may contain trans fats from:</strong></p> |
|
|
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin: 15px 0;"> |
|
|
{"".join([f'<span style="background-color: #dc3545; color: white; padding: 5px 10px; border-radius: 15px; font-size: 0.9em;">{ingredient.title()}</span>' for ingredient in trans_fat_ingredients])} |
|
|
</div> |
|
|
<p style="font-weight: bold; margin-top: 15px;">π‘ <em>Recommendation: Check ingredient labels and consider healthier alternatives</em></p> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
else: |
|
|
trans_fat_html = f""" |
|
|
<div style="background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%); border: 2px solid #28a745; border-radius: 12px; padding: 20px; margin: 15px 0; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"> |
|
|
<h4 style="color: #155724; margin-top: 0; text-align: center;">π§ͺ Trans Fat Analysis - LOW RISK</h4> |
|
|
<div style="color: #155724; text-align: center;"> |
|
|
<p><strong>β
Great Choice!</strong></p> |
|
|
<p>This food typically contains minimal or no artificial trans fats.</p> |
|
|
<p style="font-style: italic; margin-top: 10px;">Keep enjoying healthy foods like this! π</p> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
return trans_fat_html |
|
|
|
|
|
def generate_recipe(food_name): |
|
|
"""Enhanced recipe generation based on detected ingredients and user preferences""" |
|
|
try: |
|
|
api_key = st.secrets.get("OPENAI_API_KEY") |
|
|
if not api_key: |
|
|
return "Error: OPENAI_API_KEY not found." |
|
|
|
|
|
openai.api_key = api_key |
|
|
|
|
|
|
|
|
dietary_restrictions = "" |
|
|
if st.session_state.dietary_preferences: |
|
|
dietary_restrictions = f"Make it {', '.join(st.session_state.dietary_preferences).lower()}. " |
|
|
|
|
|
allergen_restrictions = "" |
|
|
if st.session_state.user_allergens: |
|
|
allergen_restrictions = f"Avoid using {', '.join(st.session_state.user_allergens).lower()}. " |
|
|
|
|
|
trans_fat_note = "" |
|
|
if st.session_state.avoid_trans_fat: |
|
|
trans_fat_note = "Use healthy oils and avoid trans fats. " |
|
|
|
|
|
prompt = f"""Create a recipe for {food_name}. {dietary_restrictions}{allergen_restrictions}{trans_fat_note} |
|
|
|
|
|
Format the response with these exact headings: |
|
|
Ingredients: |
|
|
Instructions: |
|
|
Chef's Tips: |
|
|
|
|
|
Make it practical for home cooking and include nutritional benefits.""" |
|
|
|
|
|
response = openai.chat.completions.create( |
|
|
model="gpt-3.5-turbo", |
|
|
messages=[ |
|
|
{"role": "system", "content": "You are a professional chef who creates healthy, personalized recipes based on dietary preferences and restrictions."}, |
|
|
{"role": "user", "content": prompt} |
|
|
], |
|
|
temperature=0.7, |
|
|
max_tokens=600 |
|
|
) |
|
|
|
|
|
recipe_text = response.choices[0].message.content |
|
|
return recipe_text.replace("**Ingredients:**", "Ingredients:").replace("**Instructions:**", "Instructions:").replace("**Chef's Tips:**", "Chef's Tips:") |
|
|
except Exception as e: |
|
|
return f"The AI Chef is busy right now. Error: {str(e)}" |
|
|
|
|
|
@st.cache_resource |
|
|
def load_model_resources(): |
|
|
try: |
|
|
num_classes = len(CLASS_NAMES) |
|
|
|
|
|
|
|
|
convnext_model_path = 'models/food_classifier_convnext_large_cpu_full.pth' |
|
|
efficientnet_model_path = 'models/food101_efficientnet_best.pth' |
|
|
old_model_path = 'models/final_model.pth' |
|
|
|
|
|
if os.path.exists(convnext_model_path): |
|
|
model_path = convnext_model_path |
|
|
model = get_convnext_model(num_classes=num_classes) |
|
|
print(f"π― Using NEW ConvNeXt Large model: {model_path}") |
|
|
elif os.path.exists(efficientnet_model_path): |
|
|
model_path = efficientnet_model_path |
|
|
model = get_efficientnet_model(num_classes=num_classes) |
|
|
print(f"β οΈ Using EfficientNet model: {model_path}") |
|
|
elif os.path.exists(old_model_path): |
|
|
model_path = old_model_path |
|
|
model = get_efficientnet_model(num_classes=num_classes) |
|
|
print(f"β οΈ Using old fallback model: {model_path}") |
|
|
else: |
|
|
st.error(f"FATAL: No model file found. Looking for:\n- {convnext_model_path}\n- {efficientnet_model_path}\n- {old_model_path}") |
|
|
return None, None, None, None |
|
|
|
|
|
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
|
|
checkpoint = torch.load(model_path, map_location=device) |
|
|
model.load_state_dict(checkpoint['model_state_dict']) |
|
|
model.to(device) |
|
|
model.eval() |
|
|
|
|
|
|
|
|
model_info = { |
|
|
'path': model_path, |
|
|
'type': 'ConvNeXt Large' if 'convnext' in model_path else 'EfficientNet-V2-S', |
|
|
'accuracy': checkpoint.get('accuracy', 'Unknown') |
|
|
} |
|
|
|
|
|
health_data = load_json_data('health_data.json') |
|
|
allergen_data = load_json_data('allergen_data.json') |
|
|
return model, health_data, allergen_data, model_info |
|
|
except Exception as e: |
|
|
st.error(f"A critical error occurred while loading the model: {e}") |
|
|
return None, None, None, None |
|
|
|
|
|
model, health_data, allergen_data, model_info = load_model_resources() |
|
|
|
|
|
def transform_image(image_bytes): |
|
|
transform = transforms.Compose([ |
|
|
transforms.Resize((224, 224)), transforms.ToTensor(), |
|
|
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]) |
|
|
image = Image.open(io.BytesIO(image_bytes)).convert("RGB") |
|
|
return transform(image).unsqueeze(0) |
|
|
|
|
|
def get_prediction(image_tensor): |
|
|
if model is None: return "Error: Model not loaded", 0.0 |
|
|
with torch.no_grad(): |
|
|
outputs = model(image_tensor.to(torch.device('cuda' if torch.cuda.is_available() else 'cpu'))) |
|
|
probabilities = torch.nn.functional.softmax(outputs, dim=1) |
|
|
confidence, predicted_idx = torch.max(probabilities, 1) |
|
|
predicted_idx_item = predicted_idx.item() |
|
|
if predicted_idx_item >= len(CLASS_NAMES): |
|
|
return "Prediction Error: Index out of bounds.", 0.0 |
|
|
predicted_class = CLASS_NAMES[predicted_idx_item].replace('_', ' ').title() |
|
|
return predicted_class, confidence.item() |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<div style="text-align: center; padding: 20px 0;"> |
|
|
<h1 style="font-size: 3em; margin: 0;"> |
|
|
π½οΈ <span style="color: #28a745;">Eat</span><span style="color: #dc3545;">Smart</span> |
|
|
<span style="color: #17a2b8;">Pro</span> |
|
|
</h1> |
|
|
<p style="font-size: 1.2em; color: #6c757d; margin: 10px 0;"> |
|
|
π Your <span style="color: #28a745;">AI-Powered</span> |
|
|
<span style="color: #dc3545;">Nutrition</span> Assistant π |
|
|
</p> |
|
|
<div style="display: flex; justify-content: center; gap: 10px; margin: 15px 0;"> |
|
|
<span style="background: linear-gradient(45deg, #28a745, #20c997); color: white; padding: 5px 15px; border-radius: 20px; font-size: 0.9em;"> |
|
|
π₯ Healthy Analysis |
|
|
</span> |
|
|
<span style="background: linear-gradient(45deg, #dc3545, #fd7e14); color: white; padding: 5px 15px; border-radius: 20px; font-size: 0.9em;"> |
|
|
β οΈ Allergen Alerts |
|
|
</span> |
|
|
<span style="background: linear-gradient(45deg, #17a2b8, #6f42c1); color: white; padding: 5px 15px; border-radius: 20px; font-size: 0.9em;"> |
|
|
π³ Smart Recipes |
|
|
</span> |
|
|
</div> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
if model_info: |
|
|
model_type = model_info['type'] |
|
|
model_accuracy = model_info['accuracy'] |
|
|
|
|
|
if 'ConvNeXt' in model_type: |
|
|
status_color = "#28a745" |
|
|
status_bg = "#d4edda" |
|
|
status_icon = "π" |
|
|
status_text = f"HIGH ACCURACY MODEL ACTIVE" |
|
|
else: |
|
|
status_color = "#ffc107" |
|
|
status_bg = "#fff3cd" |
|
|
status_icon = "β οΈ" |
|
|
status_text = f"FALLBACK MODEL (Training in Progress)" |
|
|
|
|
|
st.markdown(f""" |
|
|
<div style="background-color: {status_bg}; border: 2px solid {status_color}; border-radius: 10px; padding: 15px; margin: 15px 0; text-align: center;"> |
|
|
<div style="color: {status_color}; font-size: 1.2em; font-weight: bold;"> |
|
|
{status_icon} {status_text} |
|
|
</div> |
|
|
<div style="color: #495057; font-size: 0.9em; margin-top: 5px;"> |
|
|
Model: {model_type} | Validation Accuracy: {model_accuracy} |
|
|
</div> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
with st.sidebar: |
|
|
st.markdown(""" |
|
|
<div style="text-align: center; padding: 15px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 10px; margin-bottom: 20px;"> |
|
|
<h3 style="color: white; margin: 0;">βοΈ Your Preferences</h3> |
|
|
<p style="color: #f8f9fa; margin: 5px 0; font-size: 0.9em;">Set your dietary needs</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
if 'user_allergens' not in st.session_state: |
|
|
st.session_state.user_allergens = [] |
|
|
if 'avoid_trans_fat' not in st.session_state: |
|
|
st.session_state.avoid_trans_fat = False |
|
|
if 'dietary_preferences' not in st.session_state: |
|
|
st.session_state.dietary_preferences = [] |
|
|
|
|
|
|
|
|
st.markdown("### π¨ Allergen Alerts") |
|
|
st.markdown("*Select allergens you want to be warned about:*") |
|
|
|
|
|
common_allergens = ["Gluten", "Dairy", "Egg", "Fish", "Shellfish", "Nuts", "Peanuts", "Soy", "Sesame"] |
|
|
|
|
|
for allergen in common_allergens: |
|
|
if st.checkbox(f"π‘οΈ {allergen}", key=f"allergen_{allergen}", |
|
|
value=allergen in st.session_state.user_allergens): |
|
|
if allergen not in st.session_state.user_allergens: |
|
|
st.session_state.user_allergens.append(allergen) |
|
|
else: |
|
|
if allergen in st.session_state.user_allergens: |
|
|
st.session_state.user_allergens.remove(allergen) |
|
|
|
|
|
|
|
|
st.markdown("### π§ͺ Trans Fat Settings") |
|
|
st.session_state.avoid_trans_fat = st.checkbox( |
|
|
"β οΈ Alert me about trans fats", |
|
|
value=st.session_state.avoid_trans_fat, |
|
|
help="Get warnings about foods that may contain trans fats" |
|
|
) |
|
|
|
|
|
|
|
|
st.markdown("### π± Dietary Preferences") |
|
|
dietary_options = ["Vegetarian", "Vegan", "Keto", "Low-Carb", "High-Protein", "Gluten-Free"] |
|
|
|
|
|
for diet in dietary_options: |
|
|
if st.checkbox(f"π₯¬ {diet}", key=f"diet_{diet}", |
|
|
value=diet in st.session_state.dietary_preferences): |
|
|
if diet not in st.session_state.dietary_preferences: |
|
|
st.session_state.dietary_preferences.append(diet) |
|
|
else: |
|
|
if diet in st.session_state.dietary_preferences: |
|
|
st.session_state.dietary_preferences.remove(diet) |
|
|
|
|
|
|
|
|
if st.session_state.user_allergens or st.session_state.dietary_preferences or st.session_state.avoid_trans_fat: |
|
|
st.markdown("---") |
|
|
st.markdown("### π Active Preferences") |
|
|
if st.session_state.user_allergens: |
|
|
st.markdown(f"π¨ **Allergen Alerts:** {', '.join(st.session_state.user_allergens)}") |
|
|
if st.session_state.dietary_preferences: |
|
|
st.markdown(f"π± **Diet:** {', '.join(st.session_state.dietary_preferences)}") |
|
|
if st.session_state.avoid_trans_fat: |
|
|
st.markdown("π§ͺ **Trans Fat Alerts:** Enabled") |
|
|
|
|
|
if 'image_buffer' not in st.session_state: st.session_state.image_buffer = None |
|
|
if 'prediction_result' not in st.session_state: st.session_state.prediction_result = None |
|
|
if 'last_image_buffer' not in st.session_state: st.session_state.last_image_buffer = None |
|
|
|
|
|
col1, col2 = st.columns([1, 1.2]) |
|
|
with col1: |
|
|
st.markdown(""" |
|
|
<div style="text-align: center; padding: 15px; background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%); border-radius: 10px; margin-bottom: 20px;"> |
|
|
<h3 style="color: white; margin: 0;">πΈ Upload Food Image</h3> |
|
|
<p style="color: #ddd; margin: 5px 0; font-size: 0.9em;">Drag & drop or browse to analyze</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
uploaded_file = st.file_uploader("Choose your food image", type=["jpg", "jpeg", "png"], label_visibility="collapsed") |
|
|
|
|
|
|
|
|
if uploaded_file is not None: |
|
|
st.session_state.image_buffer = uploaded_file.getvalue() |
|
|
else: |
|
|
st.session_state.image_buffer = None |
|
|
|
|
|
|
|
|
if st.session_state.image_buffer is not None: |
|
|
st.image(st.session_state.image_buffer, caption='π½οΈ Your Food Image', use_column_width=True) |
|
|
|
|
|
with col2: |
|
|
st.markdown(""" |
|
|
<div style="text-align: center; padding: 15px; background: linear-gradient(135deg, #00b894 0%, #00a085 100%); border-radius: 10px; margin-bottom: 20px;"> |
|
|
<h3 style="color: white; margin: 0;">π¬ Smart Analysis & Recipes</h3> |
|
|
<p style="color: #ddd; margin: 5px 0; font-size: 0.9em;">AI-powered nutrition insights</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
if model and st.session_state.image_buffer: |
|
|
if st.session_state.image_buffer != st.session_state.last_image_buffer: |
|
|
st.session_state.last_image_buffer = st.session_state.image_buffer |
|
|
with st.spinner('Analyzing image...'): |
|
|
image_tensor = transform_image(st.session_state.image_buffer) |
|
|
st.session_state.prediction_result = get_prediction(image_tensor) |
|
|
if 'recipe' in st.session_state: del st.session_state.recipe |
|
|
if st.session_state.prediction_result: |
|
|
food_name, confidence = st.session_state.prediction_result |
|
|
st.metric(label="Predicted Food", value=food_name) |
|
|
st.progress(confidence, text=f"Confidence: {confidence:.2%}") |
|
|
tab1, tab2, tab3, tab4 = st.tabs(["Health Info", "Allergen Alert", "Trans Fat Analysis", "AI Recipes"]) |
|
|
with tab1: |
|
|
health_info_html = get_health_info(food_name, health_data) |
|
|
if "BENEFITS_SECTION" in health_info_html: |
|
|
|
|
|
st.markdown(health_info_html.replace("BENEFITS_SECTION", ""), unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
nutrition_info = health_data.get("nutrition_info", {}) |
|
|
food_info = nutrition_info.get(food_name.replace('_', ' ').title()) or nutrition_info.get(food_name.lower()) or health_data.get(food_name.replace('_', ' ').title()) |
|
|
|
|
|
if food_info and food_info.get("benefits"): |
|
|
st.markdown("### β¨ Health Benefits") |
|
|
for benefit in food_info.get("benefits", []): |
|
|
st.markdown(f"β’ {benefit}") |
|
|
else: |
|
|
st.markdown(health_info_html, unsafe_allow_html=True) |
|
|
with tab2: |
|
|
get_allergen_info(food_name, allergen_data) |
|
|
with tab3: |
|
|
st.markdown(get_trans_fat_analysis(food_name, health_data), unsafe_allow_html=True) |
|
|
with tab4: |
|
|
st.subheader(f"AI-Generated Recipe for {food_name}") |
|
|
if 'recipe' not in st.session_state or st.session_state.get('recipe_food') != food_name: |
|
|
with st.spinner("Chef AI is thinking of a recipe..."): |
|
|
st.session_state.recipe = generate_recipe(food_name) |
|
|
st.session_state.recipe_food = food_name |
|
|
if 'recipe' in st.session_state: |
|
|
recipe_text = st.session_state.recipe |
|
|
sections = {"ingredients": [], "instructions": [], "tips": []} |
|
|
current_section_key = None |
|
|
for line in recipe_text.split('\n'): |
|
|
line_lower = line.strip().lower() |
|
|
if line_lower.startswith("ingredients"): current_section_key = "ingredients" |
|
|
elif line_lower.startswith("instructions"): current_section_key = "instructions" |
|
|
elif line_lower.startswith("tips") or line_lower.startswith("chef's tips"): current_section_key = "tips" |
|
|
elif line.strip() and current_section_key: sections[current_section_key].append(line.strip().lstrip('*- ')) |
|
|
with st.expander("Ingredients", expanded=True): st.markdown("\n".join(f"- {item}" for item in sections["ingredients"]) or "No ingredients listed.") |
|
|
with st.expander("Instructions", expanded=True): st.markdown("\n".join(f"{i+1}. {item}" for i, item in enumerate(sections["instructions"])) or "No instructions provided.") |
|
|
with st.expander("Chef's Tips"): st.markdown("\n".join(f"- {item}" for item in sections["tips"]) or "No special tips provided.") |
|
|
elif not model: |
|
|
st.error("Application has failed to start. Please check the logs for errors.") |
|
|
else: |
|
|
st.info("Upload an image to get started.") |