<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shivang</title>
    <description>The latest articles on DEV Community by Shivang (@shangkaul).</description>
    <link>https://dev.to/shangkaul</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F736092%2Fcdde5539-e1a2-4cf8-ac06-34b7845ce6b6.jpg</url>
      <title>DEV Community: Shivang</title>
      <link>https://dev.to/shangkaul</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shangkaul"/>
    <language>en</language>
    <item>
      <title>I built my parents a recipe app to help with their protein goals</title>
      <dc:creator>Shivang</dc:creator>
      <pubDate>Sun, 04 Oct 2026 22:22:12 +0000</pubDate>
      <link>https://dev.to/shangkaul/i-built-my-parents-a-recipe-app-to-help-with-their-protein-goals-41j</link>
      <guid>https://dev.to/shangkaul/i-built-my-parents-a-recipe-app-to-help-with-their-protein-goals-41j</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/shangkaul/recipe-protein" rel="noopener noreferrer"&gt;https://github.com/shangkaul/recipe-protein&lt;/a&gt;&lt;br&gt;
📹 &lt;strong&gt;Demo video:&lt;/strong&gt; below&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The rule that shaped the whole project: &lt;strong&gt;the AI never supplies a number.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;There is no account, no health profile, no cloud model, and no per-request cost.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/KxEiKo3gZN4" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo3brh9gbi2vqs9bwgz4f.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo3brh9gbi2vqs9bwgz4f.jpg" alt="Image of the app homepage and search screen" width="780" height="1688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The browse-first opening starts with appetising choices, not a form wall.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it is for
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That pushed every product decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;familiar meals before configuration;&lt;/li&gt;
&lt;li&gt;readable controls and large touch targets, because it is used in a kitchen on a phone;&lt;/li&gt;
&lt;li&gt;pantry fit ahead of nutrition optimisation;&lt;/li&gt;
&lt;li&gt;Indian-first ranking without dropping global options;&lt;/li&gt;
&lt;li&gt;automatic exclusion of red meat and red-meat-derived ingredients;&lt;/li&gt;
&lt;li&gt;the user sets their own target; the app never prescribes one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why open and local
&lt;/h2&gt;

&lt;p&gt;Gemma is not decoration here. Running an open-weight model on their own hardware is what makes the private workflow possible at all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pantry text, exclusions, and targets never leave the device;&lt;/li&gt;
&lt;li&gt;deterministic search still works when Ollama is stopped;&lt;/li&gt;
&lt;li&gt;the model is swappable and inspectable rather than a remote dependency;&lt;/li&gt;
&lt;li&gt;there is no inference cost;&lt;/li&gt;
&lt;li&gt;malformed, timed-out, or unsafe output can be rejected without losing the source recipe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app never silently falls back to a cloud model. Docker Compose can run the PWA, the Flask API, Ollama, and &lt;code&gt;gemma3:1b&lt;/code&gt; in one local stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gemma has a narrow job
&lt;/h2&gt;

&lt;p&gt;The most important decision was keeping Gemma outside the nutrition trust boundary.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;add&lt;/code&gt; or &lt;code&gt;increase&lt;/code&gt; operations, an ID from that enum, a quantity in grams, and short method notes.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Gemma does not provide nutrient values and does not guess serving counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deterministic nutrition, not plausible prose
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That strictness has a real cost, and I kept it. Only &lt;strong&gt;17,200 of 818,408 safe Recipe1M+ recipes&lt;/strong&gt; currently have validated ingredient-level nutrition, so the UI adds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;Verified nutrition only&lt;/strong&gt; filter that queries that subset directly;&lt;/li&gt;
&lt;li&gt;nutrition-availability labels on every result, before you open anything;&lt;/li&gt;
&lt;li&gt;an explanation before adaptation controls appear;&lt;/li&gt;
&lt;li&gt;user-confirmed servings, because Recipe1M has no reliable yield.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bug that taught me the most
&lt;/h2&gt;

&lt;p&gt;Late in the release I saw a detail screen claiming &lt;strong&gt;1573.2 g protein and 15200.4 kcal for 1 serving&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Two independent guards fixed it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Absolute scale limits at import.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A serving guard at calculation time.&lt;/strong&gt; Anything above 150 g protein or 1500 kcal per serving is refused outright, regardless of what is stored.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Then a second review caught something subtler. Cards were labelled &lt;strong&gt;Verified nutrition&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This recipe only divides into a plausible portion at 9 servings or more, so we have filled that in for you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Verified recipes labelled verified now return a real number in every case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model chooses the ingredient, not the grams
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gemma3:1b&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Gemma still decides &lt;strong&gt;which&lt;/strong&gt; pantry ingredients to change, &lt;strong&gt;whether&lt;/strong&gt; to add or increase, and &lt;strong&gt;why&lt;/strong&gt;. Python decides the numbers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9d9sspdi0qwhlivqau97.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9d9sspdi0qwhlivqau97.jpg" alt="List of recipes with verified nutrition counts" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;With the filter on, every result is labelled verified, because the label and the calculation now use the same rule.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwmuy28wzni7ipcuyb7te.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwmuy28wzni7ipcuyb7te.jpg" alt="Serving count and ingredient increase based on protein limit set by user" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieval without cloud inference
&lt;/h2&gt;

