Alptraum commited on
Commit
ca47b0e
·
verified ·
1 Parent(s): 3ff43c7

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +20 -115
app.py CHANGED
@@ -1,21 +1,17 @@
1
- """Epicurean Simmer — Build Small Hackathon 2026 (Backyard AI track).
2
-
3
- Tell it what you're cooking and what's actually in your kitchen. A 12B agent
4
- (JetBrains Mellum 2, well under the 32B cap) reasons over the Epicure
5
- flavour-science server to build a STAGED seasoning plan grounded in real
6
- pairing datawhat to add, what to substitute for what you lack, in what
7
- order. Then the pot's temperature acts as a clock, surfacing each step on a
8
- 16x2 LCD at the moment the cook needs it.
9
-
10
- The hard part is the flavour reasoning, not the temperature. Modes:
11
- - Space/ZeroGPU: full model builds the plan
12
- - MOCK_LLM=1: scripted planner, still calling Epicure live (no GPU needed)
13
  """
14
 
15
  import json
16
  import os
17
- import threading
18
- import time
19
 
20
  # Load .env for local dev BEFORE importing modules that read os.environ at import
21
  # time (agent/vision/imagery). No-op on a Space (no file) or if dotenv is absent.
@@ -29,10 +25,7 @@ except ImportError:
29
  import gradio as gr
30
  import pandas as pd
31
 
32
- from agent import (
33
- build_plan, classify_phase, lcd_for_phase, parse_pantry, scripted_plan,
34
- suggest_dish,
35
- )
36
  from epicure_client import EpicureMCP
37
  from imagery import generate_dish_image
38
  from vision import detect_ingredients
@@ -92,69 +85,22 @@ MCP = EpicureMCP()
92
 
93
  # --------------------------------------------------------------------- state
94
 
95
- class KitchenState:
96
- def __init__(self):
97
- self.lock = threading.Lock()
98
- self.reset()
99
-
100
- def reset(self):
101
- self.t0 = time.monotonic()
102
- self.log = []
103
- self.phase = "warming up"
104
- self.plan = {"dish": "", "core": "", "steps": [], "notes": [], "source": ""}
105
- self.lcd = {"line1": "Epicurean", "line2": "Plan a dish", "stage": ""}
106
-
107
- def add_reading(self, temp: float):
108
- with self.lock:
109
- self.log.append((time.monotonic() - self.t0, float(temp)))
110
- self.phase = classify_phase(self.log)
111
- if self.plan["steps"]:
112
- self.lcd = lcd_for_phase(self.plan, self.phase, temp)
113
- return self.phase
114
-
115
 
116
- STATE = KitchenState()
117
 
118
 
119
- # ----------------------------------------------------------- planning (the AI)
120
-
121
  def make_plan(dish: str, pantry: str, constraints: str) -> dict:
122
- """Run the agent once to build the staged plan. The expensive reasoning
123
- happens here, not per-reading — cooking then costs zero inference."""
124
  if MOCK_LLM:
125
  plan = scripted_plan(dish, pantry, constraints, MCP)
126
  else:
127
  plan = build_plan(dish, pantry, constraints, llm_generate, MCP)
128
- with STATE.lock:
129
- STATE.plan = plan
130
  return plan
131
 
132
 
133
- # ----------------------------------------------------- bridge API (hardware)
134
-
135
- def ingest_reading(temp: float) -> str:
136
- """Called by bridge/serial_bridge.py per probe reading. Just advances the
137
- playhead through the already-built plan — no inference per reading."""
138
- STATE.add_reading(temp)
139
- with STATE.lock:
140
- return json.dumps({"line1": STATE.lcd["line1"], "line2": STATE.lcd["line2"]})
141
-
142
-
143
- # ------------------------------------------------------------------ simulator
144
-
145
- class SimKettle:
146
- def __init__(self):
147
- self.temp = 21.0
148
-
149
- def step(self) -> float:
150
- import random
151
- self.temp += (101.0 - self.temp) * 0.035 + random.uniform(-0.3, 0.3)
152
- return round(min(self.temp, 100.2), 1)
153
-
154
-
155
- SIM = SimKettle()
156
-
157
-
158
  # ------------------------------------------------------------------------- ui
