This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend.
π Source code: https://github.com/shangkaul/recipe-protein
πΉ Demo video: below
Protein Pantry is a private, local-first recipe finder I built for my parents. They describe what is already in the kitchen, choose their own protein target, and get meals ranked by pantry fit. When a recipe needs help, a local Gemma model running on their own laptop proposes a small, grounded change.
The rule that shaped the whole project: the AI never supplies a number. Gemma chooses which pantry ingredients to change and why. Python calculates every gram and every nutrient value from verified records. If a proposal cannot be validated, the original recipe is still there.
There is no account, no health profile, no cloud model, and no per-request cost.
Demo
In the video: browse-first opening, a pantry search with the verified-nutrition filter, confirming a serving count on a Recipe1M recipe, and a local Gemma adaptation that takes the recipe from 11.4 g to 29.9 g of protein per serving against a 30 g target.
The browse-first opening starts with appetising choices, not a form wall.
Who it is for
I built this for my parents. The problem was never "find the mathematically highest-protein meal." It was deciding what to cook without opening dozens of recipe pages, translating ingredient names, or sending dietary details to a cloud service.
That pushed every product decision:
- familiar meals before configuration;
- readable controls and large touch targets, because it is used in a kitchen on a phone;
- pantry fit ahead of nutrition optimisation;
- Indian-first ranking without dropping global options;
- automatic exclusion of red meat and red-meat-derived ingredients;
- the user sets their own target; the app never prescribes one.
Why open and local
Gemma is not decoration here. Running an open-weight model on their own hardware is what makes the private workflow possible at all:
- pantry text, exclusions, and targets never leave the device;
- deterministic search still works when Ollama is stopped;
- the model is swappable and inspectable rather than a remote dependency;
- there is no inference cost;
- malformed, timed-out, or unsafe output can be rejected without losing the source recipe.
The app never silently falls back to a cloud model. Docker Compose can run the PWA, the Flask API, Ollama, and gemma3:1b in one local stack.
Gemma has a narrow job
The most important decision was keeping Gemma outside the nutrition trust boundary.
Gemma receives one selected source recipe, the user's pantry, exclusions, confirmed servings, and a closed enum of supported ingredient IDs. It may return only one to three add or increase operations, an ID from that enum, a quantity in grams, and short method notes.
Python then verifies every proposal and rejects unknown ingredients, duplicates, non-pantry additions, increases to ingredients absent from the source recipe, excluded ingredients, red meat, unsafe text, excessive quantities, invalid servings, and unsupported nutrition.
Gemma does not provide nutrient values and does not guess serving counts.
Deterministic nutrition, not plausible prose
The bundled reference is a small set of USDA FoodData Central records with their FDC IDs, source descriptions, public-domain provenance, and protein, calories, and fat per 100 grams. Python does the arithmetic.
For the optional local Recipe1M+ index, the importer validates ingredient weights and nutrient arrays before storing whole-recipe totals. A record is rejected when values are missing, structurally inconsistent, physically implausible, or inconsistent with the source's own per-100 g totals.
That strictness has a real cost, and I kept it. Only 17,200 of 818,408 safe Recipe1M+ recipes currently have validated ingredient-level nutrition, so the UI adds:
- a Verified nutrition only filter that queries that subset directly;
- nutrition-availability labels on every result, before you open anything;
- an explanation before adaptation controls appear;
- user-confirmed servings, because Recipe1M has no reliable yield.
The bug that taught me the most
Late in the release I saw a detail screen claiming 1573.2 g protein and 15200.4 kcal for 1 serving.
The cause was my own validation. I checked the per-100 g values and their ratios, and those looked perfect. But Recipe1M quantity parsing occasionally turns one measure into a multi-kilogram ingredient, so a single "recipe" could total 11 kg while still producing believable per-100 g numbers. Every ratio check passed, the record was stored, and dividing a catering batch by one serving produced nonsense.
Two independent guards fixed it:
- Absolute scale limits at import. Recipe weight at most 10 kg, one ingredient at most 5 kg, energy density at most 6 kcal/g, and protein no greater than the mass of food it is measured in.
- A serving guard at calculation time. Anything above 150 g protein or 1500 kcal per serving is refused outright, regardless of what is stored.
Those stricter import rules cut the nutrition-ready set from 47,047 to 17,200. I kept the trade, because a smaller number of trustworthy recipes beats a large number of wrong ones. For reference, the Recipe1M+ paper itself describes "the 50k recipes with nutritional information we have within Recipe1M+", so the larger original figure was on the right order of magnitude and the cut was a deliberate quality decision rather than lost data.
Then a second review caught something subtler. Cards were labelled Verified nutrition whenever totals existed, but the new serving guard could still refuse those same recipes, so a verified result dead-ended with a bare error. That is worse than showing nothing, because it teaches people not to trust the label.
Each recipe now carries the smallest serving count that produces a plausible portion. The label is driven by that, the search filter mirrors the rule in SQL, and the API returns the floor so the detail view can correct the input and say why:
This recipe only divides into a plausible portion at 9 servings or more, so we have filled that in for you.
Verified recipes labelled verified now return a real number in every case.
The model chooses the ingredient, not the grams
gemma3:1b reliably proposed token quantities such as 1 g, no matter what the prompt asked for. Rather than display a cosmetic difference or silently overrule it, the host owns every gram: it keeps the model's ratio between the chosen ingredients and solves for the weight that actually closes the protein gap. It also refuses a boost above 300 g per serving, because past that the dish is no longer the source recipe with an addition.
Gemma still decides which pantry ingredients to change, whether to add or increase, and why. Python decides the numbers.
With the filter on, every result is labelled verified, because the label and the calculation now use the same rule.
Confirm the serving count, then Gemma proposes the ingredient and Python solves the grams: 11.4 g to 29.9 g protein, landing 0.1 g under the 30 g target.
Retrieval without cloud inference
The public corpus is UniTools World Recipes: 501 attributed recipes from 127 countries with per-serving nutrition. An optional private Recipe1M+ SQLite FTS5 index adds breadth without committing or deploying the source data.
At runtime the API retrieves at most 160 local candidates, merges them with the bundled corpus, applies hard exclusions, and reranks deterministically. Full matches on core pantry ingredients always outrank partial matches, and staples or vague seasoning terms cannot overpower meal-defining ingredients.
Gemma is used only when deterministic parsing cannot resolve a term, and for the optional adaptation. Search remains useful without it.
In a measured local search for tofu, rice, spinach, the nutrition-only query returned 24 results from 231 eligible matches in about 0.12 seconds, because SQLite applies the nutrition predicate before FTS candidates are returned.
Safety, accessibility, and failure states
Designed for use in a kitchen on a phone, including by older adults:
- keyboard-operable dialogs with Escape-to-close, focus containment, and focus restoration;
- readable mobile layouts and 44-pixel minimum touch targets;
- visible focus styles and reduced-motion handling;
- search progress that keeps current results visible instead of blanking the screen;
- offline access to the PWA shell and saved meal ideas;
- actionable errors when Ollama or the model is unavailable;
- explicit source, licence, and nutrition-basis labels;
- no medical recommendations.
Built with
React 19, TypeScript, Vite PWA, Flask, Pydantic, Gemma 3 1B through local Ollama structured output, SQLite FTS5, BM25 with deterministic reranking, USDA FoodData Central nutrient records, and Docker Compose with Gunicorn and Nginx for local deployment.
Validation
- 45 backend tests, 8 frontend journey and component tests
- TypeScript production build and lint clean
- full private index rebuild: 1,029,720 source records scanned, 818,408 safe recipes retained, 17,200 nutrition-ready
- end-to-end check that every recipe labelled verified returns a plausible number
- fail-closed tests for unsafe, unsupported, and model-failure paths
What I learned
The model was not the hardest part. Trustworthy boundaries were.
Nutrition data from large food datasets can look plausible while being structurally impossible. Retrieval can look relevant while matching only a derived ingredient like "chicken stock." A model can return perfectly valid JSON and still pick something outside the user's pantry.
The pattern that worked every time: keep generation narrow, preserve provenance, validate against host-controlled data, and fail closed without blocking the useful deterministic path.
What comes next
- Expand the compact USDA-backed ingredient reference.
- Add grounded smaller protein-boost suggestions where the corpus supports them.
- Complete a clean Docker deployment run.
- Watch my parents use it, and record what they actually say.
Prize categories
I'm submitting for Best Use of Gemma.
Gemma 3 1B runs locally through Ollama and does real work here, not decoration. It resolves ambiguous pantry words like chana, dahi, and bhendi against the bundled ingredient vocabulary, and for an adaptation it receives one source recipe plus a closed enum of supported ingredients and returns only bounded add or increase operations with reasons.
The interesting constraint is that Gemma is deliberately kept outside the trust boundary. It never returns a nutrient value or a serving count. Python validates the proposal against pantry contents, exclusions, red-meat rules, the source recipe, and locally stored USDA records, then solves the quantities itself. A proposal that fails validation is rejected and the original recipe survives.
That is why open-weight and local mattered for this use case specifically: a 1B model on a laptop is accurate enough to suggest which protein to add and why, and cheap and private enough to run on demand β while the numbers stay verifiable. A closed API could have suggested the ingredient too, but it could not have kept the pantry contents on the device, and it would have been the source of the nutrition figures rather than a check on them.
I'm not entering the other partner categories, since this project doesn't use those technologies.
Credits and data sources
Bundled recipe data: UniTools World Recipes, CC BY-SA 4.0, with per-photo licences retained.
Nutrient reference: USDA FoodData Central, public domain. Bundled FDC IDs with per-100 g protein, calories, and fat.
Optional local retrieval corpus: Recipe1M+ from the MIT CSAILβQCRI collaboration β Salvador, Hynes, Biswas, Aytar, Marin, Ofli, Weber, and Torralba. TPAMI 2019, CVPR 2017, code. The project page states no dataset licence, which is exactly why the raw data, generated index, and image cache are git-ignored, built locally from a copy the user supplies, and never deployed or redistributed. Recipes reached through it are labelled with their original source terms and link back to the source page.
Community reading that informed the data-quality boundary: Taming Open Food Facts on cleaning crowdsourced nutrition data, and the UniTools dataset release.
This project and article were created with meaningful AI assistance. I reviewed the implementation, tests, factual claims, sources, and this article. It is a new project built during the challenge window: the first repository commit is dated October 4, 2026, and the implementation was completed before the October 5, 2026 06:59 UTC deadline.



Top comments (0)