Sakshi commited on
Commit
96f792c
·
1 Parent(s): 4f679f4
.DS_Store ADDED
Binary file (8.2 kB). View file
 
Dockerfile ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies (tesseract-ocr)
7
+ RUN apt-get update && apt-get install -y \
8
+ tesseract-ocr \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy requirements file
12
+ COPY requirements.txt .
13
+
14
+ # Install python dependencies
15
+ RUN pip install --no-cache-dir -r requirements.txt
16
+
17
+ # Copy application code
18
+ COPY . .
19
+
20
+ # Create .tmp directory and set permissions
21
+ RUN mkdir -p .tmp && chmod 777 .tmp
22
+
23
+ # Create a non-root user (Hugging Face requirement)
24
+ RUN useradd -m -u 1000 user
25
+ USER user
26
+
27
+ # Expose the port
28
+ EXPOSE 7860
29
+
30
+ # Command to run the application
31
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,12 +1,130 @@
1
- ---
2
- title: Potholes Yolo
3
- emoji: 🐨
4
- colorFrom: blue
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- license: apache-2.0
9
- short_description: potholes
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Nutrition Analysis API
2
+
3
+ ## Overview
4
+ A Python FastAPI backend system that provides comprehensive nutritional analysis and health recommendations. The system manages user authentication with role-based access (admin and normal users), product database management, health issue tracking, and AI-powered nutritional analysis using OCR and Google's Gemini API.
5
+
6
+ ## Features
7
+
8
+ ### Authentication & User Management
9
+ - **Role-Based Access Control**: Admin and normal user roles
10
+ - **JWT Token Authentication**: Secure authentication using JSON Web Tokens
11
+ - **User Registration & Login**: Email and username-based registration
12
+ - **Password Security**: Bcrypt password hashing
13
+
14
+ ### Admin Capabilities
15
+ - Add products with complete nutrition facts to the database
16
+ - List all products in the system
17
+ - Delete products from the database
18
+
19
+ ### User Features
20
+ - Manage personal health profile
21
+ - Add/track health issues (diabetes, hypertension, cholesterol, etc.)
22
+ - View and manage health issue records
23
+ - Upload nutrition label images for analysis
24
+
25
+ ### AI-Powered Nutrition Analysis
26
+ - **OCR Processing**: Extract nutrition facts from images using Tesseract OCR
27
+ - **Gemini AI Integration**: Analyze nutrition data with Google's Gemini API
28
+ - **Health Rating**: Products rated on a 1-10 scale based on nutritional value
29
+ - **Personalized Recommendations**: Health-specific advice based on user's tracked health issues
30
+ - **Alternative Suggestions**: Healthier product alternatives from the admin database
31
+
32
+ ## Project Structure
33
+
34
+ ```
35
+ .
36
+ ├── app/
37
+ │ ├── __init__.py
38
+ │ ├── database.py # SQLite database configuration
39
+ │ ├── models.py # SQLAlchemy ORM models
40
+ │ ├── schemas.py # Pydantic validation schemas
41
+ │ ├── auth.py # JWT authentication utilities
42
+ │ └── routes/
43
+ │ ├── __init__.py
44
+ │ ├── admin.py # Admin endpoints
45
+ │ ├── user.py # User auth and health management
46
+ │ └── nutrition.py # OCR and AI analysis endpoints
47
+ ├── main.py # FastAPI application entry point
48
+ ├── nutrition_app.db # SQLite database (auto-generated)
49
+ └── pyproject.toml # Python dependencies
50
+
51
+ ```
52
+
53
+ ## Database Schema
54
+
55
+ ### Users Table
56
+ - id, username, email, hashed_password, role (admin/user)
57
+
58
+ ### Products Table
59
+ - id, name, brand, calories, protein, fat, carbohydrates, sodium, sugar, fiber, cholesterol, serving_size
60
+
61
+ ### Health Issues Table
62
+ - id, user_id (FK), issue_type, severity, notes
63
+
64
+ ## API Endpoints
65
+
66
+ ### Authentication
67
+ - `POST /auth/register` - Register new user
68
+ - `POST /auth/login` - Login and get JWT token
69
+ - `GET /user/me` - Get current user info
70
+
71
+ ### User Health Management
72
+ - `POST /user/health-issues` - Add health issue
73
+ - `GET /user/health-issues` - List user's health issues
74
+ - `DELETE /user/health-issues/{id}` - Delete health issue
75
+
76
+ ### Admin Product Management
77
+ - `POST /admin/products` - Add new product (admin only)
78
+ - `GET /admin/products` - List all products (admin only)
79
+ - `DELETE /admin/products/{id}` - Delete product (admin only)
80
+ - `POST /admin/users/{user_id}/promote` - Promote user to admin role (admin only)
81
+
82
+ ### Nutrition Analysis
83
+ - `POST /nutrition/analyze` - Upload image for nutrition analysis
84
+
85
+ ## Environment Variables
86
+
87
+ - `SESSION_SECRET` - JWT secret key (auto-configured by)
88
+ - `GEMINI_API_KEY` - Google Gemini API key (required for AI analysis)
89
+
90
+ ## Security Notes
91
+
92
+ ### Creating Admin Users
93
+ For security, all new user registrations default to normal user role. To create admin users:
94
+ 1. Register a regular user account via `POST /auth/register`
95
+ 2. Manually promote the user to admin using one of these methods:
96
+ - Use an existing admin account to call `POST /admin/users/{user_id}/promote`
97
+ - Directly modify the database to set the first admin (SQLite: `UPDATE users SET role='admin' WHERE id=1;`)
98
+ 3. Once you have at least one admin, use the promotion endpoint for additional admins
99
+
100
+ ### Production Deployment
101
+ - Ensure `SESSION_SECRET` is set to a strong, random value in production
102
+ - Keep `GEMINI_API_KEY` secure and never expose it in client-side code
103
+ - Consider adding rate limiting for authentication endpoints
104
+ - Regularly audit admin user accounts
105
+
106
+ ## Recent Changes
107
+
108
+ - **2025-11-17**: Initial project setup with complete FastAPI backend implementation
109
+ - Configured SQLite database with SQLAlchemy ORM
110
+ - Implemented secure JWT-based authentication system with role-based access control
111
+ - Created admin and user role-based endpoints
112
+ - Integrated Tesseract OCR for nutrition label extraction
113
+ - Added Gemini API integration for AI-powered analysis
114
+ - Set up comprehensive error handling and validation
115
+ - Fixed critical security vulnerability: removed self-service admin role assignment
116
+ - Added admin-only user promotion endpoint
117
+
118
+ ## Technology Stack
119
+
120
+ - **Framework**: FastAPI
121
+ - **Database**: SQLite with SQLAlchemy ORM
122
+ - **Authentication**: JWT (python-jose) + bcrypt
123
+ - **OCR**: Tesseract + pytesseract
124
+ - **AI**: Google Gemini API
125
+ - **Image Processing**: Pillow
126
+ - **Server**: Uvicorn ASGI server
127
+
128
+ ## User Preferences
129
+
130
+ None specified yet.
__pycache__/main.cpython-311.pyc ADDED
Binary file (1.75 kB). View file
 