159
 
160
  STAGE_ORDER = ["bloom", "aromatics", "body", "finish"]
@@ -186,26 +132,6 @@ def render_plan(plan: dict) -> str:
186
  return "\n\n".join(blocks)
187
 
188
 
189
- def lcd_html(line1: str, line2: str) -> str:
190
- return f"""
191
- <div style="background:#0a2a12;border:6px solid #1a1a1a;border-radius:8px;
192
- padding:14px 18px;width:fit-content;font-family:monospace;">
193
- <div style="color:#7CFC8A;font-size:1.6em;letter-spacing:0.25em;
194
- white-space:pre;">{line1:<16}</div>
195
- <div style="color:#7CFC8A;font-size:1.6em;letter-spacing:0.25em;
196
- white-space:pre;">{line2:<16}</div>
197
- </div>"""
198
-
199
-
200
- def cook_snapshot():
201
- with STATE.lock:
202
- df = pd.DataFrame(STATE.log, columns=["seconds", "celsius"])
203
- lcd, phase, plan = dict(STATE.lcd), STATE.phase, dict(STATE.plan)
204
- stage = lcd.get("stage", "")
205
- status = f"**Pot:** {phase}" + (f" → cooking stage **{stage}**" if stage else "")
206
- return df, lcd_html(lcd["line1"], lcd["line2"]), status
207
-
208
-
209
  @spaces.GPU(duration=60)
210
  def detect_from_photo(image, current_pantry):
