srilathatata commited on
Commit
8ce8f4c
·
1 Parent(s): 8e03a78

feat: persistent pantry on HF Spaces /data, TTS wired, clean pipeline

Browse files
Files changed (3) hide show
  1. app.py +104 -48
  2. modal_services.py +44 -58
  3. utils.py +7 -10
app.py CHANGED
@@ -11,15 +11,18 @@ from PIL import Image as PILImage
11
 
12
  sys.path.append(os.path.dirname(__file__))
13
  from utils import (
14
- estimate_expiry, format_parsed_items, get_pantry,
15
- mark_used, save_to_db, expiry_status
16
  )
17
 
 
 
 
18
  CHARACTERS = {
19
- "Grandma (Ammamma)": "You are Ammamma, a warm but judgemental grandma who loves cooking. Speak in English only. Scold gently about food waste but show love.",
20
- "Chef": "You are a sharp professional chef. Direct, technically precise, slightly impatient with bad ingredients, but brilliant at making something from nothing.",
21
- "Fitness Coach": "You are an enthusiastic fitness coach. Every recipe must be high protein, low waste, macro-balanced. You relate everything back to gains and clean eating.",
22
- "Food Critic": "You are a pompous but secretly warm food critic. You critique the ingredients dramatically before reluctantly producing a brilliant recipe.",
23
  }
24
 
25
  CUISINES = ["South Indian", "North Indian", "Italian", "Mediterranean", "East Asian", "Surprise me"]
@@ -32,6 +35,56 @@ CHAR_EMOJI = {
32
  }
33
 
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  def parse_receipt_gradio(image):
36
  if image is None:
37
  return "Please upload a receipt image.", []
@@ -55,7 +108,7 @@ def parse_receipt_gradio(image):
55
  estimate_expiry(item["name"])
56
  )
57
 
58
- save_to_db(result["items"], result.get("shop", "unknown"))
59
 
60
  return format_parsed_items(result["items"]), result["items"]
61
 
@@ -64,55 +117,58 @@ def parse_receipt_gradio(image):
64
 
65
 
66
  def generate_recipe_with_audio(character, cuisine, use_expiring):
67
- conn = sqlite3.connect(
68
- "/data/pantry.db" if os.path.exists("/data") else "db/pantry.db"
69
- )
70
- c = conn.cursor()
71
- c.execute(
72
- "SELECT item_name, quantity, estimated_expiry FROM pantry WHERE used=0 ORDER BY estimated_expiry ASC"
73
- )
74
- rows = c.fetchall()
75
- conn.close()
76
 
77
- if not rows:
78
- return "Your pantry is empty — scan a receipt first.", None
79
 
80
- all_items = [{"name": r[0], "quantity": r[1], "expiry": r[2]} for r in rows]
81
 
82
- for item in all_items:
83
- _, days = expiry_status(item["expiry"])
84
- item["days_left"] = days
85
 
86
- expiring = [i for i in all_items if i["days_left"] <= 5]
87
- expiring_names = [i["name"] for i in expiring]
88
 
89
- if not expiring_names:
90
- expiring_names = [all_items[0]["name"]] if all_items else []
91
 
92
- dialogue_fn = modal.Function.from_name("zerowastekitchen", "generate_dialogue")
93
- recipe_fn = modal.Function.from_name("zerowastekitchen", "generate_recipe_llm")
94
- tts_fn = modal.Function.from_name("zerowastekitchen", "text_to_speech")
95
 
96
- with concurrent.futures.ThreadPoolExecutor() as executor:
97
- dialogue_future = executor.submit(
98
- dialogue_fn.remote, character, expiring_names[:5], cuisine
99
- )
100
- recipe_future = executor.submit(
101
- recipe_fn.remote, all_items, expiring, cuisine
102
- )
103
- dialogue = dialogue_future.result()
104
- recipe = recipe_future.result()
 
 
105
 
106
- audio_bytes = tts_fn.remote(dialogue, character)
 
 
107
 