__pycache__/main.cpython-312.pyc ADDED
Binary file (1.59 kB). View file
 
add_image_column.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+
3
+ def migrate():
4
+ conn = sqlite3.connect("nutrition_app.db")
5
+ cursor = conn.cursor()
6
+
7
+ try:
8
+ cursor.execute("ALTER TABLE products ADD COLUMN image_path TEXT")
9
+ conn.commit()
10
+ print("Successfully added image_path column to products table")
11
+ except sqlite3.OperationalError as e:
12
+ if "duplicate column name" in str(e):
13
+ print("Column image_path already exists")
14
+ else:
15
+ print(f"Error: {e}")
16
+ finally:
17
+ conn.close()
18
+
19
+ if __name__ == "__main__":
20
+ migrate()
app/.DS_Store ADDED
Binary file (6.15 kB). View file
 
app/__init__.py ADDED
File without changes
app/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (147 Bytes). View file
 
app/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (155 Bytes). View file
 
app/__pycache__/auth.cpython-311.pyc ADDED
Binary file (4.13 kB). View file
 
app/__pycache__/auth.cpython-312.pyc ADDED
Binary file (2.01 kB). View file
 
app/__pycache__/database.cpython-311.pyc ADDED
Binary file (996 Bytes). View file
 
app/__pycache__/database.cpython-312.pyc ADDED
Binary file (871 Bytes). View file
 
app/__pycache__/models.cpython-311.pyc ADDED
Binary file (3.15 kB). View file
 
app/__pycache__/models.cpython-312.pyc ADDED
Binary file (2.58 kB). View file
 
app/__pycache__/schemas.cpython-311.pyc ADDED
Binary file (5.61 kB). View file
 
app/__pycache__/schemas.cpython-312.pyc ADDED
Binary file (4.52 kB). View file
 
