import gradio as gr import sqlite3 import pandas as pd from datetime import datetime, timedelta import re from difflib import get_close_matches import plotly.express as px import plotly.graph_objects as go import numpy as np # ============================================================================ # DATABASE SETUP # ============================================================================ def init_db(): conn = sqlite3.connect("health_tracker.db") c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS food_library (id INTEGER PRIMARY KEY AUTOINCREMENT, food_name TEXT UNIQUE NOT NULL, calories TEXT, protein REAL, fat REAL, carbs REAL, fiber REAL, sodium REAL, cholesterol REAL, sugar REAL, saturated_fat REAL, vitamin_a REAL, vitamin_b1 REAL, vitamin_b2 REAL, vitamin_b3 REAL, vitamin_b5 REAL, vitamin_b6 REAL, vitamin_b12 REAL, vitamin_c REAL, vitamin_d REAL, vitamin_e REAL, vitamin_k REAL, folate REAL, calcium REAL, iron REAL, magnesium REAL, phosphorus REAL, potassium REAL, zinc REAL, copper REAL, manganese REAL, selenium REAL, iodine REAL, chromium REAL, molybdenum REAL, omega_3 REAL, omega_6 REAL, water REAL, ash REAL, created_date TEXT)''') c.execute('''CREATE TABLE IF NOT EXISTS meals (id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, time TEXT, food_name TEXT NOT NULL, portion TEXT, calories TEXT, protein REAL, fat REAL, carbs REAL, fiber REAL, sodium REAL, cholesterol REAL, sugar REAL, saturated_fat REAL, vitamin_a REAL, vitamin_b1 REAL, vitamin_b2 REAL, vitamin_b3 REAL, vitamin_b5 REAL, vitamin_b6 REAL, vitamin_b12 REAL, vitamin_c REAL, vitamin_d REAL, vitamin_e REAL, vitamin_k REAL, folate REAL, calcium REAL, iron REAL, magnesium REAL, phosphorus REAL, potassium REAL, zinc REAL, copper REAL, manganese REAL, selenium REAL, iodine REAL, chromium REAL, molybdenum REAL, omega_3 REAL, omega_6 REAL, water REAL, ash REAL, notes TEXT)''') conn.commit() conn.close() init_db() # ============================================================================ # PARSER # ============================================================================ def parse_nutrition_text(text): result = {} key_variants = { 'calories': ['calories', 'kcal', 'energy'], 'protein': ['protein', 'prot'], 'fat': ['fat'], 'saturated_fat': ['saturated fat', 'sat fat', 'saturated'], 'carbs': ['carbs', 'carbohydrates'], 'fiber': ['fiber', 'fibre'], 'sugar': ['sugar'], 'omega_3': ['omega-3', 'omega 3'], 'omega_6': ['omega-6', 'omega 6'], 'sodium': ['sodium'], 'potassium': ['potassium'], 'calcium': ['calcium'], 'magnesium': ['magnesium'], 'phosphorus': ['phosphorus'], 'iron': ['iron'], 'zinc': ['zinc'], 'copper': ['copper'], 'manganese': ['manganese'], 'selenium': ['selenium'], 'iodine': ['iodine'], 'chromium': ['chromium'], 'molybdenum': ['molybdenum'], 'vitamin_a': ['vitamin a', 'vit a', 'a'], 'vitamin_b1': ['b1', 'vitamin b1', 'thiamin'], 'vitamin_b2': ['b2', 'vitamin b2', 'riboflavin'], 'vitamin_b3': ['b3', 'vitamin b3', 'niacin'], 'vitamin_b5': ['b5', 'vitamin b5'], 'vitamin_b6': ['b6', 'vitamin b6'], 'vitamin_b12': ['b12', 'vitamin b12'], 'folate': ['folate', 'folic'], 'vitamin_c': ['vitamin c', 'vit c', 'c'], 'vitamin_d': ['vitamin d', 'vit d', 'd'], 'vitamin_e': ['vitamin e', 'vit e', 'e'], 'vitamin_k': ['vitamin k', 'vit k', 'k'], 'cholesterol': ['cholesterol'], 'water': ['water'], 'ash': ['ash'], } lines = [l.strip() for l in text.split('\n') if l.strip()] for line in lines: numbers = re.findall(r'\d+\.?\d*', line) if not numbers: continue val_str = numbers[-1] pos = line.rfind(val_str) label = line[:pos].strip().lower() label = re.sub(r'\s*\([^)]*\)', '', label).strip() all_keys = [] for k, vs in key_variants.items(): all_keys.extend(vs) matches = get_close_matches(label, all_keys, n=1, cutoff=0.65) if matches: found = matches[0] for canonical, variants in key_variants.items(): if found in variants: result[canonical] = float(val_str) break return result # ============================================================================ # DB HELPERS # ============================================================================ def get_db(): conn = sqlite3.connect("health_tracker.db") conn.row_factory = sqlite3.Row return conn def add_food_to_library(name, nutrition): conn = get_db() c = conn.cursor() try: cols = ', '.join(nutrition.keys()) vals = ', '.join(['?'] * len(nutrition)) c.execute(f"INSERT INTO food_library (food_name, created_date, {cols}) VALUES (?, ?, {vals})", (name, datetime.now().isoformat(), *nutrition.values())) conn.commit() return f"✅ Saved: {name}" except sqlite3.IntegrityError: return f"⚠️ {name} already exists" finally: conn.close() def get_all_foods(): conn = get_db() df = pd.read_sql_query("SELECT * FROM food_library ORDER BY food_name", conn) conn.close() return df def delete_food(fid): conn = get_db() c = conn.cursor() c.execute("DELETE FROM food_library WHERE id = ?", (fid,)) conn.commit() conn.close() return "Deleted" def log_meal(date_str, time_str, food_name, portion, nutrition): conn = get_db() c = conn.cursor() cols = ', '.join(nutrition.keys()) vals = ', '.join(['?'] * len(nutrition)) c.execute(f"INSERT INTO meals (date, time, food_name, portion, {cols}) VALUES (?, ?, ?, ?, {vals})", (date_str, time_str, food_name, portion, *nutrition.values())) conn.commit() conn.close() return "Meal logged" def get_meals(start_date, end_date): conn = get_db() df = pd.read_sql_query("SELECT * FROM meals WHERE date BETWEEN ? AND ? ORDER BY date DESC, time DESC", conn, params=(start_date, end_date)) conn.close() return df # ============================================================================ # GRADIO APP – fixed for 4.44.0 # ============================================================================ def parse_and_show(text): parsed = parse_nutrition_text(text) return parsed, "Parsed! Now enter name and save." def save_food(name, parsed): if not name.strip(): return "Enter food name", None if not parsed: return "No data parsed", None msg = add_food_to_library(name.strip(), parsed) return msg, None def refresh_foods(): df = get_all_foods() if df.empty: return "No foods yet", gr.update(choices=[]) choices = [f"{row['id']} - {row['food_name']}" for _, row in df.iterrows()] return df.to_string(index=False), gr.update(choices=choices) def delete_selected(selected): if not selected: return "Select food" fid = int(selected.split(" - ")[0]) msg = delete_food(fid) _, choices = refresh_foods() return msg, choices def log_meal_fn(food_choice, qty, date_obj, time_obj): if not food_choice: return "Select food" fid = int(food_choice.split(" - ")[0]) foods = get_all_foods() if foods.empty or fid not in foods['id'].values: return "Food not found" food_row = foods[foods['id'] == fid].iloc[0].to_dict() nutrition = {} for k in ['calories', 'protein', 'fat', 'carbs', 'fiber', 'sodium', 'cholesterol', 'sugar', 'saturated_fat', 'vitamin_a', 'vitamin_b1', 'vitamin_b2', 'vitamin_b3', 'vitamin_b5', 'vitamin_b6', 'vitamin_b12', 'vitamin_c', 'vitamin_d', 'vitamin_e', 'vitamin_k', 'folate', 'calcium', 'iron', 'magnesium', 'phosphorus', 'potassium', 'zinc', 'copper', 'manganese', 'selenium', 'iodine', 'chromium', 'molybdenum', 'omega_3', 'omega_6', 'water', 'ash']: val = food_row.get(k, 0) if k == 'calories': nutrition[k] = str(float(val) * qty if val else 0) else: nutrition[k] = float(val) * qty if val else 0 date_str = date_obj.strftime("%Y-%m-%d") if date_obj else datetime.now().strftime("%Y-%m-%d") time_str = time_obj.strftime("%H:%M") if time_obj else datetime.now().strftime("%H:%M") msg = log_meal(date_str, time_str, food_row['food_name'], f"{qty}x", nutrition) return msg def show_today(): today = datetime.now().strftime("%Y-%m-%d") df = get_meals(today, today) if df.empty: return "No meals today" df['calories'] = pd.to_numeric(df['calories'], errors='coerce').fillna(0) cals = df['calories'].sum() pro = df['protein'].sum() fat = df['fat'].sum() carb = df['carbs'].sum() summary = f"Today: {cals:.0f} kcal | Protein {pro:.1f}g | Fat {fat:.1f}g | Carbs {carb:.1f}g\n\n" return summary + df[['time', 'food_name', 'portion', 'calories']].to_string(index=False) with gr.Blocks(title="Nutrition Tracker") as app: gr.Markdown("# 🍛 Nutrition Tracker - Paste & Track") with gr.Tabs(): with gr.Tab("Add Food"): with gr.Row(): paste = gr.Textbox(label="Paste Nutrition Text", lines=15, placeholder="Calories (kcal) 340\nProtein (g) 13.00\n...") parse_btn = gr.Button("Parse") parsed_json = gr.JSON(label="Parsed Result") status_parse = gr.Textbox(label="Parse Status") food_name_in = gr.Textbox(label="Food Name") save_btn = gr.Button("Save Food", variant="primary") save_status = gr.Textbox(label="Save Status") parse_btn.click(parse_and_show, inputs=paste, outputs=[parsed_json, status_parse]) save_btn.click(save_food, inputs=[food_name_in, parsed_json], outputs=[save_status, parsed_json]) with gr.Tab("Log Meal"): food_dropdown = gr.Dropdown(label="Select Food", choices=[]) qty_input = gr.Number(label="Quantity", value=1.0, minimum=0.1) date_input = gr.DatePicker(label="Date") time_input = gr.TimePicker(label="Time") log_btn = gr.Button("Log Meal") log_status_out = gr.Textbox(label="Log Status") log_btn.click(log_meal_fn, inputs=[food_dropdown, qty_input, date_input, time_input], outputs=log_status_out) with gr.Tab("Today's Log"): today_text = gr.Textbox(label="Today's Summary", lines=12, interactive=False) refresh_today_btn = gr.Button("Refresh") refresh_today_btn.click(show_today, outputs=today_text) with gr.Tab("Food Library"): foods_text = gr.Textbox(label="Foods List", lines=15, interactive=False) delete_dropdown = gr.Dropdown(label="Delete Food", choices=[]) delete_btn = gr.Button("Delete", variant="stop") lib_status = gr.Textbox(label="Status") refresh_lib_btn = gr.Button("Refresh Library") refresh_lib_btn.click(refresh_foods, outputs=[foods_text, delete_dropdown]) delete_btn.click(delete_selected, inputs=delete_dropdown, outputs=[lib_status, delete_dropdown]) # Initial load app.load(refresh_foods, outputs=[foods_text, delete_dropdown]) app.load(refresh_foods, outputs=[food_dropdown]) # for log tab app.launch(server_name="0.0.0.0", server_port=7860, theme=gr.themes.Soft())