108
- emoji = CHAR_EMOJI.get(character, "🧓")
109
- output = f"{emoji} {character} says:\n\"{dialogue}\"\n\n"
110
- output += recipe
111
 
112
- buf = io.BytesIO(audio_bytes)
113
- audio_np, sr = sf.read(buf)
114
 
115
- return output, (sr, audio_np)
 
116
 
117
 
118
  with gr.Blocks(title="ZeroWasteKitchen", theme=gr.themes.Soft()) as app:
@@ -153,7 +209,7 @@ with gr.Blocks(title="ZeroWasteKitchen", theme=gr.themes.Soft()) as app:
153
  label="Pantry status",
154
  lines=15,
155
  interactive=False,
156
- value=get_pantry()
157
  )
158
  with gr.Row():
159
  mark_item = gr.Textbox(
@@ -161,9 +217,9 @@ with gr.Blocks(title="ZeroWasteKitchen", theme=gr.themes.Soft()) as app:
161
  placeholder="e.g. CURRIS LEAVES PACKET"
162
  )
163
  mark_btn = gr.Button("Mark as used", size="sm")
164
- refresh_btn.click(fn=get_pantry, outputs=[pantry_output])
165
  mark_btn.click(
166
- fn=mark_used,
167
  inputs=[mark_item],
168
  outputs=[pantry_output]
169
  )
 
11
 
12
  sys.path.append(os.path.dirname(__file__))
13
  from utils import (
14
+ estimate_expiry, format_parsed_items,
15
+ save_to_db, expiry_status, init_db
16
  )
17
 
18
+ # Use HF Spaces persistent storage if available
19
+ DB_PATH = "/data/pantry.db" if os.path.exists("/data") else "db/pantry.db"
20
+
21
  CHARACTERS = {
22
+ "Grandma (Ammamma)": "Warm loving grandma who hates food waste.",
23
+ "Chef": "Sharp professional chef. Direct and brilliant.",
24
+ "Fitness Coach": "Enthusiastic fitness coach obsessed with gains.",
25
+ "Food Critic": "Pompous but secretly warm food critic.",
26
  }
27
 
28
  CUISINES = ["South Indian", "North Indian", "Italian", "Mediterranean", "East Asian", "Surprise me"]
 
35
  }
36
 
37
 
38
+ def get_pantry_display():
39
+ init_db(DB_PATH)
40
+ conn = sqlite3.connect(DB_PATH)
41
+ c = conn.cursor()
42
+ c.execute("SELECT item_name, quantity, estimated_expiry FROM pantry WHERE used=0 ORDER BY estimated_expiry ASC")
43
+ rows = c.fetchall()
44
+ conn.close()
45
+
46
+ if not rows:
47
+ return "Your pantry is empty — scan a receipt to get started."
48
+
49
+ red = [r for r in rows if expiry_status(r[2])[1] <= 1]
50
+ amber = [r for r in rows if 1 < expiry_status(r[2])[1] <= 5]
51
+ green = [r for r in rows if expiry_status(r[2])[1] > 5]
52
+
53
+ lines = []
54
+ lines.append(f"📦 {len(rows)} items in pantry")
55
+ lines.append(f"🔴 {len(red)} expiring today 🟡 {len(amber)} expiring soon 🟢 {len(green)} fine")
56
+ lines.append("─" * 40)
57
+
58
+ if red:
59
+ lines.append("\n🔴 COOK TODAY:")
60
+ for r in red:
61
+ lines.append(f" • {r[0]} (x{r[1]})")
62
+
63
+ if amber:
64
+ lines.append("\n🟡 USE THIS WEEK:")
65
+ for r in amber:
66
+ _, days = expiry_status(r[2])
67
+ lines.append(f" • {r[0]} (x{r[1]}) — {days} days left")
68
+
69
+ if green:
70
+ lines.append("\n🟢 ALL GOOD:")
71
+ for r in green:
72
+ _, days = expiry_status(r[2])
73
+ lines.append(f" • {r[0]} (x{r[1]}) — {days} days")
74
+
75
+ return "\n".join(lines)
76
+
77
+
78
+ def mark_used_local(item_name):
79
+ init_db(DB_PATH)
80
+ conn = sqlite3.connect(DB_PATH)
81
+ c = conn.cursor()
82
+ c.execute("UPDATE pantry SET used=1 WHERE item_name=? AND used=0", (item_name,))
83
+ conn.commit()
84
+ conn.close()
85
+ return get_pantry_display()
86
+
87
+
88
  def parse_receipt_gradio(image):