app/auth.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from fastapi import Depends, HTTPException, status, Header
3
+ from sqlalchemy.orm import Session
4
+ from app.database import get_db
5
+ from app import models
6
+
7
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
8
+ return plain_password == hashed_password
9
+
10
+ def get_password_hash(password: str) -> str:
11
+ return password
12
+
13
+ def get_current_user(username: Optional[str] = Header(None, alias="X-Username"), db: Session = Depends(get_db)):
14
+ credentials_exception = HTTPException(
15
+ status_code=status.HTTP_401_UNAUTHORIZED,
16
+ detail="Authentication required"
17
+ )
18
+
19
+ if not username:
20
+ raise credentials_exception
21
+
22
+ user = db.query(models.User).filter(models.User.username == username).first()
23
+ if user is None:
24
+ raise credentials_exception
25
+ return user
26
+
27
+ def get_current_admin_user(current_user: models.User = Depends(get_current_user)):
28
+ if current_user.role != models.UserRole.ADMIN:
29
+ raise HTTPException(
30
+ status_code=status.HTTP_403_FORBIDDEN,
31
+ detail="Admin access required"
32
+ )
33
+ return current_user
app/database.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.ext.declarative import declarative_base
3
+ from sqlalchemy.orm import sessionmaker
4
+
5
+ SQLALCHEMY_DATABASE_URL = "sqlite:///./nutrition_app.db"
6
+
7
+ engine = create_engine(
8
+ SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
9
+ )
10
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
11
+
12
+ Base = declarative_base()
13
+
14
+ def get_db():
15
+ db = SessionLocal()
16
+ try:
17
+ yield db
18
+ finally:
19
+ db.close()
app/models.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, Float, ForeignKey, Enum
2
+ from sqlalchemy.orm import relationship
3
+ from app.database import Base
4
+ import enum
5
+
6
+ class UserRole(str, enum.Enum):
7
+ ADMIN = "admin"
8
+ USER = "user"
9
+
10
+ class User(Base):
11
+ __tablename__ = "users"
12
+
13
+ id = Column(Integer, primary_key=True, index=True)
14
+ username = Column(String, unique=True, index=True, nullable=False)
15
+ email = Column(String, unique=True, index=True, nullable=False)
16
+ hashed_password = Column(String, nullable=False)
17
+ role = Column(Enum(UserRole), default=UserRole.USER, nullable=False)
18
+
19
+ health_issues = relationship("HealthIssue", back_populates="user", cascade="all, delete-orphan")
20
+
21
+ class Product(Base):
22
+ __tablename__ = "products"
23
+
24
+ id = Column(Integer, primary_key=True, index=True)
25
+ name = Column(String, index=True, nullable=False)
26
+ brand = Column(String, nullable=True)
27
+ calories = Column(Float, nullable=False)
28
+ protein = Column(Float, nullable=False)
29
+ fat = Column(Float, nullable=False)
30
+ carbohydrates = Column(Float, nullable=False)
31
+ sodium = Column(Float, nullable=False)
32
+ sugar = Column(Float, nullable=False)
33
+ fiber = Column(Float, nullable=True)
34
+ cholesterol = Column(Float, nullable=True)
35
+ serving_size = Column(String, nullable=True)
36
+ image_path = Column(String, nullable=True)
37
+
38
+ class HealthIssue(Base):
39
+ __tablename__ = "health_issues"
40
+
41
+ id = Column(Integer, primary_key=True, index=True)
42
+ user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
43
+ issue_type = Column(String, nullable=False)
44
+ severity = Column(String, nullable=True)
45
+ notes = Column(String, nullable=True)
46
+
47
+ user = relationship("User", back_populates="health_issues")
app/routes/__init__.py ADDED
File without changes
app/routes/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (154 Bytes). View file
 
app/routes/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (162 Bytes). View file
 
app/routes/__pycache__/admin.cpython-311.pyc ADDED
Binary file (4.22 kB). View file
 
app/routes/__pycache__/admin.cpython-312.pyc ADDED
Binary file (5.03 kB). View file
 
app/routes/__pycache__/nutrition.cpython-311.pyc ADDED
Binary file (11.7 kB). View file
 
app/routes/__pycache__/nutrition.cpython-312.pyc ADDED
Binary file (7.61 kB). View file
 
app/routes/__pycache__/user.cpython-311.pyc ADDED
Binary file (6.78 kB). View file
 
app/routes/__pycache__/user.cpython-312.pyc ADDED
Binary file (5.97 kB). View file
 
