Spaces:
Running
Running
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) |