Add on-demand meal-log health/calorie analyzer + git-sync README trap pitfall
- scripts/analyze_history.py: read-only per-meal/per-day kcal+protein+micronutrient review of meal-history.json, unit-aware via references/nutrition-db.json (g_per_unit conversion so veg logged in g vs count both resolve correctly) - references/nutrition-db.json: estimate per-item nutrition DB (ESTIMATE-grade; condiments/oil not logged => ~+80-120 kcal/meal real) - SKILL.md: 'Health / calorie review (on-demand)' section; 'Git-sync README trap' pitfall (live README may be a stub while repo README is the real doc -- safe-sync procedure); scripts + references pointers
This commit is contained in:
120
scripts/analyze_history.py
Normal file
120
scripts/analyze_history.py
Normal file
@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
analyze_history.py — one-off health/calorie review of the meal-history.json log.
|
||||
|
||||
Reads the rolling 7-day meal log and prints, per meal and per day:
|
||||
- estimated kcal and protein (g)
|
||||
- daily totals + averages across the window
|
||||
- a rough micronutrient read (fibre, sat fat, vit C, vit A, calcium)
|
||||
- whether each day meets the protein / fibre / sat-fat targets
|
||||
|
||||
Nutrition is ESTIMATE-grade (see references/nutrition-db.json). Each DB item carries its
|
||||
canonical `unit` and `g_per_unit`; the script converts the LOGGED qty+unit to grams, then
|
||||
scales — so a veg logged in 'g' and one logged in 'count' both resolve correctly. Condiments,
|
||||
oils and cooking fat are NOT in the log, so real meals run ~80-120 kcal higher per cooked meal.
|
||||
This is a review tool, not a send gate. It does NOT modify any file.
|
||||
|
||||
Usage:
|
||||
python3 scripts/analyze_history.py # whole logged window
|
||||
python3 scripts/analyze_history.py --days 4 # last N days only
|
||||
"""
|
||||
import json, os, argparse
|
||||
from datetime import datetime
|
||||
|
||||
SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
FIELDS = ["kcal", "protein", "fibre", "satfat", "vitC", "vitA", "calcium"]
|
||||
|
||||
|
||||
def load():
|
||||
with open(os.path.join(SKILL_DIR, "meal-history.json")) as f:
|
||||
hist = json.load(f)
|
||||
with open(os.path.join(SKILL_DIR, "references", "nutrition-db.json")) as f:
|
||||
db = json.load(f)
|
||||
entries = hist["entries"] if isinstance(hist, dict) else hist
|
||||
return entries, db
|
||||
|
||||
|
||||
def to_grams(name, qty, unit, db):
|
||||
spec = next((db["items"][k] for k in db["items"] if k.lower() == name.lower()), None)
|
||||
if not spec:
|
||||
return 0.0
|
||||
if unit in ("g", "ml", "kg"):
|
||||
return qty * (1000 if unit == "kg" else 1)
|
||||
# count/can/serving/portion: grams = qty * g_per_unit of the canonical unit
|
||||
return qty * spec.get("g_per_unit", 100)
|
||||
|
||||
|
||||
def item_nutrition(name, qty, unit, db):
|
||||
spec = next((db["items"][k] for k in db["items"] if k.lower() == name.lower()), None)
|
||||
if not spec:
|
||||
return {f: 0 for f in FIELDS}
|
||||
grams = to_grams(name, qty, unit, db)
|
||||
factor = grams / spec["g_per_unit"] # g_per_unit == grams per one canonical unit
|
||||
return {f: spec.get(f, 0) * factor for f in FIELDS}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--days", type=int, default=0, help="limit to last N days (0 = all)")
|
||||
args = ap.parse_args()
|
||||
entries, db = load()
|
||||
entries = sorted(entries, key=lambda e: e.get("date", ""))
|
||||
if args.days:
|
||||
dates = sorted({e["date"] for e in entries})[-args.days:]
|
||||
entries = [e for e in entries if e["date"] in dates]
|
||||
|
||||
by_day = {}
|
||||
for e in entries:
|
||||
by_day.setdefault(e["date"], []).append(e)
|
||||
|
||||
grand = {f: 0 for f in FIELDS}
|
||||
day_proteins = []
|
||||
print("=" * 78)
|
||||
print("MEAL-HISTORY HEALTH REVIEW (estimated; condiments/oil not logged => +~80-120 kcal/meal)")
|
||||
print("=" * 78)
|
||||
for d in sorted(by_day):
|
||||
day_tot = {f: 0 for f in FIELDS}
|
||||
veg = set()
|
||||
print(f"\n--- {d} ({by_day[d][0].get('weekday','?')}) ---")
|
||||
for e in by_day[d]:
|
||||
mtot = {f: 0 for f in FIELDS}
|
||||
for it in e.get("items", []):
|
||||
n = item_nutrition(it["name"], it["qty"], it.get("unit", "count"), db)
|
||||
for f in FIELDS:
|
||||
mtot[f] += n[f]
|
||||
if it["name"] in db.get("veg_names", []):
|
||||
veg.add(it["name"])
|
||||
for f in FIELDS:
|
||||
day_tot[f] += mtot[f]
|
||||
print(f" {e['meal']:6} {e['name']}")
|
||||
print(f" ~{mtot['kcal']:.0f} kcal | {mtot['protein']:.0f} g protein | "
|
||||
f"fibre {mtot['fibre']:.0f} | satfat {mtot['satfat']:.0f} | "
|
||||
f"vitC {mtot['vitC']:.0f} | vitA {mtot['vitA']:.0f} | Ca {mtot['calcium']:.0f}")
|
||||
for f in FIELDS:
|
||||
grand[f] += day_tot[f]
|
||||
day_proteins.append(day_tot["protein"])
|
||||
t = db["targets"]
|
||||
p_ok = "OK" if day_tot["protein"] >= t["protein_per_day_g"] else "LOW"
|
||||
sf = "HIGH" if day_tot["satfat"] > t["satfat_per_day_g"] else "ok"
|
||||
print(f" DAY TOTAL ~{day_tot['kcal']:.0f} kcal | protein {day_tot['protein']:.0f} g [{p_ok}] | "
|
||||
f"fibre {day_tot['fibre']:.0f} | satfat {day_tot['satfat']:.0f} [{sf}] | "
|
||||
f"veg types {len(veg)}")
|
||||
|
||||
n = len(by_day) or 1
|
||||
t = db["targets"]
|
||||
print("\n" + "=" * 78)
|
||||
print(f"WINDOW AVERAGE/DAY (over {len(by_day)} day(s)):")
|
||||
print(f" kcal ~{grand['kcal']/n:.0f}")
|
||||
print(f" protein {grand['protein']/n:.0f} g (target >= {t['protein_per_day_g']}) "
|
||||
f"-> days meeting: {sum(1 for p in day_proteins if p >= t['protein_per_day_g'])}/{len(day_proteins)}")
|
||||
print(f" fibre {grand['fibre']/n:.0f} g (target ~{t['fibre_per_day_g']})")
|
||||
print(f" satfat {grand['satfat']/n:.0f} g (target <= {t['satfat_per_day_g']})")
|
||||
print(f" vitC {grand['vitC']/n:.0f} mg (RDA ~{t['vitC_mg']})")
|
||||
print(f" vitA {grand['vitA']/n:.0f} mcg (RDA ~{t['vitA_mcg']})")
|
||||
print(f" calcium {grand['calcium']/n:.0f} mg (RDA ~{t['calcium_mg']})")
|
||||
print("=" * 78)
|
||||
print("Not medical advice — defer to the user's clinical/dietitian team.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user