A Flask-based web application that calculates glycemic load (GL) for meals using a curated Indian food database with AI-powered fallback via OpenAI GPT-4o.
- Intelligent Food Recognition: Natural language meal input with AI parsing
- Hybrid Data Approach: 56 curated Indian foods + unlimited AI estimations
- User Authentication: JWT-based auth with 24-hour token expiry
- Rate Limiting: 4 meal calculations per day per user
- AI Recommendations: Smart suggestions for high GL meals (GL >= 11)
- Visual Results: Color-coded GL display with detailed breakdowns
- Python 3.11+
- PostgreSQL database
- OpenAI API key
SESSION_SECRET=your-secret-key-for-jwt # Required - app fails without it
DATABASE_URL=postgresql://... # PostgreSQL connection string
OPENAI_API_KEY=sk-... # OpenAI API key for GPT-4o# Install dependencies
pip install -r requirements.txt
# Or with uv
uv sync
# Run the application
gunicorn --bind 0.0.0.0:5000 main:app┌─────────────────────────────────────────────────────────────┐
│ Flask App │
├─────────────────────────────────────────────────────────────┤
│ Templates (Jinja2) │ API Endpoints │ Auth Middleware │
│ - dashboard.html │ - /calculate-gl│ - JWT tokens │
│ - review.html │ - /parse-meal │ - Rate limiting │
│ - results.html │ - /auth/* │ - Usage tracking │
├─────────────────────────────────────────────────────────────┤
│ Food Database (JSON) │ OpenAI GPT-4o │
│ 56 Indian foods │ AI fallback │
├─────────────────────────────────────────────────────────────┤
│ PostgreSQL Database │
│ Users + MealUsage tracking │
└─────────────────────────────────────────────────────────────┘
├── app.py # Main application (routes, models, helpers)
├── main.py # Entry point (imports app)
├── attached_assets/
│ └── food_items_db_*.json # Curated food database (56 items)
├── templates/
│ ├── base.html # Base template with nav, auth, styles
│ ├── dashboard.html # Meal input and history
│ ├── review.html # Food disambiguation and portion editing
│ ├── results.html # GL results with recommendations
│ ├── login.html # Login page
│ └── register.html # Registration page
└── static/ # Static assets (if any)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(120) UNIQUE NOT NULL,
password_hash VARCHAR(256) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);CREATE TABLE meal_usages (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) NOT NULL,
endpoint VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);Health check endpoint.
Response:
{
"status": "healthy",
"database_loaded": true,
"total_foods": 56
}List all foods in the database.
Response:
{
"total_foods": 56,
"foods": [
{
"name": "White Rice",
"category": "Rice & Grains"
}
]
}Create a new account.
Request:
{
"email": "user@example.com",
"password": "securepass123"
}Response (201):
{
"status": "success",
"message": "Account created successfully",
"user": {
"id": 1,
"email": "user@example.com",
"created_at": "2024-01-15T10:30:00"
},
"token": "eyJhbGciOiJIUzI1NiIs..."
}Login and get JWT token.
Request:
{
"email": "user@example.com",
"password": "securepass123"
}Response:
{
"status": "success",
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": { "id": 1, "email": "..." },
"usage": {
"used_today": 2,
"daily_limit": 4,
"remaining": 2
}
}Get current user info (requires auth).
Headers: Authorization: Bearer <token>
Response:
{
"user": { "id": 1, "email": "..." },
"usage": {
"used_today": 2,
"daily_limit": 4,
"remaining": 2
}
}All endpoints below require:
Authorization: Bearer <token>header- Count toward daily limit (4/day)
Calculate glycemic load for a meal.
Request:
{
"meal": [
{ "food": "White Rice", "quantity": 1.5, "unit": "bowl" },
{ "food": "Dal", "quantity": 1, "unit": "bowl" }
]
}Response:
{
"total_gl": 18.45,
"items": [
{
"food": "White Rice",
"gl": 12.30,
"quantity": 1.5,
"unit": "bowl",
"source": "database"
},
{
"food": "Dal",
"gl": 6.15,
"quantity": 1,
"unit": "bowl",
"source": "database"
}
],
"suggestions": [
{
"text": "Replace half the white rice with brown rice",
"reason": "Brown rice has lower GI (50 vs 73)"
}
],
"usage": {
"used_today": 3,
"daily_limit": 4,
"remaining": 1
}
}Parse natural language meal description with database disambiguation.
Request:
{
"text": "2 rotis with dal and some rice"
}Response:
{
"items": [
{
"parsed_name": "roti",
"quantity": 2,
"status": "found",
"matches": [
{
"name": "Roti (Whole Wheat)",
"gi": 62,
"unit": "piece",
"source": "database"
}
],
"selected": { ... }
}
],
"usage": { ... }
}Parse meal description using natural language.
Request:
{
"text": "2 pooris and chole"
}Response:
{
"meal": [
{ "food": "Poori", "quantity": 2 },
{ "food": "Chole Masala", "quantity": 1 }
],
"usage": { ... }
}Get portion information for a specific food item.
Request:
{
"food": "White Rice"
}Response (database match):
{
"food": "White Rice",
"unit": "bowl",
"unit_desc": "1 medium bowl = 150g cooked",
"source": "database"
}Response (AI generated):
{
"food": "Paneer Tikka",
"unit": "serving",
"unit_desc": "1 serving = 100g, 6-8 pieces",
"source": "ai_generated"
}| Route | Template | Description |
|---|---|---|
/ |
register.html | Landing page (redirects to register) |
/register |
register.html | User registration form |
/login |
login.html | User login form |
/dashboard |
dashboard.html | Meal input and history |
/review |
review.html | Food disambiguation and portion editing |
/results |
results.html | GL results with recommendations |
The GL formula used:
Net Carbs = Carbs per Unit - Fiber per Unit
GL = (GI × Net Carbs / 100) × Quantity
| GL Range | Category | Color |
|---|---|---|
| 0-10 | Low | Green |
| 11-19 | Medium | Yellow |
| 20+ | High | Red |
1. User registers → POST /auth/register
└── Returns JWT token (valid 24 hours)
2. User logs in → POST /auth/login
└── Returns JWT token + usage stats
3. Protected requests include:
└── Header: Authorization: Bearer <token>
4. Rate limiting:
└── 4 meal calculations per day (resets at midnight UTC)
└── Tracked in meal_usages table
- Food Not in Database: If a food item isn't found in the 56-item database, GPT-4o estimates its nutrition
- Meal Parsing: Natural language input is parsed by GPT-4o
- Recommendations: High GL meals (>= 11) get AI-powered suggestions
When a food isn't in the database, the system calls GPT-4o with:
prompt = """Give glycemic index (GI), carbs per unit, fiber per unit,
unit, and unit_desc for one serving of the specified food item.
Return only JSON with keys: gi, carbs_per_unit, fiber_per_unit, unit, unit_desc."""For meals with GL >= 11, suggestions are categorized as:
- Portion Control: Reducing quantities
- Food Swaps: Lower GI alternatives
Each food item in attached_assets/food_items_db_*.json:
{
"name": "White Rice",
"category": "Rice & Grains",
"gi": 73,
"unit": "bowl",
"unit_desc": "1 medium bowl = 150g cooked",
"carbs_per_unit": 45,
"fiber_per_unit": 0.6
}- Rice & Grains
- Breads & Rotis
- Lentils & Legumes
- Vegetables
- Fruits
- Sweets & Desserts
- Beverages
- Snacks
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Dashboard │ ──> │ Review │ ──> │ Results │ ──> │ Dashboard │
│ │ │ │ │ │ │ │
│ Enter meal │ │ Disambiguate│ │ View GL │ │ Start over │
│ description │ │ + portions │ │ + tips │ │ │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
- Text input for meal description (500 char limit)
- Auto-detects meal type by time (Breakfast/Lunch/Dinner/Snack)
- Shows remaining daily calculations
- Two-panel layout: food list + portion editor
- Disambiguate between database matches
- Adjust portions with presets (0.5x Small, 1x Medium, 1.5x Large)
- Option to use AI estimation for unlisted foods
- Circular ring indicator with total GL
- Color-coded breakdown per item
- AI recommendations for high GL meals
- Congratulations for low GL meals (0-10)
- Password Hashing: Werkzeug's
generate_password_hash(default algorithm) - JWT Tokens: HS256 algorithm, 24-hour expiry, signed with SESSION_SECRET
- Rate Limiting: Multi-layer protection
- Daily limit: 4 meal calculations per user per day
- Per-minute limit: 10 requests per minute per user (prevents burst abuse)
- AI Response Caching: 24-hour cache for nutrition estimates (reduces OpenAI costs)
- Input Validation:
- Email validation via
email-validator - 500 character limit on meal input
- JSON schema validation on all endpoints
- Email validation via
- CORS: Enabled for cross-origin requests
- ProxyFix: Proper HTTPS handling behind reverse proxies
// Per-minute limit exceeded (429)
{
"error": "Rate limit exceeded",
"message": "Too many requests. Maximum 10 requests per minute. Please wait a moment.",
"requests_per_minute": 10,
"current_count": 10
}
// Daily limit exceeded (429)
{
"error": "Daily limit reached",
"message": "You have used all 4 meal calculations for today. Please try again tomorrow.",
"daily_limit": 4,
"used_today": 4
}// 400 Bad Request
{
"error": "Invalid request format",
"message": "Request must contain 'meal' array"
}
// 401 Unauthorized
{
"error": "Invalid or expired token",
"message": "Please login again to get a new token"
}
// 429 Rate Limited
{
"error": "Daily limit reached",
"message": "You have used all 4 meal calculations for today",
"daily_limit": 4,
"used_today": 4
}
// 500 Internal Error
{
"error": "Internal server error",
"message": "An unexpected error occurred"
}pip install -r requirements.txtgunicorn --bind 0.0.0.0:5000 main:appSESSION_SECRET: Secret key for JWT signingDATABASE_URL: PostgreSQL connection stringOPENAI_API_KEY: OpenAI API key
# Set environment variables
export SESSION_SECRET=dev-secret
export DATABASE_URL=postgresql://localhost/gl_calculator
export OPENAI_API_KEY=sk-...
# Run with hot reload
gunicorn --bind 0.0.0.0:5000 --reload main:appDebug logging is enabled by default:
logging.basicConfig(level=logging.DEBUG)Check logs for:
- Food database loading status
- AI API calls and responses
- Authentication failures
- Rate limit enforcement
# Register
curl -X POST http://localhost:5000/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com", "password": "test123"}'
# Login
curl -X POST http://localhost:5000/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com", "password": "test123"}'
# Calculate GL (with token)
curl -X POST http://localhost:5000/calculate-gl \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"meal": [{"food": "White Rice", "quantity": 1}]}'MIT License
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
For questions or issues, please open a GitHub issue.