app/routes/admin.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status, File, UploadFile, Form
2
+ import os
3
+ from sqlalchemy.orm import Session
4
+ from typing import List
5
+ from app.database import get_db
6
+ from app import models, schemas
7
+ from app.auth import get_current_admin_user
8
+
9
+ router = APIRouter(prefix="/admin", tags=["admin"])
10
+
11
+ @router.post("/users/{user_id}/promote", response_model=schemas.UserResponse)
12
+ def promote_user_to_admin(
13
+ user_id: int,
14
+ db: Session = Depends(get_db),
15
+ current_user: models.User = Depends(get_current_admin_user)
16
+ ):
17
+ user = db.query(models.User).filter(models.User.id == user_id).first()
18
+ if not user:
19
+ raise HTTPException(status_code=404, detail="User not found")
20
+ user.role = models.UserRole.ADMIN
21
+ db.commit()
22
+ db.refresh(user)
23
+ return user
24
+
25
+ @router.post("/products", response_model=schemas.ProductResponse, status_code=status.HTTP_201_CREATED)
26
+ def create_product(
27
+ name: str = Form(...),
28
+ brand: str = Form(None),
29
+ calories: float = Form(...),
30
+ protein: float = Form(...),
31
+ fat: float = Form(...),
32
+ carbohydrates: float = Form(...),
33
+ sodium: float = Form(...),
34
+ sugar: float = Form(...),
35
+ fiber: float = Form(None),
36
+ cholesterol: float = Form(None),
37
+ serving_size: str = Form(None),
38
+ image: UploadFile = File(None),
39
+ db: Session = Depends(get_db),
40
+ current_user: models.User = Depends(get_current_admin_user)
41
+ ):
42
+ image_path = None
43
+ if image:
44
+ # Create .tmp directory if it doesn't exist
45
+ os.makedirs(".tmp", exist_ok=True)
46
+ file_location = f".tmp/{image.filename}"
47
+ with open(file_location, "wb+") as file_object:
48
+ file_object.write(image.file.read())
49
+ image_path = file_location
50
+
51
+ product_data = {
52
+ "name": name,
53
+ "brand": brand,
54
+ "calories": calories,
55
+ "protein": protein,
56
+ "fat": fat,
57
+ "carbohydrates": carbohydrates,
58
+ "sodium": sodium,
59
+ "sugar": sugar,
60
+ "fiber": fiber,
61
+ "cholesterol": cholesterol,
62
+ "serving_size": serving_size,
63
+ "image_path": image_path
64
+ }
65
+
66
+ db_product = models.Product(**product_data)
67
+ db.add(db_product)
68
+ db.commit()
69
+ db.refresh(db_product)
70
+ return db_product
71
+
72
+ @router.get("/products", response_model=List[schemas.ProductResponse])
73
+ def list_products(
74
+ skip: int = 0,
75
+ limit: int = 100,
76
+ db: Session = Depends(get_db),
77
+ current_user: models.User = Depends(get_current_admin_user)
78
+ ):
79
+ products = db.query(models.Product).offset(skip).limit(limit).all()
80
+ return products
81
+
82
+ @router.delete("/products/{product_id}", status_code=status.HTTP_204_NO_CONTENT)
83
+ def delete_product(
84
+ product_id: int,
85
+ db: Session = Depends(get_db),
86
+ current_user: models.User = Depends(get_current_admin_user)
87
+ ):
88
+ product = db.query(models.Product).filter(models.Product.id == product_id).first()
89
+ if not product:
90
+ raise HTTPException(status_code=404, detail="Product not found")
91
+ db.delete(product)
92
+ db.commit()
93
+ return None
app/routes/nutrition.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, status
2
+ from sqlalchemy.orm import Session
3
+ from typing import List
4
+ import io
5
+ import re
6
+ import os
7
+ import json
8
+
9
+ from app.database import get_db
10
+ from app import models, schemas
11
+ from app.auth import get_current_user
12
+
13
+ try:
14
+ import google.generativeai as genai
15
+ GEMINI_AVAILABLE = True
16
+ except ImportError:
17
+ GEMINI_AVAILABLE = False
18
+ genai = None
19
+
20
+ router = APIRouter(prefix="/nutrition", tags=["nutrition"])
21
+
22
+ GEMINI_API_KEY = "AIzaSyD2H-Oct8iMxsvpaDC_oaqgB1FiTzRF62k"
23
+ if GEMINI_API_KEY and GEMINI_AVAILABLE and genai:
24
+ genai.configure(api_key="AIzaSyD2H-Oct8iMxsvpaDC_oaqgB1FiTzRF62k")
25
+
26
+
27
+
28
+ def analyze_with_gemini(user_health_issues: List[models.HealthIssue], all_products: List[models.Product], image_data: bytes) -> dict:
29
+ if not GEMINI_API_KEY:
30
+ return {
31
+ "extracted_nutrition": {},
32
+ "health_rating": 5.0,
33
+ "health_recommendations": ["Gemini API key not configured. Please set GEMINI_API_KEY environment variable."],
34
+ "suggested_alternatives": [],
35
+ "analysis_summary": "Analysis unavailable without Gemini API key."
36
+ }
37
+
38
+ try:
39
+ model = genai.GenerativeModel('gemini-2.0-flash')
40
+
41
+ health_issues_text = ", ".join([str(issue.issue_type) for issue in user_health_issues]) if user_health_issues else "None reported"
42
+
43
+ products_text = ""
44
+ if all_products:
45
+ products_text = "\n".join([
46
+ f"- {str(p.name)} (Brand: {str(p.brand) if p.brand else 'N/A'}): Calories: {p.calories}, Protein: {p.protein}g, Fat: {p.fat}g, Carbs: {p.carbohydrates}g, Sodium: {p.sodium}mg, Sugar: {p.sugar}g"
47
+ for p in all_products[:10]
48
+ ])
49
+
50
+ prompt_text = f"""Analyze this nutrition label image.
51
+
52
+ First, extract the following nutrition facts from the image:
53
+ - Calories
54
+ - Protein (g)
55
+ - Fat (g)
56
+ - Sugar (g)
57
+ - Fiber (g)
58
+ - Extract others if they're available
59
+ Then, based on the extracted data and the user's health issues, provide health recommendations.
60
+
61
+ User's Health Issues: {health_issues_text}
62
+
63
+ Available Alternative Products:
64
+ {products_text if products_text else "No products in database"}
65
+
66
+ Please provide:
67
+ 1. The extracted nutrition data in JSON format.
68
+ 2. A health rating from 1-10 (where 1 is unhealthy and 10 is very healthy)
69
+ 3. 3-5 specific health recommendations based on the user's health issues
70
+ 4. Names of 2-3 healthier alternative products from the list above (if available)
71
+ 5. A brief analysis summary
72
+
73
+ Format your response strictly as follows:
74
+ ```json
75
+ {{
76
+ "nutrition": {{
77
+ "calories": 0.0,
78
+ "protein": 0.0,
79
+ "fat": 0.0,
80
+ "carbohydrates": 0.0,
81
+ "sugar": 0.0,
82
+ "fiber": 0.0,
83
+ }},
84
+ "rating": 0.0,
85
+ "recommendations": [
86
+ "recommendation 1",
87
+ "recommendation 2"
88
+ ],
89
+ "alternatives": [
90
+ "product name 1",
91
+ "product name 2"
92
+ ],
93
+ "summary": "your analysis summary"
94
+ }}
95
+ ```
96
+ """
97
+
98
+ content = [prompt_text]
99
+ if image_data:
100
+ image_part = {
101
+ "mime_type": "image/jpeg",
102
+ "data": image_data
103
+ }
104
+ content.append(image_part)
105
+
106
+ response = model.generate_content(content)
107
+ result_text = response.text
108
+
109
+ # Clean up the response to get just the JSON part
110
+ json_match = re.search(r'```json\s*(.*?)\s*```', result_text, re.DOTALL)
111
+ if json_match:
112
+ json_str = json_match.group(1)
113
+ else:
114
+ # Try to find the first { and last }
115
+ start = result_text.find('{')
116
+ end = result_text.rfind('}')
117
+ if start != -1 and end != -1:
118
+ json_str = result_text[start:end+1]
119
+ else:
120
+ raise ValueError("Could not parse JSON from Gemini response")
121
+
122
+ result_data = json.loads(json_str)
123
+
124
+ extracted_nutrition = result_data.get("nutrition", {})
125
+ health_rating = float(result_data.get("rating", 5.0))
126
+ recommendations = result_data.get("recommendations", [])
127
+ alternative_names = result_data.get("alternatives", [])
128
+ analysis_summary = result_data.get("summary", "Analysis completed.")
129
+
130
+ suggested_products = []
131
+ for alt_name in alternative_names:
132
+ for product in all_products:
133
+ if alt_name.lower() in product.name.lower():
134
+ suggested_products.append(product)
135
+ break
136
+
137
+ return {
138
+ "extracted_nutrition": extracted_nutrition,
139
+ "health_rating": health_rating,
140
+ "health_recommendations": recommendations,
141
+ "suggested_alternatives": suggested_products[:3],
142
+ "analysis_summary": analysis_summary
143
+ }
144
+
145
+ except Exception as e:
146
+ print(f"Gemini Error: {e}")
147
+ return {
148
+ "extracted_nutrition": {},
149
+ "health_rating": 5.0,
150
+ "health_recommendations": [f"Error during analysis: {str(e)}"],
151
+ "suggested_alternatives": [],
152
+ "analysis_summary": "Analysis failed due to an error."
153
+ }
154
+
155
+ @router.post("/analyze", response_model=schemas.NutritionAnalysisResponse)
156
+ async def analyze_nutrition_image(
157
+ file: UploadFile = File(...),
158
+ current_user: models.User = Depends(get_current_user),
159
+ db: Session = Depends(get_db)
160
+ ):
161
+ if not file.content_type or not file.content_type.startswith('image/'):
162
+ raise HTTPException(status_code=400, detail="File must be an image")
163
+
164
+ image_data = await file.read()
165
+
166
+ user_health_issues = db.query(models.HealthIssue).filter(
167
+ models.HealthIssue.user_id == current_user.id
168
+ ).all()
169
+
170
+ all_products = db.query(models.Product).all()
171
+
172
+ gemini_analysis = analyze_with_gemini(user_health_issues, all_products, image_data)
173
+
174
+ return schemas.NutritionAnalysisResponse(
175
+ extracted_nutrition=gemini_analysis["extracted_nutrition"],
176
+ health_rating=gemini_analysis["health_rating"],
177
+ health_recommendations=gemini_analysis["health_recommendations"],
178
+ suggested_alternatives=gemini_analysis["suggested_alternatives"],
179
+ analysis_summary=gemini_analysis["analysis_summary"]
180
+ )
app/routes/user.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status
2
+ from sqlalchemy.orm import Session
3
+
4
+ from typing import List
5
+ from app.database import get_db
6
+ from app import models, schemas
7
+ from app.auth import (
8
+ get_password_hash,
9
+ verify_password,
10
+ get_current_user,
11
+ )
12
+
13
+ router = APIRouter(tags=["user"])
14
+
15
+ @router.post("/auth/register", response_model=schemas.UserResponse, status_code=status.HTTP_201_CREATED)
16
+ def register(user: schemas.UserCreate, db: Session = Depends(get_db)):
17
+ db_user = db.query(models.User).filter(models.User.username == user.username).first()
18
+ if db_user:
19
+ raise HTTPException(status_code=400, detail="Username already registered")
20
+
21
+ db_email = db.query(models.User).filter(models.User.email == user.email).first()
22
+ if db_email:
23
+ raise HTTPException(status_code=400, detail="Email already registered")
24
+
25
+ hashed_password = get_password_hash(user.password)
26
+ db_user = models.User(
27
+ username=user.username,
28
+ email=user.email,
29
+ hashed_password=hashed_password,
30
+ role=models.UserRole.USER
31
+ )
32
+ db.add(db_user)
33
+ db.commit()
34
+ db.refresh(db_user)
35
+ return db_user
36
+
37
+ @router.post("/auth/login", response_model=schemas.UserResponse)
38
+ def login(credentials: schemas.UserLogin, db: Session = Depends(get_db)):
39
+ user = db.query(models.User).filter(models.User.username == credentials.username).first()
40
+ if not user:
41
+ raise HTTPException(
42
+ status_code=status.HTTP_401_UNAUTHORIZED,
43
+ detail="Incorrect username or password"
44
+ )
45
+ if not verify_password(credentials.password, str(user.hashed_password)):
46
+ raise HTTPException(
47
+ status_code=status.HTTP_401_UNAUTHORIZED,
48
+ detail="Incorrect username or password"
49
+ )
50
+
51
+ return user
52
+
53
+ @router.get("/user/me", response_model=schemas.UserResponse)
54
+ def get_current_user_info(current_user: models.User = Depends(get_current_user)):
55
+ return current_user
56
+
57
+ @router.post("/user/health-issues", response_model=schemas.HealthIssueResponse, status_code=status.HTTP_201_CREATED)
58
+ def add_health_issue(
59
+ health_issue: schemas.HealthIssueCreate,
60
+ current_user: models.User = Depends(get_current_user),
61
+ db: Session = Depends(get_db)
62
+ ):
63
+ db_health_issue = models.HealthIssue(
64
+ user_id=current_user.id,
65
+ **health_issue.dict()
66
+ )
67
+ db.add(db_health_issue)
68
+ db.commit()
69
+ db.refresh(db_health_issue)
70
+ return db_health_issue
71
+
72
+ @router.get("/user/health-issues", response_model=List[schemas.HealthIssueResponse])
73
+ def get_health_issues(
74
+ current_user: models.User = Depends(get_current_user),
75
+ db: Session = Depends(get_db)
76
+ ):
77
+ return db.query(models.HealthIssue).filter(models.HealthIssue.user_id == current_user.id).all()
78
+
79
+ @router.delete("/user/health-issues/{issue_id}", status_code=status.HTTP_204_NO_CONTENT)
80
+ def delete_health_issue(
81
+ issue_id: int,
82
+ current_user: models.User = Depends(get_current_user),
83
+ db: Session = Depends(get_db)
84
+ ):
85
+ health_issue = db.query(models.HealthIssue).filter(
86
+ models.HealthIssue.id == issue_id,
87
+ models.HealthIssue.user_id == current_user.id
88
+ ).first()
89
+ if not health_issue:
90
+ raise HTTPException(status_code=404, detail="Health issue not found")
91
+ db.delete(health_issue)
92
+ db.commit()
93
+ return None
app/schemas.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, EmailStr, Field
2
+ from typing import Optional, List
3
+ from enum import Enum
4
+
5
+ class UserRole(str, Enum):
6
+ ADMIN = "admin"
7
+ USER = "user"
8
+
9
+ class UserCreate(BaseModel):
10
+ username: str
11
+ email: EmailStr
12
+ password: str
13
+
14
+ class UserLogin(BaseModel):
15
+ username: str
16
+ password: str
17
+
18
+ class UserResponse(BaseModel):
19
+ id: int
20
+ username: str
21
+ email: str
22
+ role: UserRole
23
+
24
+ class Config:
25
+ from_attributes = True
26
+
27
+ class Token(BaseModel):
28
+ access_token: str
29
+ token_type: str
30
+
31
+ class TokenData(BaseModel):
32
+ username: Optional[str] = None
33
+ role: Optional[str] = None
34
+
35
+ class ProductCreate(BaseModel):
36
+ name: str
37
+ brand: Optional[str] = None
38
+ calories: float
39
+ protein: float
40
+ fat: float
41
+ carbohydrates: float
42
+ sodium: float
43
+ sugar: float
44
+ fiber: Optional[float] = None
45
+ cholesterol: Optional[float] = None
46
+ serving_size: Optional[str] = None
47
+
48
+ class ProductResponse(BaseModel):
49
+ id: int
50
+ name: str
51
+ brand: Optional[str]
52
+ calories: float
53
+ protein: float
54
+ fat: float
55
+ carbohydrates: float
56
+ sodium: float
57
+ sugar: float
58
+ fiber: Optional[float]
59
+ cholesterol: Optional[float]
60
+ serving_size: Optional[str]
61
+ image_path: Optional[str] = None
62
+
63
+ class Config:
64
+ from_attributes = True
65
+
66
+ class HealthIssueCreate(BaseModel):
67
+ issue_type: str = Field(..., description="Type of health issue (e.g., diabetes, hypertension, high cholesterol)")
68
+ severity: Optional[str] = None
69
+ notes: Optional[str] = None
70
+
71
+ class HealthIssueResponse(BaseModel):
72
+ id: int
73
+ user_id: int
74
+ issue_type: str
75
+ severity: Optional[str]
76
+ notes: Optional[str]
77
+
78
+ class Config:
79
+ from_attributes = True
80
+
81
+ class NutritionAnalysisResponse(BaseModel):
82
+ extracted_nutrition: dict
83
+ health_rating: float = Field(..., description="Product rating from 1-10")
84
+ health_recommendations: List[str]
85
+ suggested_alternatives: List[ProductResponse]
86
+ analysis_summary: str
create_admin.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.database import SessionLocal
2
+ from app import models
3
+ from app.auth import get_password_hash
4
+
5
+ def create_admin(username, email, password):
6
+ db = SessionLocal()
7
+ try:
8
+ # Check if user exists
9
+ existing_user = db.query(models.User).filter(models.User.username == username).first()
10
+ if existing_user:
11
+ print(f"User {username} already exists.")
12
+ return
13
+
14
+ existing_email = db.query(models.User).filter(models.User.email == email).first()
15
+ if existing_email:
16
+ print(f"Email {email} already registered.")
17
+ return
18
+
19
+ # Create admin user
20
+ hashed_password = get_password_hash(password)
21
+ db_user = models.User(
22
+ username=username,
23
+ email=email,
24
+ hashed_password=hashed_password,
25
+ role=models.UserRole.ADMIN
26
+ )
27
+ db.add(db_user)
28
+ db.commit()
29
+ db.refresh(db_user)
30
+ print(f"Admin user '{username}' created successfully.")
31
+ except Exception as e:
32
+ print(f"Error creating admin: {e}")
33
+ finally:
34
+ db.close()
35
+
36
+ if __name__ == "__main__":
37
+ import sys
38
+ if len(sys.argv) != 4:
39
+ print("Usage: python create_admin.py <username> <email> <password>")
40
+ else:
41
+ create_admin(sys.argv[1], sys.argv[2], sys.argv[3])
main.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from app.database import engine, Base
4
+ from app.routes import admin, user, nutrition
5
+
6
+ Base.metadata.create_all(bind=engine)
7
+
8
+ app = FastAPI(
9
+ title="Nutrition Analysis API",
10
+ description="FastAPI backend for nutrition analysis with OCR and AI-powered recommendations",
11
+ version="1.0.0"
12
+ )
13
+
14
+ app.add_middleware(
15
+ CORSMiddleware,
16
+ allow_origins=["*"],
17
+ allow_credentials=True,
18
+ allow_methods=["*"],
19
+ allow_headers=["*"],
20
+ )
21
+
22
+ app.include_router(user.router)
23
+ app.include_router(admin.router)
24
+ app.include_router(nutrition.router)
25
+
26
+ @app.get("/")
27
+ def root():
28
+ return {
29
+ "message": "Welcome to Nutrition Analysis API",
30
+ "docs": "/docs",
31
+ "redoc": "/redoc"
32
+ }
33
+
34
+ @app.get("/health")
35
+ def health_check():
36
+ return {"status": "healthy"}
nutrition_app.db ADDED
Binary file (41 kB). View file
 
