import gradio as gr from ultralytics import YOLO import PIL.Image import numpy as np from typing import List, Tuple, Dict, Optional from huggingface_hub import InferenceClient # Load the trained model model = YOLO('best.pt') # Initialize state structure def init_user_state() -> Dict: """Initialize the user state dictionary.""" return { 'name': '', 'age': None, 'weight_lbs': None, 'height_cm': None, 'gender': '', 'activity_level': '', 'goal': '', 'calorie_target': None, 'cuisine_preference': '', 'detected_ingredients': [], 'ingredient_list_text': '' } # ==================== BMR & CALORIE CALCULATION ==================== def convert_height_to_cm(height_ft: Optional[float], height_in: Optional[float]) -> Optional[float]: """Convert feet and inches to centimeters.""" if height_ft is None or height_in is None: return None total_inches = (height_ft * 12) + height_in return total_inches * 2.54 def calculate_bmr(weight_kg: float, height_cm: float, age: int, gender: str) -> float: """ Calculate Basal Metabolic Rate using Mifflin-St Jeor Equation. BMR (Men) = 10 × weight(kg) + 6.25 × height(cm) - 5 × age(years) + 5 BMR (Women) = 10 × weight(kg) + 6.25 × height(cm) - 5 × age(years) - 161 """ base_bmr = (10 * weight_kg) + (6.25 * height_cm) - (5 * age) if gender.lower() == 'male': bmr = base_bmr + 5 else: # female bmr = base_bmr - 161 return bmr def get_activity_multiplier(activity_level: str) -> float: """Get activity multiplier based on activity level.""" multipliers = { 'Sedentary': 1.2, 'Light': 1.375, 'Moderate': 1.55, 'Active': 1.725, 'Very Active': 1.9 } return multipliers.get(activity_level, 1.2) def get_goal_adjustment(goal: str) -> int: """Get calorie adjustment based on goal.""" adjustments = { 'Cutting': -500, 'Maintain': 0, 'Bulking': +500, 'Custom': 0 # Will be handled separately } return adjustments.get(goal, 0) def calculate_calorie_target( weight_lbs: Optional[float], height_ft: Optional[float], height_in: Optional[float], age: Optional[int], gender: Optional[str], activity_level: Optional[str], goal: Optional[str], custom_calories: Optional[float], state: Dict ) -> Tuple[Dict, str]: """ Calculate daily calorie target based on user inputs. Updates state and returns formatted result. """ # Validate inputs if not all([weight_lbs, height_ft is not None, height_in is not None, age, gender, activity_level, goal]): return state, "**Please fill in all required fields.**" # Convert weight to kg weight_kg = weight_lbs * 0.453592 # Convert height to cm height_cm = convert_height_to_cm(height_ft, height_in) if height_cm is None: return state, "**Please enter valid height values.**" # Calculate BMR bmr = calculate_bmr(weight_kg, height_cm, age, gender) # Get activity multiplier activity_mult = get_activity_multiplier(activity_level) # Calculate TDEE (Total Daily Energy Expenditure) tdee = bmr * activity_mult # Apply goal adjustment if goal == 'Custom' and custom_calories is not None: calorie_target = custom_calories else: goal_adj = get_goal_adjustment(goal) calorie_target = tdee + goal_adj # Update state state['weight_lbs'] = weight_lbs state['height_cm'] = height_cm state['age'] = age state['gender'] = gender state['activity_level'] = activity_level state['goal'] = goal state['calorie_target'] = calorie_target # Format output result_text = f""" ## 📊 Your Daily Calorie Target **BMR (Basal Metabolic Rate):** {bmr:.0f} calories/day **Activity Level:** {activity_level} (×{activity_mult:.2f}) **TDEE (Total Daily Energy Expenditure):** {tdee:.0f} calories/day **Goal Adjustment:** {get_goal_adjustment(goal):+.0f} calories ### 🎯 **Daily Calorie Target: {calorie_target:.0f} calories** *This target is based on your profile and has been saved for recipe generation.* """ return state, result_text # ==================== INGREDIENT DETECTION ==================== def detect_ingredients(images: List, state: Dict) -> Tuple[Dict, List, str]: """ Process multiple images and return detected ingredients. Also updates the state with detected ingredients. Args: images: List of uploaded images (file paths) state: User state dictionary Returns: Tuple of (updated_state, processed_images, ingredient_list_text) """ if not images or len(images) == 0: return state, [], "**No images uploaded.**" processed_images = [] all_detected_items = set() # Process each uploaded image for image_file in images: if image_file is None: continue # Get file path (Gradio File component returns file objects) image_path = image_file.name if hasattr(image_file, 'name') else image_file # Run prediction with your local settings (conf=0.7) results = model.predict(source=image_path, conf=0.7, iou=0.3, verbose=False) # Get the image with bounding boxes drawn result_image = results[0].plot() # Extract detected ingredients from this image for box in results[0].boxes: class_id = int(box.cls) class_name = model.names[class_id] all_detected_items.add(class_name) # Convert numpy array to PIL Image for display # YOLO returns BGR, convert to RGB if len(result_image.shape) == 3: result_image_rgb = result_image[..., ::-1] # BGR to RGB processed_images.append(PIL.Image.fromarray(result_image_rgb)) else: processed_images.append(PIL.Image.fromarray(result_image)) # Create formatted ingredient list if all_detected_items: ingredient_list = sorted(list(all_detected_items)) ingredient_list_text = "**Detected Ingredients:**\n\n" ingredient_list_text += "\n".join([f"• {item.capitalize()}" for item in ingredient_list]) ingredient_list_text += f"\n\n**Total unique items:** {len(ingredient_list)}" # Update state with detected ingredients state['detected_ingredients'] = ingredient_list state['ingredient_list_text'] = ingredient_list_text else: ingredient_list_text = "**No ingredients detected.**\n\nTry adjusting the image quality or lighting." state['detected_ingredients'] = [] state['ingredient_list_text'] = ingredient_list_text return state, processed_images, ingredient_list_text # ==================== RECIPE GENERATION ==================== def generate_recipes(cuisine_preference: Optional[str], state: Dict) -> Tuple[Dict, str]: """ Generate recipes using LLM based on user profile and detected ingredients. """ # Validate that we have the necessary data if not state.get('calorie_target'): return state, "**⚠️ Please complete your User Profile & Goals first to set your calorie target.**" if not state.get('detected_ingredients'): return state, "**⚠️ Please scan ingredients in the Ingredient Scanner tab first.**" if not cuisine_preference: return state, "**⚠️ Please select a cuisine preference.**" # Update state state['cuisine_preference'] = cuisine_preference # Get user data calorie_target = int(state['calorie_target']) goal = state.get('goal', 'Maintain') ingredients = state['detected_ingredients'] ingredient_list = ", ".join([item.capitalize() for item in ingredients]) # Map goal to dietary focus goal_descriptions = { 'Cutting': 'weight loss and calorie deficit', 'Maintain': 'maintaining current weight', 'Bulking': 'muscle gain with high protein', 'Custom': 'your custom calorie target' } goal_desc = goal_descriptions.get(goal, 'your goals') # Construct prompt prompt = f"""You are a professional nutritionist and chef. Create 3 distinct, detailed recipes that: 1. Use these available ingredients: {ingredient_list} 2. Fit within a daily calorie target of approximately {calorie_target} calories per day 3. Match {cuisine_preference} cuisine style 4. Align with the goal of {goal_desc} For each recipe, provide: - Recipe name - Serving size - Estimated calories per serving - Complete ingredient list (you may suggest additional common pantry items if needed) - Step-by-step cooking instructions - Nutritional highlights relevant to the goal Format each recipe clearly with headers. Make the recipes practical, delicious, and suitable for home cooking.""" try: # Use Hugging Face Inference API import os # Try multiple ways to get the token hf_token = None # Method 1: Check HF_TOKEN environment variable hf_token = os.getenv("HF_TOKEN", None) # Method 2: Check HUGGING_FACE_HUB_TOKEN (alternative name) if not hf_token: hf_token = os.getenv("HUGGING_FACE_HUB_TOKEN", None) # Method 3: Try to get from Hugging Face cache (for Spaces or logged-in users) if not hf_token: try: from huggingface_hub import HfFolder hf_token = HfFolder.get_token() except: pass if not hf_token: return state, """**⚠️ Hugging Face Token Required** Please set your HF_TOKEN environment variable to use recipe generation. **For Hugging Face Spaces:** 1. Go to your Space Settings (gear icon) 2. Scroll to "Repository secrets" 3. Click "New secret" 4. Name: `HF_TOKEN` 5. Value: Your Hugging Face token 6. Click "Add secret" and restart your Space **For Local Development (Windows):** 1. Press Win+R, type `sysdm.cpl`, press Enter 2. Go to "Advanced" tab → "Environment Variables" 3. Under "User variables", click "New" 4. Variable name: `HF_TOKEN` 5. Variable value: Your Hugging Face token 6. Click OK and restart your application Get your token at: https://huggingface.co/settings/tokens""" client = InferenceClient(token=hf_token) # Try using models that support text-generation # List of models to try in order of preference (all verified to work with text-generation) models_to_try = [ "meta-llama/Llama-3.2-3B-Instruct", # Fast and reliable "meta-llama/Llama-3.1-8B-Instruct", # Better quality "mistralai/Mistral-7B-Instruct-v0.3", # Alternative option "microsoft/Phi-3-mini-4k-instruct", # Lightweight fallback "google/gemma-2-2b-it", # Additional reliable option ] response = None last_error = None successful_model = None for model_name in models_to_try: try: response = client.text_generation( prompt, model=model_name, max_new_tokens=1500, temperature=0.7, ) successful_model = model_name break # Success, exit the loop except Exception as model_error: last_error = model_error continue # Try next model # If all models failed, raise error with details if response is None: error_msg = f"All models failed. Last error: {str(last_error)}" if not hf_token: error_msg += "\n\n💡 TIP: Make sure you have set your HF_TOKEN environment variable." raise Exception(error_msg) # Extract text if response is a formatted object if hasattr(response, 'generated_text'): response_text = response.generated_text elif isinstance(response, str): response_text = response else: response_text = str(response) recipes_text = f"""## 🍳 Recipe Suggestions for {cuisine_preference} Cuisine **Your Profile:** - Daily Calorie Target: {calorie_target} calories - Goal: {goal} - Available Ingredients: {ingredient_list} --- {response_text} --- *Recipes generated based on your profile and available ingredients.*""" return state, recipes_text except Exception as e: error_msg = f"""**⚠️ Error generating recipes.** Please try again. If the issue persists, you may need to: 1. Check your internet connection 2. Ensure you have a Hugging Face API token set (if required) 3. Try a different cuisine preference Error details: {str(e)}""" return state, error_msg # ==================== GRADIO INTERFACE ==================== # Custom CSS for a modern, clean interface custom_css = """ .gradio-container { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } .main-header { text-align: center; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 10px; margin-bottom: 20px; } .description-box { background: #f8f9fa; padding: 15px; border-radius: 8px; border-left: 4px solid #667eea; margin-bottom: 20px; color: #000000 !important; } .description-box * { color: #000000 !important; } .ingredient-list { background: #ffffff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); min-height: 200px; color: #000000 !important; } .ingredient-list * { color: #000000 !important; } .calorie-result { background: #e8f5e9; padding: 20px; border-radius: 8px; border-left: 4px solid #4caf50; margin-top: 20px; color: #000000 !important; } .calorie-result * { color: #000000 !important; } """ # Create the Gradio interface with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo: # Header gr.Markdown( """ # 🥗 Forked Recipe-Pal Your AI-powered kitchen companion: Scan ingredients, calculate calories, and generate personalized recipes! """, elem_classes=["main-header"] ) # Initialize state user_state = gr.State(value=init_user_state) # Tab structure with gr.Tabs() as tabs: # ========== TAB 1: USER PROFILE & GOALS ========== with gr.Tab("👤 User Profile & Goals"): gr.Markdown( """