89
  if image is None:
90
  return "Please upload a receipt image.", []
 
108
  estimate_expiry(item["name"])
109
  )
110
 
111
+ save_to_db(result["items"], result.get("shop", "unknown"), DB_PATH)
112
 
113
  return format_parsed_items(result["items"]), result["items"]
114
 
 
117
 
118
 
119
  def generate_recipe_with_audio(character, cuisine, use_expiring):
120
+ try:
121
+ init_db(DB_PATH)
122
+ conn = sqlite3.connect(DB_PATH)
123
+ c = conn.cursor()
124
+ c.execute(
125
+ "SELECT item_name, quantity, estimated_expiry FROM pantry WHERE used=0 ORDER BY estimated_expiry ASC"
126
+ )
127
+ rows = c.fetchall()
128
+ conn.close()
129
 
130
+ if not rows:
131
+ return "Your pantry is empty — scan a receipt first.", None
132
 
133
+ all_items = [{"name": r[0], "quantity": r[1], "expiry": r[2]} for r in rows]
134
 
135
+ for item in all_items:
136
+ _, days = expiry_status(item["expiry"])
137
+ item["days_left"] = days
138
 
139
+ expiring = [i for i in all_items if i["days_left"] <= 5]
140
+ expiring_names = [i["name"] for i in expiring]
141
 
142
+ if not expiring_names:
143
+ expiring_names = [all_items[0]["name"]] if all_items else []
144
 
145
+ dialogue_fn = modal.Function.from_name("zerowastekitchen", "generate_dialogue")
146
+ recipe_fn = modal.Function.from_name("zerowastekitchen", "generate_recipe_llm")
147
+ tts_fn = modal.Function.from_name("zerowastekitchen", "text_to_speech")
148
 
149
+ with concurrent.futures.ThreadPoolExecutor() as executor:
150
+ dialogue_future = executor.submit(
151
+ dialogue_fn.remote, character, expiring_names[:5], cuisine
152
+ )
153
+ recipe_future = executor.submit(
154
+ recipe_fn.remote, all_items, expiring, cuisine
155
+ )
156
+ dialogue = dialogue_future.result()
157
+ recipe = recipe_future.result()
158
+
159
+ audio_bytes = tts_fn.remote(dialogue, character)
160
 
161
+ emoji = CHAR_EMOJI.get(character, "🧓")
162
+ output = f"{emoji} {character} says:\n\"{dialogue}\"\n\n"
163
+ output += recipe
164
 
165
+ buf = io.BytesIO(audio_bytes)
166
+ audio_np, sr = sf.read(buf)
 
167
 
168
+ return output, (sr, audio_np)
 
169
 
170
+ except Exception as e:
171
+ return f"Error: {str(e)}", None
172
 
173
 
174
  with gr.Blocks(title="ZeroWasteKitchen", theme=gr.themes.Soft()) as app:
 
209
  label="Pantry status",
210
  lines=15,
211
  interactive=False,
212
+ value=get_pantry_display()
213
  )
214
  with gr.Row():
215
  mark_item = gr.Textbox(
 
217
  placeholder="e.g. CURRIS LEAVES PACKET"
218
  )
219
  mark_btn = gr.Button("Mark as used", size="sm")
