--- title: Pinch emoji: ๐Ÿค colorFrom: yellow colorTo: green sdk: gradio sdk_version: 6.18.0 app_file: app.py pinned: false license: apache-2.0 tags: - build-small-hackathon - track:backyard - sponsor:openbmb - sponsor:modal - off-brand - best-agent - best-demo - tiny-titan --- # ๐Ÿค Pinch โ€” photograph your ingredients, get a grounded plan **Build Small Hackathon 2026 ยท ๐Ÿก Backyard AI track** **Snap a photo of what's in your kitchen โ€” Pinch works out what to cook, plans the seasoning grounded in real flavour science, and computes the amounts.** A vision model reads the ingredients from your photo; an agent reasons over the Epicure flavour model (1,790 ingredients distilled from ~4M recipes) to decide *what* to add, *what to substitute* for what you lack, and *in what order*; and it runs Python in a sandbox for the quantitative side (servings, ratios, salt %, timing). Optionally, FLUX turns your ingredients photo into a picture of the finished dish. Small models, each load-bearing, **โ‰ˆ24B total โ€” under the 32B cap**: [MiniCPM-V](https://huggingface.co/openbmb/MiniCPM-V-4.6) (OpenBMB) sees the ingredients, [Mellum 2](https://huggingface.co/JetBrains/Mellum2-12B-A2.5B-Instruct) (12B, JetBrains) reasons about flavour, and [FLUX.2 klein](https://huggingface.co/black-forest-labs/FLUX.2-klein-4B) (4B) renders the dish. ### Why this needs an agent The hard parts aren't a lookup. **Flavour** โ€” *given the messy set of things in my kitchen and this dish, what's worth adding, and in what order?* โ€” is combinatorial and grounded in the Epicure pairing model, not the LLM's opinion. **Arithmetic** โ€” scaling to servings, ratios, salt % โ€” is something LLMs get wrong in their heads, so the agent offloads it to real code in a sandbox. The model reasons and sequences; the tools provide the facts. **๐ŸŽฅ Demo video:** https://www.youtube.com/watch?v=gpXQS35QWW0 **๐Ÿ“ฃ Social post:** https://x.com/ssaacar/status/2066645089942708604 ## Architecture ``` You: photo (or typed ingredients) + optional constraints โ”‚ MiniCPM-V reads the photo โ†’ ingredient list (you edit it) โ–ผ suggest_dish() + build_plan() โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” pairing_score โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ JetBrains Mellum 2 (12B) โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ Epicure MCP (Kaikaku) โ”‚ โ”‚ strict-JSON planner loop โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ pairing scores, neighbours โ”‚ โ”‚ โ”‚ run_python โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ sandbox (kitchen math: โ”‚ โ”‚ โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ amounts, ratios, timing) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ stdout โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ staged plan + amounts โ”€โ”€โ–บ (optional) FLUX.2 klein โ†’ dish photo ``` - **Models:** [Mellum 2](https://huggingface.co/JetBrains/Mellum2-12B-A2.5B-Instruct) reasons + writes the math code (served on Modal); [MiniCPM-V](https://huggingface.co/openbmb/MiniCPM-V-4.6) reads the photo (OpenBMB's free hosted API); [FLUX.2 klein](https://huggingface.co/black-forest-labs/FLUX.2-klein-4B) renders the dish (HF Inference / fal-ai). All remote โ€” **the Space itself needs no GPU.** - **Flavour grounding:** [Epicure MCP](https://github.com/KAIKAKU-AI/epicure-mcp) โ€” public, anonymous flavour server (`find_pairings`, `pairing_score`, `neighbors`, โ€ฆ) via a hand-rolled MCP client (`epicure_client.py`) with 404/429 retry + a disk cache. Each step's evidence is re-derived from a **real** pairing score, never the model's guess. - **Sandbox:** the quantitative math runs as real Python (`sandbox.py`), tailored to the dish type (a salad gets no simmer time; a dessert gets no savoury salt). `SANDBOX_BACKEND=local|modal|mock`. - **Robustness:** the model emits one JSON object per turn โ€” an Epicure tool call, a `run_python`, or the finished plan (`agent.py`). If it doesn't converge, a grounded scripted planner (real Epicure scores + sandbox math, no LLM) takes over. - **Honesty note:** pairing *scores* are grounded facts; the substitution hint uses Epicure `neighbors` (co-occurrence, not a functional swap), so it's surfaced as "closest in flavour space," not "use X for the sourness of Y." ## Run it **In the browser:** add a photo (or type ingredients) โ†’ **Detect ingredients** โ†’ tidy the list to what you actually have โ†’ **Decide a dish & plan** โ†’ optionally **โœจ See the dish**. **Local dev:** ```bash pip install -r requirements.txt cp .env.example .env # fill in MODAL_REASON_URL, HF_TOKEN, etc. python app.py # fully offline (no models, scripted planner + sample detection): MOCK_LLM=1 python app.py ``` ## Where the models run (inference backend) Each model is configured independently via env vars (see `.env.example`). **Vision (MiniCPM-V) โ€” `VISION_BACKEND`:** has a free hosted API, so it runs real with **no GPU**. | `VISION_BACKEND` | runs on | cost | |---|---|---| | `openbmb` | OpenBMB's free hosted API (`api.modelbest.cn`, OpenAI-compatible) | free | | `zerogpu` | in-Space ZeroGPU | free, quota | | `modal` | your Modal endpoint | credits | **Reasoning (Mellum 2) โ€” `INFERENCE_BACKEND`:** Mellum has **no hosted API** anywhere, so it runs on a GPU you control. | `INFERENCE_BACKEND` | runs on | cost | |---|---|---| | `modal` | your Modal endpoint (no quota) | credits | | `zerogpu` | in-Space ZeroGPU (40 min/day) | free | | `mock` (`MOCK_LLM=1`) | scripted planner, no model | free (dev) | **Dish image โ€” FLUX.2 klein:** the "โœจ See the dish" button renders the finished plate via HF Inference (`provider="fal-ai"`), billed to your account. Set `HF_TOKEN`; without it the button just shows a hint. **Deploy the Modal reasoning backend** (one-time; `pip install modal` + `modal token new`): ```bash modal deploy modal_app.py # serves Mellum 2 on A100-40GB; weights cached in a Volume ``` It prints a URL. Set on the Space (Settings โ†’ Variables & secrets): ``` INFERENCE_BACKEND=modal MODAL_REASON_URL= VISION_BACKEND=openbmb ``` `min_containers=0` โ†’ idle costs nothing; cold start ~30โ€“60s (weights cached).