| 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"> | |
| """ | |
| # Split ingredients by new line and create list items | |
| 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""" | |
| # Initialize the HTML content with the recipe suggestions header | |
| html_content = """ | |
| <div class="recipe-suggestions"> | |
| <h2>π½οΈ Recipe Suggestions</h2> | |
| """ | |
| # Split the text by recipe (assume recipes are separated by a recipe name heading) | |
| recipe_sections = [] | |
| current_recipe = "" | |
| lines = recipe_text.split('\n') | |
| # Process lines to identify recipe sections | |
| for line in lines: | |
| if line.strip().startswith(("Recipe ", "# ", "## ", "### ")): | |
| # If we've collected some content for a recipe, save it | |
| if current_recipe: | |
| recipe_sections.append(current_recipe) | |
| current_recipe = "" | |
| # Add line to current recipe | |
| current_recipe += line + "\n" | |
| # Add the last recipe if exists | |
| if current_recipe: | |
| recipe_sections.append(current_recipe) | |
| # Process each recipe section into HTML | |
| 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: | |
| # Handle recipe title | |
| 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 | |
| # Handle description | |
| 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 | |
| # Start ingredients section | |
| 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 | |
| # Start instructions section | |
| 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 | |
| # Handle cooking time | |
| 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 | |
| # Handle difficulty level | |
| if "difficulty" in line.lower(): | |
| difficulty = line.strip() | |
| html_content += f'<p class="recipe-difficulty"><strong>π {difficulty}</strong></p>\n' | |
| continue | |
| # Handle nutritional info | |
| 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 | |
| # Process ingredient line | |
| 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 | |
| # Process instruction line | |
| 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 | |
| # Process nutrition line | |
| 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 | |
| # Close any open sections | |
| if in_ingredients: | |
| html_content += '</ul>\n</div>\n' | |
| if in_instructions: | |
| html_content += '</ol>\n</div>\n' | |
| html_content += '</div>\n' # Close recipe card | |
| html_content += '</div>\n' # Close recipe suggestions div | |
| 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=2048, | |
| temperature=0.7 | |
| ) | |
| recipe_text = response.choices[0].message.content | |
| # Format ingredients and recipes as HTML | |
| ingredients_html = format_ingredients_html(all_ingredients) | |
| recipes_html = format_recipe_to_html(recipe_text) | |
| # Combine HTML content | |
| html_content = ingredients_html + "<hr>" + recipes_html | |
| # Create a downloadable HTML file with styling | |
| full_html = create_downloadable_html(ingredients_html, recipes_html, dietary_restrictions, cuisine_preference) | |
| # Generate a unique filename for the downloadable HTML | |
| 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: #FF6F61; /* Warm coral */ | |
| --secondary-color: #4BB543; /* Fresh green */ | |
| --accent-color: #F0A500; /* Golden yellow */ | |
| --background-color: #F4F4F9; /* Light grayish background */ | |
| --text-color: #333333; /* Dark gray text */ | |
| --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 { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| .app-header { | |
| background-color: var(--primary-color); | |
| color: white; | |
| padding: 60px 20px; | |
| text-align: center; | |
| border-radius: 0 0 30px 30px; | |
| box-shadow: var(--box-shadow); | |
| } | |
| .app-title { | |
| font-size: 2.8em; | |
| font-weight: 700; | |
| margin-bottom: 10px; | |
| } | |
| .app-subtitle { | |
| font-size: 1.3em; | |
| font-weight: 300; | |
| max-width: 800px; | |
| margin: 0 auto; | |
| } | |
| .input-section, .output-section { | |
| background-color: var(--card-background); | |
| border-radius: var(--border-radius); | |
| padding: 30px; | |
| box-shadow: var(--box-shadow); | |
| margin-bottom: 30px; | |
| } | |
| .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 { | |
| border: 2px dashed var(--secondary-color); | |
| padding: 40px; | |
| text-align: center; | |
| background-color: rgba(75, 181, 67, 0.1); | |
| transition: all 0.3s ease; | |
| } | |
| .image-upload-container:hover { | |
| border-color: var(--primary-color); | |
| background-color: rgba(255, 111, 97, 0.1); | |
| } | |
| 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; | |
| } | |
| 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; | |
| } | |
| .loading-container { | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| z-index: 1000; | |
| opacity: 0; | |
| visibility: hidden; | |
| transition: opacity 0.3s ease, visibility 0.3s ease; | |
| } | |
| .loading-container.visible { | |
| opacity: 1; | |
| visibility: visible; | |
| } | |
| .loading-spinner { | |
| border: 8px solid #f3f3f3; | |
| border-top: 8px solid var(--primary-color); | |
| border-radius: 50%; | |
| width: 60px; | |
| height: 60px; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .loading-text { | |
| color: white; | |
| font-size: 1.3em; | |
| margin-top: 20px; | |
| } | |
| .footer { | |
| background-color: var(--card-background); | |
| 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); | |
| } | |
| .footer-content { | |
| max-width: 800px; | |
| margin: 0 auto; | |
| } | |
| .footer-brand { | |
| font-weight: 700; | |
| color: var(--primary-color); | |
| } | |
| .footer-links a { | |
| color: var(--secondary-color); | |
| text-decoration: none; | |
| margin: 0 15px; | |
| transition: color 0.3s ease; | |
| } | |
| .footer-links a:hover { | |
| color: var(--primary-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> | |
| <div id="loading-overlay" class="loading-container"> | |
| <div class="loading-spinner"></div> | |
| <div class="loading-text">Generating your recipes...</div> | |
| </div> | |
| <script> | |
| function showLoading() { | |
| document.getElementById('loading-overlay').classList.add('visible'); | |
| } | |
| function hideLoading() { | |
| document.getElementById('loading-overlay').classList.remove('visible'); | |
| } | |
| </script> | |
| """ | |
| 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="#" target="_blank">How It Works</a> | |
| <a href="#" target="_blank">Privacy Policy</a> | |
| <a href="#" target="_blank">Contact Us</a> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const submitBtn = document.querySelector('button.primary-button'); | |
| if (submitBtn) { | |
| submitBtn.addEventListener('click', function() { | |
| showLoading(); | |
| // Check every second for output content | |
| const checkInterval = setInterval(function() { | |
| const output = document.querySelector('.recipe-output'); | |
| if (output && output.innerHTML.trim().length > 0) { | |
| hideLoading(); | |
| clearInterval(checkInterval); | |
| clearTimeout(forceHideTimeout); | |
| // Show download button if it exists | |
| const downloadBtn = document.getElementById('download-button'); | |
| if (downloadBtn) { | |
| downloadBtn.style.display = 'flex'; | |
| } | |
| } | |
| }, 1000); | |
| // Force hide after 120 seconds | |
| const forceHideTimeout = setTimeout(function() { | |
| hideLoading(); | |
| clearInterval(checkInterval); | |
| }, 120000); | |
| }); | |
| } | |
| }); | |
| </script> | |
| """ | |
| with gr.Blocks(css=custom_css) as app: | |
| gr.HTML(html_header) | |
| # Store the generated html file path for download | |
| 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 as HTML", elem_classes="download-button", visible=False, elem_id="download-button") | |
| gr.HTML(html_footer) | |
| # Update functions | |
| 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] | |
| ) | |
| # Handle download button click | |
| download_button.click( | |
| fn=lambda x: x, | |
| inputs=html_file_path, | |
| outputs=gr.File(label="Download Recipe HTML") | |
| ) | |
| if __name__ == "__main__": | |
| app.launch() |