Spaces:
Build error
Build error
| import streamlit as st | |
| import pandas as pd | |
| from collections import Counter | |
| import json | |
| import os | |
| import uuid | |
| from datetime import datetime | |
| import base64 | |
| import glob | |
| import re | |
| from typing import List, Dict, Tuple | |
| # AI/ML imports - lightweight transformers | |
| try: | |
| from sentence_transformers import SentenceTransformer | |
| from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification | |
| import torch | |
| import numpy as np | |
| from sklearn.metrics.pairwise import cosine_similarity | |
| AI_AVAILABLE = True | |
| except ImportError: | |
| AI_AVAILABLE = False | |
| st.warning("⚠️ AI features require additional packages. Install with: pip install sentence-transformers transformers torch scikit-learn") | |
| # --- Page Configuration --- | |
| st.set_page_config( | |
| page_title="రుచి చూడు (Ruchi Chudu)", | |
| page_icon="🍲", | |
| layout="wide" | |
| ) | |
| # --- AI Model Management --- | |
| def load_ai_models(): | |
| """Load AI models with caching to prevent reloading""" | |
| if not AI_AVAILABLE: | |
| return None, None, None | |
| try: | |
| # Lightweight sentence transformer for recipe similarity | |
| similarity_model = SentenceTransformer('all-MiniLM-L6-v2') | |
| # Sentiment analysis for recipe stories | |
| sentiment_analyzer = pipeline( | |
| "sentiment-analysis", | |
| model="cardiffnlp/twitter-roberta-base-sentiment-latest", | |
| return_all_scores=True | |
| ) | |
| # Text classification for cuisine type - using a lighter model | |
| cuisine_classifier = pipeline( | |
| "text-classification", | |
| model="cardiffnlp/twitter-roberta-base-sentiment-latest" # Reuse sentiment model | |
| ) | |
| return similarity_model, sentiment_analyzer, cuisine_classifier | |
| except Exception as e: | |
| st.error(f"Error loading AI models: {e}") | |
| return None, None, None | |
| # --- Enhanced AI Functions --- | |
| def analyze_recipe_sentiment(story: str, sentiment_analyzer) -> Dict: | |
| """Analyze the emotional tone of recipe stories""" | |
| if not story or not sentiment_analyzer: | |
| return {"sentiment": "neutral", "confidence": 0.5, "emotion": "nostalgic"} | |
| try: | |
| results = sentiment_analyzer(story[:500]) # Limit text length | |
| # Map sentiment labels to readable format | |
| sentiment_map = { | |
| 'LABEL_0': 'negative', | |
| 'LABEL_1': 'neutral', | |
| 'LABEL_2': 'positive', | |
| 'NEGATIVE': 'negative', | |
| 'NEUTRAL': 'neutral', | |
| 'POSITIVE': 'positive' | |
| } | |
| top_result = max(results[0], key=lambda x: x['score']) | |
| sentiment = sentiment_map.get(top_result['label'], 'neutral') | |
| confidence = top_result['score'] | |
| # Add emotion context for Telugu recipes | |
| emotion_keywords = { | |
| 'joyful': ['celebration', 'festival', 'happy', 'joy', 'smile', 'laugh'], | |
| 'nostalgic': ['grandmother', 'childhood', 'memory', 'tradition', 'old', 'ammamma'], | |
| 'proud': ['special', 'unique', 'best', 'famous', 'perfect', 'excellent'], | |
| 'loving': ['family', 'mother', 'love', 'care', 'warmth', 'together'] | |
| } | |
| story_lower = story.lower() | |
| emotion = 'nostalgic' # default for recipe stories | |
| for emotion_type, keywords in emotion_keywords.items(): | |
| if any(keyword in story_lower for keyword in keywords): | |
| emotion = emotion_type | |
| break | |
| return { | |
| "sentiment": sentiment, | |
| "confidence": confidence, | |
| "emotion": emotion | |
| } | |
| except Exception as e: | |
| return {"sentiment": "neutral", "confidence": 0.5, "emotion": "nostalgic"} | |
| def extract_smart_tags(recipe_data: Dict, similarity_model) -> List[str]: | |
| """Extract intelligent tags using NLP and pattern matching""" | |
| tags = set() | |
| # Combine text for analysis | |
| text_content = f"{recipe_data.get('dish_name', '')} {recipe_data.get('ingredients', '')} {recipe_data.get('instructions', '')} {recipe_data.get('story', '')}" | |
| # Traditional keyword-based tagging (enhanced) | |
| keyword_categories = { | |
| 'spice_level': { | |
| 'mild': ['mild', 'less spicy', 'children', 'sweet'], | |
| 'medium': ['medium', 'moderate', 'balanced'], | |
| 'hot': ['spicy', 'hot', 'chili', 'pepper', 'karam', 'guntur'], | |
| 'very_hot': ['very spicy', 'extra hot', 'burning', 'fire'] | |
| }, | |
| 'cooking_time': { | |
| 'quick': ['quick', 'fast', 'minutes', '15 min', '20 min', 'instant'], | |
| 'medium': ['30 min', '45 min', '1 hour', 'moderate time'], | |
| 'slow': ['hours', 'slow cook', 'overnight', 'patience'] | |
| }, | |
| 'meal_type': { | |
| 'breakfast': ['breakfast', 'morning', 'tiffin', 'idli', 'dosa'], | |
| 'lunch': ['lunch', 'meal', 'rice', 'curry', 'sambar'], | |
| 'dinner': ['dinner', 'night', 'heavy'], | |
| 'snack': ['snack', 'evening', 'tea time', 'biscuit'] | |
| }, | |
| 'health': { | |
| 'healthy': ['healthy', 'nutritious', 'vitamin', 'protein', 'fiber'], | |
| 'diabetic_friendly': ['sugar free', 'diabetic', 'low sugar', 'jaggery'], | |
| 'high_protein': ['protein', 'dal', 'lentils', 'sprouts'], | |
| 'low_fat': ['oil free', 'steamed', 'boiled', 'grilled'] | |
| }, | |
| 'occasion': { | |
| 'festival': ['festival', 'celebration', 'ugadi', 'sankranti', 'diwali'], | |
| 'wedding': ['wedding', 'marriage', 'function', 'ceremony'], | |
| 'everyday': ['daily', 'regular', 'simple', 'routine'], | |
| 'special': ['special', 'guest', 'party', 'occasion'] | |
| } | |
| } | |
| text_lower = text_content.lower() | |
| for category, subcategories in keyword_categories.items(): | |
| for tag, keywords in subcategories.items(): | |
| if any(keyword in text_lower for keyword in keywords): | |
| tags.add(tag.replace('_', ' ').title()) | |
| # Telugu-specific patterns | |
| telugu_patterns = { | |
| 'Regional': ['Andhra', 'Telangana', 'Hyderabad', 'Vijayawada', 'Guntur'], | |
| 'Traditional': ['traditional', 'authentic', 'original', 'sampradayam'], | |
| 'Modern': ['fusion', 'modern', 'new style', 'innovative'], | |
| 'Street Food': ['street', 'vendor', 'roadside', 'chat'] | |
| } | |
| for tag, patterns in telugu_patterns.items(): | |
| if any(pattern.lower() in text_lower for pattern in patterns): | |
| tags.add(tag) | |
| # Ingredient-based classification | |
| ingredient_text = recipe_data.get('ingredients', '').lower() | |
| # Detect vegetarian/non-vegetarian | |
| non_veg_ingredients = ['chicken', 'mutton', 'fish', 'egg', 'meat', 'prawn', 'crab'] | |
| if any(ingredient in ingredient_text for ingredient in non_veg_ingredients): | |
| tags.add('Non-Vegetarian') | |
| else: | |
| tags.add('Vegetarian') | |
| # Detect main ingredients | |
| main_ingredients = { | |
| 'Rice Based': ['rice', 'biryani', 'pulao', 'annam'], | |
| 'Dal Based': ['dal', 'lentil', 'pappu', 'sambar'], | |
| 'Vegetable': ['vegetable', 'curry', 'fry', 'kura'], | |
| 'Sweet': ['sweet', 'sugar', 'jaggery', 'dessert', 'halwa'], | |
| 'Pickle': ['pickle', 'achar', 'pachadi'] | |
| } | |
| for tag, ingredients in main_ingredients.items(): | |
| if any(ingredient in ingredient_text for ingredient in ingredients): | |
| tags.add(tag) | |
| return list(tags) | |
| def find_similar_recipes(current_recipe: Dict, all_recipes: List[Dict], similarity_model, top_k=3) -> List[Tuple[Dict, float]]: | |
| """Find similar recipes using semantic similarity""" | |
| if not similarity_model or len(all_recipes) < 2: | |
| return [] | |
| try: | |
| # Create text representations | |
| current_text = f"{current_recipe.get('dish_name', '')} {current_recipe.get('ingredients', '')} {current_recipe.get('recipe_type', '')}" | |
| recipe_texts = [] | |
| valid_recipes = [] | |
| for recipe in all_recipes: | |
| if recipe.get('id') != current_recipe.get('id'): # Exclude current recipe | |
| recipe_text = f"{recipe.get('dish_name', '')} {recipe.get('ingredients', '')} {recipe.get('recipe_type', '')}" | |
| recipe_texts.append(recipe_text) | |
| valid_recipes.append(recipe) | |
| if not recipe_texts: | |
| return [] | |
| # Generate embeddings | |
| current_embedding = similarity_model.encode([current_text]) | |
| recipe_embeddings = similarity_model.encode(recipe_texts) | |
| # Calculate similarities | |
| similarities = cosine_similarity(current_embedding, recipe_embeddings)[0] | |
| # Get top similar recipes | |
| similar_indices = np.argsort(similarities)[::-1][:top_k] | |
| similar_recipes = [] | |
| for idx in similar_indices: | |
| if similarities[idx] > 0.3: # Minimum similarity threshold | |
| similar_recipes.append((valid_recipes[idx], similarities[idx])) | |
| return similar_recipes | |
| except Exception as e: | |
| st.error(f"Error finding similar recipes: {e}") | |
| return [] | |
| def generate_cooking_tips(recipe_data: Dict) -> List[str]: | |
| """Generate contextual cooking tips based on recipe content""" | |
| tips = [] | |
| ingredients = recipe_data.get('ingredients', '').lower() | |
| instructions = recipe_data.get('instructions', '').lower() | |
| dish_name = recipe_data.get('dish_name', '').lower() | |
| # Ingredient-specific tips | |
| if 'oil' in ingredients: | |
| tips.append("🔥 Tip: Heat oil until it shimmers but doesn't smoke for best results") | |
| if 'onion' in ingredients: | |
| tips.append("🧅 Tip: Soak sliced onions in cold water for 10 minutes to reduce tears while cutting") | |
| if any(spice in ingredients for spice in ['turmeric', 'chili', 'coriander']): | |
| tips.append("🌶️ Tip: Dry roast spices lightly before grinding for enhanced flavor") | |
| if 'rice' in ingredients: | |
| tips.append("🍚 Tip: Soak rice for 20-30 minutes before cooking for better texture") | |
| # Cooking method tips | |
| if 'fry' in instructions or 'deep fry' in instructions: | |
| tips.append("🍳 Tip: Maintain oil temperature at 350°F (175°C) for perfect frying") | |
| if 'boil' in instructions: | |
| tips.append("💧 Tip: Add salt to boiling water for vegetables to retain color and nutrients") | |
| # Dish-specific tips | |
| if 'curry' in dish_name: | |
| tips.append("🍛 Tip: Let curry rest for 10 minutes after cooking to allow flavors to meld") | |
| if 'pickle' in dish_name: | |
| tips.append("🥒 Tip: Store pickle in airtight containers and use dry spoons to prevent spoilage") | |
| # Regional tips | |
| tips.append("🏠 Traditional Tip: Taste and adjust seasoning as family preferences vary by region") | |
| return tips[:3] # Return top 3 tips | |
| # --- Data Storage Functions --- | |
| RECIPES_FOLDER = "recipes" | |
| IMAGES_FOLDER = "recipe_images" | |
| def ensure_folders_exist(): | |
| """Create necessary folders if they don't exist""" | |
| os.makedirs(RECIPES_FOLDER, exist_ok=True) | |
| os.makedirs(IMAGES_FOLDER, exist_ok=True) | |
| def generate_recipe_id(): | |
| """Generate a unique ID for each recipe""" | |
| return str(uuid.uuid4())[:8] | |
| def save_recipe(recipe_data): | |
| """Save individual recipe to its own JSON file""" | |
| try: | |
| ensure_folders_exist() | |
| recipe_id = recipe_data['id'] | |
| filename = f"recipe_{recipe_id}.json" | |
| filepath = os.path.join(RECIPES_FOLDER, filename) | |
| with open(filepath, 'w', encoding='utf-8') as f: | |
| json.dump(recipe_data, f, ensure_ascii=False, indent=2) | |
| return True | |
| except Exception as e: | |
| st.error(f"Error saving recipe: {e}") | |
| return False | |
| def load_all_recipes(): | |
| """Load all recipes from individual JSON files""" | |
| recipes = [] | |
| try: | |
| ensure_folders_exist() | |
| recipe_files = glob.glob(os.path.join(RECIPES_FOLDER, "recipe_*.json")) | |
| for filepath in recipe_files: | |
| try: | |
| with open(filepath, 'r', encoding='utf-8') as f: | |
| recipe = json.load(f) | |
| recipes.append(recipe) | |
| except Exception as e: | |
| continue | |
| recipes.sort(key=lambda x: x.get('submitted_date', ''), reverse=True) | |
| except Exception as e: | |
| st.error(f"Error loading recipes: {e}") | |
| return recipes | |
| # --- Language and Text --- | |
| TEXT = { | |
| "en": { | |
| "title": "Ruchi Chudu", | |
| "subtitle": "AI-Powered Community Cookbook to Preserve Telugu Cuisine", | |
| "language_select": "Choose Language", | |
| "sidebar_header": "Contribute Your Recipe!", | |
| "tab_submit": "📝 Submit Your Recipe", | |
| "tab_gallery": "🍲 Community Gallery", | |
| "tab_ai_insights": "🤖 AI Recipe Insights", | |
| "tab_analytics": "📊 Recipe Analytics", | |
| "form_header": "Tell us about your dish", | |
| "dish_name": "Dish Name", | |
| "your_name": "Your Name", | |
| "district": "Your District", | |
| "select_district": "--- Select a District ---", | |
| "recipe_type": "Recipe Type (e.g., Curry, Pickle, Snack)", | |
| "ingredients": "Ingredients (one per line)", | |
| "instructions": "Instructions", | |
| "story": "The Story Behind Your Dish (your memories, family traditions, etc.)", | |
| "image_upload": "Upload a photo of the dish (optional)", | |
| "submit_button": "Submit Recipe", | |
| "success_message": "Thank you! Your recipe has been submitted successfully.", | |
| "gallery_header": "Explore Recipes from the Community", | |
| "ai_analysis": "🤖 AI Analysis", | |
| "similar_recipes": "👥 Similar Recipes", | |
| "cooking_tips": "💡 Smart Cooking Tips", | |
| "recipe_sentiment": "😊 Recipe Story Mood", | |
| "search_placeholder": "Search recipes or ask AI...", | |
| "ai_search_help": "Try: 'spicy vegetarian curry' or 'quick breakfast recipe'", | |
| }, | |
| "te": { | |
| "title": "రుచి చూడు", | |
| "subtitle": "AI శక్తితో మన తెలుగు వంటల సంస్కృతిని కాపాడడం", | |
| "language_select": "భాషను ఎంచుకోండి", | |
| "sidebar_header": "మీ వంటకాన్ని పంపండి!", | |
| "tab_submit": "📝 మీ వంటకాన్ని పంపండి", | |
| "tab_gallery": "🍲 కమ్యూనిటీ గ్యాలరీ", | |
| "tab_ai_insights": "🤖 AI వంటకాల విశ్లేషణ", | |
| "tab_analytics": "📊 వంటకాల విశ్లేషణ", | |
| "form_header": "మీ వంటకం గురించి చెప్పండి", | |
| "dish_name": "వంటకం పేరు", | |
| "your_name": "మీ పేరు", | |
| "district": "మీ జిల్లా", | |
| "select_district": "--- జిల్లాను ఎంచుకోండి ---", | |
| "recipe_type": "వంటకం రకం (ఉదా. కూర, పచ్చడి, స్నాక్)", | |
| "ingredients": "కావలసినవి (ఒకదానికి ఒకటి)", | |
| "instructions": "తయారీ విధానం", | |
| "story": "ఈ వంటతో మీ కథ (మీ జ్ఞాపకాలు, కుటుంబ సంప్రదాయాలు మొదలైనవి)", | |
| "image_upload": "వంటకం ఫోటోను అప్లోడ్ చేయండి (ఐచ్ఛికం)", | |
| "submit_button": "వంటకాన్ని పంపండి", | |
| "success_message": "ధన్యవాదాలు! మీ వంటకం విజయవంతంగా సమర్పించబడింది.", | |
| "gallery_header": "కమ్యూనిటీ నుంది వంటకాలను అన్వేషించండి", | |
| "ai_analysis": "🤖 AI విశ్లేషణ", | |
| "similar_recipes": "👥 సమాన వంటకాలు", | |
| "cooking_tips": "💡 తెలివైన వంట చిట్కాలు", | |
| "recipe_sentiment": "😊 వంటకం కథ భావం", | |
| "search_placeholder": "వంటకాలను వెతకండి లేదా AI ని అడగండి...", | |
| "ai_search_help": "ప్రయత్నించండి: 'కారంగా వెజిటేరియన్ కూర' లేదా 'తొందర అల్పాహారం'", | |
| } | |
| } | |
| # --- Data for Dropdowns --- | |
| DISTRICTS = [ | |
| "Adilabad", "Bhadradri Kothagudem", "Hanumakonda", "Hyderabad", "Jagtial", "Jangaon", | |
| "Jayashankar Bhupalpally", "Jogulamba Gadwal", "Kamareddy", "Karimnagar", "Khammam", | |
| "Komaram Bheem", "Mahabubabad", "Mahbubnagar", "Mancherial", "Medak", "Medchal-Malkajgiri", | |
| "Mulugu", "Nagarkurnool", "Nalgonda", "Narayanpet", "Nirmal", "Nizamabad", "Peddapalli", | |
| "Rajanna Sircilla", "Ranga Reddy", "Sangareddy", "Siddipet", "Suryapet", "Vikarabad", | |
| "Wanaparthy", "Warangal", "Yadadri Bhuvanagiri", "Alluri Sitharama Raju", "Anakapalli", | |
| "Anantapur", "Annamayya", "Bapatla", "Chittoor", "East Godavari", "Eluru", "Guntur", | |
| "Kakinada", "Konaseema", "Krishna", "Kurnool", "Nandyal", "NTR", "Palnadu", "Parvathipuram Manyam", | |
| "Prakasam", "Sri Potti Sriramulu Nellore", "Sri Sathya Sai", "Srikakulam", "Tirupati", | |
| "Visakhapatnam", "Vizianagaram", "West Godavari", "YSR Kadapa" | |
| ] | |
| DISTRICTS.sort() | |
| RECIPE_TYPES = ["Curry (కూర)", "Fry (వేపుడు)", "Pickle (పచ్చడి)", "Chutney (చట్నీ)", "Pulusu (పులుసు)", "Snack (చిరుతిండి)", "Sweet (తీపి)", "Breakfast (అల్పాహారం)", "Rice Dish (అన్నం రకం)", "Other (ఇతర)"] | |
| # --- Session State Initialization --- | |
| if 'language' not in st.session_state: | |
| st.session_state['language'] = 'en' | |
| if 'recipes' not in st.session_state: | |
| st.session_state['recipes'] = load_all_recipes() | |
| if 'ai_models_loaded' not in st.session_state: | |
| st.session_state['ai_models_loaded'] = False | |
| # --- Helper Functions --- | |
| def get_text(key): | |
| """Fetches text from the dictionary based on the selected language.""" | |
| return TEXT[st.session_state['language']][key] | |
| # --- Load AI Models --- | |
| similarity_model, sentiment_analyzer, cuisine_classifier = None, None, None | |
| if AI_AVAILABLE and not st.session_state.get('ai_models_loaded', False): | |
| with st.spinner("🤖 Loading AI models for the first time... This may take a moment."): | |
| similarity_model, sentiment_analyzer, cuisine_classifier = load_ai_models() | |
| if similarity_model: | |
| st.session_state['ai_models_loaded'] = True | |
| st.success("✅ AI models loaded successfully!") | |
| else: | |
| similarity_model, sentiment_analyzer, cuisine_classifier = load_ai_models() | |
| # --- Sidebar --- | |
| with st.sidebar: | |
| st.title(f"🍲 {get_text('title')}") | |
| st.markdown(get_text('subtitle')) | |
| if AI_AVAILABLE and similarity_model: | |
| st.success("🤖 AI Features: Active") | |
| else: | |
| st.warning("🤖 AI Features: Limited") | |
| st.markdown("---") | |
| # Language selector | |
| lang_choice = st.radio( | |
| get_text('language_select'), | |
| ('English', 'తెలుగు'), | |
| horizontal=True, | |
| key='lang_radio' | |
| ) | |
| st.session_state['language'] = 'te' if lang_choice == 'తెలుగు' else 'en' | |
| st.info(get_text('sidebar_header')) | |
| # --- Main App Layout --- | |
| st.title(get_text('title')) | |
| # Create tabs including the new AI insights tab | |
| tab_submit, tab_gallery, tab_ai, tab_analytics = st.tabs([ | |
| get_text('tab_submit'), | |
| get_text('tab_gallery'), | |
| get_text('tab_ai_insights'), | |
| get_text('tab_analytics') | |
| ]) | |
| # --- Submission Form Tab (Enhanced with AI) --- | |
| with tab_submit: | |
| st.header(get_text('form_header')) | |
| with st.form(key="recipe_form"): | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| dish_name = st.text_input(label=get_text('dish_name')) | |
| your_name = st.text_input(label=get_text('your_name')) | |
| image_file = st.file_uploader(get_text('image_upload'), type=['jpg', 'jpeg', 'png']) | |
| with col2: | |
| recipe_type = st.selectbox(label=get_text('recipe_type'), options=RECIPE_TYPES) | |
| district = st.selectbox( | |
| label=get_text('district'), | |
| options=[get_text('select_district')] + DISTRICTS | |
| ) | |
| ingredients = st.text_area(label=get_text('ingredients'), height=150) | |
| instructions = st.text_area(label=get_text('instructions'), height=200) | |
| story = st.text_area(label=get_text('story'), height=150) | |
| submitted = st.form_submit_button(get_text('submit_button')) | |
| if submitted: | |
| if not dish_name: | |
| st.error("Please enter a dish name before submitting.") | |
| else: | |
| recipe_id = generate_recipe_id() | |
| # Create recipe data for AI analysis | |
| recipe_data = { | |
| "id": recipe_id, | |
| "dish_name": dish_name, | |
| "your_name": your_name, | |
| "district": district if district != get_text('select_district') else '', | |
| "recipe_type": recipe_type, | |
| "ingredients": ingredients, | |
| "instructions": instructions, | |
| "story": story, | |
| "submitted_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| } | |
| # AI Analysis | |
| if AI_AVAILABLE and sentiment_analyzer: | |
| with st.spinner("🤖 AI is analyzing your recipe..."): | |
| # Sentiment analysis | |
| sentiment_data = analyze_recipe_sentiment(story, sentiment_analyzer) | |
| # Smart tag extraction | |
| ai_tags = extract_smart_tags(recipe_data, similarity_model) | |
| # Add AI analysis to recipe data | |
| recipe_data.update({ | |
| "ai_tags": ai_tags, | |
| "sentiment_analysis": sentiment_data, | |
| "cooking_tips": generate_cooking_tips(recipe_data) | |
| }) | |
| # Show AI insights | |
| st.subheader("🤖 AI Analysis Results") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.metric("Recipe Mood", sentiment_data['emotion'].title(), f"{sentiment_data['confidence']:.0%} confidence") | |
| st.write("**AI Tags:**", ", ".join([f"`{tag}`" for tag in ai_tags])) | |
| with col2: | |
| st.write("**Smart Cooking Tips:**") | |
| for tip in recipe_data['cooking_tips']: | |
| st.write(f"• {tip}") | |
| else: | |
| # Fallback basic tagging | |
| recipe_data["ai_tags"] = ["Community Recipe"] | |
| recipe_data["sentiment_analysis"] = {"sentiment": "positive", "emotion": "nostalgic"} | |
| recipe_data["cooking_tips"] = ["Follow the recipe step by step for best results."] | |
| # Save recipe | |
| if save_recipe(recipe_data): | |
| st.session_state.recipes.insert(0, recipe_data) | |
| st.success(f"{get_text('success_message')} (ID: {recipe_id})") | |
| st.balloons() | |
| else: | |
| st.error("Failed to save recipe. Please try again.") | |
| # --- Community Gallery Tab (Enhanced with AI) --- | |
| with tab_gallery: | |
| st.header(get_text('gallery_header')) | |
| # AI-powered search | |
| if AI_AVAILABLE and similarity_model: | |
| st.subheader("🔍 AI-Powered Recipe Search") | |
| search_query = st.text_input( | |
| "Search recipes with natural language:", | |
| placeholder=get_text('ai_search_help'), | |
| help="Ask in natural language like 'show me spicy vegetarian curries' or 'quick breakfast ideas'" | |
| ) | |
| if search_query and st.session_state.recipes: | |
| with st.spinner("🤖 AI is searching..."): | |
| # Use semantic search | |
| query_embedding = similarity_model.encode([search_query]) | |
| recipe_texts = [] | |
| valid_recipes = [] | |
| for recipe in st.session_state.recipes: | |
| recipe_text = f"{recipe.get('dish_name', '')} {recipe.get('ingredients', '')} {recipe.get('recipe_type', '')} {' '.join(recipe.get('ai_tags', []))}" | |
| recipe_texts.append(recipe_text) | |
| valid_recipes.append(recipe) | |
| if recipe_texts: | |
| recipe_embeddings = similarity_model.encode(recipe_texts) | |
| similarities = cosine_similarity(query_embedding, recipe_embeddings)[0] | |
| # Get top matches | |
| top_indices = np.argsort(similarities)[::-1][:10] | |
| matching_recipes = [] | |
| for idx in top_indices: | |
| if similarities[idx] > 0.2: # Minimum similarity | |
| matching_recipes.append((valid_recipes[idx], similarities[idx])) | |
| if matching_recipes: | |
| st.success(f"🎯 Found {len(matching_recipes)} recipes matching your search") | |
| display_recipes = [recipe for recipe, _ in matching_recipes] | |
| else: | |
| st.info("No recipes found matching your search. Try different keywords.") | |
| display_recipes = st.session_state.recipes[:5] | |
| else: | |
| display_recipes = st.session_state.recipes | |
| else: | |
| display_recipes = st.session_state.recipes | |
| else: | |
| # Fallback to basic search | |
| search_term = st.text_input("Search recipes:", placeholder=get_text('search_placeholder')) | |
| if search_term: | |
| display_recipes = [r for r in st.session_state.recipes if | |
| search_term.lower() in r['dish_name'].lower() or | |
| search_term.lower() in r.get('ingredients', '').lower()] | |
| else: | |
| display_recipes = st.session_state.recipes | |
| # Display recipes with AI insights | |
| for recipe in display_recipes[:10]: # Show top 10 | |
| with st.expander(f"🍽️ {recipe['dish_name']} (ID: {recipe.get('id', 'N/A')})"): | |
| # Basic recipe info | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.write(f"**By:** {recipe.get('your_name', 'Anonymous')}") | |
| st.write(f"**District:** {recipe.get('district', 'Not specified')}") | |
| st.write(f"**Type:** {recipe.get('recipe_type', 'Not specified')}") | |
| with col2: | |
| # AI Tags | |
| if recipe.get('ai_tags'): | |
| st.write("**AI Tags:**") | |
| for tag in recipe['ai_tags'][:3]: # Show top 3 tags | |
| st.badge(tag) | |
| # Sentiment analysis | |
| if recipe.get('sentiment_analysis'): | |
| sentiment = recipe['sentiment_analysis'] | |
| st.write(f"**Story Mood:** {sentiment['emotion'].title()}") | |
| with col3: | |
| st.write(f"**Submitted:** {recipe.get('submitted_date', 'Unknown')}") | |
| if recipe.get('cooking_tips'): | |
| with st.popover("💡 Cooking Tips"): | |
| for tip in recipe['cooking_tips']: | |
| st.write(f"• {tip}") | |
| # Recipe content | |
| st.markdown("### Ingredients:") | |
| st.text(recipe.get('ingredients', 'No ingredients provided')) | |
| st.markdown("### Instructions:") | |
| st.text(recipe.get('instructions', 'No instructions provided')) | |
| if recipe.get('story'): | |
| st.markdown("### Story:") | |
| st.text(recipe['story']) | |
| # AI-powered similar recipes | |
| if AI_AVAILABLE and similarity_model and len(st.session_state.recipes) > 1: | |
| similar_recipes = find_similar_recipes(recipe, st.session_state.recipes, similarity_model) | |
| if similar_recipes: | |
| st.markdown("### 👥 Similar Recipes:") | |
| sim_cols = st.columns(len(similar_recipes)) | |
| for idx, (sim_recipe, similarity) in enumerate(similar_recipes): | |
| with sim_cols[idx]: | |
| st.write(f"**{sim_recipe['dish_name']}**") | |
| st.write(f"Similarity: {similarity:.0%}") | |
| st.caption(f"By {sim_recipe.get('your_name', 'Anonymous')}") | |
| # --- AI Insights Tab --- | |
| with tab_ai: | |
| st.header("🤖 AI Recipe Insights") | |
| if not AI_AVAILABLE or not similarity_model: | |
| st.warning("AI features are not available. Please install required packages to enable AI insights.") | |
| st.code("pip install sentence-transformers transformers torch scikit-learn") | |
| st.stop() | |
| if not st.session_state.recipes: | |
| st.info("No recipes available for analysis. Submit some recipes first!") | |
| st.stop() | |
| # AI Analysis Dashboard | |
| st.subheader("📊 Community Recipe Analysis") | |
| # Sentiment distribution | |
| sentiments = [recipe.get('sentiment_analysis', {}).get('emotion', 'nostalgic') | |
| for recipe in st.session_state.recipes] | |
| sentiment_counts = Counter(sentiments) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown("**Recipe Story Moods:**") | |
| for emotion, count in sentiment_counts.most_common(): | |
| percentage = (count / len(st.session_state.recipes)) * 100 | |
| st.write(f"😊 {emotion.title()}: {count} recipes ({percentage:.1f}%)") | |
| with col2: | |
| # Most common AI tags | |
| all_tags = [] | |
| for recipe in st.session_state.recipes: | |
| all_tags.extend(recipe.get('ai_tags', [])) | |
| if all_tags: | |
| tag_counts = Counter(all_tags) | |
| st.markdown("**Most Popular Recipe Types:**") | |
| for tag, count in tag_counts.most_common(5): | |
| st.write(f"🏷️ {tag}: {count} recipes") | |
| st.markdown("---") | |
| # Recipe Recommendation Engine | |
| st.subheader("🎯 AI Recipe Recommendations") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| preference = st.selectbox( | |
| "What are you in the mood for?", | |
| ["Spicy dishes", "Sweet treats", "Quick meals", "Traditional recipes", | |
| "Healthy options", "Festival specials", "Comfort food"] | |
| ) | |
| with col2: | |
| dietary_pref = st.selectbox( | |
| "Dietary preference:", | |
| ["Any", "Vegetarian only", "Non-vegetarian", "Healthy options"] | |
| ) | |
| if st.button("🤖 Get AI Recommendations"): | |
| with st.spinner("🤖 AI is finding perfect recipes for you..."): | |
| # Create recommendation query based on preferences | |
| query_map = { | |
| "Spicy dishes": "spicy hot chili pepper karam", | |
| "Sweet treats": "sweet jaggery sugar dessert halwa", | |
| "Quick meals": "quick fast instant easy", | |
| "Traditional recipes": "traditional authentic grandmother ammamma", | |
| "Healthy options": "healthy nutritious protein vitamin", | |
| "Festival specials": "festival celebration ugadi sankranti", | |
| "Comfort food": "comfort home family love" | |
| } | |
| search_query = query_map.get(preference, preference) | |
| # Filter by dietary preference | |
| filtered_recipes = st.session_state.recipes | |
| if dietary_pref == "Vegetarian only": | |
| filtered_recipes = [r for r in filtered_recipes if 'Vegetarian' in r.get('ai_tags', [])] | |
| elif dietary_pref == "Non-vegetarian": | |
| filtered_recipes = [r for r in filtered_recipes if 'Non-Vegetarian' in r.get('ai_tags', [])] | |
| elif dietary_pref == "Healthy options": | |
| filtered_recipes = [r for r in filtered_recipes if any(tag in ['Healthy', 'High Protein', 'Low Fat'] for tag in r.get('ai_tags', []))] | |
| if filtered_recipes: | |
| # Use AI to find best matches | |
| query_embedding = similarity_model.encode([search_query]) | |
| recipe_texts = [] | |
| for recipe in filtered_recipes: | |
| recipe_text = f"{recipe.get('dish_name', '')} {recipe.get('ingredients', '')} {' '.join(recipe.get('ai_tags', []))}" | |
| recipe_texts.append(recipe_text) | |
| if recipe_texts: | |
| recipe_embeddings = similarity_model.encode(recipe_texts) | |
| similarities = cosine_similarity(query_embedding, recipe_embeddings)[0] | |
| # Get top recommendations | |
| top_indices = np.argsort(similarities)[::-1][:3] | |
| st.success(f"🎯 Here are your personalized recommendations:") | |
| for idx in top_indices: | |
| if similarities[idx] > 0.1: # Minimum relevance | |
| recipe = filtered_recipes[idx] | |
| match_score = similarities[idx] * 100 | |
| with st.container(): | |
| st.markdown(f"### 🍽️ {recipe['dish_name']}") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.write(f"**Match Score:** {match_score:.0f}%") | |
| st.write(f"**By:** {recipe.get('your_name', 'Anonymous')}") | |
| with col2: | |
| st.write(f"**Type:** {recipe.get('recipe_type', 'N/A')}") | |
| st.write(f"**District:** {recipe.get('district', 'N/A')}") | |
| with col3: | |
| if recipe.get('ai_tags'): | |
| st.write("**Tags:**") | |
| for tag in recipe['ai_tags'][:2]: | |
| st.badge(tag) | |
| if recipe.get('story'): | |
| st.write(f"*{recipe['story'][:150]}...*") | |
| st.markdown("---") | |
| else: | |
| st.info("No recipes found matching your preferences. Try different criteria!") | |
| st.markdown("---") | |
| # Recipe Insights | |
| st.subheader("🔍 Deep Recipe Analysis") | |
| if st.session_state.recipes: | |
| selected_recipe = st.selectbox( | |
| "Select a recipe to analyze:", | |
| options=[f"{r['dish_name']} (by {r.get('your_name', 'Anonymous')})" for r in st.session_state.recipes], | |
| index=0 | |
| ) | |
| if selected_recipe: | |
| # Find the selected recipe | |
| recipe_idx = next(i for i, r in enumerate(st.session_state.recipes) | |
| if f"{r['dish_name']} (by {r.get('your_name', 'Anonymous')})" == selected_recipe) | |
| recipe = st.session_state.recipes[recipe_idx] | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown("**🤖 AI Analysis:**") | |
| # Sentiment analysis details | |
| if recipe.get('sentiment_analysis'): | |
| sentiment = recipe['sentiment_analysis'] | |
| st.metric( | |
| "Story Emotion", | |
| sentiment['emotion'].title(), | |
| f"{sentiment['confidence']:.0%} confidence" | |
| ) | |
| # AI tags | |
| if recipe.get('ai_tags'): | |
| st.write("**AI-Detected Categories:**") | |
| for tag in recipe['ai_tags']: | |
| st.badge(tag) | |
| with col2: | |
| st.markdown("**💡 Smart Cooking Tips:**") | |
| if recipe.get('cooking_tips'): | |
| for tip in recipe['cooking_tips']: | |
| st.info(tip) | |
| else: | |
| # Generate tips on demand | |
| tips = generate_cooking_tips(recipe) | |
| for tip in tips: | |
| st.info(tip) | |
| # Similar recipes | |
| st.markdown("**👥 Similar Recipes:**") | |
| similar_recipes = find_similar_recipes(recipe, st.session_state.recipes, similarity_model, top_k=5) | |
| if similar_recipes: | |
| for sim_recipe, similarity in similar_recipes: | |
| with st.expander(f"{sim_recipe['dish_name']} - {similarity:.0%} similar"): | |
| st.write(f"**By:** {sim_recipe.get('your_name', 'Anonymous')}") | |
| st.write(f"**Type:** {sim_recipe.get('recipe_type', 'N/A')}") | |
| if sim_recipe.get('story'): | |
| st.write(f"**Story:** {sim_recipe['story'][:200]}...") | |
| else: | |
| st.info("No similar recipes found in the current database.") | |
| # --- Analytics Tab --- | |
| with tab_analytics: | |
| st.header("📊 Recipe Analytics Dashboard") | |
| if not st.session_state.recipes: | |
| st.info("No recipes available for analysis. Submit some recipes first!") | |
| st.stop() | |
| # Basic Statistics | |
| total_recipes = len(st.session_state.recipes) | |
| st.metric("Total Recipes", total_recipes) | |
| # Create dataframe for analysis | |
| df_data = [] | |
| for recipe in st.session_state.recipes: | |
| df_data.append({ | |
| 'dish_name': recipe.get('dish_name', ''), | |
| 'your_name': recipe.get('your_name', 'Anonymous'), | |
| 'district': recipe.get('district', 'Unknown'), | |
| 'recipe_type': recipe.get('recipe_type', 'Other'), | |
| 'submitted_date': recipe.get('submitted_date', ''), | |
| 'ai_tags': recipe.get('ai_tags', []), | |
| 'emotion': recipe.get('sentiment_analysis', {}).get('emotion', 'neutral') | |
| }) | |
| df = pd.DataFrame(df_data) | |
| # District-wise distribution | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.subheader("📍 Recipes by District") | |
| district_counts = df['district'].value_counts().head(10) | |
| if not district_counts.empty: | |
| st.bar_chart(district_counts) | |
| else: | |
| st.info("No district data available") | |
| with col2: | |
| st.subheader("🍽️ Recipe Types Distribution") | |
| type_counts = df['recipe_type'].value_counts() | |
| if not type_counts.empty: | |
| st.bar_chart(type_counts) | |
| else: | |
| st.info("No recipe type data available") | |
| # Contributors | |
| st.subheader("👥 Top Contributors") | |
| contributor_counts = df['your_name'].value_counts().head(5) | |
| for name, count in contributor_counts.items(): | |
| st.write(f"🏆 {name}: {count} recipe(s)") | |
| # Emotional analysis | |
| if AI_AVAILABLE: | |
| st.subheader("😊 Recipe Story Emotions") | |
| emotion_counts = df['emotion'].value_counts() | |
| emotion_colors = { | |
| 'joyful': '#FFD700', | |
| 'nostalgic': '#DDA0DD', | |
| 'proud': '#FF6347', | |
| 'loving': '#FF69B4', | |
| 'neutral': '#87CEEB' | |
| } | |
| emotion_data = [] | |
| for emotion, count in emotion_counts.items(): | |
| emotion_data.append({ | |
| 'Emotion': emotion.title(), | |
| 'Count': count, | |
| 'Percentage': f"{(count/total_recipes)*100:.1f}%" | |
| }) | |
| emotion_df = pd.DataFrame(emotion_data) | |
| st.dataframe(emotion_df, use_container_width=True) | |
| # Recent activity | |
| st.subheader("📅 Recent Recipe Submissions") | |
| recent_recipes = st.session_state.recipes[:5] # Show 5 most recent | |
| for recipe in recent_recipes: | |
| with st.container(): | |
| col1, col2, col3 = st.columns([3, 2, 1]) | |
| with col1: | |
| st.write(f"**{recipe['dish_name']}**") | |
| st.caption(f"by {recipe.get('your_name', 'Anonymous')}") | |
| with col2: | |
| st.write(f"{recipe.get('recipe_type', 'N/A')}") | |
| st.caption(f"{recipe.get('district', 'Unknown')}") | |
| with col3: | |
| st.write(recipe.get('submitted_date', 'Unknown')[:10]) # Show date only | |
| st.markdown("---") | |
| # Export functionality | |
| st.subheader("📤 Export Data") | |
| if st.button("Download Recipe Database as CSV"): | |
| # Flatten the data for CSV export | |
| export_data = [] | |
| for recipe in st.session_state.recipes: | |
| export_data.append({ | |
| 'ID': recipe.get('id', ''), | |
| 'Dish Name': recipe.get('dish_name', ''), | |
| 'Contributor': recipe.get('your_name', ''), | |
| 'District': recipe.get('district', ''), | |
| 'Recipe Type': recipe.get('recipe_type', ''), | |
| 'Ingredients': recipe.get('ingredients', ''), | |
| 'Instructions': recipe.get('instructions', ''), | |
| 'Story': recipe.get('story', ''), | |
| 'AI Tags': ', '.join(recipe.get('ai_tags', [])), | |
| 'Emotion': recipe.get('sentiment_analysis', {}).get('emotion', ''), | |
| 'Submitted Date': recipe.get('submitted_date', '') | |
| }) | |
| export_df = pd.DataFrame(export_data) | |
| csv_data = export_df.to_csv(index=False) | |
| st.download_button( | |
| label="📥 Download CSV", | |
| data=csv_data, | |
| file_name=f"ruchi_chudu_recipes_{datetime.now().strftime('%Y%m%d')}.csv", | |
| mime="text/csv" | |
| ) | |
| # --- Footer --- | |
| st.markdown("---") | |
| st.markdown(""" | |
| <div style='text-align: center; color: #666; padding: 20px;'> | |
| <p>🍲 రుచి చూడు (Ruchi Chudu) - Preserving Telugu Cuisine Through Community & AI</p> | |
| <p>Made with ❤️ for Telugu food lovers everywhere</p> | |
| <p><small>AI Features powered by Hugging Face Transformers | Data stored locally</small></p> | |
| </div> | |
| """, unsafe_allow_html=True) |