211
  """Run the VLM on the uploaded photo and merge its findings into the pantry
@@ -242,31 +168,14 @@ def on_see_dish(dish, pantry, constraints, photo):
242
  return dish, image, note
243
 
244
 
245
- def sim_tick():
246
- STATE.add_reading(SIM.step())
247
- return cook_snapshot()
248
-
249
-
250
- def reset_pot():
251
- with STATE.lock:
252
- plan = dict(STATE.plan)
253
- STATE.t0 = time.monotonic()
254
- STATE.log = []
255
- STATE.phase = "warming up"
256
- STATE.plan = plan # keep the plan; just cool the pot
257
- SIM.__init__()
258
- return cook_snapshot()
259
-
260
-
261
  with gr.Blocks(title="Pinch") as demo:
262
  gr.Markdown(
263
  "# 🤏 Pinch\n"
264
- "**Snap a photo of your ingredients — I'll work out what to cook, plan the "
265
- "seasoning grounded in real flavour science, compute the amounts, and time "
266
- "it to your pot.** \n"
267
  "_Vision reads your shelf; a 12B agent picks a dish, reasons over the Epicure "
268
  "flavour model (1,790 ingredients from ~4M recipes), and runs code for the "
269
- "quantities. The temperature is just the clock._"
270
  )
271
 
272
  with gr.Row():
@@ -287,7 +196,7 @@ with gr.Blocks(title="Pinch") as demo:
287
  label="Agent's tool calls (Epicure + sandbox)", wrap=True)
288
  with gr.Column(scale=1):
289
  gr.Markdown("### 3 · The plan")
290
- plan_md = gr.Markdown(render_plan(STATE.plan))
291
  see_btn = gr.Button("✨ See the dish (FLUX.2 klein)")
292
  dish_image = gr.Image(label="Dish preview", height=260)
293
  image_note = gr.Markdown("")
@@ -296,9 +205,5 @@ with gr.Blocks(title="Pinch") as demo:
296
  plan_btn.click(on_plan, [dish, pantry, constraints], [dish, plan_md, tool_log])
297
  see_btn.click(on_see_dish, [dish, pantry, constraints, photo], [dish, dish_image, image_note])
298
 
299
- # Kept (no UI): optional hardware bridge can still push plan steps to a
300
- # physical 16x2 LCD via this endpoint — see bridge/serial_bridge.py.
301
- gr.api(ingest_reading, api_name="ingest_reading")
302
-
303
  if __name__ == "__main__":
304
  demo.launch()
 
1
+ """Pinch — Build Small Hackathon 2026 (Backyard AI track).
2
+
3
+ Photograph your ingredients; a small vision model reads them, a 12B agent
4
+ (JetBrains Mellum 2) decides a dish and builds a seasoning plan grounded in the
5
+ Epicure flavour model — what to add, what to substitute for what you lack, in
6
+ what orderand runs code for the amounts. Optionally FLUX renders the dish.
7
+
8
+ Modes:
9
+ - full: MiniCPM-V (vision) + Mellum 2 (reasoning) + Epicure + sandbox
10
+ - MOCK_LLM=1: scripted planner, still calling Epicure live (no model needed)
 
 
11
  """
12
 
13
  import json
14
  import os
 
 
15
 
16
  # Load .env for local dev BEFORE importing modules that read os.environ at import
17
  # time (agent/vision/imagery). No-op on a Space (no file) or if dotenv is absent.
 
25
  import gradio as gr
26
  import pandas as pd
27
 
28
+ from agent import build_plan, parse_pantry, scripted_plan, suggest_dish
 
 
 
29
  from epicure_client import EpicureMCP
30
  from imagery import generate_dish_image
31
  from vision import detect_ingredients
 
85
 
86
  # --------------------------------------------------------------------- state
87
 
88
+ # ----------------------------------------------------------- planning (the AI)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
+ CURRENT_PLAN = {"dish": "", "core": "", "steps": [], "notes": [], "source": ""}
91
 
92
 
 
 
93
  def make_plan(dish: str, pantry: str, constraints: str) -> dict:
94
+ """Run the agent once to build the plan (and remember it for the UI)."""
95
+ global CURRENT_PLAN
96
  if MOCK_LLM:
97
  plan = scripted_plan(dish, pantry, constraints, MCP)
98
  else:
99
  plan = build_plan(dish, pantry, constraints, llm_generate, MCP)
100
+ CURRENT_PLAN = plan
 
101
  return plan
102
 
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  # ------------------------------------------------------------------------- ui
105
 
106
  STAGE_ORDER = ["bloom", "aromatics", "body", "finish"]
 
132
  return "\n\n".join(blocks)
133
 
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  @spaces.GPU(duration=60)
136
  def detect_from_photo(image, current_pantry):
137
  """Run the VLM on the uploaded photo and merge its findings into the pantry
 
168
  return dish, image, note
169
 
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  with gr.Blocks(title="Pinch") as demo:
172
  gr.Markdown(
173
  "# 🤏 Pinch\n"
174
+ "**Snap a photo of your ingredients — I'll work out what to cook and plan the "
175
+ "seasoning, grounded in real flavour science, with the amounts worked out.** \n"
 
176
  "_Vision reads your shelf; a 12B agent picks a dish, reasons over the Epicure "
177
  "flavour model (1,790 ingredients from ~4M recipes), and runs code for the "
178
+ "quantities._"
179
  )
180
 
181
  with gr.Row():
 
196
  label="Agent's tool calls (Epicure + sandbox)", wrap=True)
197
  with gr.Column(scale=1):
198
  gr.Markdown("### 3 · The plan")
199
+ plan_md = gr.Markdown(render_plan(CURRENT_PLAN))
200
  see_btn = gr.Button("✨ See the dish (FLUX.2 klein)")
201
  dish_image = gr.Image(label="Dish preview", height=260)
202
  image_note = gr.Markdown("")
 
205
  plan_btn.click(on_plan, [dish, pantry, constraints], [dish, plan_md, tool_log])
206
  see_btn.click(on_see_dish, [dish, pantry, constraints, photo], [dish, dish_image, image_note])
207
 
 
 
 
 
208
  if __name__ == "__main__":
209
  demo.launch()