pyproject.toml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "repl-nix-workspace"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "email-validator>=2.3.0",
8
+ "fastapi>=0.121.2",
9
+ "google-genai>=1.50.1",
10
+ "google-generativeai>=0.8.5",
11
+ "passlib[bcrypt]>=1.7.4",
12
+ "pillow>=12.0.0",
13
+ "pydantic>=2.12.4",
14
+ "pytesseract>=0.3.13",
15
+ "python-jose[cryptography]>=3.5.0",
16
+ "python-multipart>=0.0.20",
17
+ "sqlalchemy>=2.0.44",
18
+ "uvicorn>=0.38.0",
19
+ ]
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ email-validator>=2.3.0
2
+ fastapi>=0.121.2
3
+ google-genai>=1.50.1
4
+ google-generativeai>=0.8.5
5
+ passlib[bcrypt]>=1.7.4
6
+ pillow>=12.0.0
7
+ pydantic>=2.12.4
8
+ pytesseract>=0.3.13
9
+ python-jose[cryptography]>=3.5.0
10
+ python-multipart>=0.0.20
11
+ sqlalchemy>=2.0.44
12
+ uvicorn>=0.38.0
uv.lock ADDED
The diff for this file is too large to render. See raw diff
 
verify_image_upload.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import os
3
+ import sys
4
+
5
+ BASE_URL = "http://localhost:8000"
6
+ ADMIN_USERNAME = "admin_test"
7
+ ADMIN_EMAIL = "admin_test@example.com"
8
+ ADMIN_PASSWORD = "password"
9
+
10
+ def create_admin_user():
11
+ # Use the existing create_admin.py logic or just call the register endpoint and then promote?
12
+ # Or just rely on the create_admin.py script.
13
+ # Let's try to register and then promote if possible, or just use create_admin.py
14
+ # Since I can't easily import create_admin.py if it's not a module, I'll just run it via subprocess or assume it's run.
15
+ # Actually, I'll just use the create_admin.py script in the main flow.
16
+ pass
17
+
18
+ def test_create_product_with_image():
19
+ print("Testing create product with image...")
20
+
21
+ # Create a dummy image
22
+ with open("test_image.jpg", "wb") as f:
23
+ f.write(b"fake image content")
24
+
25
+ url = f"{BASE_URL}/admin/products"
26
+ headers = {"X-Username": ADMIN_USERNAME}
27
+
28
+ data = {
29
+ "name": "Test Product",
30
+ "calories": 100,
31
+ "protein": 10,
32
+ "fat": 5,
33
+ "carbohydrates": 20,
34
+ "sodium": 50,
35
+ "sugar": 5
36
+ }
37
+
38
+ files = {
39
+ "image": ("test_image.jpg", open("test_image.jpg", "rb"), "image/jpeg")
40
+ }
41
+
42
+ try:
43
+ response = requests.post(url, headers=headers, data=data, files=files)
44
+ if response.status_code != 201:
45
+ print(f"Failed to create product: {response.status_code} {response.text}")
46
+ return False
47
+
48
+ product = response.json()
49
+ print(f"Product created: {product}")
50
+
51
+ if not product.get("image_path"):
52
+ print("Error: image_path is missing in response")
53
+ return False
54
+
55
+ if ".tmp/test_image.jpg" not in product["image_path"]:
56
+ print(f"Error: Unexpected image path: {product['image_path']}")
57
+ return False
58
+
59
+ # Verify file exists
60
+ if not os.path.exists(product["image_path"]):
61
+ print(f"Error: File does not exist at {product['image_path']}")
62
+ return False
63
+
64
+ print("Image upload verified successfully.")
65
+ return True
66
+
67
+ except Exception as e:
68
+ print(f"Exception: {e}")
69
+ return False
70
+ finally:
71
+ # Cleanup
72
+ if os.path.exists("test_image.jpg"):
73
+ os.remove("test_image.jpg")
74
+
75
+ def test_list_products():
76
+ print("Testing list products...")
77
+ url = f"{BASE_URL}/admin/products"
78
+ headers = {"X-Username": ADMIN_USERNAME}
79
+
80
+ response = requests.get(url, headers=headers)
81
+ if response.status_code != 200:
82
+ print(f"Failed to list products: {response.status_code}")
83
+ return False
84
+
85
+ products = response.json()
86
+ # Find our test product
87
+ found = False
88
+ for p in products:
89
+ if p["name"] == "Test Product":
90
+ found = True
91
+ if not p.get("image_path"):
92
+ print("Error: image_path missing in list response")
93
+ return False
94
+ print(f"Found product in list with image_path: {p['image_path']}")
95
+ break
96
+
97
+ if not found:
98
+ print("Error: Test product not found in list")
99
+ return False
100
+
101
+ print("List products verified successfully.")
102
+ return True
103
+
104
+ if __name__ == "__main__":
105
+ if not test_create_product_with_image():
106
+ sys.exit(1)
107
+ if not test_list_products():
108
+ sys.exit(1)
109
+ print("All tests passed!")