220
+ refresh_btn.click(fn=get_pantry_display, outputs=[pantry_output])
221
  mark_btn.click(
222
+ fn=mark_used_local,
223
  inputs=[mark_item],
224
  outputs=[pantry_output]
225
  )
modal_services.py CHANGED
@@ -2,37 +2,24 @@ import modal
2
  from modal import Volume
3
  from pathlib import Path
4
 
5
- # App definition
6
  app = modal.App("zerowastekitchen")
7
 
8
- # Volume for model storage
9
  model_volume = Volume.from_name("model-cache", create_if_missing=True)
10
  MODEL_DIR = Path("/models")
11
 
12
- # Download image - lightweight, just needs huggingface_hub
13
  download_image = (
14
  modal.Image.debian_slim()
15
  .pip_install("huggingface_hub", "hf_transfer")
16
  .env({"HF_HUB_ENABLE_HF_TRANSFER": "1"})
17
  )
18
 
19
- # Base image for OCR, expiry, dialogue, recipe
20
  base_image = (
21
  modal.Image.debian_slim()
22
  .pip_install(
23
- "torch",
24
- "torchvision",
25
- "transformers>=5.7.0",
26
- "accelerate",
27
- "pillow",
28
- "einops",
29
- "sentencepiece",
30
- "timm",
31
- "open_clip_torch",
32
- "av",
33
- "numpy",
34
- "huggingface_hub",
35
- "hf_transfer"
36
  )
37
  .env({"HF_HUB_ENABLE_HF_TRANSFER": "1"})
38
  )
@@ -51,7 +38,7 @@ tts_image = (
51
  )
52
  )
53
 
54
- # ── 0. DOWNLOAD MODELS ───────────────────────────────────────
55
  @app.function(
56
  image=download_image,
57
  volumes={str(MODEL_DIR): model_volume},
@@ -69,33 +56,22 @@ def download_models():
69
  local_dir=str(MODEL_DIR / "minicpm-v"),
70
  token=token
71
  )
72
-
73
  print("Downloading tiny-aya-fire...")
74
  snapshot_download(
75
  "CohereLabs/tiny-aya-fire",
76
  local_dir=str(MODEL_DIR / "tiny-aya-fire"),
77
  token=token
78
  )
79
-
80
  print("Downloading Nemotron 4B...")
81
  snapshot_download(
82
  "nvidia/Nemotron-Mini-4B-Instruct",
83
  local_dir=str(MODEL_DIR / "nemotron-4b"),
84
  token=token
85
  )
86
-
87
- print("Downloading Magpie TTS...")
88
- snapshot_download(
89
- "nvidia/magpie_tts_multilingual_357m",
90
- local_dir=str(MODEL_DIR / "magpie-tts"),
91
- token=token
92
- )
93
-
94
  model_volume.commit()
95
  print("All models downloaded.")
96
 
97
 
