|
import gradio as gr |
|
import base64 |
|
import requests |
|
import io |
|
from PIL import Image |
|
import json |
|
import os |
|
from together import Together |
|
import tempfile |
|
import uuid |
|
|
|
def encode_image_to_base64(image_path): |
|
"""Convert image to base64 encoding""" |
|
with open(image_path, "rb") as image_file: |
|
return base64.b64encode(image_file.read()).decode('utf-8') |
|
|
|
def save_uploaded_image(image): |
|
"""Save uploaded image to a temporary file and return the path""" |
|
if image is None: |
|
return None |
|
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as temp_file: |
|
if isinstance(image, dict) and "path" in image: |
|
|
|
with open(image["path"], "rb") as img_file: |
|
temp_file.write(img_file.read()) |
|
elif isinstance(image, Image.Image): |
|
|
|
image.save(temp_file.name, format="JPEG") |
|
else: |
|
|
|
try: |
|
Image.open(image).save(temp_file.name, format="JPEG") |
|
except Exception: |
|
return None |
|
|
|
return temp_file.name |
|
|
|
def get_recipe_suggestions(api_key, images, num_recipes=3, dietary_restrictions="None", cuisine_preference="Any"): |
|
""" |
|
Get recipe suggestions based on the uploaded images of ingredients |
|
""" |
|
if not api_key: |
|
return "Please provide your Together API key." |
|
|
|
if not images or len(images) == 0 or all(img is None for img in images): |
|
return "Please upload at least one image of ingredients." |
|
|
|
|
|
valid_images = [img for img in images if img is not None] |
|
|
|
if len(valid_images) == 0: |
|
return "No valid images were uploaded. Please try again." |
|
|
|
|
|
image_paths = [] |
|
for img in valid_images: |
|
img_path = save_uploaded_image(img) |
|
if img_path: |
|
image_paths.append(img_path) |
|
|
|
if not image_paths: |
|
return "Failed to process the uploaded images." |
|
|
|
try: |
|
|
|
client = Together(api_key=api_key) |
|
|
|
|
|
system_prompt = """You are a culinary expert AI assistant that specializes in creating recipes based on available ingredients. |
|
Analyze the provided images of ingredients and suggest creative, detailed recipes that use as many of the shown ingredients as possible. |
|
|
|
For each recipe suggestion, include: |
|
1. Recipe name |
|
2. Brief description of the dish |
|
3. Complete ingredients list (including estimated quantities and any additional staple ingredients that might be needed) |
|
4. Step-by-step cooking instructions |
|
5. Approximate cooking time |
|
6. Difficulty level (Easy, Medium, Advanced) |
|
7. Nutritional highlights |
|
|
|
Consider any dietary restrictions and cuisine preferences mentioned by the user.""" |
|
|
|
user_prompt = f"""Based on the ingredients shown in these images, suggest {num_recipes} creative and delicious recipes. |
|
Dietary restrictions to consider: {dietary_restrictions} |
|
Cuisine preference: {cuisine_preference} |
|
Please be specific about what ingredients you can identify in the images and creative with your recipe suggestions. Try to use ingredients from all images if possible.""" |
|
|
|
|
|
content = [{"type": "text", "text": user_prompt}] |
|
|
|
|
|
for img_path in image_paths: |
|
content.append({ |
|
"type": "image_url", |
|
"image_url": { |
|
"url": f"file://{img_path}" |
|
} |
|
}) |
|
|
|
response = client.chat.completions.create( |
|
model="meta-llama/Llama-Vision-Free", |
|
messages=[ |
|
{ |
|
"role": "system", |
|
"content": system_prompt |
|
}, |
|
{ |
|
"role": "user", |
|
"content": content |
|
} |
|
], |
|
max_tokens=2048, |
|
temperature=0.7 |
|
) |
|
|
|
|
|
for img_path in image_paths: |
|
try: |
|
os.unlink(img_path) |
|
except: |
|
pass |
|
|
|
return response.choices[0].message.content |
|
|
|
except Exception as e: |
|
|
|
for img_path in image_paths: |
|
try: |
|
os.unlink(img_path) |
|
except: |
|
pass |
|
return f"Error: {str(e)}" |
|
|
|
|
|
custom_css = """ |
|
:root { |
|
--primary-color: #FF6B6B; |
|
--secondary-color: #4ECDC4; |
|
--accent-color: #FFD166; |
|
--background-color: #f8f9fa; |
|
--text-color: #212529; |
|
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
--border-radius: 10px; |
|
--font-family: 'Poppins', sans-serif; |
|
} |
|
|
|
body { |
|
font-family: var(--font-family); |
|
background-color: var(--background-color); |
|
color: var(--text-color); |
|
} |
|
|
|
.container { |
|
max-width: 1200px; |
|
margin: 0 auto; |
|
padding: 20px; |
|
} |
|
|
|
.app-header { |
|
text-align: center; |
|
margin-bottom: 30px; |
|
padding: 30px 0; |
|
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); |
|
border-radius: var(--border-radius); |
|
color: white; |
|
box-shadow: var(--card-shadow); |
|
} |
|
|
|
.app-title { |
|
font-size: 3em; |
|
margin-bottom: 10px; |
|
font-weight: bold; |
|
} |
|
|
|
.app-subtitle { |
|
font-size: 1.2em; |
|
opacity: 0.9; |
|
max-width: 700px; |
|
margin: 0 auto; |
|
} |
|
|
|
.input-section, .output-section { |
|
background-color: white; |
|
border-radius: var(--border-radius); |
|
padding: 25px; |
|
box-shadow: var(--card-shadow); |
|
margin-bottom: 20px; |
|
} |
|
|
|
.input-section h3, .output-section h3 { |
|
color: var(--primary-color); |
|
margin-top: 0; |
|
font-size: 1.5em; |
|
border-bottom: 2px solid var(--secondary-color); |
|
padding-bottom: 10px; |
|
margin-bottom: 20px; |
|
} |
|
|
|
.image-upload-container { |
|
border: 2px dashed var(--secondary-color); |
|
border-radius: var(--border-radius); |
|
padding: 20px; |
|
text-align: center; |
|
margin-bottom: 20px; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.image-upload-container:hover { |
|
border-color: var(--primary-color); |
|
background-color: rgba(255, 107, 107, 0.05); |
|
} |
|
|
|
button.primary-button { |
|
background: linear-gradient(135deg, var(--primary-color) 0%, #FF8E8E 100%); |
|
color: white; |
|
border: none; |
|
padding: 12px 25px; |
|
border-radius: 30px; |
|
font-size: 1.1em; |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
box-shadow: 0 2px 5px rgba(255, 107, 107, 0.3); |
|
font-weight: bold; |
|
display: block; |
|
width: 100%; |
|
margin-top: 20px; |
|
} |
|
|
|
button.primary-button:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 4px 8px rgba(255, 107, 107, 0.4); |
|
background: linear-gradient(135deg, #FF8E8E 0%, var(--primary-color) 100%); |
|
} |
|
|
|
.gradio-slider.svelte-17l1npl { |
|
margin-bottom: 20px; |
|
} |
|
|
|
.tab-nav { |
|
background-color: var(--secondary-color); |
|
border-radius: var(--border-radius) var(--border-radius) 0 0; |
|
} |
|
|
|
.tab-nav button { |
|
color: white; |
|
font-weight: bold; |
|
} |
|
|
|
.recipe-card { |
|
border-left: 5px solid var(--accent-color); |
|
padding: 15px; |
|
background-color: #f9f9f9; |
|
margin-bottom: 15px; |
|
border-radius: 0 var(--border-radius) var(--border-radius) 0; |
|
} |
|
|
|
.recipe-title { |
|
color: var(--primary-color); |
|
font-size: 1.3em; |
|
margin-bottom: 5px; |
|
} |
|
|
|
.footer { |
|
text-align: center; |
|
margin-top: 40px; |
|
color: #6c757d; |
|
font-size: 0.9em; |
|
} |
|
|
|
.icon { |
|
color: var(--primary-color); |
|
margin-right: 5px; |
|
} |
|
|
|
.input-group { |
|
margin-bottom: 20px; |
|
} |
|
|
|
.input-group label { |
|
display: block; |
|
margin-bottom: 8px; |
|
font-weight: 600; |
|
color: var(--text-color); |
|
} |
|
|
|
.gallery-item { |
|
border-radius: var(--border-radius); |
|
overflow: hidden; |
|
box-shadow: var(--card-shadow); |
|
transition: transform 0.3s ease; |
|
} |
|
|
|
.gallery-item:hover { |
|
transform: scale(1.02); |
|
} |
|
|
|
.loading-spinner { |
|
text-align: center; |
|
padding: 20px; |
|
} |
|
|
|
/* Responsive styles */ |
|
@media (max-width: 768px) { |
|
.app-title { |
|
font-size: 2em; |
|
} |
|
|
|
.input-section, .output-section { |
|
padding: 15px; |
|
} |
|
} |
|
|
|
/* Custom styling for the API key input */ |
|
input[type="password"] { |
|
border: 2px solid #e9ecef; |
|
border-radius: var(--border-radius); |
|
padding: 10px 15px; |
|
font-size: 1em; |
|
width: 100%; |
|
transition: border-color 0.3s ease; |
|
} |
|
|
|
input[type="password"]:focus { |
|
border-color: var(--secondary-color); |
|
outline: none; |
|
} |
|
|
|
/* Custom dropdown styling */ |
|
select { |
|
appearance: none; |
|
background: white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23FF6B6B' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E") no-repeat right 10px center; |
|
border: 2px solid #e9ecef; |
|
border-radius: var(--border-radius); |
|
padding: 10px 40px 10px 15px; |
|
font-size: 1em; |
|
width: 100%; |
|
transition: border-color 0.3s ease; |
|
} |
|
|
|
select:focus { |
|
border-color: var(--secondary-color); |
|
outline: none; |
|
} |
|
|
|
/* Remove Gradio branding */ |
|
.gradio-container { |
|
max-width: 100% !important; |
|
} |
|
|
|
.footer-logo, .footer-links { |
|
display: none !important; |
|
} |
|
""" |
|
|
|
|
|
html_header = """ |
|
<div class="app-header"> |
|
<div class="app-title">π² Visual Recipe Assistant</div> |
|
<div class="app-subtitle">Upload images of ingredients you have on hand and get personalized recipe suggestions powered by AI</div> |
|
</div> |
|
""" |
|
|
|
|
|
html_footer = """ |
|
<div class="footer"> |
|
<p>π§ͺ Powered by Meta's Llama-Vision-Free Model & Together AI</p> |
|
<p>πΈ Upload multiple ingredient images for more creative recipe combinations</p> |
|
</div> |
|
""" |
|
|
|
|
|
with gr.Blocks(css=custom_css) as app: |
|
gr.HTML(html_header) |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=1): |
|
with gr.Box(elem_classes="input-section"): |
|
gr.HTML("<h3>π API Configuration</h3>") |
|
api_key_input = gr.Textbox( |
|
label="Together API Key", |
|
placeholder="Enter your Together API key here...", |
|
type="password", |
|
elem_classes="input-group" |
|
) |
|
|
|
gr.HTML("<h3>π· Upload Ingredients</h3>") |
|
image_input = gr.Gallery( |
|
label="", |
|
elem_id="ingredient-gallery", |
|
elem_classes="gallery-container", |
|
columns=3, |
|
rows=2, |
|
height="auto", |
|
object_fit="contain" |
|
) |
|
|
|
|
|
file_upload = gr.File( |
|
label="Upload images of ingredients", |
|
file_types=["image"], |
|
file_count="multiple", |
|
elem_classes="image-upload-container" |
|
) |
|
|
|
gr.HTML("<h3>βοΈ Recipe Preferences</h3>") |
|
with gr.Row(): |
|
num_recipes = gr.Slider( |
|
minimum=1, |
|
maximum=5, |
|
value=3, |
|
step=1, |
|
label="Number of Recipe Suggestions", |
|
elem_classes="input-group" |
|
) |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
dietary_restrictions = gr.Dropdown( |
|
choices=["None", "Vegetarian", "Vegan", "Gluten-Free", "Dairy-Free", "Low-Carb", "Keto", "Paleo"], |
|
value="None", |
|
label="Dietary Restrictions", |
|
elem_classes="input-group" |
|
) |
|
|
|
with gr.Column(): |
|
cuisine_preference = gr.Dropdown( |
|
choices=["Any", "Italian", "Asian", "Mexican", "Mediterranean", "Indian", "American", "French", "Middle Eastern"], |
|
value="Any", |
|
label="Cuisine Preference", |
|
elem_classes="input-group" |
|
) |
|
|
|
submit_button = gr.Button("Get Recipe Suggestions", elem_classes="primary-button") |
|
|
|
with gr.Column(scale=1): |
|
with gr.Box(elem_classes="output-section"): |
|
gr.HTML("<h3>π½οΈ Your Personalized Recipes</h3>") |
|
output = gr.Markdown(elem_classes="recipe-output") |
|
|
|
gr.HTML(html_footer) |
|
|
|
|
|
def update_gallery(files): |
|
if not files: |
|
return None |
|
return [file.name for file in files] |
|
|
|
file_upload.change(fn=update_gallery, inputs=file_upload, outputs=image_input) |
|
|
|
|
|
def process_recipe_request(api_key, files, num_recipes, dietary_restrictions, cuisine_preference): |
|
if not files: |
|
return "Please upload at least one image of ingredients." |
|
|
|
|
|
images = [file.name for file in files] |
|
return get_recipe_suggestions(api_key, images, num_recipes, dietary_restrictions, cuisine_preference) |
|
|
|
|
|
submit_button.click( |
|
fn=process_recipe_request, |
|
inputs=[api_key_input, file_upload, num_recipes, dietary_restrictions, cuisine_preference], |
|
outputs=output |
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
app.launch() |