Files
hermes-meal-suggestion/scripts/analyze_history.py
hermes d4c5a8f658 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
2026-08-29 08:11:16 +01:00

121 lines
5.2 KiB
Python

#!/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()