98
- # ── 1. RECEIPT OCR ──────────────────────────────────────────
99
  @app.function(
100
  gpu="T4",
101
  image=base_image,
@@ -175,7 +151,6 @@ Return as JSON only, no other text:
175
  return {"shop": "unknown", "items": [], "raw": str(e)}
176
 
177
 
178
- # ── 2. EXPIRY ESTIMATION ─────────────────────────────────────
179
  @app.function(
180
  gpu="T4",
181
  image=base_image,
@@ -231,7 +206,6 @@ Reply with a single integer only. No explanation.
231
  return expiry_map
232
 
233
 
234
- # ── 3. CHARACTER DIALOGUE ────────────────────────────────────
235
  @app.function(
236
  gpu="T4",
237
  image=base_image,
@@ -242,6 +216,7 @@ Reply with a single integer only. No explanation.
242
  )
243
  def generate_dialogue(character: str, expiring_items: list, cuisine: str) -> str:
244
  import torch
 
245
  from transformers import AutoTokenizer, AutoModelForCausalLM
246
 
247
  tokenizer = AutoTokenizer.from_pretrained(str(MODEL_DIR / "tiny-aya-fire"))
@@ -252,11 +227,13 @@ def generate_dialogue(character: str, expiring_items: list, cuisine: str) -> str
252
  )
253
 
254
  character_prompts = {
255
- "Grandma (Ammamma)": """You are Ammamma, a warm but dramatic grandma who loves cooking.
256
- Speak in English only. Scold gently about food waste but show love. 2-3 sentences only. Be concise.""",
257
- "Chef": "You are a sharp professional chef. Be direct and impatient but brilliant. 2-3 sentences only.",
258
- "Fitness Coach": "You are an enthusiastic fitness coach obsessed with gains and clean eating. 2-3 sentences only.",
259
- "Food Critic": "You are a pompous food critic who is secretly warm. Be dramatic. 2-3 sentences only.",
 
 
260
  }
261
 
262
  system = character_prompts.get(character, character_prompts["Grandma (Ammamma)"])
@@ -267,7 +244,7 @@ def generate_dialogue(character: str, expiring_items: list, cuisine: str) -> str
267
  <|user|>
268
  These grocery items are expiring soon: {items_str}
269
  We are cooking {cuisine} food today.
270
- Give your in-character reaction about the expiring food. 2-3 sentences maximum.
271
  <|assistant|>
272
  """
273
 
@@ -287,15 +264,25 @@ Give your in-character reaction about the expiring food. 2-3 sentences maximum.
287
  skip_special_tokens=True
288
  ).strip()
289
 
290
- # Clean up leaked system prompt
291
  response = response.split("<|system|>")[0].strip()
292
  response = response.split("system|>")[0].strip()
 
 
293
  response = response.split("<|")[0].strip()
294
  response = response.split("##")[0].strip()
 
 
 
 
 
 
 
 
 
295
  return response
296
 
297
 
298
- # ── 4. RECIPE GENERATION ─────────────────────────────────────
299
  @app.function(
300
  gpu="T4",
301
  image=base_image,
@@ -343,7 +330,7 @@ STEPS:
343
  3. ...
344
  4. ...
345
  5. ...
346
- IMPORTANT: Maximum 5 steps. Stop here. Do not write more than 5 steps.
347
  <|assistant|>
348
  """
349
 
@@ -363,7 +350,6 @@ IMPORTANT: Maximum 5 steps. Stop here. Do not write more than 5 steps.
363
  skip_special_tokens=True
364
  ).strip()
365
 
366
- response = response.split("<|")[0].strip()
367
  response = response.split("##")[0].strip()
368
 
369
  # Force truncate to 5 steps
@@ -377,11 +363,10 @@ IMPORTANT: Maximum 5 steps. Stop here. Do not write more than 5 steps.
377
  if step_count >= 5:
378
  break
379
 
380
- response = "\n".join(truncated)
381
  return response
382
 
383
 
