magic-recipe / app.py
Ashhar
round off resizing numbers
e101a7b
raw
history blame
15 kB
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;
}
.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)
pprint({
"function": "resize_image",
"original_size_bytes": original_size,
"original_size_kb": round(original_size / 1024)
})
# 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 Decoder")
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)