#!/usr/bin/env python3 """ Estimate protein (g), calories (kcal), fibre (g) and saturated fat (g) for a meal from its items, and check the RATIONAL PORTIONS caps (kcal cap + per-item protein max_per_meal). Reads references/calories.md-style numbers inline (kept in sync with that file). Usage: python3 calc_meal.py '[{"name":"Eggs","qty":4,"unit":"count"}, ...]' Prints JSON: {protein_g, kcal, fibre_g, satfat_g, over_kcal, over_item}. Exit non-zero if over_kcal (so it can gate sends in the daily job). """ import json, sys, os SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # kcal per logged portion. For g/ml-unit items the value is PER GRAM (× qty). # For count/can/portion items the value is the whole item. Keep in sync with # references/calories.md (which shows dairy per 100g for humans — divide by 100 here). KCAL = { "Eggs": 70, "Chicken breast": 165, "Tinned tuna in veg oil": 190, "Tinned tuna in spring water": 130, "Tinned green lentils": 230, "Greek yogurt (0%)": 0.5, "Cottage cheese": 0.95, "Cheese": 4.0, "Halloumi": 3.2, "Whey protein powder": 4, "Milk (semi)": 0.5, "Dried red lentils": 3.5, "Baked beans": 200, "Chickpeas": 180, "Tofu (firm)": 1.4, "Edamame (frozen)": 1.2, "Wholemeal pasta": 350, "Microwave rice": 220, "Scottish oats": 3.7, "Broccoli": 35, "Carrots": 0.41, "Courgette": 20, "Sweet potatoes": 130, "Aubergine": 40, "Red cabbage": 0.04, "Cauliflower": 30, "Leeks": 55, "Mushrooms": 0.23, "Lettuce": 15, "Salad tomatoes": 15, "Orange bell pepper": 30, "Small white onions": 15, "Frozen mixed veg": 0.73, "Peeled plum tomatoes": 90, "Cumberland sausages": 55, "Lamb mince": 2.5, } PROTEIN_G = { "Eggs": 6, "Chicken breast": 33, "Tinned tuna in veg oil": 25, "Tinned tuna in spring water": 25, "Tinned green lentils": 18, "Greek yogurt (0%)": 0.10, "Cottage cheese": 0.12, "Cheese": 0.25, "Halloumi": 0.19, "Whey protein powder": 0.8, "Milk (semi)": 3.4, "Dried red lentils": 0.25, "Baked beans": 18, "Chickpeas": 8, "Tofu (firm)": 13, "Edamame (frozen)": 11, "Wholemeal pasta": 7, "Microwave rice": 3, "Scottish oats": 11, "Broccoli": 3, "Carrots": 0.01, "Courgette": 1, "Sweet potatoes": 2, "Aubergine": 1, "Red cabbage": 0.01, "Cauliflower": 3, "Leeks": 1, "Mushrooms": 0.03, "Lettuce": 1, "Salad tomatoes": 1, "Orange bell pepper": 1, "Small white onions": 1, "Frozen mixed veg": 0.03, "Peeled plum tomatoes": 2, "Cumberland sausages": 11, "Lamb mince": 18, } FIBRE_G = { "Dried red lentils": 0.11, "Baked beans": 14, "Chickpeas": 14, "Tinned green lentils": 14, "Tofu (firm)": 0.004, "Edamame (frozen)": 0.1, "Wholemeal pasta": 5, "Scottish oats": 0.1, "Broccoli": 3, "Carrots": 0.03, "Courgette": 1, "Sweet potatoes": 4, "Aubergine": 3, "Red cabbage": 0.04, "Cauliflower": 3, "Leeks": 3, "Mushrooms": 0.02, "Lettuce": 1, "Salad tomatoes": 1, "Orange bell pepper": 2, "Small white onions": 1, "Frozen mixed veg": 0.04, "Peeled plum tomatoes": 2, "Cumberland sausages": 0, "Lamb mince": 0, } SATFAT_G = { "Eggs": 6, "Cheese": 0.21, "Halloumi": 0.26, "Cumberland sausages": 18, "Lamb mince": 9, "Tinned tuna in veg oil": 3, "Chicken breast": 1, "Whey protein powder": 0, } KCAL_CAP = 1000 GRAMS_PER_UNIT = { "g": 1, "ml": 1, "kg": 1000, "count": 1, "can": 1, "small block": 120, "block": 120, "serving": 1, "portion": 1, "pack": 1, "jar": 1, "tub": 1, "bulb": 1, "bottle": 1, "loaf": 1, } def estimate(items, inv): tot = {"protein_g": 0.0, "kcal": 0.0, "fibre_g": 0.0, "satfat_g": 0.0} over_kcal = False over_item = [] for it in items: name, qty, unit = it["name"], it["qty"], it.get("unit", "count") # kcal kp = KCAL.get(name, 0) k = kp * (qty if unit not in ("g", "ml") else qty) # protein pp = PROTEIN_G.get(name, 0) p = pp * (qty if unit not in ("g", "ml") else qty) # fibre fp = FIBRE_G.get(name, 0) f = fp * (qty if unit not in ("g", "ml") else qty) # satfat sp = SATFAT_G.get(name, 0) s = sp * (qty if unit not in ("g", "ml") else qty) tot["kcal"] += k; tot["protein_g"] += p; tot["fibre_g"] += f; tot["satfat_g"] += s # per-item max_per_meal (protein) check — convert both sides to grams inv_item = next((i for i in inv["items"] if i["name"].lower() == name.lower()), None) cap = inv_item.get("max_per_meal") if inv_item else None if cap is not None: cap_unit = inv_item.get("unit", unit) used_g = qty * GRAMS_PER_UNIT.get(unit, 1) cap_g = cap * GRAMS_PER_UNIT.get(cap_unit, 1) if used_g > cap_g + 1e-6: over_item.append(f"{name} {qty}{unit} > cap {cap}{cap_unit}") if tot["kcal"] > KCAL_CAP + 1e-6: over_kcal = True for key in tot: tot[key] = round(tot[key], 1) return {**tot, "over_kcal": over_kcal, "over_item": over_item} def main(): items = json.loads(sys.argv[1]) if len(sys.argv) > 1 else [] inv = json.load(open(os.path.join(SKILL_DIR, "inventory.json"))) res = estimate(items, inv) print(json.dumps(res)) if res["over_kcal"] or res["over_item"]: sys.exit(1) if __name__ == "__main__": main()