384
- # ── 5. TEXT TO SPEECH ────────────────────────────────────────
385
  @app.function(
386
  gpu="T4",
387
  image=tts_image,
@@ -393,13 +378,13 @@ IMPORTANT: Maximum 5 steps. Stop here. Do not write more than 5 steps.
393
  def text_to_speech(text: str, character: str) -> bytes:
394
  import torch
395
  import io
396
- import soundfile as sf
397
  import os
 
398
 
399
- print("Downloading XTTS-v2...")
400
  os.environ["COQUI_TOS_AGREED"] = "1"
401
  from TTS.api import TTS
402
- tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2")
 
403
 
404
  speaker_map = {
405
  "Grandma (Ammamma)": "Claribel Dervla",
@@ -409,14 +394,21 @@ def text_to_speech(text: str, character: str) -> bytes:
409
  }
410
  speaker = speaker_map.get(character, "Claribel Dervla")
411
 
412
- # Keep short
413
  sentences = text.replace("!", ".").replace("?", ".").split(".")
414
- short_text = ". ".join([s.strip() for s in sentences[:2] if s.strip()]) + "."
 
 
 
 
 
 
 
415
 
416
  wav = tts.tts(
417
- text=short_text,
418
- speaker=speaker,
419
- language="en"
420
  )
421
 
422
  buf = io.BytesIO()
@@ -426,16 +418,12 @@ def text_to_speech(text: str, character: str) -> bytes:
426
 
427
  @app.local_entrypoint()
428
  def main():
429
- print("Downloading all models to volume...")
430
- download_models.remote()
431
- print("Done.")
432
- # Test expiry estimation
433
  test_items = ["INDIA WALA PANEER", "CURRIS LEAVES PACKET", "BASMATI RICE"]
 
434
  print("Testing expiry estimation...")
435
  expiry_map = estimate_expiry_llm.remote(test_items)
436
  print(expiry_map)
437
 
438
- # Test dialogue
439
  print("\nTesting Ammamma dialogue...")
440
  dialogue = generate_dialogue.remote(
441
  "Grandma (Ammamma)",
@@ -444,14 +432,12 @@ def main():
444
  )
445
  print(dialogue)
446
 
447
- # Test recipe
448
  print("\nTesting recipe generation...")
449
  all_items = [{"name": i} for i in test_items]
450
  expiring = [{"name": i} for i in test_items[:2]]
451
  recipe = generate_recipe_llm.remote(all_items, expiring, "South Indian")
452
  print(recipe)
453
 
454
- # Test TTS
455
  print("\nTesting TTS...")
456
  audio_bytes = text_to_speech.remote(dialogue, "Grandma (Ammamma)")
457
  with open("test_audio.wav", "wb") as f:
 
2
  from modal import Volume
3
  from pathlib import Path
4
 
 
5
  app = modal.App("zerowastekitchen")
6
 
 
7
  model_volume = Volume.from_name("model-cache", create_if_missing=True)
8
  MODEL_DIR = Path("/models")
9
 
 
10
  download_image = (
11
  modal.Image.debian_slim()
12
  .pip_install("huggingface_hub", "hf_transfer")
13
  .env({"HF_HUB_ENABLE_HF_TRANSFER": "1"})
14
  )
15
 
 
16
  base_image = (
17
  modal.Image.debian_slim()
18
  .pip_install(
19
+ "torch", "torchvision", "transformers>=5.7.0",
20
+ "accelerate", "pillow", "einops", "sentencepiece",
21
+ "timm", "open_clip_torch", "av", "numpy",
22
+ "huggingface_hub", "hf_transfer"
 
 
 
 
 
 
 
 
 
23
  )
24
  .env({"HF_HUB_ENABLE_HF_TRANSFER": "1"})
25
  )
 
38
  )
39
  )
40
 
41
+
42
  @app.function(
43
  image=download_image,
44
  volumes={str(MODEL_DIR): model_volume},
 
56
  local_dir=str(MODEL_DIR / "minicpm-v"),
57
  token=token
58
  )
 
59
  print("Downloading tiny-aya-fire...")
60
  snapshot_download(
61
  "CohereLabs/tiny-aya-fire",
62
  local_dir=str(MODEL_DIR / "tiny-aya-fire"),
63
  token=token
64
  )
 
65
  print("Downloading Nemotron 4B...")
66
  snapshot_download(
67
  "nvidia/Nemotron-Mini-4B-Instruct",
68
  local_dir=str(MODEL_DIR / "nemotron-4b"),
69
  token=token
70
  )
 
 
 
 
 
 
 
 
71
  model_volume.commit()
72
  print("All models downloaded.")
73
 
