Spaces:
Sleeping
Sleeping
| import base64 | |
| import io | |
| import time | |
| import streamlit as st | |
| from openai import OpenAI | |
| import os | |
| from PIL import Image | |
| from utils import pprint, getFontsUrl | |
| # Load environment variables | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY")) | |
| # model = "gpt-4o-mini" | |
| model = "gpt-4o" | |
| # Set up page configuration | |
| st.set_page_config( | |
| page_title="Magic Recipe Decoder π½οΈ", | |
| page_icon="π₯", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # Custom CSS for styling | |
| st.markdown( | |
| f""" | |
| <head> | |
| <link href="{getFontsUrl()}" rel="stylesheet"> | |
| </head> | |
| """ """ | |
| <style> | |
| h1 { | |
| font-family: 'Whisper' !important; | |
| # font-size: 2.2rem !important; | |
| } | |
| h3 { | |
| font-size: 1.5rem !important; | |
| } | |
| .big-font { | |
| font-size:20px !important; | |
| color: #2C3E50; | |
| } | |
| .highlight-box { | |
| background-color: #F5F5F5; /* Soft light gray */ | |
| border-radius: 10px; | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| color: #333333; /* Very dark gray for text */ | |
| border: 1px solid #E0E0E0; /* Subtle border */ | |
| } | |
| .highlight-box h1 { | |
| color: #1A5F7A; /* Deep teal for main heading */ | |
| font-size: 24px; | |
| margin-bottom: 15px; | |
| } | |
| .highlight-box h2 { | |
| color: #2C7DA0; /* Slightly lighter teal for subheadings */ | |
| font-size: 20px; | |
| margin-top: 15px; | |
| margin-bottom: 10px; | |
| } | |
| .highlight-box h3 { | |
| color: #468FAF; /* Even lighter teal for smaller headings */ | |
| font-size: 18px; | |
| margin-top: 10px; | |
| margin-bottom: 8px; | |
| } | |
| .highlight-box p { | |
| color: #333333; /* Dark gray for paragraphs */ | |
| line-height: 1.6; | |
| } | |
| </style> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |
| # Initialize session state | |
| if "ipAddress" not in st.session_state: | |
| st.session_state.ipAddress = st.context.headers.get("x-forwarded-for") | |
| if 'cooking_equipment' not in st.session_state: | |
| st.session_state.cooking_equipment = { | |
| 'Stove': True, | |
| 'Oven': False, | |
| 'Microwave': False, | |
| 'Blender': False, | |
| 'Pressure Cooker': False | |
| } | |
| if 'original_recipe' not in st.session_state: | |
| st.session_state.original_recipe = None | |
| # Add language selection to session state | |
| if 'recipe_language' not in st.session_state: | |
| st.session_state.recipe_language = 'English' | |
| def google_image_search(query): | |
| """Placeholder for image search - you'll need to implement actual image search API""" | |
| return "https://via.placeholder.com/300x200.png?text=" + query.replace(" ", "+") | |
| def resize_image(image_base64, max_size=1024): | |
| """ | |
| Resize an image from base64 to max dimension of 1024 pixels while maintaining aspect ratio | |
| Args: | |
| image_base64 (str): Base64 encoded image | |
| max_size (int): Maximum dimension for the image | |
| Returns: | |
| str: Resized image as base64 encoded string | |
| """ | |
| # Decode base64 image | |
| image_bytes = base64.b64decode(image_base64) | |
| # Log original image size | |
| original_size = len(image_bytes) | |
| # Open image with Pillow | |
| img = Image.open(io.BytesIO(image_bytes)) | |
| # Calculate resize ratio | |
| width, height = img.size | |
| resize_ratio = min(max_size / width, max_size / height) | |
| # If image is already smaller than max_size, return original | |
| if resize_ratio >= 1: | |
| pprint({ | |
| "function": "resize_image", | |
| "result": "no_resize_needed", | |
| "original_size_bytes": original_size, | |
| "original_size_kb": round(original_size / 1024) | |
| }) | |
| return image_base64 | |
| # Calculate new dimensions | |
| new_width = int(width * resize_ratio) | |
| new_height = int(height * resize_ratio) | |
| # Resize image | |
| resized_img = img.resize((new_width, new_height), Image.LANCZOS) | |
| # Convert back to base64 | |
| buffered = io.BytesIO() | |
| resized_img.save(buffered, format=img.format) | |
| resized_bytes = buffered.getvalue() | |
| resized_base64 = base64.b64encode(resized_bytes).decode('utf-8') | |
| # Log resized image size | |
| resized_size = len(resized_bytes) | |
| pprint({ | |
| "function": "resize_image", | |
| "original_size_kb": round(original_size / 1024), | |
| "resized_size_kb": round(resized_size / 1024), | |
| "size_reduction_percentage": round(((original_size - resized_size) / original_size) * 100) | |
| }) | |
| return resized_base64 | |
| def analyze_and_generate_recipe(uploaded_image, available_equipment=None, language='English'): | |
| """Analyze food image and generate recipe in a single LLM call""" | |
| progress_stages = [ | |
| {"message": "π Scanning the delicious image...", "progress": 10}, | |
| {"message": "π§ Identifying culinary ingredients...", "progress": 30}, | |
| {"message": "π³ Consulting virtual chef's expertise...", "progress": 50}, | |
| {"message": "π Crafting personalized recipe...", "progress": 70}, | |
| {"message": "π Finalizing gourmet instructions...", "progress": 90} | |
| ] | |
| # Create a progress bar | |
| progress_bar = st.progress(0) | |
| status_text = st.empty() | |
| try: | |
| # Update progress stages | |
| for stage in progress_stages: | |
| status_text.text(stage["message"]) | |
| progress_bar.progress(stage["progress"]) | |
| time.sleep(1) # Short delay between stages | |
| # Resize the image before sending to LLM | |
| resized_image_base64 = resize_image(uploaded_image) | |
| # Prepare the system and user messages | |
| messages = [ | |
| { | |
| "role": "system", | |
| "content": f"""You are a professional chef and food analyst. | |
| When analyzing a food image, provide a comprehensive recipe in {language} that considers: | |
| 1. Detailed food description | |
| 2. Complete ingredient list | |
| 3. Cooking method | |
| 4. Step-by-step instructions""" | |
| }, | |
| { | |
| "role": "user", | |
| "content": [ | |
| { | |
| "type": "image_url", | |
| "image_url": {"url": f"data:image/jpeg;base64,{resized_image_base64}"} | |
| }, | |
| { | |
| "type": "text", | |
| "text": f"""Analyze this food image and generate a detailed recipe in {language}. | |
| {'Available cooking equipment: ' + ', '.join(available_equipment) if available_equipment else 'No equipment restrictions'} | |
| If specific equipment is available, prioritize cooking methods that use those tools. | |
| Provide: | |
| - Detailed food description | |
| - Ingredient list | |
| - Cooking method adapted to available equipment | |
| - Difficulty level | |
| - Estimated cooking time | |
| - Precise, step-by-step instructions | |
| Use markdown formatting for clear presentation.""" | |
| } | |
| ] | |
| } | |
| ] | |
| # Log API call details | |
| pprint({ | |
| "function": "analyze_and_generate_recipe", | |
| "model": model, | |
| "language": language, | |
| "available_equipment": available_equipment | |
| }) | |
| # Make the LLM call | |
| status_text.text("π Generating recipe with AI...") | |
| progress_bar.progress(95) | |
| response = client.chat.completions.create( | |
| model=model, | |
| messages=messages | |
| ) | |
| # Final progress update | |
| status_text.text("β Recipe generated successfully!") | |
| progress_bar.progress(100) | |
| # Clear the progress bar and status text after a short delay | |
| time.sleep(1) | |
| progress_bar.empty() | |
| status_text.empty() | |
| # Log response details | |
| pprint({ | |
| "function": "analyze_and_generate_recipe_response", | |
| "tokens_used": response.usage.total_tokens if response.usage else None, | |
| "response_length": len(response.choices[0].message.content) | |
| }) | |
| return response.choices[0].message.content | |
| except Exception as e: | |
| # Clear progress indicators in case of error | |
| progress_bar.empty() | |
| status_text.empty() | |
| st.error(f"Error analyzing image and generating recipe: {e}") | |
| return None | |
| def refine_recipe(original_recipe, user_refinement, language='English'): | |
| """Refine the recipe based on user input""" | |
| try: | |
| # Log API call details | |
| pprint({ | |
| "function": "refine_recipe", | |
| "model": model, | |
| "language": language, | |
| "user_refinement_length": len(user_refinement) | |
| }) | |
| response = client.chat.completions.create( | |
| model=model, | |
| messages=[ | |
| { | |
| "role": "system", | |
| "content": f"You are a professional chef who can modify recipes based on specific user preferences. Respond in {language}." | |
| }, | |
| { | |
| "role": "user", | |
| "content": f"""Original Recipe: | |
| {original_recipe} | |
| User Refinement Request: {user_refinement} | |
| Please modify the recipe according to the user's preferences in {language}. | |
| Provide the updated recipe with clear instructions, | |
| maintaining the original recipe's core structure.""" | |
| } | |
| ] | |
| ) | |
| # Log response details | |
| pprint({ | |
| "function": "refine_recipe_response", | |
| "tokens_used": response.usage.total_tokens if response.usage else None, | |
| "response_length": len(response.choices[0].message.content) | |
| }) | |
| return response.choices[0].message.content | |
| except Exception as e: | |
| st.error(f"Error refining recipe: {e}") | |
| return None | |
| # Main Streamlit App | |
| st.title("π₯ Magic Recipe") | |
| st.markdown("*Discover the secrets behind your favorite dishes!*", unsafe_allow_html=True) | |
| # Sidebar for Cooking Equipment | |
| st.sidebar.header("π§ Cooking Equipment") | |
| st.sidebar.markdown("Check the equipment you have available:") | |
| for equipment, available in st.session_state.cooking_equipment.items(): | |
| st.session_state.cooking_equipment[equipment] = st.sidebar.checkbox(equipment, value=available) | |
| # Language Selection | |
| st.sidebar.header("π Recipe Language") | |
| st.sidebar.markdown("Choose your preferred language:") | |
| # Top 5 Indian languages + English | |
| languages = [ | |
| 'English', | |
| 'Hindi', | |
| 'Hinglish', | |
| 'Bengali', | |
| 'Telugu', | |
| 'Marathi', | |
| 'Tamil' | |
| ] | |
| st.session_state.recipe_language = st.sidebar.selectbox( | |
| "Select Recipe Language", | |
| languages, | |
| index=0 | |
| ) | |
| # Image Upload and Analysis Section | |
| st.markdown("### πΈ Upload Your Food Image", unsafe_allow_html=True) | |
| # Add camera input option | |
| img_source = st.radio("Choose image source:", ["Upload from device", "Take a photo"]) | |
| if img_source == "Upload from device": | |
| uploaded_file = st.file_uploader("Choose an image...", type=['jpg', 'jpeg', 'png']) | |
| else: | |
| uploaded_file = st.camera_input( | |
| "Take a photo of your dish", | |
| help="Please hold your device vertically for best results", | |
| # Set aspect ratio to portrait (3:4) | |
| key="portrait_camera", | |
| args={ | |
| "landscape": False, # Force portrait mode | |
| "aspectRatio": 3 / 4 # Portrait aspect ratio | |
| } | |
| ) | |
| # Food Analysis and Recipe Generation | |
| if uploaded_file is not None: | |
| # Display uploaded image | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.image(uploaded_file, caption='Uploaded Image', use_container_width=True) | |
| with col2: | |
| # Checkbox to use available cooking equipment | |
| use_available_equipment = st.checkbox("Use only available cooking equipment", value=False) | |
| # Prepare available equipment list if checkbox is selected | |
| available_equipment = [] | |
| if use_available_equipment: | |
| available_equipment = [ | |
| equip for equip, available in st.session_state.cooking_equipment.items() | |
| if available | |
| ] | |
| # Analyze and Generate Recipe | |
| if st.button("Generate Recipe"): | |
| # Analyze image and generate recipe | |
| image_base64 = base64.b64encode(uploaded_file.getvalue()).decode('utf-8') | |
| recipe = analyze_and_generate_recipe( | |
| image_base64, | |
| available_equipment if use_available_equipment else None, | |
| st.session_state.recipe_language | |
| ) | |
| if recipe: | |
| # Store original recipe in session state | |
| st.session_state.original_recipe = recipe | |
| # Display the generated recipe | |
| st.markdown("### π³ Generated Recipe", unsafe_allow_html=True) | |
| st.markdown(f"<div class='highlight-box'>{recipe}</div>", unsafe_allow_html=True) | |
| # Recipe Refinement Section | |
| if st.session_state.original_recipe: | |
| st.markdown("### π§βπ³ Refine Your Recipe", unsafe_allow_html=True) | |
| # Refinement Prompt | |
| user_refinement = st.text_input("Want to modify the recipe? Add your preferences here:") | |
| if st.button("Refine Recipe"): | |
| if user_refinement: | |
| # Refine the recipe | |
| with st.spinner('πͺ Refining your recipe...'): | |
| refined_recipe = refine_recipe( | |
| st.session_state.original_recipe, | |
| user_refinement, | |
| st.session_state.recipe_language | |
| ) | |
| if refined_recipe: | |
| # Display the refined recipe | |
| st.markdown("### π½οΈ Refined Recipe", unsafe_allow_html=True) | |
| st.markdown(f"<div class='highlight-box'>{refined_recipe}</div>", unsafe_allow_html=True) | |
| else: | |
| st.warning("Please enter refinement preferences.") | |
| # # Visual References | |
| # st.markdown("### πΌοΈ Visual References", unsafe_allow_html=True) | |
| # if st.session_state.original_recipe: | |
| # food_name = st.session_state.original_recipe.split('\n')[0] | |
| # image_urls = [google_image_search(food_name) for _ in range(3)] | |
| # cols = st.columns(3) | |
| # for i, url in enumerate(image_urls): | |
| # cols[i].image(url, use_container_width=True) |