diff --git a/.env_sample b/.env_sample new file mode 100644 index 0000000..959bc43 --- /dev/null +++ b/.env_sample @@ -0,0 +1 @@ +GEMINI_API_KEY= Your gemini api \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9ec96d --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Environment variables +.env + +# Flask stuff +instance/ +.webassets-cache + +# Uploaded spreadsheets +static/uploads/* +!static/uploads/.gitkeep + +# Downloaded files +static/downloads/* +!static/downloads/.gitkeep + +# JSON cache files +static/json/* +!static/json/.gitkeep + +# Generated script files +src/script/* +!src/script/__init__.py +!src/script/.gitkeep + +# Virtual environment +venv/ +env/ +ENV/ + +# IDE files +.idea/ +.vscode/ +*.sublime-workspace +*.sublime-project + +# Distribution / packaging +dist/ +build/ +*.egg-info/ + +# Logs +logs/ +*.log + +# OS specific files +.DS_Store +Thumbs.db diff --git a/README.md b/README.md index b58f55c..a908a1e 100644 --- a/README.md +++ b/README.md @@ -2,193 +2,91 @@ 🎉 **Welcome to the FoodSnap AI Hackathon!** 🎉 -Get ready to combine your coding skills with the fascinating world of artificial intelligence and nutrition! We're excited to see what innovative solutions you come up with. +## Overview -**The Challenge:** -Your mission, should you choose to accept it, is to develop a solution that can estimate the caloric content (and ideally, macronutrient breakdown – protein, carbs, fats) of a food item from an input image. +FoodSnap AI is a web application that leverages deep learning and large language models to analyze food images. Users can upload a photo of their meal, and the app will identify the food items, estimate their calories, and suggest portion sizes. The project demonstrates the integration of computer vision, multimodal LLMs, and a modern web UI. -## Table of Contents +## Features -1. [Project Overview](#project-overview) -2. [Why This Project?](#why-this-project) -3. [Scope & Implementation Freedom](#project-scope--implementation-freedom) -4. [Key Objectives / Potential Features](#key-objectives--potential-features) -5. [Helpful Resources & APIs](#helpful-resources--apis) - * [Food Image Datasets](#food-image-datasets) - * [Nutrition Information Databases & APIs](#nutrition-information-databases--apis) - * [Machine Learning / AI Tools](#machine-learning--ai-tools) -6. [Getting Started: Git & Submission Workflow](#getting-started-git--submission-workflow) - * [1. Fork the Repository](#1-fork-the-repository) - * [2. Clone Your Forked Repository](#2-clone-your-forked-repository) - * [3. Create a New Branch](#3-create-a-new-branch) - * [4. Develop Your Project](#4-develop-your-project) - * [5. Push Your Branch to Your Fork](#5-push-your-branch-to-your-fork) - * [6. Submit Your Project (Create a Pull Request)](#6-submit-your-project-create-a-pull-request) -7. [What to Include in Your Submission](#what-to-include-in-your-submission) -8. [Judging Criteria (General Pointers)](#judging-criteria-general-pointers) +- **User Authentication:** Sign up, login, and logout functionality. +- **Modern UI:** Responsive, theme-switchable (light/dark) interface for uploading and analyzing food images. +- **Food Image Validation:** Uses a pretrained ResNet18 model to check if the uploaded image is likely to be food. +- **Image Preprocessing:** Uploaded images are resized and compressed for efficient processing. +- **Multimodal LLM Integration:** Calls Google Gemini's multimodal API to analyze the food image and extract structured information. +- **Results Table:** Displays detected food items, estimated calories, and portion sizes in a clear table, including total calories. +- **Robust Error Handling:** User-friendly error messages for invalid uploads or processing failures. +- **Extensible Architecture:** Modular codebase for easy extension or replacement of components. +## Tech Stack ---- +- **Frontend:** HTML, CSS (theme-light/dark), JavaScript (vanilla, no frameworks), Bootstrap for forms. +- **Backend:** Python, Flask (REST API), PyTorch (torchvision), Google Generative AI (Gemini API), Pillow, python-dotenv. +- **Model:** Pretrained ResNet18 (ImageNet) for food image validation. +- **LLM:** Google Gemini multimodal API for food, calorie, and portion analysis. -## Project Overview +## System Architecture -**Project Name:** FoodSnap AI +- **MVC Pattern:** + - *Model*: Food classifier, preprocessing, and LLM integration in backend. + - *View*: Jinja2 HTML templates and CSS for UI. + - *Controller*: Flask routes handle user requests and orchestrate processing. -**The Challenge:** -Develop a solution that can estimate the caloric content (and ideally, macronutrient breakdown – protein, carbs, fats) of a food item from an input image. +- **Separation of Concerns:** + - Configuration (labels, prompts) in `appconfig.py`. + - Static files (CSS/JS) and templates in dedicated folders. -## Why This Project? +- **API Gateway:** + - All user interactions go through Flask API endpoints. -Understanding calorie intake is crucial for health and fitness. Manually logging food can be tedious. FoodSnap AI aims to simplify this process, making nutritional awareness more accessible to everyone. Imagine snapping a photo of your meal and instantly getting its nutritional information! +- **Internal Service Calls:** + - `/upload/` endpoint internally calls `/analyze-food/` for streamlined workflow. -## Project Scope & Implementation Freedom +- **Security & Validation:** + - File type and content validation on backend. + - Only allowed image formats are processed. -You have complete freedom in how you bring FoodSnap AI to life! +## How It Works -* **Platform:** Mobile App (iOS, Android, cross-platform), Web App, Website, or even a Command Line Interface (CLI) tool. -* **Technology Stack:** Use any programming languages, frameworks, libraries, or APIs you prefer. Python, JavaScript, Java, Swift, Kotlin, Ruby, Go – the choice is yours! -* **Approach:** You can use pre-trained machine learning models, train your own, leverage existing food recognition APIs, or come up with a completely novel approach. +1. **User uploads a food image** via the web UI. +2. **Frontend** converts the image to base64 and sends it as JSON to the `/upload/` endpoint. +3. **Backend** decodes and saves the image, validates it as food, and preprocesses it. +4. The backend **calls the Gemini multimodal API** with the image and a prompt. +5. **Gemini LLM** returns a JSON with food items, calories, and portions. +6. **Frontend displays** the results in a table, including a total calories row. -## Key Objectives / Potential Features +## Usage Instructions -1. **Image Input:** The system must accept an image of food as input. -2. **Food Identification (Implicit or Explicit):** The system needs to identify the food item(s) in the image. This could be a direct output or an internal step. -3. **Calorie Estimation:** Based on the identified food, estimate its total calories. -4. **Macronutrient Breakdown (Bonus):** If possible, also estimate protein, carbohydrates, and fats. -5. **Portion Size Consideration (Advanced Bonus):** Accurately estimating portion size from an image is challenging but would be a significant enhancement. -6. **User Interface (for non-CLI):** If you're building an app or website, make it user-friendly and intuitive. -7. **Multiple Food Items (Advanced Bonus):** Can your solution handle an image with multiple food items on a plate? - -## Helpful Resources & APIs - -To get you started, here are some resources that might be useful. You are **not limited** to these and are encouraged to explore! - -### Food Image Datasets -*(For training/inspiration if you go the custom ML route)* +1. **Clone the repository** and install dependencies: + ```bash + pip install -r requirements.txt + ``` -* **Food-101:** [https://data.vision.ee.ethz.ch/cvl/datasets_extra/food-101/](https://data.vision.ee.ethz.ch/cvl/datasets_extra/food-101/) (101 food categories, 101,000 images) -* **UECFood-100 / UECFood-256:** [http://foodcam.mobi/dataset.html](http://foodcam.mobi/dataset.html) (Japanese food primarily, good for object detection) -* **Recipe1M+:** [http://pic2recipe.csail.mit.edu/](http://pic2recipe.csail.mit.edu/) (Images and recipes) -* **Google Images / Flickr:** Can be used for scraping specific food images (be mindful of terms of service). +2. **Set up your environment variables:** + - Copy `.env_sample` to `.env` and add your Gemini API key. -### Nutrition Information Databases & APIs -*(For calorie/macro lookup)* +3. **Run the Flask app:** + ```bash + python app.py + ``` -* **USDA FoodData Central API:** [https://fdc.nal.usda.gov/api-guide.html](https://fdc.nal.usda.gov/api-guide.html) (Comprehensive US food composition database) -* **Edamam Food Database API:** [https://developer.edamam.com/food-database-api](https://developer.edamam.com/food-database-api) (Offers free tier for recipe analysis and food database lookup) -* **Spoonacular API:** [https://spoonacular.com/food-api](https://spoonacular.com/food-api) (Nutrition, recipes, food products, free tier available) -* **MyFitnessPal / CalorieKing / FatSecret:** While direct API access might be limited or paid, these websites are excellent sources for manual data collection or understanding how nutritional information is presented. Web scraping *could* be an option, but always respect `robots.txt` and terms of service. -* **Open Food Facts:** [https://world.openfoodfacts.org/](https://world.openfoodfacts.org/) (A collaborative, free, and open database of food products from around the world. They have an API.) +4. **Open your browser** and go to `http://localhost:5000`. -### Machine Learning / AI Tools +5. **Sign up or log in**, then upload a food image to analyze. -* **TensorFlow / Keras:** For building and training custom models. -* **PyTorch:** Another popular deep learning framework. -* **OpenCV:** For image processing tasks. -* **Pre-trained Image Recognition Models:** (e.g., MobileNet, ResNet, InceptionV3 available via TensorFlow Hub, PyTorch Hub, etc.) These can often be fine-tuned for food recognition. -* **Cloud AI Services:** Google Cloud Vision AI, AWS Rekognition, Azure Computer Vision (these often have free tiers for experimentation and can perform object/food recognition out-of-the-box). +## Configuration -## Getting Started: Git & Submission Workflow +- **Food labels and prompts** are managed in `appconfig.py`. +- **Allowed image formats:** PNG, JPG, JPEG. +- **Uploads** are saved in `static/uploads/`. -We will be using GitHub for version control and submission. Please follow these steps carefully. +## Extending the Project -**This Repository (Main Project):** `https://github.com/WeCode-Community-Dev/foodsnap-ai` +- Swap out the food classifier in `app.py` for a more accurate model. +- add a storage either azure blob storage or S3 bucket due to scalability ,accesibility. +- Update the prompt in `appconfig.py` for different LLM behaviors. +- Add more endpoints or UI features as needed. -### 1. Fork the Repository -* Go to the main project repository: `https://github.com/WeCode-Community-Dev/foodsnap-ai` -* In the top-right corner of the page, click the "**Fork**" button. -* This will create a copy of the repository under your own GitHub account (e.g., `https://github.com/YOUR_USERNAME/foodsnap-ai`). This is *your* personal remote copy. +## License -### 2. Clone Your Forked Repository -* On your GitHub page for *your forked repository* (`https://github.com/YOUR_USERNAME/foodsnap-ai`), click the green "**Code**" button. -* Copy the HTTPS or SSH URL. -* Open your terminal or Git client and run: - ```bash - git clone https://github.com/YOUR_USERNAME/foodsnap-ai.git - cd foodsnap-ai - ``` - (Replace `YOUR_USERNAME` with your actual GitHub username.) +This project is for educational and hackathon purposes only. -### 3. Create a New Branch -* It's crucial to work on a new branch rather than directly on `main` or `master`. -* Choose a descriptive branch name, for example, `feature/your-team-name` or `solution-john-doe`. -* In your terminal, inside the `foodsnap-ai` directory, run: - ```bash - git checkout -b feature/your-team-name - ``` - (e.g., `git checkout -b feature/awesome-coders` or `git checkout -b solution-jane-doe`) -* You are now on your new branch. Verify by running `git branch`. - -### 4. Develop Your Project -* Start coding! Add your files, write your logic, and build your FoodSnap AI solution. -* Commit your changes frequently with clear commit messages: - ```bash - # Stage all new and modified files - git add . - # Or stage specific files - # git add path/to/your/file.py path/to/another/file.js - - # Commit your changes - git commit -m "feat: Implement image upload functionality" - # Example commit types: feat, fix, docs, style, refactor, test, chore - ``` - -### 5. Push Your Branch to Your Fork -* When you're ready to save your progress to *your remote fork on GitHub*, push your branch: - ```bash - git push origin feature/your-team-name - ``` - (Replace `feature/your-team-name` with your actual branch name.) -* If it's the first time pushing this branch, Git might suggest a command like `git push --set-upstream origin feature/your-team-name`. Use that command. - -### 6. Submit Your Project (Create a Pull Request) -* Once your project is complete (or at a submittable stage), go to *your* forked repository on GitHub (`https://github.com/YOUR_USERNAME/foodsnap-ai`). -* You should see a prompt saying "`feature/your-team-name` had recent pushes". Click the "**Compare & pull request**" button. -* If you don't see the prompt, go to the "**Pull requests**" tab and click "**New pull request**". -* **Crucially, ensure the settings are:** - * **Base repository:** `WeCode-Community-Dev/foodsnap-ai` - * **Base branch:** `main` (or `master`, whichever is the default for this repository) - * **Head repository:** `YOUR_USERNAME/foodsnap-ai` - * **Compare branch:** `feature/your-team-name` (your development branch) -* Write a clear title and a detailed description for your Pull Request (PR). Include: - * A brief overview of your solution. - * Technologies used. - * How to run/test your project (setup, commands, etc.). - * Any known issues or limitations. - * A link to a live demo if applicable (e.g., Heroku, Netlify, GitHub Pages). - * Screenshots or a short video showcasing your project can be very helpful! -* Click "**Create pull request**". - -## What to Include in Your Submission -*(In your project's directory, pushed to your branch and included in the PR)* - -* **Source Code:** All the code for your project. -* **`README.md` (Your Project's README):** This is very important! Your project's `README.md` (different from this main hackathon `README.md`) should include: - * Project Title & Team Name/Members (if applicable). - * A brief description of your FoodSnap AI solution. - * What features you implemented. - * Tech stack used. - * **Clear instructions on how to set up and run *your specific project* locally.** This includes dependencies, environment variables, build steps, and run commands. - * Any API keys or environment variables needed (explain how to get them, but **DO NOT COMMIT ACTUAL KEYS** to the repository). Use a `.env.example` file to show what's needed. - * Link to a live demo (if any). -* **(Optional but Recommended)** A short demo video or presentation slides (you can link these in your PR description or in your project's README). - -## Judging Criteria (General Pointers) - -While specific criteria might be announced, generally projects are evaluated on: - -* **Functionality:** Does it work as intended? Does it achieve the core goal of calorie estimation from an image? -* **Accuracy:** How close are the calorie/macro estimations? (We understand this is complex!) -* **Innovation & Creativity:** Did you come up with a unique approach or an interesting feature? -* **Technical Implementation:** Quality of code, choice of technology, and complexity handled. -* **User Experience (UX/UI):** If applicable, is the application easy and pleasant to use? -* **Presentation/Demo:** How well you explain and showcase your project. -* **Adherence to Submission Guidelines:** Including a good project `README.md` and following the Git workflow. - - ---- - -Good luck, innovators! We can't wait to see your FoodSnap AI creations. Remember to have fun, learn, and collaborate! - -**Happy Hacking!** -The WeCode Community Dev Team diff --git a/app.py b/app.py new file mode 100644 index 0000000..38d75a6 --- /dev/null +++ b/app.py @@ -0,0 +1,193 @@ +from flask import Flask, request, jsonify, render_template, redirect, url_for, session +from PIL import Image +import os +import io +import torch +from torchvision import transforms, models +from dotenv import load_dotenv +import google.generativeai as genai +from appconfig import FOOD_ANALYSIS_PROMPT, test_secret_key, ALLOWED_EXTENSIONS +import time +from login import login + +# --- Config and Setup --- + +load_dotenv() +GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") +UPLOAD_DIR = "uploads" +os.makedirs(UPLOAD_DIR, exist_ok=True) + +# Flask app +flask_app = Flask(__name__, template_folder="templates", static_folder="static") +flask_app.secret_key = test_secret_key # Change this in production + +# Register login, logout, signup routes +login(flask_app) + +# Pretrained model for food classification +food_model = models.resnet18(pretrained=True) +food_model.eval() +food_transform = transforms.Compose([ + transforms.Resize((224, 224)), + transforms.ToTensor(), + transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), +]) + +# --- Utility Functions --- +def allowed_file(filename): + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + +def is_food_image(image_bytes, threshold=0.5): + image = Image.open(io.BytesIO(image_bytes)).convert("RGB") + input_tensor = food_transform(image).unsqueeze(0) + with torch.no_grad(): + output = food_model(input_tensor) + probabilities = torch.nn.functional.softmax(output[0], dim=0) + top5_prob, top5_catid = torch.topk(probabilities, 5) + # For demonstration, treat ImageNet class indices 948-1000 as food (not accurate, just for demo) + is_food = any(948 <= catid <= 1000 for catid in top5_catid.tolist()) + return is_food or top5_prob[0].item() > threshold + +def preprocess_image(image_bytes, max_size_mb=2, target_size=512): + image = Image.open(io.BytesIO(image_bytes)).convert("RGB") + width, height = image.size + if max(width, height) > target_size: + if width > height: + new_width = target_size + new_height = int(target_size * height / width) + else: + new_height = target_size + new_width = int(target_size * width / height) + image = image.resize((new_width, new_height)) + buf = io.BytesIO() + quality = 95 + image.save(buf, format='JPEG', quality=quality) + while buf.tell() > max_size_mb * 1024 * 1024 and quality > 20: + buf.seek(0) + buf.truncate() + quality -= 5 + image.save(buf, format='JPEG', quality=quality) + return buf.getvalue() + +def call_gemini_multimodal(image_bytes): + genai.configure(api_key=GEMINI_API_KEY) + model = genai.GenerativeModel('gemini-1.5-flash') + # Use the correct way to send image data (as a dict with mime_type and data) + prompt = FOOD_ANALYSIS_PROMPT + response = model.generate_content( + [ + prompt, + { + "mime_type": "image/jpeg", + "data": image_bytes + } + ], + stream=False + ) + try: + text = response.candidates[0].content.parts[0].text + start = text.find('{') + end = text.rfind('}') + 1 + if start != -1 and end != -1: + json_str = text[start:end] + import json + return json.loads(json_str) + else: + return {"error": "Uploaded image is not recognized as food.", "raw_response": text} + except Exception: + return {"error": "Failed to parse Gemini response", "raw_response": str(response)} + +# --- Flask Endpoints --- +@flask_app.route("/", methods=["GET"]) +def flask_index(): + if "user" not in session: + return redirect(url_for("login_view")) + return render_template("index.html") + +@flask_app.route("/upload/", methods=["POST"]) +def flask_upload_image(): + if request.is_json: + # Simulate upload delay + time.sleep(1) + data = request.get_json() + image_data = data.get("image_data") + filename = data.get("filename", "uploaded_image.jpg") + extension = filename.rsplit('.', 1)[-1].lower() + if not image_data: + return jsonify({"error": "No image data provided."}) + # Simulate base64 conversion delay + time.sleep(1) + # Remove base64 header if present + if "," in image_data: + image_data = image_data.split(",")[1] + import base64 + image_bytes = base64.b64decode(image_data) + if not allowed_file(f"file.{extension}"): + return jsonify({"error": "Invalid file type. Only png, jpg, jpeg allowed."}) + if not is_food_image(image_bytes): + return jsonify({"error": "Uploaded image is not recognized as food."}) + # Save image in static/uploads + static_upload_dir = os.path.join("static", UPLOAD_DIR) + os.makedirs(static_upload_dir, exist_ok=True) + file_location = os.path.join(static_upload_dir, filename) + with open(file_location, "wb") as buffer: + buffer.write(image_bytes) + # Simulate file save delay + time.sleep(1) + # Pass the file path to analyze-food + data = { + "file_path": file_location, + "extension": extension + } + with flask_app.test_request_context( + "/analyze-food/", + method="POST", + json=data + ): + return flask_analyze_food() + + +@flask_app.route("/analyze-food/", methods=["POST"]) +def flask_analyze_food(): + if request.is_json: + # Simulate processing delay + time.sleep(1) + data = request.get_json() + file_path = data.get("file_path") + extension = data.get("extension", "jpg") + if not file_path or not os.path.exists(file_path): + return jsonify({"error": "Image file not found."}) + with open(file_path, "rb") as f: + image_bytes = f.read() + if not allowed_file(f"file.{extension}"): + return jsonify({"error": "Invalid file type. Only png, jpg, jpeg allowed."}) + if not is_food_image(image_bytes): + return jsonify({"error": "Uploaded image is not recognized as food."}) + else: + if "file" not in request.files: + return jsonify({}) + file = request.files["file"] + if file.filename == "": + return jsonify({}) + if not allowed_file(file.filename): + return jsonify({}) + image_bytes = file.read() + if not is_food_image(image_bytes): + return jsonify({}) + # Simulate preprocessing delay + time.sleep(1) + processed_image = preprocess_image(image_bytes) + # Simulate LLM call delay + time.sleep(1) + result = call_gemini_multimodal(processed_image) + return jsonify(result) + +# --- Run Section --- +if __name__ == "__main__": + import sys + if "fastapi" in sys.argv: + import uvicorn + uvicorn.run("app:fastapi_app", host="0.0.0.0", port=8000, reload=True) + else: + flask_app.run(debug=True) + diff --git a/appconfig.py b/appconfig.py new file mode 100644 index 0000000..41d772f --- /dev/null +++ b/appconfig.py @@ -0,0 +1,30 @@ +FOOD_LABELS = [ + "apple_pie", "baby_back_ribs", "baklava", "beef_carpaccio", "beef_tartare", "beet_salad", "beignets", + "bibimbap", "bread_pudding", "breakfast_burrito", "bruschetta", "caesar_salad", "cannoli", "caprese_salad", + "carrot_cake", "ceviche", "cheesecake", "cheese_plate", "chicken_curry", "chicken_quesadilla", "chicken_wings", + "chocolate_cake", "chocolate_mousse", "churros", "clam_chowder", "club_sandwich", "crab_cakes", "creme_brulee", + "croque_madame", "cup_cakes", "deviled_eggs", "donuts", "dumplings", "edamame", "eggs_benedict", "escargots", + "falafel", "filet_mignon", "fish_and_chips", "foie_gras", "french_fries", "french_onion_soup", "french_toast", + "fried_calamari", "fried_rice", "frozen_yogurt", "garlic_bread", "gnocchi", "greek_salad", "grilled_cheese_sandwich", + "grilled_salmon", "guacamole", "gyoza", "hamburger", "hot_and_sour_soup", "hot_dog", "huevos_rancheros", + "hummus", "ice_cream", "lasagna", "lobster_bisque", "lobster_roll_sandwich", "macaroni_and_cheese", "macarons", + "miso_soup", "mussels", "nachos", "omelette", "onion_rings", "oysters", "pad_thai", "paella", "pancakes", + "panna_cotta", "peking_duck", "pho", "pizza", "pork_chop", "poutine", "prime_rib", "pulled_pork_sandwich", + "ramen", "ravioli", "red_velvet_cake", "risotto", "samosa", "sashimi", "scallops", "seaweed_salad", "shrimp_and_grits", + "spaghetti_bolognese", "spaghetti_carbonara", "spring_rolls", "steak", "strawberry_shortcake", "sushi", "tacos", + "takoyaki", "tiramisu", "tuna_tartare", "waffles" +] + +FOOD_ANALYSIS_PROMPT = ( + "You are a food analysis assistant. " + "Given the following image, identify the food items present, estimate the calories for each item, " + "and calculate the portion size or amount for each food. " + "Return the result as a JSON object with keys: 'foods' (list of food items), " + "'calories' (list of calorie estimates), and 'portions' (list of portion descriptions). " + "Example: {\"foods\": [\"pizza\", \"salad\"], \"calories\": [300, 80], \"portions\": [\"2 slices\", \"1 cup\"]}" +) + +#secret key for development +test_secret_key = "supersecretkey" + +ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg"} diff --git a/diagrams/foodsnap_ai_systemarchitecture_diagram.png b/diagrams/foodsnap_ai_systemarchitecture_diagram.png new file mode 100644 index 0000000..7d6b547 Binary files /dev/null and b/diagrams/foodsnap_ai_systemarchitecture_diagram.png differ diff --git a/login.py b/login.py new file mode 100644 index 0000000..5812f89 --- /dev/null +++ b/login.py @@ -0,0 +1,38 @@ +from flask import render_template, request, redirect, url_for, session +from werkzeug.security import generate_password_hash, check_password_hash + +# Simple in-memory user store for demonstration +USERS = { + "username": generate_password_hash("password") +} + +def login(flask_app): + @flask_app.route("/login", methods=["GET", "POST"]) + def login_view(): + if request.method == "POST": + username = request.form.get("username") + password = request.form.get("password") + if username in USERS and check_password_hash(USERS[username], password): + session["user"] = username + return redirect(url_for("flask_index")) + else: + return render_template("login.html", error="Invalid username or password") + return render_template("login.html") + + @flask_app.route("/logout") + def logout_view(): + session.pop("user", None) + return redirect(url_for("login_view")) + + @flask_app.route("/signup", methods=["GET", "POST"]) + def signup_view(): + if request.method == "POST": + username = request.form.get("username") + password = request.form.get("password") + if not username or not password: + return render_template("signup.html", error="Username and password required") + if username in USERS: + return render_template("signup.html", error="Username already exists") + USERS[username] = generate_password_hash(password) + return redirect(url_for("login_view")) + return render_template("signup.html") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8be6a22 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,47 @@ +annotated-types==0.7.0 +blinker==1.9.0 +cachetools==5.5.2 +certifi==2025.6.15 +charset-normalizer==3.4.2 +click==8.2.1 +colorama==0.4.6 +filelock==3.18.0 +Flask==3.1.1 +fsspec==2025.5.1 +google-ai-generativelanguage==0.6.15 +google-api-core==2.25.1 +google-api-python-client==2.172.0 +google-auth==2.40.3 +google-auth-httplib2==0.2.0 +google-generativeai==0.8.5 +googleapis-common-protos==1.70.0 +grpcio==1.73.0 +grpcio-status==1.71.0 +httplib2==0.22.0 +idna==3.10 +itsdangerous==2.2.0 +Jinja2==3.1.6 +MarkupSafe==3.0.2 +mpmath==1.3.0 +networkx==3.5 +numpy==2.3.0 +pillow==11.2.1 +proto-plus==1.26.1 +protobuf==5.29.5 +pyasn1==0.6.1 +pyasn1_modules==0.4.2 +pydantic==2.11.7 +pydantic_core==2.33.2 +pyparsing==3.2.3 +python-dotenv==1.1.0 +requests==2.32.4 +rsa==4.9.1 +sympy==1.14.0 +torch==2.7.1 +torchvision==0.22.1 +tqdm==4.67.1 +typing-inspection==0.4.1 +typing_extensions==4.14.0 +uritemplate==4.2.0 +urllib3==2.4.0 +Werkzeug==3.1.3 diff --git a/sample_data/Masala-Dosa.jpg b/sample_data/Masala-Dosa.jpg new file mode 100644 index 0000000..4c289c6 Binary files /dev/null and b/sample_data/Masala-Dosa.jpg differ diff --git a/sample_data/food2.jpg b/sample_data/food2.jpg new file mode 100644 index 0000000..ac63034 Binary files /dev/null and b/sample_data/food2.jpg differ diff --git a/sample_data/food3.jpg b/sample_data/food3.jpg new file mode 100644 index 0000000..ff9fe76 Binary files /dev/null and b/sample_data/food3.jpg differ diff --git a/sample_data/omlette.jpg b/sample_data/omlette.jpg new file mode 100644 index 0000000..c47e858 Binary files /dev/null and b/sample_data/omlette.jpg differ diff --git a/sample_data/sample2.jpg b/sample_data/sample2.jpg new file mode 100644 index 0000000..487317f Binary files /dev/null and b/sample_data/sample2.jpg differ diff --git a/sample_data/samplebird.jpg b/sample_data/samplebird.jpg new file mode 100644 index 0000000..0b0fd7b Binary files /dev/null and b/sample_data/samplebird.jpg differ diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..5df3264 --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,98 @@ +body, html { + margin: 0; + padding: 0; + font-family: 'Segoe UI', Arial, sans-serif; + min-height: 100vh; + background: var(--bg); + color: var(--text); + transition: background 0.3s, color 0.3s; +} +.container { + max-width: 500px !important; + margin: 2rem auto; + background: var(--card-bg); + border-radius: 16px; + box-shadow: 0 4px 24px rgba(0,0,0,0.08); + padding: 2rem; +} +.container-narrow-sm { + max-width: 320px; + margin-left: auto; + margin-right: auto; +} +header { + display: flex; + justify-content: space-between; + align-items: center; +} +h1 { + margin: 0; + font-size: 2rem; + letter-spacing: 1px; +} +#theme-toggle { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: var(--text); + transition: color 0.3s; +} +.upload-section { + display: flex; + gap: 1rem; + margin: 2rem 0 1rem 0; + align-items: center; +} +.upload-label { + background: var(--button-bg); + color: var(--button-text); + padding: 0.7rem 1.2rem; + border-radius: 8px; + cursor: pointer; + border: none; + font-weight: 500; + transition: background 0.3s, color 0.3s; +} +#analyzeBtn { + background: var(--accent); + color: #fff; + border: none; + border-radius: 8px; + padding: 0.7rem 1.2rem; + font-weight: 600; + cursor: pointer; + transition: background 0.3s; +} +#analyzeBtn:disabled { + background: #aaa; + cursor: not-allowed; +} +#preview img { + max-width: 100%; + margin: 1rem 0; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.07); +} +.result-card { + background: var(--result-bg); + border-radius: 12px; + padding: 1.2rem; + margin-top: 1.5rem; + box-shadow: 0 2px 8px rgba(0,0,0,0.05); +} +.result-card h2 { + margin-top: 0; +} +#error { + color: #e74c3c; + margin-top: 1rem; + font-weight: 500; + min-height: 1.5em; +} +footer { + text-align: center; + margin-top: 2rem; + color: var(--footer-text); + font-size: 0.95rem; +} diff --git a/static/css/theme-dark.css b/static/css/theme-dark.css new file mode 100644 index 0000000..0a49d09 --- /dev/null +++ b/static/css/theme-dark.css @@ -0,0 +1,10 @@ +:root { + --bg: #181818; + --text: #f7f7f7; + --card-bg: #232323; + --button-bg: #232323; + --button-text: #f7f7f7; + --accent: #00b4d8; + --result-bg: #23272e; + --footer-text: #aaa; +} diff --git a/static/css/theme-light.css b/static/css/theme-light.css new file mode 100644 index 0000000..442cc6c --- /dev/null +++ b/static/css/theme-light.css @@ -0,0 +1,10 @@ +:root { + --bg: #f7f7f7; + --text: #181818; + --card-bg: #fff; + --button-bg: #eaeaea; + --button-text: #181818; + --accent: #0078d4; + --result-bg: #f3f6fa; + --footer-text: #888; +} diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..59ef29e --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,131 @@ +document.addEventListener("DOMContentLoaded", function () { + const themeToggle = document.getElementById("theme-toggle"); + const themeStyle = document.getElementById("theme-style"); + const imageInput = document.getElementById("imageInput"); + const analyzeBtn = document.getElementById("analyzeBtn"); + const preview = document.getElementById("preview"); + const resultCard = document.getElementById("result-card"); + const resultDiv = document.getElementById("result"); + const errorDiv = document.getElementById("error"); + + // Theme toggle logic + function setTheme(theme) { + if (theme === "dark") { + themeStyle.href = "/static/css/theme-dark.css"; + themeToggle.textContent = "☀️"; + } else { + themeStyle.href = "/static/css/theme-light.css"; + themeToggle.textContent = "🌙"; + } + localStorage.setItem("theme", theme); + } + themeToggle.addEventListener("click", () => { + const current = localStorage.getItem("theme") || "light"; + setTheme(current === "light" ? "dark" : "light"); + }); + setTheme(localStorage.getItem("theme") || "light"); + + // Image preview + imageInput.addEventListener("change", function () { + preview.innerHTML = ""; + errorDiv.textContent = ""; + resultCard.style.display = "none"; + const file = this.files[0]; + if (file) { + const img = document.createElement("img"); + img.src = URL.createObjectURL(file); + img.onload = () => URL.revokeObjectURL(img.src); + preview.appendChild(img); + } + }); + + // Move the event listener registration inside DOMContentLoaded + if (analyzeBtn) { + analyzeBtn.addEventListener("click", async function () { + errorDiv.textContent = ""; + resultCard.style.display = "none"; + // resultDiv.textContent = ""; + const file = imageInput.files[0]; + if (!file) { + errorDiv.textContent = "Please select an image."; + return; + } + analyzeBtn.disabled = true; + analyzeBtn.textContent = "Analyzing..."; + try { + // Read file as base64 and send as JSON + const reader = new FileReader(); + reader.onload = async function (e) { + const base64Data = e.target.result; + const extension = file.name.split('.').pop().toLowerCase(); + const res = await fetch("/upload/", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + image_data: base64Data, + extension: extension + }) + }); + const data = await res.json(); + if (data.error) { + errorDiv.textContent = data.error; + } else { + resultCard.style.display = "block"; + // Inject results into result-table-body + const tableBody = document.getElementById("result-table-body"); + if (tableBody) { + tableBody.innerHTML = ""; + const foods = Array.isArray(data.foods) ? data.foods : []; + const calories = Array.isArray(data.calories) ? data.calories : []; + const portions = Array.isArray(data.portions) ? data.portions : []; + let totalCalories = 0; + for (let i = 0; i < foods.length; i++) { + const tr = document.createElement("tr"); + const tdFood = document.createElement("td"); + tdFood.textContent = foods[i] || "-"; + const tdCal = document.createElement("td"); + tdCal.textContent = calories[i] !== undefined ? calories[i] : "-"; + const tdPortion = document.createElement("td"); + tdPortion.textContent = portions[i] || "-"; + tr.appendChild(tdFood); + tr.appendChild(tdCal); + tr.appendChild(tdPortion); + tableBody.appendChild(tr); + // Sum calories if it's a number + const calVal = parseFloat(calories[i]); + if (!isNaN(calVal)) { + totalCalories += calVal; + } + } + // Add total calories row + if (foods.length > 0) { + const totalTr = document.createElement("tr"); + const tdLabel = document.createElement("td"); + tdLabel.textContent = "Total Calories"; + tdLabel.colSpan = 1; + tdLabel.style.fontWeight = "bold"; + const tdTotal = document.createElement("td"); + tdTotal.textContent = totalCalories; + tdTotal.style.fontWeight = "bold"; + const tdEmpty = document.createElement("td"); + totalTr.appendChild(tdLabel); + totalTr.appendChild(tdTotal); + totalTr.appendChild(tdEmpty); + tableBody.appendChild(totalTr); + } + } + } + analyzeBtn.disabled = false; + analyzeBtn.textContent = "Analyze"; + }; + reader.readAsDataURL(file); + } catch (e) { + errorDiv.textContent = "An error occurred. Please try again."; + analyzeBtn.disabled = false; + analyzeBtn.textContent = "Analyze"; + } + }); + } +}); diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..b003553 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,51 @@ + + + + + FoodSnap AI + + + + + +
+
+

FoodSnap AI

+ + Logout +
+
+
+ + +
+
+ +
+
+ +
+ + + diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..0a5dee4 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,38 @@ + + + + + Login - FoodSnap AI + + + + + + +
+
+

FoodSnap AI

+

Login

+
+ {% if error %} +
{{ error }}
+ {% endif %} +
+
+ + +
+
+ + +
+ +
+ +
+ + + + diff --git a/templates/signup.html b/templates/signup.html new file mode 100644 index 0000000..0e0241b --- /dev/null +++ b/templates/signup.html @@ -0,0 +1,38 @@ + + + + + Sign Up - FoodSnap AI + + + + + + +
+
+

FoodSnap AI

+

Sign Up

+
+ {% if error %} +
{{ error }}
+ {% endif %} +
+
+ + +
+
+ + +
+ +
+ +
+ + + +