74
 
 
75
  @app.function(
76
  gpu="T4",
77
  image=base_image,
 
151
  return {"shop": "unknown", "items": [], "raw": str(e)}
152
 
153
 
 
154
  @app.function(
155
  gpu="T4",
156
  image=base_image,
 
206
  return expiry_map
207
 
208
 
 
209
  @app.function(
210
  gpu="T4",
211
  image=base_image,
 
216
  )
217
  def generate_dialogue(character: str, expiring_items: list, cuisine: str) -> str:
218
  import torch
219
+ import re
220
  from transformers import AutoTokenizer, AutoModelForCausalLM
221
 
222
  tokenizer = AutoTokenizer.from_pretrained(str(MODEL_DIR / "tiny-aya-fire"))
 
227
  )
228
 
229
  character_prompts = {
230
+ "Grandma (Ammamma)": """You are Ammamma, a warm loving grandma who hates food waste.
231
+ Speak warmly in English. Mention the specific expiring items by name.
232
+ Express urgency about using them before they go bad.
233
+ Show love and excitement about cooking. 2-3 sentences only. Be specific and concise.""",
234
+ "Chef": "You are a sharp professional chef. Be direct and impatient but brilliant. Mention the expiring items. 2-3 sentences only.",
235
+ "Fitness Coach": "You are an enthusiastic fitness coach obsessed with gains and clean eating. Mention the expiring items. 2-3 sentences only.",
236
+ "Food Critic": "You are a pompous food critic who is secretly warm. Be dramatic about the expiring items. 2-3 sentences only.",
237
  }
238
 
239
  system = character_prompts.get(character, character_prompts["Grandma (Ammamma)"])
 
244
  <|user|>
245
  These grocery items are expiring soon: {items_str}
246
  We are cooking {cuisine} food today.
247
+ Give your in-character reaction. 2-3 sentences maximum.
248
  <|assistant|>
249
  """
250
 
 
264
  skip_special_tokens=True
265
  ).strip()
266
 
267
+ # Clean up leaked content
268
  response = response.split("<|system|>")[0].strip()
269
  response = response.split("system|>")[0].strip()
270
+ response = response.split("<|user|>")[0].strip()
271
+ response = response.split("user|>")[0].strip()
272
  response = response.split("<|")[0].strip()
273
  response = response.split("##")[0].strip()
274
+ response = response.split("Can you")[0].strip()
275
+ response = response.split("Please")[0].strip()
276
+ response = response.split("Ammamma,")[0].strip()
277
+ response = re.sub(r'#\w*', '', response).strip()
278
+
279
+ # Limit to 3 sentences
280
+ sentences = [s.strip() for s in response.split(".") if s.strip()]
281
+ response = ". ".join(sentences[:3]) + "."
282
+
283
  return response
284
 
285
 
 
286
  @app.function(
287
  gpu="T4",
288
  image=base_image,
 
330
  3. ...
331
  4. ...
332
  5. ...
333
+ Stop here. Do not write more than 5 steps.
334
  <|assistant|>
335
  """
336
 
 
350
  skip_special_tokens=True
351
  ).strip()
352
 
 
353
  response = response.split("##")[0].strip()
354
 
355
  # Force truncate to 5 steps
 
363
  if step_count >= 5:
364
  break
365
 
366
+ response = "\n".join(truncated)
367
  return response
368
 
