|
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 |
|
import time |
|
|
|
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 analyze_single_image(client, img_path): |
|
"""Analyze a single image to identify ingredients""" |
|
system_prompt = """You are a culinary expert AI assistant that specializes in identifying ingredients in images. |
|
Your task is to analyze the provided image and list all the food ingredients you can identify. |
|
Be specific and detailed about what you see. Only list ingredients, don't suggest recipes yet.""" |
|
|
|
user_prompt = "Please identify all the food ingredients visible in this image. List each ingredient on a new line." |
|
|
|
try: |
|
with open(img_path, "rb") as image_file: |
|
base64_image = base64.b64encode(image_file.read()).decode('utf-8') |
|
|
|
content = [ |
|
{"type": "text", "text": user_prompt}, |
|
{ |
|
"type": "image_url", |
|
"image_url": { |
|
"url": f"data:image/jpeg;base64,{base64_image}" |
|
} |
|
} |
|
] |
|
|
|
response = client.chat.completions.create( |
|
model="meta-llama/Llama-Vision-Free", |
|
messages=[ |
|
{"role": "system", "content": system_prompt}, |
|
{"role": "user", "content": content} |
|
], |
|
max_tokens=500, |
|
temperature=0.2 |
|
) |
|
|
|
return response.choices[0].message.content |
|
except Exception as e: |
|
return f"Error analyzing image: {str(e)}" |
|
|
|
def format_ingredients_html(all_ingredients): |
|
"""Format the identified ingredients in HTML""" |
|
html_content = """ |
|
<div class="ingredients-identified"> |
|
<h2>π Ingredients Identified</h2> |
|
""" |
|
|
|
for i, ingredients in enumerate(all_ingredients): |
|
html_content += f""" |
|
<div class="ingredient-group"> |
|
<h3>Image {i+1} Ingredients</h3> |
|
<ul class="ingredient-list"> |
|
""" |
|
|
|
|
|
ingredient_lines = ingredients.strip().split('\n') |
|
for line in ingredient_lines: |
|
if line.strip(): |
|
html_content += f"<li>{line.strip()}</li>\n" |
|
|
|
html_content += """ |
|
</ul> |
|
</div> |
|
""" |
|
|
|
html_content += "</div>" |
|
return html_content |
|
|
|
def format_recipe_to_html(recipe_text): |
|
"""Convert the recipe text to formatted HTML""" |
|
|
|
html_content = """ |
|
<div class="recipe-suggestions"> |
|
<h2>π½οΈ Recipe Suggestions</h2> |
|
""" |
|
|
|
|
|
recipe_sections = [] |
|
current_recipe = "" |
|
lines = recipe_text.split('\n') |
|
|
|
|
|
for line in lines: |
|
if line.strip().startswith(("Recipe ", "# ", "## ", "### ")): |
|
|
|
if current_recipe: |
|
recipe_sections.append(current_recipe) |
|
current_recipe = "" |
|
|
|
|
|
current_recipe += line + "\n" |
|
|
|
|
|
if current_recipe: |
|
recipe_sections.append(current_recipe) |
|
|
|
|
|
for recipe in recipe_sections: |
|
if not recipe.strip(): |
|
continue |
|
|
|
html_content += '<div class="recipe-card">' |
|
|
|
lines = recipe.split('\n') |
|
in_ingredients = False |
|
in_instructions = False |
|
|
|
for line in lines: |
|
|
|
if any(x in line.lower() for x in ["recipe ", "# recipe"]) or line.strip().startswith(("# ", "## ")): |
|
title = line.replace("#", "").replace("Recipe:", "").replace("Recipe", "").strip() |
|
html_content += f'<h3 class="recipe-title">{title}</h3>\n' |
|
continue |
|
|
|
|
|
if "description" in line.lower() and ":" in line: |
|
description = line.split(":", 1)[1].strip() |
|
html_content += f'<p class="recipe-description">{description}</p>\n' |
|
continue |
|
|
|
|
|
if "ingredients" in line.lower() and not in_ingredients: |
|
in_ingredients = True |
|
in_instructions = False |
|
html_content += '<div class="recipe-ingredients">\n' |
|
html_content += '<h4>Ingredients</h4>\n<ul>\n' |
|
continue |
|
|
|
|
|
if any(x in line.lower() for x in ["instructions", "directions", "steps", "preparation"]) and not in_instructions: |
|
if in_ingredients: |
|
html_content += '</ul>\n</div>\n' |
|
in_ingredients = False |
|
|
|
in_instructions = True |
|
html_content += '<div class="recipe-instructions">\n' |
|
html_content += '<h4>Instructions</h4>\n<ol>\n' |
|
continue |
|
|
|
|
|
if "cooking time" in line.lower() or "prep time" in line.lower() or "time" in line.lower(): |
|
if in_ingredients: |
|
html_content += '</ul>\n</div>\n' |
|
in_ingredients = False |
|
if in_instructions: |
|
html_content += '</ol>\n</div>\n' |
|
in_instructions = False |
|
|
|
time_info = line.strip() |
|
html_content += f'<p class="recipe-time"><strong>β±οΈ {time_info}</strong></p>\n' |
|
continue |
|
|
|
|
|
if "difficulty" in line.lower(): |
|
difficulty = line.strip() |
|
html_content += f'<p class="recipe-difficulty"><strong>π {difficulty}</strong></p>\n' |
|
continue |
|
|
|
|
|
if "nutritional" in line.lower(): |
|
if in_ingredients: |
|
html_content += '</ul>\n</div>\n' |
|
in_ingredients = False |
|
if in_instructions: |
|
html_content += '</ol>\n</div>\n' |
|
in_instructions = False |
|
|
|
html_content += '<div class="recipe-nutrition">\n' |
|
html_content += f'<h4>{line.strip()}</h4>\n<ul>\n' |
|
continue |
|
|
|
|
|
if in_ingredients and line.strip() and not line.lower().startswith(("ingredients", "instructions")): |
|
item = line.strip() |
|
if item.startswith("- "): |
|
item = item[2:] |
|
elif item.startswith("* "): |
|
item = item[2:] |
|
|
|
if item: |
|
html_content += f'<li>{item}</li>\n' |
|
continue |
|
|
|
|
|
if in_instructions and line.strip() and not line.lower().startswith(("ingredients", "instructions")): |
|
step = line.strip() |
|
if step.startswith("- "): |
|
step = step[2:] |
|
elif step.startswith("* "): |
|
step = step[2:] |
|
elif step.startswith(". "): |
|
step = step[2:] |
|
elif step[0].isdigit() and step[1] in [".", ")"]: |
|
step = step[2:].strip() |
|
|
|
if step: |
|
html_content += f'<li>{step}</li>\n' |
|
continue |
|
|
|
|
|
if "nutritional" in line.lower() and line.strip() and not line.startswith(("ingredients", "instructions")): |
|
item = line.strip() |
|
if item.startswith("- "): |
|
item = item[2:] |
|
elif item.startswith("* "): |
|
item = item[2:] |
|
|
|
if item and not item.lower().startswith("nutritional"): |
|
html_content += f'<li>{item}</li>\n' |
|
continue |
|
|
|
|
|
if in_ingredients: |
|
html_content += '</ul>\n</div>\n' |
|
if in_instructions: |
|
html_content += '</ol>\n</div>\n' |
|
|
|
html_content += '</div>\n' |
|
|
|
html_content += '</div>\n' |
|
|
|
return html_content |
|
|
|
def get_recipe_suggestions(api_key, image_paths, 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 image_paths or len(image_paths) == 0: |
|
return "Please upload at least one image of ingredients." |
|
|
|
try: |
|
client = Together(api_key=api_key) |
|
|
|
all_ingredients = [] |
|
for img_path in image_paths: |
|
ingredients_text = analyze_single_image(client, img_path) |
|
all_ingredients.append(ingredients_text) |
|
|
|
combined_ingredients = "\n\n".join([f"Image {i+1} ingredients:\n{ingredients}" |
|
for i, ingredients in enumerate(all_ingredients)]) |
|
|
|
system_prompt = """You are a culinary expert AI assistant that specializes in creating recipes based on available ingredients. |
|
You will be provided with lists of ingredients identified from multiple images. Your task is to suggest creative, |
|
detailed recipes that use as many of the identified 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 |
|
|
|
Format each recipe clearly with headings for each section. Make sure to separate recipes clearly. |
|
Consider any dietary restrictions and cuisine preferences mentioned by the user.""" |
|
|
|
user_prompt = f"""Based on the following ingredients identified from multiple images, suggest {num_recipes} creative and delicious recipes. |
|
|
|
{combined_ingredients} |
|
|
|
Dietary restrictions to consider: {dietary_restrictions} |
|
Cuisine preference: {cuisine_preference} |
|
|
|
Please be creative with your recipe suggestions and try to use ingredients from multiple images if possible.""" |
|
|
|
response = client.chat.completions.create( |
|
model="meta-llama/Llama-Vision-Free", |
|
messages=[ |
|
{"role": "system", "content": system_prompt}, |
|
{"role": "user", "content": user_prompt} |
|
], |
|
max_tokens=20000, |
|
temperature=0.7 |
|
) |
|
|
|
recipe_text = response.choices[0].message.content |
|
|
|
|
|
ingredients_html = format_ingredients_html(all_ingredients) |
|
recipes_html = format_recipe_to_html(recipe_text) |
|
|
|
|
|
html_content = ingredients_html + "<hr>" + recipes_html |
|
|
|
|
|
full_html = create_downloadable_html(ingredients_html, recipes_html, dietary_restrictions, cuisine_preference) |
|
|
|
|
|
timestamp = int(time.time()) |
|
file_name = f"recipes_{timestamp}.html" |
|
file_path = os.path.join(tempfile.gettempdir(), file_name) |
|
|
|
with open(file_path, "w", encoding="utf-8") as f: |
|
f.write(full_html) |
|
|
|
return [html_content, file_path] |
|
except Exception as e: |
|
return [f"Error: {str(e)}", None] |
|
|
|
def create_downloadable_html(ingredients_html, recipes_html, dietary_restrictions, cuisine_preference): |
|
"""Create a complete HTML document with styling for download""" |
|
html = f"""<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Your Personalized Recipes</title> |
|
<style> |
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); |
|
:root {{ |
|
--primary-color: #FF6F61; |
|
--secondary-color: #4BB543; |
|
--accent-color: #F0A500; |
|
--background-color: #F4F4F9; |
|
--text-color: #333333; |
|
--card-background: #FFFFFF; |
|
--border-radius: 12px; |
|
--box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 20px; |
|
}} |
|
body {{ |
|
font-family: 'Poppins', sans-serif; |
|
background-color: var(--background-color); |
|
color: var(--text-color); |
|
margin: 0; |
|
padding: 0; |
|
line-height: 1.6; |
|
}} |
|
.container {{ |
|
max-width: 1000px; |
|
margin: 0 auto; |
|
padding: 20px; |
|
}} |
|
header {{ |
|
background-color: var(--primary-color); |
|
color: white; |
|
padding: 40px 20px; |
|
text-align: center; |
|
border-radius: 0 0 20px 20px; |
|
margin-bottom: 30px; |
|
}} |
|
h1 {{ |
|
font-size: 2.5em; |
|
margin-bottom: 10px; |
|
}} |
|
.recipe-info {{ |
|
display: flex; |
|
justify-content: center; |
|
gap: 20px; |
|
margin-bottom: 20px; |
|
flex-wrap: wrap; |
|
}} |
|
.info-badge {{ |
|
background-color: rgba(255, 255, 255, 0.2); |
|
padding: 8px 16px; |
|
border-radius: 20px; |
|
font-size: 0.9em; |
|
}} |
|
.ingredients-identified, .recipe-suggestions {{ |
|
background-color: var(--card-background); |
|
border-radius: var(--border-radius); |
|
padding: 25px; |
|
margin-bottom: 30px; |
|
box-shadow: var(--box-shadow); |
|
}} |
|
h2 {{ |
|
color: var(--primary-color); |
|
border-bottom: 2px solid var(--primary-color); |
|
padding-bottom: 10px; |
|
margin-top: 0; |
|
}} |
|
.ingredient-group {{ |
|
margin-bottom: 20px; |
|
}} |
|
h3 {{ |
|
color: var(--accent-color); |
|
margin-bottom: 10px; |
|
}} |
|
.ingredient-list {{ |
|
list-style-type: disc; |
|
padding-left: 20px; |
|
}} |
|
.recipe-card {{ |
|
background-color: #f9f9f9; |
|
border-radius: var(--border-radius); |
|
padding: 20px; |
|
margin-bottom: 30px; |
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); |
|
}} |
|
.recipe-title {{ |
|
color: var(--secondary-color); |
|
font-size: 1.8em; |
|
margin-top: 0; |
|
margin-bottom: 15px; |
|
}} |
|
.recipe-description {{ |
|
font-style: italic; |
|
margin-bottom: 20px; |
|
color: #666; |
|
}} |
|
.recipe-ingredients, .recipe-instructions, .recipe-nutrition {{ |
|
margin-bottom: 20px; |
|
}} |
|
.recipe-ingredients h4, .recipe-instructions h4, .recipe-nutrition h4 {{ |
|
color: var(--primary-color); |
|
margin-bottom: 10px; |
|
}} |
|
.recipe-ingredients ul {{ |
|
list-style-type: disc; |
|
}} |
|
.recipe-instructions ol {{ |
|
padding-left: 20px; |
|
}} |
|
.recipe-instructions li {{ |
|
margin-bottom: 10px; |
|
}} |
|
.recipe-time, .recipe-difficulty {{ |
|
color: #666; |
|
margin: 10px 0; |
|
}} |
|
hr {{ |
|
border: 0; |
|
height: 1px; |
|
background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)); |
|
margin: 30px 0; |
|
}} |
|
footer {{ |
|
text-align: center; |
|
margin-top: 50px; |
|
padding: 20px; |
|
color: #666; |
|
font-size: 0.9em; |
|
}} |
|
</style> |
|
</head> |
|
<body> |
|
<header> |
|
<h1>π² Your Personalized Recipes</h1> |
|
<div class="recipe-info"> |
|
<span class="info-badge">Dietary: {dietary_restrictions}</span> |
|
<span class="info-badge">Cuisine: {cuisine_preference}</span> |
|
<span class="info-badge">Generated: {time.strftime("%Y-%m-%d")}</span> |
|
</div> |
|
</header> |
|
|
|
<div class="container"> |
|
{ingredients_html} |
|
|
|
<hr> |
|
|
|
{recipes_html} |
|
|
|
<footer> |
|
<p>Generated by Visual Recipe Assistant</p> |
|
<p>Powered by Meta's Llama-Vision-Free Model & Together AI</p> |
|
</footer> |
|
</div> |
|
</body> |
|
</html> |
|
""" |
|
return html |
|
|
|
def update_gallery(files): |
|
"""Update the gallery with uploaded image paths""" |
|
if not files or len(files) == 0: |
|
return gr.update(visible=False) |
|
return gr.update(value=files, visible=True) |
|
|
|
def process_recipe_request(api_key, files, num_recipes, dietary_restrictions, cuisine_preference): |
|
"""Process the recipe request with uploaded files""" |
|
if not files: |
|
return ["Please upload at least one image of ingredients.", None] |
|
return get_recipe_suggestions(api_key, files, num_recipes, dietary_restrictions, cuisine_preference) |
|
|
|
custom_css = """ |
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); |
|
:root { |
|
--primary-color: #2C786C; /* Rich teal */ |
|
--primary-light: #40A799; /* Lighter teal for hover */ |
|
--secondary-color: #F58634; /* Warm orange */ |
|
--accent-color: #FAB95B; /* Soft amber */ |
|
--background-color: #F8F9FA; /* Clean off-white */ |
|
--text-color: #2A2D34; /* Deep charcoal */ |
|
--card-background: #FFFFFF; /* White for cards */ |
|
--border-radius: 12px; |
|
--font-family: 'Poppins', sans-serif; |
|
--box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 20px; |
|
--hover-shadow: rgba(0, 0, 0, 0.15) 0px 8px 30px; |
|
} |
|
body { |
|
font-family: var(--font-family); |
|
background-color: var(--background-color); |
|
color: var(--text-color); |
|
margin: 0; |
|
padding: 0; |
|
} |
|
.container { |
|
width: 100%; |
|
margin: 0 auto; |
|
padding: 20px; |
|
} |
|
.app-header { |
|
background: linear-gradient(135deg, var(--primary-color), var(--primary-light)); |
|
color: white; |
|
padding: 60px 20px; |
|
text-align: center; |
|
border-radius: 0 0 30px 30px; |
|
box-shadow: var(--box-shadow); |
|
width: 100%; |
|
} |
|
.app-title { |
|
font-size: 2.8em; |
|
font-weight: 700; |
|
margin-bottom: 10px; |
|
color: navajowhite; |
|
} |
|
.app-subtitle { |
|
font-size: 1.3em; |
|
font-weight: 400; |
|
max-width: 800px; |
|
margin: 0 auto; |
|
color: white; |
|
} |
|
.input-section, .output-section { |
|
background-color: var(--card-background); |
|
border-radius: var(--border-radius); |
|
padding: 30px; |
|
box-shadow: var(--box-shadow); |
|
margin-bottom: 30px; |
|
width: 100%; |
|
} |
|
.section-header { |
|
font-size: 1.6em; |
|
font-weight: 600; |
|
color: var(--text-color); |
|
margin-bottom: 20px; |
|
border-bottom: 2px solid var(--primary-color); |
|
padding-bottom: 10px; |
|
} |
|
.section-header2 { |
|
font-size: 1.6em; |
|
font-weight: 600; |
|
color: var(--text-color); |
|
border-bottom: 2px solid var(--primary-color); |
|
padding-bottom: 10px; |
|
} |
|
.image-upload-container { |
|
padding: 40px; |
|
text-align: center; |
|
background-color: white; |
|
transition: all 0.3s ease; |
|
} |
|
.image-upload-container:hover { |
|
border-color: var(--primary-color); |
|
background-color: rgba(44, 120, 108, 0.08); |
|
} |
|
button.primary-button { |
|
background-color: var(--primary-color); |
|
color: white; |
|
border: none; |
|
padding: 16px 32px; |
|
border-radius: 6px; |
|
font-size: 1.1em; |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
width: 100%; |
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
|
} |
|
button.primary-button:hover { |
|
background-color: #E15F52; |
|
box-shadow: var(--hover-shadow); |
|
} |
|
button.download-button { |
|
background-color: var(--secondary-color); |
|
color: white; |
|
border: none; |
|
padding: 12px 24px; |
|
border-radius: 6px; |
|
font-size: 1em; |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
margin-top: 15px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
gap: 8px; |
|
margin-left: auto; |
|
margin-right: auto; |
|
margin-bottom: 25px !important; |
|
} |
|
button.download-button:hover { |
|
background-color: #3da037; |
|
box-shadow: var(--hover-shadow); |
|
} |
|
.gallery-container { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); |
|
gap: 20px; |
|
margin-top: 30px; |
|
} |
|
.gallery-item { |
|
border-radius: var(--border-radius); |
|
overflow: hidden; |
|
box-shadow: var(--box-shadow); |
|
transition: transform 0.3s ease; |
|
aspect-ratio: 1 / 1; |
|
object-fit: cover; |
|
} |
|
.gallery-item:hover { |
|
transform: scale(1.05); |
|
box-shadow: var(--hover-shadow); |
|
} |
|
.recipe-output { |
|
font-size: 1.2em; |
|
line-height: 1.7; |
|
color: var(--text-color); |
|
max-height: 600px; |
|
overflow-y: auto; |
|
padding-right: 15px; |
|
} |
|
.ingredients-identified, .recipe-suggestions { |
|
background-color: #f9f9f9; |
|
border-radius: var(--border-radius); |
|
padding: 20px; |
|
margin-bottom: 20px; |
|
} |
|
.recipe-card { |
|
background-color: white; |
|
border-radius: var(--border-radius); |
|
padding: 20px; |
|
margin-bottom: 20px; |
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
|
} |
|
.recipe-title { |
|
color: var(--secondary-color); |
|
font-size: 1.8em; |
|
margin-top: 0; |
|
margin-bottom: 15px; |
|
} |
|
.recipe-description { |
|
font-style: italic; |
|
margin-bottom: 20px; |
|
color: #666; |
|
} |
|
.recipe-ingredients, .recipe-instructions, .recipe-nutrition { |
|
margin-bottom: 20px; |
|
} |
|
.recipe-ingredients h4, .recipe-instructions h4, .recipe-nutrition h4 { |
|
color: var(--primary-color); |
|
margin-bottom: 10px; |
|
} |
|
.footer { |
|
background: linear-gradient(135deg, var(--primary-color), var(--primary-light)); |
|
padding: 40px 20px; |
|
text-align: center; |
|
color: var(--text-color); |
|
font-size: 1.1em; |
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); |
|
width: 100%; |
|
} |
|
.footer-content { |
|
max-width: 800px; |
|
margin: 0 auto; |
|
} |
|
.footer-brand { |
|
font-weight: 700; |
|
color: navajowhite; |
|
} |
|
.footer-links a { |
|
color: white; |
|
text-decoration: none; |
|
margin: 0 15px; |
|
transition: color 0.3s ease; |
|
} |
|
.footer-links a:hover { |
|
color: var(--secondary-color); |
|
} |
|
""" |
|
|
|
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"> |
|
<div class="footer-content"> |
|
<p><span class="footer-brand">π² Visual Recipe Assistant</span></p> |
|
<p>Powered by Meta's Llama-Vision-Free Model & Together AI</p> |
|
<p>Upload multiple ingredient images for more creative recipe combinations</p> |
|
<div class="footer-links"> |
|
<a href="#" onclick="document.getElementById('how-to-use').scrollIntoView({ behavior: 'smooth' }); return false;">How It Works</a> |
|
<a href="https://api.whatsapp.com/send/?phone=%2B8801719296601&text&type=phone_number&app_absent=0" target="_blank">Contact Us</a> |
|
</div> |
|
</div> |
|
</div> |
|
""" |
|
|
|
with gr.Blocks(css=custom_css) as app: |
|
gr.HTML(html_header) |
|
|
|
with gr.Accordion("How It Works", open=False, elem_id="how-to-use"): |
|
gr.Markdown(""" |
|
### Getting Started: |
|
1. Enter your Together Ai API key. |
|
2. Upload multiple ingredient images. |
|
3. Select Number of Recipes Suggestions. |
|
4. Select Dietary Restrictions. |
|
5. Cuisine Preference |
|
|
|
### Results: |
|
- Click Get Recipe Suggestions. |
|
- Wait for few minutes to get full suggestions. |
|
- You can download the suggested recipe. |
|
""") |
|
|
|
|
|
html_file_path = gr.State(None) |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=1): |
|
with gr.Group(elem_classes="input-section"): |
|
gr.HTML('<h3 class="section-header">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 class="section-header">Upload Ingredients</h3>') |
|
file_upload = gr.File( |
|
label="Upload images of ingredients", |
|
file_types=["image"], |
|
file_count="multiple", |
|
elem_classes="image-upload-container" |
|
) |
|
|
|
image_input = gr.Gallery( |
|
label="Uploaded Ingredients", |
|
elem_id="ingredient-gallery", |
|
columns=3, |
|
rows=2, |
|
height="auto", |
|
visible=False |
|
) |
|
|
|
gr.HTML('<h3 class="section-header2">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.Group(elem_classes="output-section"): |
|
gr.HTML('<h3 class="section-header">Your Personalized Recipes</h3>') |
|
output = gr.HTML(elem_classes="recipe-output") |
|
download_button = gr.Button("π₯ Download Recipes (See below to download)", elem_classes="download-button", visible=False, elem_id="download-button") |
|
|
|
gr.HTML(html_footer) |
|
|
|
|
|
def process_and_update_file_path(api_key, files, num_recipes, dietary_restrictions, cuisine_preference): |
|
"""Process recipe request and update file path state""" |
|
result = process_recipe_request(api_key, files, num_recipes, dietary_restrictions, cuisine_preference) |
|
html_content = result[0] |
|
file_path = result[1] |
|
return html_content, file_path, gr.update(visible=file_path is not None) |
|
|
|
file_upload.change(fn=update_gallery, inputs=file_upload, outputs=image_input) |
|
|
|
submit_button.click( |
|
fn=process_and_update_file_path, |
|
inputs=[api_key_input, file_upload, num_recipes, dietary_restrictions, cuisine_preference], |
|
outputs=[output, html_file_path, download_button] |
|
) |
|
|
|
|
|
download_button.click( |
|
fn=lambda x: x, |
|
inputs=html_file_path, |
|
outputs=gr.File(label="Download Recipe HTML") |
|
) |
|
|
|
if __name__ == "__main__": |
|
app.launch() |