Commit ·
4864b5a
1
Parent(s): 09a7abe
feat: receipt OCR working - MiniCPM-V 4.6 parsing real receipts
Browse files- __pycache__/modal_services.cpython-312.pyc +0 -0
- __pycache__/test_modal.cpython-312.pyc +0 -0
- app.py +246 -0
- modal_services.py +104 -0
- test_modal.py +19 -0
__pycache__/modal_services.cpython-312.pyc
ADDED
|
Binary file (4.19 kB). View file
|
|
|
__pycache__/test_modal.cpython-312.pyc
ADDED
|
Binary file (1.3 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import sqlite3
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
|
| 7 |
+
DB_PATH = "db/pantry.db"
|
| 8 |
+
|
| 9 |
+
def init_db():
|
| 10 |
+
os.makedirs("db", exist_ok=True)
|
| 11 |
+
conn = sqlite3.connect(DB_PATH)
|
| 12 |
+
c = conn.cursor()
|
| 13 |
+
c.execute('''CREATE TABLE IF NOT EXISTS pantry (
|
| 14 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 15 |
+
item_name TEXT,
|
| 16 |
+
quantity TEXT,
|
| 17 |
+
purchase_date TEXT,
|
| 18 |
+
estimated_expiry TEXT,
|
| 19 |
+
category TEXT,
|
| 20 |
+
used INTEGER DEFAULT 0
|
| 21 |
+
)''')
|
| 22 |
+
conn.commit()
|
| 23 |
+
conn.close()
|
| 24 |
+
|
| 25 |
+
SHELF_LIFE = {
|
| 26 |
+
"spinach": 5, "baby spinach": 5, "tomatoes": 7, "cherry tomatoes": 7,
|
| 27 |
+
"yoghurt": 14, "greek yoghurt": 14, "milk": 7, "eggs": 21,
|
| 28 |
+
"chicken": 2, "basmati rice": 180, "rice": 180, "lentils": 365,
|
| 29 |
+
"coconut milk": 365, "butter": 30, "cheese": 21, "bread": 7,
|
| 30 |
+
"onion": 30, "garlic": 60, "potato": 30, "carrot": 21,
|
| 31 |
+
"cucumber": 7, "pepper": 7, "courgette": 7, "mushroom": 5,
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
def estimate_expiry(item_name):
|
| 35 |
+
name_lower = item_name.lower()
|
| 36 |
+
for key, days in SHELF_LIFE.items():
|
| 37 |
+
if key in name_lower:
|
| 38 |
+
expiry = datetime.now() + timedelta(days=days)
|
| 39 |
+
return expiry.strftime("%Y-%m-%d")
|
| 40 |
+
expiry = datetime.now() + timedelta(days=7)
|
| 41 |
+
return expiry.strftime("%Y-%m-%d")
|
| 42 |
+
|
| 43 |
+
def expiry_status(expiry_str):
|
| 44 |
+
try:
|
| 45 |
+
expiry = datetime.strptime(expiry_str, "%Y-%m-%d")
|
| 46 |
+
days_left = (expiry - datetime.now()).days
|
| 47 |
+
if days_left <= 1:
|
| 48 |
+
return "🔴", days_left
|
| 49 |
+
elif days_left <= 5:
|
| 50 |
+
return "🟡", days_left
|
| 51 |
+
else:
|
| 52 |
+
return "🟢", days_left
|
| 53 |
+
except:
|
| 54 |
+
return "🟢", 999
|
| 55 |
+
|
| 56 |
+
DUMMY_ITEMS = [
|
| 57 |
+
{"name": "Baby spinach 200g", "qty": "1", "expiry": (datetime.now() + timedelta(days=0)).strftime("%Y-%m-%d")},
|
| 58 |
+
{"name": "Cherry tomatoes 400g", "qty": "1", "expiry": (datetime.now() + timedelta(days=3)).strftime("%Y-%m-%d")},
|
| 59 |
+
{"name": "Greek yoghurt 500g", "qty": "1", "expiry": (datetime.now() + timedelta(days=5)).strftime("%Y-%m-%d")},
|
| 60 |
+
{"name": "Basmati rice 1kg", "qty": "1", "expiry": (datetime.now() + timedelta(days=180)).strftime("%Y-%m-%d")},
|
| 61 |
+
{"name": "Coconut milk 400ml", "qty": "2", "expiry": (datetime.now() + timedelta(days=365)).strftime("%Y-%m-%d")},
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
CHARACTERS = {
|
| 65 |
+
"Grandma (Paati)": "You are Paati, a warm but judgemental South Indian grandma. You speak in a mix of English with occasional Tamil words. You scold gently about food waste, express love through feeding people, and always know the best traditional recipe for any ingredient.",
|
| 66 |
+
"Chef": "You are a sharp, no-nonsense professional chef. You are direct, technically precise, slightly impatient with bad ingredients, but brilliant at making something out of nothing.",
|
| 67 |
+
"Fitness Coach": "You are an enthusiastic fitness coach. Every recipe must be high protein, low waste, macro-balanced. You relate everything back to gains, recovery, and clean eating.",
|
| 68 |
+
"Food Critic": "You are a pompous but secretly warm food critic. You critique the ingredients dramatically before reluctantly producing a brilliant recipe.",
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
CUISINES = ["South Indian", "North Indian", "Italian", "Mediterranean", "East Asian", "Surprise me"]
|
| 72 |
+
|
| 73 |
+
def parse_receipt(image):
|
| 74 |
+
if image is None:
|
| 75 |
+
return format_parsed_items(DUMMY_ITEMS), DUMMY_ITEMS
|
| 76 |
+
return format_parsed_items(DUMMY_ITEMS), DUMMY_ITEMS
|
| 77 |
+
|
| 78 |
+
def format_parsed_items(items):
|
| 79 |
+
lines = []
|
| 80 |
+
for item in items:
|
| 81 |
+
emoji, days = expiry_status(item["expiry"])
|
| 82 |
+
if days <= 1:
|
| 83 |
+
label = "expires today"
|
| 84 |
+
elif days <= 5:
|
| 85 |
+
label = f"expires in {days} days"
|
| 86 |
+
else:
|
| 87 |
+
label = f"expires in {days} days"
|
| 88 |
+
lines.append(f"{emoji} {item['name']} (qty: {item['qty']}) — {label}")
|
| 89 |
+
return "\n".join(lines)
|
| 90 |
+
|
| 91 |
+
def save_to_pantry(items_state):
|
| 92 |
+
init_db()
|
| 93 |
+
conn = sqlite3.connect(DB_PATH)
|
| 94 |
+
c = conn.cursor()
|
| 95 |
+
for item in items_state:
|
| 96 |
+
c.execute('''INSERT INTO pantry (item_name, quantity, purchase_date, estimated_expiry, category)
|
| 97 |
+
VALUES (?, ?, ?, ?, ?)''',
|
| 98 |
+
(item["name"], item["qty"], datetime.now().strftime("%Y-%m-%d"),
|
| 99 |
+
item["expiry"], "general"))
|
| 100 |
+
conn.commit()
|
| 101 |
+
conn.close()
|
| 102 |
+
return "Saved to pantry"
|
| 103 |
+
|
| 104 |
+
def get_pantry():
|
| 105 |
+
init_db()
|
| 106 |
+
conn = sqlite3.connect(DB_PATH)
|
| 107 |
+
c = conn.cursor()
|
| 108 |
+
c.execute("SELECT id, item_name, quantity, estimated_expiry FROM pantry WHERE used=0 ORDER BY estimated_expiry ASC")
|
| 109 |
+
rows = c.fetchall()
|
| 110 |
+
conn.close()
|
| 111 |
+
if not rows:
|
| 112 |
+
return "Your pantry is empty — scan a receipt to get started."
|
| 113 |
+
lines = []
|
| 114 |
+
expiring_today = 0
|
| 115 |
+
for row in rows:
|
| 116 |
+
emoji, days = expiry_status(row[3])
|
| 117 |
+
if days <= 1:
|
| 118 |
+
expiring_today += 1
|
| 119 |
+
label = "TODAY"
|
| 120 |
+
elif days <= 5:
|
| 121 |
+
label = f"{days} days"
|
| 122 |
+
else:
|
| 123 |
+
label = f"{days} days"
|
| 124 |
+
lines.append(f"{emoji} {row[1]} (x{row[2]}) — {label}")
|
| 125 |
+
header = ""
|
| 126 |
+
if expiring_today > 0:
|
| 127 |
+
header = f"🚨 {expiring_today} item(s) expiring today — cook them now!\n\n"
|
| 128 |
+
return header + "\n".join(lines)
|
| 129 |
+
|
| 130 |
+
def mark_used(item_name):
|
| 131 |
+
init_db()
|
| 132 |
+
conn = sqlite3.connect(DB_PATH)
|
| 133 |
+
c = conn.cursor()
|
| 134 |
+
c.execute("UPDATE pantry SET used=1 WHERE item_name=? AND used=0", (item_name,))
|
| 135 |
+
conn.commit()
|
| 136 |
+
conn.close()
|
| 137 |
+
return get_pantry()
|
| 138 |
+
|
| 139 |
+
def generate_recipe(character, cuisine, use_expiring):
|
| 140 |
+
expiring = [i for i in DUMMY_ITEMS if expiry_status(i["expiry"])[1] <= 5]
|
| 141 |
+
all_items = DUMMY_ITEMS
|
| 142 |
+
items_to_use = expiring if use_expiring and expiring else all_items
|
| 143 |
+
|
| 144 |
+
char_voice = {
|
| 145 |
+
"Grandma (Paati)": "🧓 Paati says:\n\"Ayo! That spinach is dying and you haven't touched it? Shame on you! Come, I'll show you a proper keerai kootu.\"\n\n",
|
| 146 |
+
"Chef": "👨🍳 Chef says:\n\"Right. You've got spinach on its last legs and some tomatoes. We're making something. Pay attention.\"\n\n",
|
| 147 |
+
"Fitness Coach": "💪 Coach says:\n\"Perfect! Spinach is iron-rich, tomatoes are antioxidants. We're making a macro-friendly power bowl. Let's GO.\"\n\n",
|
| 148 |
+
"Food Critic": "⭐ Critic says:\n\"These ingredients are... humble. And yet. With sufficient technique, even the wilting spinach can achieve greatness.\"\n\n",
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
cuisine_recipes = {
|
| 152 |
+
"South Indian": {
|
| 153 |
+
"name": "Keerai kootu with coconut dal",
|
| 154 |
+
"ingredients": ["Baby spinach", "Cherry tomatoes", "Coconut milk", "Basmati rice", "Mustard seeds", "Curry leaves", "Turmeric"],
|
| 155 |
+
"steps": [
|
| 156 |
+
"Blanch spinach for 2 mins, drain and roughly chop.",
|
| 157 |
+
"Heat oil, temper mustard seeds and curry leaves.",
|
| 158 |
+
"Add tomatoes, cook till soft — about 5 mins.",
|
| 159 |
+
"Add coconut milk and turmeric, simmer 3 mins.",
|
| 160 |
+
"Fold in spinach, season with salt.",
|
| 161 |
+
"Serve over steamed basmati rice."
|
| 162 |
+
]
|
| 163 |
+
},
|
| 164 |
+
"Italian": {
|
| 165 |
+
"name": "Spinach and tomato pasta",
|
| 166 |
+
"ingredients": ["Baby spinach", "Cherry tomatoes", "Garlic", "Olive oil", "Pasta", "Parmesan"],
|
| 167 |
+
"steps": [
|
| 168 |
+
"Cook pasta al dente, reserve 1 cup pasta water.",
|
| 169 |
+
"Sauté garlic in olive oil until golden.",
|
| 170 |
+
"Add tomatoes, cook until they burst — 4 mins.",
|
| 171 |
+
"Wilt spinach in the pan — 1 min.",
|
| 172 |
+
"Toss pasta with vegetables and pasta water.",
|
| 173 |
+
"Finish with parmesan and black pepper."
|
| 174 |
+
]
|
| 175 |
+
},
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
recipe = cuisine_recipes.get(cuisine, cuisine_recipes["South Indian"])
|
| 179 |
+
voice = char_voice.get(character, char_voice["Grandma (Paati)"])
|
| 180 |
+
|
| 181 |
+
expiring_tags = [f"⚠️ {i['name']}" for i in items_to_use if expiry_status(i["expiry"])[1] <= 3]
|
| 182 |
+
|
| 183 |
+
output = voice
|
| 184 |
+
output += f"🍽️ Recipe: {recipe['name']}\n"
|
| 185 |
+
output += f"Cuisine: {cuisine}\n\n"
|
| 186 |
+
if expiring_tags:
|
| 187 |
+
output += f"Using before they go: {', '.join(expiring_tags)}\n\n"
|
| 188 |
+
output += "Ingredients:\n"
|
| 189 |
+
for ing in recipe["ingredients"]:
|
| 190 |
+
output += f" • {ing}\n"
|
| 191 |
+
output += "\nSteps:\n"
|
| 192 |
+
for i, step in enumerate(recipe["steps"], 1):
|
| 193 |
+
output += f" {i}. {step}\n"
|
| 194 |
+
output += "\n[🔊 Bark TTS voice coming soon]"
|
| 195 |
+
return output
|
| 196 |
+
|
| 197 |
+
init_db()
|
| 198 |
+
|
| 199 |
+
with gr.Blocks(title="ZeroWasteKitchen", theme=gr.themes.Soft()) as app:
|
| 200 |
+
items_state = gr.State(DUMMY_ITEMS)
|
| 201 |
+
|
| 202 |
+
gr.Markdown("# 🌿 ZeroWasteKitchen\n*Scan receipts · track expiry · cook with personality*")
|
| 203 |
+
|
| 204 |
+
with gr.Tabs():
|
| 205 |
+
|
| 206 |
+
with gr.Tab("1 · Upload receipt"):
|
| 207 |
+
gr.Markdown("### Upload your grocery receipt")
|
| 208 |
+
receipt_image = gr.Image(label="Receipt photo", type="pil", sources=["upload", "webcam"])
|
| 209 |
+
parse_btn = gr.Button("Parse receipt", variant="primary")
|
| 210 |
+
parsed_output = gr.Textbox(label="Parsed items", lines=8, interactive=False)
|
| 211 |
+
save_btn = gr.Button("Save to pantry →")
|
| 212 |
+
save_status = gr.Textbox(label="", interactive=False, max_lines=1)
|
| 213 |
+
|
| 214 |
+
parse_btn.click(fn=parse_receipt, inputs=[receipt_image], outputs=[parsed_output, items_state])
|
| 215 |
+
save_btn.click(fn=lambda s: save_to_pantry(s), inputs=[items_state], outputs=[save_status])
|
| 216 |
+
|
| 217 |
+
with gr.Tab("2 · My pantry"):
|
| 218 |
+
gr.Markdown("### Your current pantry")
|
| 219 |
+
refresh_btn = gr.Button("Refresh", size="sm")
|
| 220 |
+
pantry_output = gr.Textbox(label="Pantry status", lines=12, interactive=False, value=get_pantry())
|
| 221 |
+
with gr.Row():
|
| 222 |
+
mark_item = gr.Textbox(label="Item name to mark as used", placeholder="e.g. Baby spinach 200g")
|
| 223 |
+
mark_btn = gr.Button("Mark as used", size="sm")
|
| 224 |
+
refresh_btn.click(fn=get_pantry, outputs=[pantry_output])
|
| 225 |
+
mark_btn.click(fn=mark_used, inputs=[mark_item], outputs=[pantry_output])
|
| 226 |
+
|
| 227 |
+
with gr.Tab("3 · Cook"):
|
| 228 |
+
gr.Markdown("### Pick your character and cuisine")
|
| 229 |
+
with gr.Row():
|
| 230 |
+
character = gr.Dropdown(
|
| 231 |
+
choices=list(CHARACTERS.keys()),
|
| 232 |
+
value="Grandma (Paati)",
|
| 233 |
+
label="Character"
|
| 234 |
+
)
|
| 235 |
+
cuisine = gr.Dropdown(
|
| 236 |
+
choices=CUISINES,
|
| 237 |
+
value="South Indian",
|
| 238 |
+
label="Cuisine"
|
| 239 |
+
)
|
| 240 |
+
use_expiring = gr.Checkbox(label="Prioritise expiring items", value=True)
|
| 241 |
+
cook_btn = gr.Button("Cook with what I have", variant="primary")
|
| 242 |
+
recipe_output = gr.Textbox(label="Your recipe", lines=20, interactive=False)
|
| 243 |
+
cook_btn.click(fn=generate_recipe, inputs=[character, cuisine, use_expiring], outputs=[recipe_output])
|
| 244 |
+
|
| 245 |
+
if __name__ == "__main__":
|
| 246 |
+
app.launch()
|
modal_services.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import modal
|
| 2 |
+
|
| 3 |
+
app = modal.App("zerowastekitchen")
|
| 4 |
+
|
| 5 |
+
image = (
|
| 6 |
+
modal.Image.debian_slim()
|
| 7 |
+
.pip_install(
|
| 8 |
+
"torch",
|
| 9 |
+
"torchvision",
|
| 10 |
+
"numpy",
|
| 11 |
+
"transformers>=5.7.0",
|
| 12 |
+
"accelerate",
|
| 13 |
+
"pillow",
|
| 14 |
+
"av"
|
| 15 |
+
)
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
@app.function(gpu="T4", image=image, timeout=180, memory=12288,
|
| 19 |
+
secrets=[modal.Secret.from_name("huggingface-secret")])
|
| 20 |
+
def parse_receipt(image_bytes: bytes) -> dict:
|
| 21 |
+
import torch
|
| 22 |
+
from transformers import AutoModelForImageTextToText, AutoProcessor
|
| 23 |
+
from PIL import Image
|
| 24 |
+
import io
|
| 25 |
+
import json
|
| 26 |
+
|
| 27 |
+
model_id = "openbmb/MiniCPM-V-4.6"
|
| 28 |
+
processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
|
| 29 |
+
model = AutoModelForImageTextToText.from_pretrained(
|
| 30 |
+
model_id,
|
| 31 |
+
torch_dtype="auto",
|
| 32 |
+
device_map="auto",
|
| 33 |
+
trust_remote_code=True
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
|
| 37 |
+
|
| 38 |
+
prompt = """Extract all grocery items from this receipt.
|
| 39 |
+
Return as JSON only, no other text:
|
| 40 |
+
{
|
| 41 |
+
"shop": "shop name if visible",
|
| 42 |
+
"items": [
|
| 43 |
+
{"name": "item name", "quantity": "1", "price": "price if visible"}
|
| 44 |
+
]
|
| 45 |
+
}"""
|
| 46 |
+
|
| 47 |
+
messages = [
|
| 48 |
+
{
|
| 49 |
+
"role": "user",
|
| 50 |
+
"content": [
|
| 51 |
+
{"type": "image", "image": img},
|
| 52 |
+
{"type": "text", "text": prompt}
|
| 53 |
+
]
|
| 54 |
+
}
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
text = processor.apply_chat_template(
|
| 58 |
+
messages,
|
| 59 |
+
tokenize=False,
|
| 60 |
+
add_generation_prompt=True
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
inputs = processor(
|
| 64 |
+
text=text,
|
| 65 |
+
images=[img],
|
| 66 |
+
return_tensors="pt"
|
| 67 |
+
).to(model.device)
|
| 68 |
+
|
| 69 |
+
with torch.no_grad():
|
| 70 |
+
outputs = model.generate(**inputs, max_new_tokens=2048)
|
| 71 |
+
|
| 72 |
+
result = processor.decode(outputs[0], skip_special_tokens=True)
|
| 73 |
+
|
| 74 |
+
try:
|
| 75 |
+
result = result.split("</think>")[-1].strip()
|
| 76 |
+
json_start = result.find("{")
|
| 77 |
+
json_end = result.rfind("}") + 1
|
| 78 |
+
raw_json = result[json_start:json_end]
|
| 79 |
+
|
| 80 |
+
# Fix missing commas between objects
|
| 81 |
+
import re
|
| 82 |
+
raw_json = re.sub(r'}\s*{', '},{', raw_json)
|
| 83 |
+
|
| 84 |
+
parsed = json.loads(raw_json)
|
| 85 |
+
|
| 86 |
+
# Filter junk rows — keep only real grocery items
|
| 87 |
+
parsed["items"] = [
|
| 88 |
+
item for item in parsed.get("items", [])
|
| 89 |
+
if item.get("name", "").strip()
|
| 90 |
+
and item["name"] not in ["Total", "Total To Pay", "Card", "Promotion Discount", "General Discount", ""]
|
| 91 |
+
and not item["name"].startswith("D Credit")
|
| 92 |
+
]
|
| 93 |
+
|
| 94 |
+
return parsed
|
| 95 |
+
|
| 96 |
+
except Exception as e:
|
| 97 |
+
return {"shop": "unknown", "items": [], "raw": result}
|
| 98 |
+
|
| 99 |
+
@app.local_entrypoint()
|
| 100 |
+
def main():
|
| 101 |
+
with open("test_receipt.jpg", "rb") as f:
|
| 102 |
+
image_bytes = f.read()
|
| 103 |
+
result = parse_receipt.remote(image_bytes)
|
| 104 |
+
print(result)
|
test_modal.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import modal
|
| 2 |
+
|
| 3 |
+
app = modal.App("test-zerowastekitchen")
|
| 4 |
+
|
| 5 |
+
image = modal.Image.debian_slim().pip_install("torch", "numpy")
|
| 6 |
+
|
| 7 |
+
@app.function(gpu="T4", image=image)
|
| 8 |
+
def hello():
|
| 9 |
+
import torch
|
| 10 |
+
result = f"GPU available: {torch.cuda.is_available()}"
|
| 11 |
+
if torch.cuda.is_available():
|
| 12 |
+
result += f", Device: {torch.cuda.get_device_name(0)}"
|
| 13 |
+
print(result)
|
| 14 |
+
return result
|
| 15 |
+
|
| 16 |
+
@app.local_entrypoint()
|
| 17 |
+
def main():
|
| 18 |
+
result = hello.remote()
|
| 19 |
+
print(f"Result: {result}")
|