369
 
 
370
  @app.function(
371
  gpu="T4",
372
  image=tts_image,
 
378
  def text_to_speech(text: str, character: str) -> bytes:
379
  import torch
380
  import io
 
381
  import os
382
+ import soundfile as sf
383
 
 
384
  os.environ["COQUI_TOS_AGREED"] = "1"
385
  from TTS.api import TTS
386
+
387
+ tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2")
388
 
389
  speaker_map = {
390
  "Grandma (Ammamma)": "Claribel Dervla",
 
394
  }
395
  speaker = speaker_map.get(character, "Claribel Dervla")
396
 
397
+ # Deduplicate and limit sentences
398
  sentences = text.replace("!", ".").replace("?", ".").split(".")
399
+ seen = []
400
+ for s in sentences:
401
+ s = s.strip()
402
+ if s and s not in seen:
403
+ seen.append(s)
404
+ if len(seen) >= 3:
405
+ break
406
+ short_text = ". ".join(seen) + "."
407
 
408
  wav = tts.tts(
409
+ text=short_text,
410
+ speaker=speaker,
411
+ language="en"
412
  )
413
 
414
  buf = io.BytesIO()
 
418
 
419
  @app.local_entrypoint()
420
  def main():
 
 
 
 
421
  test_items = ["INDIA WALA PANEER", "CURRIS LEAVES PACKET", "BASMATI RICE"]
422
+
423
  print("Testing expiry estimation...")
424
  expiry_map = estimate_expiry_llm.remote(test_items)
425
  print(expiry_map)
426
 
 
427
  print("\nTesting Ammamma dialogue...")
428
  dialogue = generate_dialogue.remote(
429
  "Grandma (Ammamma)",
 
432
  )
433
  print(dialogue)
434
 
 
435
  print("\nTesting recipe generation...")
436
  all_items = [{"name": i} for i in test_items]
437
  expiring = [{"name": i} for i in test_items[:2]]
438
  recipe = generate_recipe_llm.remote(all_items, expiring, "South Indian")
439
  print(recipe)
440
 
 
441
  print("\nTesting TTS...")
442
  audio_bytes = text_to_speech.remote(dialogue, "Grandma (Ammamma)")
443
  with open("test_audio.wav", "wb") as f:
utils.py CHANGED
@@ -51,9 +51,9 @@ def format_parsed_items(items):
51
  lines.append(f"{emoji} {item['name']} (qty: {item.get('quantity','1')}) — {label}")
52
  return "\n".join(lines)
53
 
54
- def init_db():
55
- os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) if os.path.dirname(DB_PATH) else None
56
- conn = sqlite3.connect(DB_PATH)
57
  c = conn.cursor()
58
  c.execute('''CREATE TABLE IF NOT EXISTS pantry (
59
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -68,13 +68,11 @@ def init_db():
68
  conn.commit()
69
  conn.close()
70
 
71
- def save_to_db(items: list, shop: str):
72
- init_db()
73
- conn = sqlite3.connect(DB_PATH)
74
  c = conn.cursor()
75
  for item in items:
76
- # If item exists, update quantity and expiry
77
- # If not, insert new row
78
  c.execute('''
79
  INSERT INTO pantry (item_name, quantity, price, purchase_date, estimated_expiry, shop)
80
  VALUES (?, ?, ?, ?, ?, ?)
@@ -82,8 +80,7 @@ def save_to_db(items: list, shop: str):
82
  quantity = quantity || '+' || excluded.quantity,
83
  estimated_expiry = excluded.estimated_expiry,
84
  purchase_date = excluded.purchase_date
85
- ''',
86
- (
87
  item["name"],
88
  item.get("quantity", "1"),
89
  item.get("price", ""),
 
51
  lines.append(f"{emoji} {item['name']} (qty: {item.get('quantity','1')}) — {label}")
52
  return "\n".join(lines)
53
 
54
+ def init_db(db_path=DB_PATH):
55
+ os.makedirs(os.path.dirname(db_path) if os.path.dirname(db_path) else ".", exist_ok=True)
56
+ conn = sqlite3.connect(db_path)
57
  c = conn.cursor()
58
  c.execute('''CREATE TABLE IF NOT EXISTS pantry (
59
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 
68
  conn.commit()
69
  conn.close()
70
 
71
+ def save_to_db(items: list, shop: str, db_path=DB_PATH):
72
+ init_db(db_path)
73
+ conn = sqlite3.connect(db_path)
74
  c = conn.cursor()
75
  for item in items:
 
 
76
  c.execute('''
77
  INSERT INTO pantry (item_name, quantity, price, purchase_date, estimated_expiry, shop)
78
  VALUES (?, ?, ?, ?, ?, ?)
 
80
  quantity = quantity || '+' || excluded.quantity,
81
  estimated_expiry = excluded.estimated_expiry,
82
  purchase_date = excluded.purchase_date
83
+ ''', (
 
84
  item["name"],
85
  item.get("quantity", "1"),
86
  item.get("price", ""),