Spaces:
Running
Running
Ashhar
commited on
Commit
·
726017c
1
Parent(s):
4a694a1
visual improvements
Browse files
app.py
CHANGED
@@ -1,8 +1,11 @@
|
|
1 |
import base64
|
|
|
|
|
2 |
import streamlit as st
|
3 |
from openai import OpenAI
|
4 |
import os
|
5 |
-
from
|
|
|
6 |
|
7 |
# Load environment variables
|
8 |
from dotenv import load_dotenv
|
@@ -21,43 +24,53 @@ st.set_page_config(
|
|
21 |
initial_sidebar_state="expanded"
|
22 |
)
|
23 |
# Custom CSS for styling
|
24 |
-
st.markdown(
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
}
|
38 |
-
.highlight-box
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
61 |
|
62 |
# Initialize session state
|
63 |
if "ipAddress" not in st.session_state:
|
@@ -84,75 +97,150 @@ def google_image_search(query):
|
|
84 |
return "https://via.placeholder.com/300x200.png?text=" + query.replace(" ", "+")
|
85 |
|
86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
def analyze_and_generate_recipe(uploaded_image, available_equipment=None, language='English'):
|
88 |
"""Analyze food image and generate recipe in a single LLM call"""
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
When analyzing a food image, provide a comprehensive recipe in {language} that considers:
|
97 |
-
1. Detailed food description
|
98 |
-
2. Complete ingredient list
|
99 |
-
3. Cooking method
|
100 |
-
4. Step-by-step instructions"""
|
101 |
-
},
|
102 |
-
{
|
103 |
-
"role": "user",
|
104 |
-
"content": [
|
105 |
-
{
|
106 |
-
"type": "image_url",
|
107 |
-
"image_url": {"url": f"data:image/jpeg;base64,{uploaded_image}"}
|
108 |
-
},
|
109 |
-
{
|
110 |
-
"type": "text",
|
111 |
-
"text": f"""Analyze this food image and generate a detailed recipe in {language}.
|
112 |
-
|
113 |
-
{'Available cooking equipment: ' + ', '.join(available_equipment) if available_equipment else 'No equipment restrictions'}
|
114 |
-
|
115 |
-
If specific equipment is available, prioritize cooking methods that use those tools.
|
116 |
-
|
117 |
-
Provide:
|
118 |
-
- Detailed food description
|
119 |
-
- Ingredient list
|
120 |
-
- Cooking method adapted to available equipment
|
121 |
-
- Precise, step-by-step instructions
|
122 |
-
- Difficulty level
|
123 |
-
- Estimated cooking time
|
124 |
-
|
125 |
-
Use markdown formatting for clear presentation."""
|
126 |
-
}
|
127 |
-
]
|
128 |
-
}
|
129 |
-
]
|
130 |
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
model=model,
|
142 |
-
messages=messages
|
143 |
-
)
|
144 |
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
156 |
|
157 |
|
158 |
def refine_recipe(original_recipe, user_refinement, language='English'):
|
@@ -241,7 +329,16 @@ img_source = st.radio("Choose image source:", ["Upload from device", "Take a pho
|
|
241 |
if img_source == "Upload from device":
|
242 |
uploaded_file = st.file_uploader("Choose an image...", type=['jpg', 'jpeg', 'png'])
|
243 |
else:
|
244 |
-
uploaded_file = st.camera_input(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
245 |
|
246 |
# Food Analysis and Recipe Generation
|
247 |
if uploaded_file is not None:
|
@@ -305,12 +402,12 @@ if uploaded_file is not None:
|
|
305 |
else:
|
306 |
st.warning("Please enter refinement preferences.")
|
307 |
|
308 |
-
# Visual References
|
309 |
-
st.markdown("### 🖼️ Visual References", unsafe_allow_html=True)
|
310 |
-
if st.session_state.original_recipe:
|
311 |
-
|
312 |
-
|
313 |
|
314 |
-
|
315 |
-
|
316 |
-
|
|
|
1 |
import base64
|
2 |
+
import io
|
3 |
+
import time
|
4 |
import streamlit as st
|
5 |
from openai import OpenAI
|
6 |
import os
|
7 |
+
from PIL import Image
|
8 |
+
from utils import pprint, getFontsUrl
|
9 |
|
10 |
# Load environment variables
|
11 |
from dotenv import load_dotenv
|
|
|
24 |
initial_sidebar_state="expanded"
|
25 |
)
|
26 |
# Custom CSS for styling
|
27 |
+
st.markdown(
|
28 |
+
f"""
|
29 |
+
<head>
|
30 |
+
<link href="{getFontsUrl()}" rel="stylesheet">
|
31 |
+
</head>
|
32 |
+
""" """
|
33 |
+
<style>
|
34 |
+
h1 {
|
35 |
+
font-family: 'Whisper';
|
36 |
+
}
|
37 |
+
.big-font {
|
38 |
+
font-size:20px !important;
|
39 |
+
color: #2C3E50;
|
40 |
+
}
|
41 |
+
.highlight-box {
|
42 |
+
background-color: #F5F5F5; /* Soft light gray */
|
43 |
+
border-radius: 10px;
|
44 |
+
padding: 20px;
|
45 |
+
margin-bottom: 20px;
|
46 |
+
color: #333333; /* Very dark gray for text */
|
47 |
+
border: 1px solid #E0E0E0; /* Subtle border */
|
48 |
+
}
|
49 |
+
.highlight-box h1 {
|
50 |
+
color: #1A5F7A; /* Deep teal for main heading */
|
51 |
+
font-size: 24px;
|
52 |
+
margin-bottom: 15px;
|
53 |
+
}
|
54 |
+
.highlight-box h2 {
|
55 |
+
color: #2C7DA0; /* Slightly lighter teal for subheadings */
|
56 |
+
font-size: 20px;
|
57 |
+
margin-top: 15px;
|
58 |
+
margin-bottom: 10px;
|
59 |
+
}
|
60 |
+
.highlight-box h3 {
|
61 |
+
color: #468FAF; /* Even lighter teal for smaller headings */
|
62 |
+
font-size: 18px;
|
63 |
+
margin-top: 10px;
|
64 |
+
margin-bottom: 8px;
|
65 |
+
}
|
66 |
+
.highlight-box p {
|
67 |
+
color: #333333; /* Dark gray for paragraphs */
|
68 |
+
line-height: 1.6;
|
69 |
+
}
|
70 |
+
</style>
|
71 |
+
""",
|
72 |
+
unsafe_allow_html=True
|
73 |
+
)
|
74 |
|
75 |
# Initialize session state
|
76 |
if "ipAddress" not in st.session_state:
|
|
|
97 |
return "https://via.placeholder.com/300x200.png?text=" + query.replace(" ", "+")
|
98 |
|
99 |
|
100 |
+
def resize_image(image_base64, max_size=1024):
|
101 |
+
"""
|
102 |
+
Resize an image from base64 to max dimension of 1024 pixels while maintaining aspect ratio
|
103 |
+
|
104 |
+
Args:
|
105 |
+
image_base64 (str): Base64 encoded image
|
106 |
+
max_size (int): Maximum dimension for the image
|
107 |
+
|
108 |
+
Returns:
|
109 |
+
str: Resized image as base64 encoded string
|
110 |
+
"""
|
111 |
+
# Decode base64 image
|
112 |
+
image_bytes = base64.b64decode(image_base64)
|
113 |
+
|
114 |
+
# Open image with Pillow
|
115 |
+
img = Image.open(io.BytesIO(image_bytes))
|
116 |
+
|
117 |
+
# Calculate resize ratio
|
118 |
+
width, height = img.size
|
119 |
+
resize_ratio = min(max_size / width, max_size / height)
|
120 |
+
|
121 |
+
# If image is already smaller than max_size, return original
|
122 |
+
if resize_ratio >= 1:
|
123 |
+
return image_base64
|
124 |
+
|
125 |
+
# Calculate new dimensions
|
126 |
+
new_width = int(width * resize_ratio)
|
127 |
+
new_height = int(height * resize_ratio)
|
128 |
+
|
129 |
+
# Resize image
|
130 |
+
resized_img = img.resize((new_width, new_height), Image.LANCZOS)
|
131 |
+
|
132 |
+
# Convert back to base64
|
133 |
+
buffered = io.BytesIO()
|
134 |
+
resized_img.save(buffered, format=img.format)
|
135 |
+
return base64.b64encode(buffered.getvalue()).decode('utf-8')
|
136 |
+
|
137 |
+
|
138 |
def analyze_and_generate_recipe(uploaded_image, available_equipment=None, language='English'):
|
139 |
"""Analyze food image and generate recipe in a single LLM call"""
|
140 |
+
progress_stages = [
|
141 |
+
{"message": "🔍 Scanning the delicious image...", "progress": 10},
|
142 |
+
{"message": "🧐 Identifying culinary ingredients...", "progress": 30},
|
143 |
+
{"message": "🍳 Consulting virtual chef's expertise...", "progress": 50},
|
144 |
+
{"message": "📝 Crafting personalized recipe...", "progress": 70},
|
145 |
+
{"message": "🌟 Finalizing gourmet instructions...", "progress": 90}
|
146 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
147 |
|
148 |
+
# Create a progress bar
|
149 |
+
progress_bar = st.progress(0)
|
150 |
+
status_text = st.empty()
|
151 |
+
|
152 |
+
try:
|
153 |
+
# Update progress stages
|
154 |
+
for stage in progress_stages:
|
155 |
+
status_text.text(stage["message"])
|
156 |
+
progress_bar.progress(stage["progress"])
|
157 |
+
time.sleep(1) # Short delay between stages
|
|
|
|
|
|
|
158 |
|
159 |
+
# Resize the image before sending to LLM
|
160 |
+
resized_image_base64 = resize_image(uploaded_image)
|
161 |
+
|
162 |
+
# Prepare the system and user messages
|
163 |
+
messages = [
|
164 |
+
{
|
165 |
+
"role": "system",
|
166 |
+
"content": f"""You are a professional chef and food analyst.
|
167 |
+
When analyzing a food image, provide a comprehensive recipe in {language} that considers:
|
168 |
+
1. Detailed food description
|
169 |
+
2. Complete ingredient list
|
170 |
+
3. Cooking method
|
171 |
+
4. Step-by-step instructions"""
|
172 |
+
},
|
173 |
+
{
|
174 |
+
"role": "user",
|
175 |
+
"content": [
|
176 |
+
{
|
177 |
+
"type": "image_url",
|
178 |
+
"image_url": {"url": f"data:image/jpeg;base64,{resized_image_base64}"}
|
179 |
+
},
|
180 |
+
{
|
181 |
+
"type": "text",
|
182 |
+
"text": f"""Analyze this food image and generate a detailed recipe in {language}.
|
183 |
+
|
184 |
+
{'Available cooking equipment: ' + ', '.join(available_equipment) if available_equipment else 'No equipment restrictions'}
|
185 |
+
|
186 |
+
If specific equipment is available, prioritize cooking methods that use those tools.
|
187 |
+
|
188 |
+
Provide:
|
189 |
+
- Detailed food description
|
190 |
+
- Ingredient list
|
191 |
+
- Cooking method adapted to available equipment
|
192 |
+
- Precise, step-by-step instructions
|
193 |
+
- Difficulty level
|
194 |
+
- Estimated cooking time
|
195 |
+
|
196 |
+
Use markdown formatting for clear presentation."""
|
197 |
+
}
|
198 |
+
]
|
199 |
+
}
|
200 |
+
]
|
201 |
+
|
202 |
+
# Log API call details
|
203 |
+
pprint({
|
204 |
+
"function": "analyze_and_generate_recipe",
|
205 |
+
"model": model,
|
206 |
+
"language": language,
|
207 |
+
"available_equipment": available_equipment
|
208 |
+
})
|
209 |
+
|
210 |
+
# Make the LLM call
|
211 |
+
status_text.text("🚀 Generating recipe with AI...")
|
212 |
+
progress_bar.progress(95)
|
213 |
+
|
214 |
+
response = client.chat.completions.create(
|
215 |
+
model=model,
|
216 |
+
messages=messages
|
217 |
+
)
|
218 |
+
|
219 |
+
# Final progress update
|
220 |
+
status_text.text("✅ Recipe generated successfully!")
|
221 |
+
progress_bar.progress(100)
|
222 |
+
|
223 |
+
# Clear the progress bar and status text after a short delay
|
224 |
+
time.sleep(1)
|
225 |
+
progress_bar.empty()
|
226 |
+
status_text.empty()
|
227 |
+
|
228 |
+
# Log response details
|
229 |
+
pprint({
|
230 |
+
"function": "analyze_and_generate_recipe_response",
|
231 |
+
"tokens_used": response.usage.total_tokens if response.usage else None,
|
232 |
+
"response_length": len(response.choices[0].message.content)
|
233 |
+
})
|
234 |
+
|
235 |
+
return response.choices[0].message.content
|
236 |
+
|
237 |
+
except Exception as e:
|
238 |
+
# Clear progress indicators in case of error
|
239 |
+
progress_bar.empty()
|
240 |
+
status_text.empty()
|
241 |
+
|
242 |
+
st.error(f"Error analyzing image and generating recipe: {e}")
|
243 |
+
return None
|
244 |
|
245 |
|
246 |
def refine_recipe(original_recipe, user_refinement, language='English'):
|
|
|
329 |
if img_source == "Upload from device":
|
330 |
uploaded_file = st.file_uploader("Choose an image...", type=['jpg', 'jpeg', 'png'])
|
331 |
else:
|
332 |
+
uploaded_file = st.camera_input(
|
333 |
+
"Take a photo of your dish",
|
334 |
+
help="Please hold your device vertically for best results",
|
335 |
+
# Set aspect ratio to portrait (3:4)
|
336 |
+
key="portrait_camera",
|
337 |
+
args={
|
338 |
+
"landscape": False, # Force portrait mode
|
339 |
+
"aspectRatio": 3 / 4 # Portrait aspect ratio
|
340 |
+
}
|
341 |
+
)
|
342 |
|
343 |
# Food Analysis and Recipe Generation
|
344 |
if uploaded_file is not None:
|
|
|
402 |
else:
|
403 |
st.warning("Please enter refinement preferences.")
|
404 |
|
405 |
+
# # Visual References
|
406 |
+
# st.markdown("### 🖼️ Visual References", unsafe_allow_html=True)
|
407 |
+
# if st.session_state.original_recipe:
|
408 |
+
# food_name = st.session_state.original_recipe.split('\n')[0]
|
409 |
+
# image_urls = [google_image_search(food_name) for _ in range(3)]
|
410 |
|
411 |
+
# cols = st.columns(3)
|
412 |
+
# for i, url in enumerate(image_urls):
|
413 |
+
# cols[i].image(url, use_container_width=True)
|
utils.py
CHANGED
@@ -3,6 +3,25 @@ import pytz
|
|
3 |
import streamlit as st
|
4 |
|
5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
def __nowInIST() -> DT.datetime:
|
7 |
return DT.datetime.now(pytz.timezone("Asia/Kolkata"))
|
8 |
|
@@ -10,4 +29,13 @@ def __nowInIST() -> DT.datetime:
|
|
10 |
def pprint(log: str):
|
11 |
now = __nowInIST()
|
12 |
now = now.strftime("%Y-%m-%d %H:%M:%S")
|
13 |
-
print(f"[{now}] [{st.session_state.ipAddress}] {log}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
import streamlit as st
|
4 |
|
5 |
|
6 |
+
FONTS = [
|
7 |
+
# "Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900",
|
8 |
+
# "Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900",
|
9 |
+
# "Raleway:ital,wght@0,100..900;1,100..900",
|
10 |
+
# "Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900",
|
11 |
+
# "Nunito:ital,wght@0,200..1000;1,200..1000",
|
12 |
+
# "Quicksand:[email protected]",
|
13 |
+
"Montserrat:ital,wght@0,100..900;1,100..900",
|
14 |
+
# "Edu+AU+VIC+WA+NT+Dots:[email protected]",
|
15 |
+
"Whisper",
|
16 |
+
# "Merienda:[email protected]",
|
17 |
+
"Playwrite+DE+Grund:[email protected]",
|
18 |
+
# "Roboto+Slab:[email protected]",
|
19 |
+
# "Open+Sans:ital,wght@0,300..800;1,300..800",
|
20 |
+
# "Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000",
|
21 |
+
# "Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700",
|
22 |
+
]
|
23 |
+
|
24 |
+
|
25 |
def __nowInIST() -> DT.datetime:
|
26 |
return DT.datetime.now(pytz.timezone("Asia/Kolkata"))
|
27 |
|
|
|
29 |
def pprint(log: str):
|
30 |
now = __nowInIST()
|
31 |
now = now.strftime("%Y-%m-%d %H:%M:%S")
|
32 |
+
print(f"[{now}] [{st.session_state.ipAddress}] {log}")
|
33 |
+
|
34 |
+
|
35 |
+
def getFontsUrl():
|
36 |
+
baseLink = "https://fonts.googleapis.com/css2"
|
37 |
+
params = "&".join([f"family={font}" for font in FONTS])
|
38 |
+
params = f"{params}&display=swap"
|
39 |
+
fontsUrl = f"{baseLink}?{params}"
|
40 |
+
# pprint(f"{fontsUrl=}")
|
41 |
+
return fontsUrl
|