&lt;p&gt;The public corpus is &lt;a href="https://theunitools.com/en/data" rel="noopener noreferrer"&gt;UniTools World Recipes&lt;/a&gt;: 501 attributed recipes from 127 countries with per-serving nutrition. An optional private &lt;a href="https://pic2recipe.csail.mit.edu/" rel="noopener noreferrer"&gt;Recipe1M+&lt;/a&gt; SQLite FTS5 index adds breadth without committing or deploying the source data.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Gemma is used only when deterministic parsing cannot resolve a term, and for the optional adaptation. Search remains useful without it.&lt;/p&gt;

&lt;p&gt;In a measured local search for &lt;code&gt;tofu, rice, spinach&lt;/code&gt;, the nutrition-only query returned 24 results from 231 eligible matches in about &lt;strong&gt;0.12 seconds&lt;/strong&gt;, because SQLite applies the nutrition predicate before FTS candidates are returned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safety, accessibility, and failure states
&lt;/h2&gt;

&lt;p&gt;Designed for use in a kitchen on a phone, including by older adults:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keyboard-operable dialogs with Escape-to-close, focus containment, and focus restoration;&lt;/li&gt;
&lt;li&gt;readable mobile layouts and 44-pixel minimum touch targets;&lt;/li&gt;
&lt;li&gt;visible focus styles and reduced-motion handling;&lt;/li&gt;
&lt;li&gt;search progress that keeps current results visible instead of blanking the screen;&lt;/li&gt;
&lt;li&gt;offline access to the PWA shell and saved meal ideas;&lt;/li&gt;
&lt;li&gt;actionable errors when Ollama or the model is unavailable;&lt;/li&gt;
&lt;li&gt;explicit source, licence, and nutrition-basis labels;&lt;/li&gt;
&lt;li&gt;no medical recommendations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Built with
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;45 backend tests, 8 frontend journey and component tests&lt;/li&gt;
&lt;li&gt;TypeScript production build and lint clean&lt;/li&gt;
&lt;li&gt;full private index rebuild: 1,029,720 source records scanned, 818,408 safe recipes retained, 17,200 nutrition-ready&lt;/li&gt;
&lt;li&gt;end-to-end check that every recipe labelled verified returns a plausible number&lt;/li&gt;
&lt;li&gt;fail-closed tests for unsafe, unsupported, and model-failure paths&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The model was not the hardest part. Trustworthy boundaries were.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Expand the compact USDA-backed ingredient reference.&lt;/li&gt;
&lt;li&gt;Add grounded smaller protein-boost suggestions where the corpus supports them.&lt;/li&gt;
&lt;li&gt;Complete a clean Docker deployment run.&lt;/li&gt;
&lt;li&gt;Watch my parents use it, and record what they actually say.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize categories
&lt;/h2&gt;

&lt;p&gt;I'm submitting for &lt;strong&gt;Best Use of Gemma&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Gemma 3 1B runs locally through Ollama and does real work here, not decoration. It resolves ambiguous pantry words like &lt;em&gt;chana&lt;/em&gt;, &lt;em&gt;dahi&lt;/em&gt;, and &lt;em&gt;bhendi&lt;/em&gt; 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 &lt;code&gt;add&lt;/code&gt; or &lt;code&gt;increase&lt;/code&gt; operations with reasons.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That is why open-weight and local mattered for this use case specifically: a 1B model on a laptop is accurate enough to suggest &lt;em&gt;which&lt;/em&gt; protein to add and &lt;em&gt;why&lt;/em&gt;, 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.&lt;/p&gt;

&lt;p&gt;I'm not entering the other partner categories, since this project doesn't use those technologies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credits and data sources
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bundled recipe data:&lt;/strong&gt; &lt;a href="https://theunitools.com/en/data" rel="noopener noreferrer"&gt;UniTools World Recipes&lt;/a&gt;, CC BY-SA 4.0, with per-photo licences retained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nutrient reference:&lt;/strong&gt; &lt;a href="https://fdc.nal.usda.gov/" rel="noopener noreferrer"&gt;USDA FoodData Central&lt;/a&gt;, public domain. Bundled FDC IDs with per-100 g protein, calories, and fat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optional local retrieval corpus:&lt;/strong&gt; &lt;a href="https://pic2recipe.csail.mit.edu/" rel="noopener noreferrer"&gt;Recipe1M+&lt;/a&gt; from the MIT CSAIL–QCRI collaboration — Salvador, Hynes, Biswas, Aytar, Marin, Ofli, Weber, and Torralba. &lt;a href="https://pic2recipe.csail.mit.edu/tpami19.pdf" rel="noopener noreferrer"&gt;TPAMI 2019&lt;/a&gt;, &lt;a href="https://pic2recipe.csail.mit.edu/im2recipe.pdf" rel="noopener noreferrer"&gt;CVPR 2017&lt;/a&gt;, &lt;a href="https://github.com/torralba-lab/im2recipe" rel="noopener noreferrer"&gt;code&lt;/a&gt;. &lt;strong&gt;The project page states no dataset licence&lt;/strong&gt;, 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.&lt;/p&gt;

&lt;p&gt;Community reading that informed the data-quality boundary: &lt;a href="https://dev.to/dietly/taming-open-food-facts-building-a-fast-searchable-nutrition-database-403l"&gt;Taming Open Food Facts&lt;/a&gt; on cleaning crowdsourced nutrition data, and the &lt;a href="https://dev.to/farcrak/501-world-recipes-as-an-open-dataset-per-serving-nutrition-step-timings-ingredient-scaling-rules-4a95"&gt;UniTools dataset release&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>gemma</category>
    </item>
  </channel>
</rss>
