Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import base64 | |
| from huggingface_hub import upload_file | |
| from openai import OpenAI | |
| from datetime import datetime | |
| import json | |
| import re | |
| # Page configuration - MUST be first Streamlit command | |
| st.set_page_config( | |
| page_title="Gen AI - Menu Visual Generator", | |
| layout="wide", | |
| page_icon="🧪" | |
| ) | |
| # API Key Input Section | |
| st.markdown("### 🔑 OpenAI API Configuration") | |
| api_key_input = st.text_input( | |
| "Enter your OpenAI API Key", | |
| type="password", | |
| placeholder="sk-...", | |
| help="Get your API key from https://platform.openai.com/api-keys" | |
| ) | |
| # Initialize OpenAI client with UI input | |
| client = None | |
| if api_key_input: | |
| try: | |
| client = OpenAI(api_key=api_key_input) | |
| st.success("✅ API key provided successfully!") | |
| except Exception as e: | |
| st.error(f"❌ Invalid API key: {str(e)}") | |
| st.stop() | |
| def create_menu_text_from_image(image_file): | |
| """Extract text from image""" | |
| try: | |
| image_bytes = image_file.read() | |
| base64_image = base64.b64encode(image_bytes).decode('utf-8') | |
| # Reset file pointer | |
| image_file.seek(0) | |
| response = client.chat.completions.create( | |
| model="gpt-4o-mini", | |
| messages=[{ | |
| "role": "user", | |
| "content": [ | |
| { | |
| "type": "text", | |
| "text": """Please extract all menu items from this image and organize them in a structured format. For each item, extract: | |
| 1. Item name | |
| 2. Description (if available) | |
| 3. Price | |
| 4. Category (appetizers, mains, desserts, bevrages, etc.) | |
| Format your response as a JSON structure like this: | |
| { | |
| "restaurant_image": "Restaurant Image (if visible), | |
| "cusine_type": "Indian/Italian/Mexico/British/etc. (best guess)", | |
| "categories": { | |
| "Appetizers": [ | |
| { "name": "Item Name", "description": "Description (if available)", "price": "$x.xx" } | |
| ], | |
| "Main Cources": [ | |
| { "name": "Item Name", "description": "Description (if available)", "price": "$x.xx" } | |
| ], | |
| } | |
| } | |
| If you can't determine the category, group similar items together. Be as accurate as possible with the text extraction.""" | |
| }, | |
| { | |
| "type": "image_url", | |
| "image_url": { | |
| "url": f"data:image/jpeg;base64,{base64_image}" | |
| } | |
| } | |
| ] | |
| }], | |
| max_tokens=2000, | |
| ) | |
| menu_text = response.choices[0].message.content | |
| # Clean the response and extract JSON | |
| json_start = menu_text.find("{") | |
| json_end = menu_text.rfind("}") + 1 | |
| if json_start != -1 and json_end != -1: | |
| menu_data = json.loads(menu_text[json_start:json_end]) | |
| return menu_data | |
| else: | |
| return { "error": "Unable to extract menu data from image", "raw_text": menu_text } | |
| except Exception as e: | |
| return { "error": f"Error extracting data from image: {str(e)}" } | |
| def get_nutrinutional_info(dish_name, cuisine_type): | |
| """Get nutrient information from Nutritionix API""" | |
| try: | |
| response = client.chat.completions.create( | |
| model="gpt-4o-mini", | |
| messages=[{ | |
| "role": "user", | |
| "content": f"""For the {cuisine_type} dish '{dish_name}', provide estimated nutritional information in JSON format: | |
| {{ | |
| "calories": 350, | |
| "protein": 10g, | |
| "carbs": 30g, | |
| "fat": 15g, | |
| "fiber": 5g | |
| }} | |
| Base your estimate on typical restaurant portions and ingredients for this type of dish. Be realistic with the values.""" | |
| }], | |
| max_tokens=200, | |
| ) | |
| nutrient_info = response.choices[0].message.content | |
| # Clean the response and extract JSON | |
| json_start = menu_text.find("{") | |
| json_end = menu_text.rfind("}") + 1 | |
| if json_start != -1 and json_end != -1: | |
| menu_data = json.loads(menu_text[json_start:json_end]) | |
| return menu_data | |
| else: | |
| return { "calories": 'N/A', "protein": 'N/A', "carbs": 'N/A', "fat": 'N/A', "fiber": 'N/A' } | |
| except Exception as e: | |
| return { "calories": 'N/A', "protein": 'N/A', "carbs": 'N/A', "fat": 'N/A', "fiber": 'N/A' } | |
| def generate_food_image(dish_name, description, cuisine_type, image_style): | |
| """Generate food image with enhanced prompting""" | |
| # style configurations | |
| style_config = { | |
| "Professional Food Photography": { | |
| "lighting": "professional studio lighting with soft shadows", | |
| "background": "clean white or neutral background", | |
| "composition": "centered plating with professional garnish", | |
| "quality": "restaurant-quality presentation, high-end food photography" | |
| }, | |
| "Rustic Homestyle": { | |
| "lighting": "warm, natural lighting", | |
| "background": "rusted wooden table or textured surface", | |
| "composition": "casual, homestyle presentation", | |
| "quality": "comforting, homemade appearance with natrual styling" | |
| }, | |
| "Modern Minimalstic": { | |
| "lighting": "clean, bright lighting", | |
| "background": "minimalistic white or light gray background", | |
| "composition": "artistic plating with negative space", | |
| "quality": "contemporary, Instagram-worthy presentation" | |
| }, | |
| "Vibrant Colorful": { | |
| "lighting": "brigt, vibrant lighting that enhances color", | |
| "background": "colorful or complementary background", | |
| "composition": "dynamic, eye-catching, presentation", | |
| "quality": "bold, appetizing colors that pop" | |
| } | |
| } | |
| selected_style = style_config.get(image_style, style_config['Professional Food Photography']) | |
| # Create detailed food photography prompt | |
| food_prompt = f""" | |
| Professional food photography of {dish_name} ({cuisine_type} cuisine). | |
| Dish details: {description} | |
| Photography specifications: | |
| - {selected_style['lighting']} | |
| - {selected_style['background']} | |
| - {selected_style['composition']} | |
| - {selected_style['quality']} | |
| Technical Requirements: | |
| - High resolution, commercial food photography quality | |
| - Appetizing and mouth-watering presentation | |
| - Perfect focus and sharp details | |
| - Colors that enhance appetite appeal | |
| - Professional plating and garnish | |
| - Shot from optimal angle to showcase the dish | |
| - No text or watermark in the image | |
| Style: Photorealistic, magazine-quality food photography that would be used in high-end restaurant menu or food advertising. | |
| """ | |
| try: | |
| gen_params = { | |
| "model": "gpt-image-1", | |
| "prompt": food_prompt, | |
| "size": "1024x1024", | |
| "quality": "high", | |
| "n": 1 | |
| } | |
| result = client.images.generate(**gen_params) | |
| if not result or not result.data or len(result.data) == 0: | |
| st.error(f"No image data returned for {dish_name}") | |
| return None, None | |
| # Get base64 image data directly ( it should work directly ) | |
| image_base64 = result.data[0].b64_json | |
| # Check if base64 data is valid | |
| if not image_base64: | |
| st.error(f"No base64 image returned for {dish_name}") | |
| return None, None | |
| image_bytes = base64.b64decode(image_base64) | |
| # Create a data URL for display purpose | |
| image_url = f"data:image/jpeg;base64,{image_base64}" | |
| return image_url, image_bytes | |
| except Exception as e: | |
| st.error(f"Error generating food image for {dish_name}: {str(e)}") | |
| return None, None | |
| # Main UI | |
| st.title("GenAI - Menu Visual Generator") | |
| st.markdown("Transforms your text menu into beautiful visual menu with food photos and nutritional Information!") | |
| if client: | |
| st.markdown("### Upload your Menu Image") | |
| st.markdown("Upload a photo of your text-only menu and we'll extract all items automatically!") | |
| uploaded_file = st.file_uploader( | |
| "Choose menu image", | |
| type=['jpg', "jpeg", "png"], | |
| help="Upload a clear image of your menu with readable text" | |
| ) | |
| if uploaded_file: | |
| st.image(uploaded_file, caption="Uploaded Menu Image", width=400) | |
| if st.button("Extract menu Items", type="primary"): | |
| with st.spinner("Analyzing menu image and extracting items...."): | |
| menu_data = create_menu_text_from_image(uploaded_file) | |
| if "error" in menu_data: | |
| st.error(f"Error extracting menu data: {menu_data['error']}") | |
| if "raw_text" in menu_data: | |
| st.text_area("Raw extracted Text", menu_data['raw_text'], height=200) | |
| else: | |
| st.success("Menu items extracted successfully!") | |
| st.session_state.menu_data = menu_data | |
| # Display extracted menu structure | |
| st.markdown("### Extracted Menu Structure") | |
| col1, col2 = st.columns([2, 1]) | |
| with col1: | |
| st.markdown(f"**Restaurant** {menu_data.get('restaurant_image', 'Not detected')}") | |
| st.markdown(f"**Cuisine Type** {menu_data.get('cuisine_type', 'Not detected')}") | |
| for category, items in menu_data.get('categories', {}).items(): | |
| st.markdown(f"**{category}**") | |
| for item in items: | |
| st.markdown(f"- {item['name']} - {item.get('price', 'N/A')}") | |
| if item.get('description'): | |
| st.markdown(f" *{item['description']}*") | |
| with col2: | |
| st.markdown("**Menu Statistics**") | |
| total_items = sum(len(items) for items in menu_data.get('categories', {}).values()) | |
| st.metric("Total Items", total_items) | |
| st.metric("Categories", len(menu_data.get('categories', {}))) | |
| if "menu_data" in st.session_state: | |
| st.markdown("---") | |
| st.markdown("## Generate Visual Menu!") | |
| #configuration options | |
| cols = st.columns(3) | |
| col1, col2, col3 = cols[0], cols[1], cols[2] | |
| with col1: | |
| image_style = st.selectbox( | |
| "Food Photo Style", | |
| [ | |
| "Professional Food Photography", | |
| "Rustic Homestyle", | |
| "Modern Minimalstic", | |
| "Vibrant Colorful" | |
| ], | |
| help="Choose the style of food photography" | |
| ) | |
| with col2: | |
| layout_style = st.selectbox( | |
| "Food Description Display", | |
| [ | |
| "Show Descriptions", | |
| "Hide Descriptions", | |
| "Short Descriptions only" | |
| ], | |
| help="Choose how to display food descriptions" | |
| ) | |
| with col3: | |
| include_nutrition = st.checkbox( | |
| "Include Nutritional Information", | |
| value=True, | |
| help="Add calories and nutritional information to each dish" | |
| ) | |
| if st.button("Generate Complete Visual Menu", type="primary", use_container_width=True): | |
| menu_data = st.session_state.menu_data | |
| with st.spinner("Generating your visual menu... This may take several minutes as we generate food photos for each item."): | |
| # progress tracking | |
| progress_bar = st.progress(0, text="Generating food photos for each item") | |
| status_text = st.empty() | |
| dish_images = {} | |
| nutritional_data = {} | |
| # get all dishes | |
| all_dishes = [] | |
| for category, items in menu_data.get('categories', {}).items(): | |
| for item in items: | |
| all_dishes.append((category, item)) | |
| total_dishes = len(all_dishes) | |
| # Generate images and nutrition for each dish | |
| for i, (category, item) in enumerate(all_dishes): | |
| dish_name = item['name'] | |
| description = item.get('description', "") | |
| status_text.text(f"Generating food photo for {dish_name} ({i+1}/{total_dishes})") | |
| # When you generate the image | |
| image_bytes, image_url = generate_food_image( | |
| dish_name, | |
| description, | |
| menu_data.get('cuisine_type', "Fine Dining"), | |
| image_style | |
| ) | |
| # Store the image data in the dish_images dictionary | |
| if image_bytes and image_url: | |
| dish_images[dish_name] = { | |
| 'bytes': image_bytes, | |
| 'url': image_url | |
| } | |
| # Get nutritional information | |
| if include_nutrition: | |
| nutritional_data[dish_name] = get_nutrinutional_info(dish_name, menu_data.get('cuisine_type', "Fine Dining")) | |
| # update progress | |
| progress_bar.progress((i+1)/total_dishes) | |
| progress_bar.progress(1.0) | |
| status_text.text("Visual menu generation complete!") | |
| # Display results | |
| st.markdown("### Your Visual Menu Results") | |
| # Show individual food photo | |
| st.markdown("### Generated Food Photos with Nutritional Information") | |
| for category, items in menu_data.get('categories', {}).items(): | |
| st.markdown(f"**{category}**") | |
| cols = st.columns(min(len(items), 3)) | |
| for i, item in enumerate(items): | |
| dish_name = item['name'] | |
| col_index = i % 3 | |
| with cols[col_index]: | |
| if dish_name in dish_images: | |
| st.image( | |
| dish_images[dish_name]['url'], | |
| caption=f"{dish_name} - {item.get('price', '')}", | |
| use_column_width=True | |
| ) | |
| if include_nutrition and dish_name in nutritional_data: | |
| nutrition = nutritional_data[dish_name] | |
| st.markdown(f"**Nutrition** {nutrition.get('calories', 'N/A')} cal, {nutrition.get('protein', 'N/A')} protein") | |
| if item.get('description'): | |
| st.markdown(f"*{item['description']}*") | |
| # Download Options | |
| st.markdown("### Download Individual Food Photos") | |
| # Create download options for individual images | |
| download_cols = st.columns(3) | |
| col_count = 0 | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| for category, items in menu_data.get('categories', {}).items(): | |
| st.markdown(f"**Download {category} Photos:**") | |
| for item in items: | |
| dish_name = item['name'] | |
| if dish_name in dish_images: | |
| clean_name = re.sub(r'[^a-zA-Z0-9]', '', dish_name) | |
| with download_cols[col_count % 3]: | |
| st.download_button( | |
| f"Download {dish_name} Photo", | |
| dish_images[dish_name]['bytes'], | |
| file_name=f"{clean_name}_{timestamp}.png", | |
| mime="image/png", | |
| help=f"Download {dish_name} food photo", | |
| use_container_width=True | |
| ) | |
| col_count += 1 | |
| restaurant_name_clean = re.sub(r'[^a-zA-Z0-9]', '', menu_data.get('restaurant_name', "menu")) | |
| st.markdown("### Download Menu Report") | |
| col_download1, col_download2 = st.columns(2) | |
| with col_download1: | |
| st.markdown(f"**Bulk Download**") | |
| st.info(f"Individual photos available above, or use menu report for compelete details") | |
| with col_download2: | |
| menu_summary = f"""VISUAL MENU GENERATED REPORT | |
| Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} | |
| Restaurant: {menu_data.get('restaurant_name', 'N/A')} | |
| Cuisine Type: {menu_data.get('cuisine_type', 'N/A')} | |
| Image Style: {image_style} | |
| Food Description Display: {layout_style} | |
| Nutritional Information: {'Included' if include_nutrition else 'Excluded'} | |
| Menu Items Generated: {total_dishes} | |
| """ | |
| for category, items in menu_data.get('categories', {}).items(): | |
| menu_summary += f"\n{category}:\n" | |
| for item in items: | |
| menu_summary += f"- {item['name']} - {item.get('price', 'N/A')}\n" | |
| if include_nutrition and item['name'] in nutritional_data: | |
| nutrition = nutritional_data[item['name']] | |
| menu_summary += f"Nutrition: {nutrition.get('calories', 'N/A')} cal, {nutrition.get('protein', 'N/A')} protein\n" | |
| menu_summary += f""" | |
| Generation Statistics: | |
| - Total Items: {sum(len(items) for items in menu_data.get('categories', {}).values())} | |
| - Categories: {len(menu_data.get('categories', {}))} | |
| - Food Photos Generated: {len(dish_images)} | |
| - Processing Time: Several minutes | |
| File Included: | |
| - Individual food photography for each menu item | |
| - Nutritional information (if selected) | |
| Usage Recommendations: | |
| - Use individual food photos for online menus and delivery apps | |
| - Share on social media to showcase specific dishes | |
| - Use for promotional materials and advertisements | |
| - Update photos seasonally or when menu changes | |
| """ | |
| st.download_button( | |
| "Download Menu Report", | |
| menu_summary.encode("utf-8"), | |
| file_name=f"menu_report_{restaurant_name_clean}_{timestamp}.txt", | |
| mime="text/plain", | |
| help="Download detailed generation report", | |
| use_container_width=True | |
| ) | |
| else: | |
| st.info("Please enter OpenAI key to get started!.") | |
| st.markdown(""" | |
| ### Trasform you Menu with AI | |
| ### **Upload Any Menu Photo** | |
| - Take a picture of your menu and we'll extract all items automatically! | |
| ### **Visualize Your Menu** | |
| - Generate beautiful visual menu with food photos and nutritional Information! | |
| ### **Download Your Menu Report** | |
| - Download detailed report of your menu with all details! | |
| """) |