This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend
For my Friend Akshitha with allergies
What I Built
Akshitha has an anaphylactic peanut allergy and is also allergic to tree nuts and shellfish. Every time we eat out, the same thing happens. She reads the menu twice, then asks the server three questions, then orders the plain rice anyway. Peanuts hide in places you wouldn't expect: satay, pesto, "crunchy topping", the wok that cooked the last order.
SafePlate is a phone-friendly web page. She takes a photo of the menu and gets back every dish sorted into 🟥 Avoid, 🟧 Ask first and 🟩 Looks safe, each with a reason. Each risky dish also comes with the exact question to ask the server, like "Is the Kung Pao cooked in the same wok as other dishes?"
At a loud table she can tap 🔊 Read it to me and hear the verdicts instead of squinting at her phone.
Demo
Live: https://safeplate-2kzw.onrender.com (Username: friend, password: AqAnIuHQImW9xF1BbcetAbLF8WrtQ7OKJYMFd/UAcho=)
Here's real output from a test menu photo, with the laptop offline:
AVOID Kung Pao Chicken Ask: "Can you confirm the wok is cleaned between dishes?"
AVOID Garlic Butter Scallops (caught by the keyword safety net)
AVOID Basil Pesto Pasta Ask: "Do you offer a pesto without pine nuts?"
AVOID Vegetable Spring Rolls Ask: "What's in the sweet chili dip?"
SAFE Steamed Jasmine Rice
That's about 13 seconds per photo on my laptop. The spoken version sounds like this: "Avoid: Kung Pao Chicken. Ask about Green Papaya Salad. Can you confirm if the fish sauce is prepared separately from any nuts? Looks safe: Steamed rice."
Code
SafePlate 🍽️
Snap a restaurant menu and SafePlate tells your friend which dishes to avoid, which to ask about, and which look safe for their allergies. It also hands them the exact question to ask the server.
Gemma 3 does the work. It reads the menu photo itself (vision) and runs locally through Ollama. Nothing leaves the laptop: no API key, no cloud, no cost.
Run it (2 commands)
ollama pull gemma3:4b
python3 app.py
Then open http://localhost:8000. You need Python 3.9+ and nothing else, because the app uses only the standard library.
To use it from a phone at the table, connect the phone to the same Wi‑Fi and open http://<laptop-ip>:8000. The camera button opens the phone's camera.
Deploy to Render (free)
Render's free tier has no GPU, so the hosted version calls the same Gemma family through Google's Gemini API (gemma-4-26b-a4b-it, which…
Two files do the work: app.py, which uses only the Python standard library, and index.html. There's nothing to pip install. To run it locally:
ollama pull gemma3:4b
python3 app.py
How I Built It
Gemma reads the menu. Locally it's Gemma 3 4B through Ollama. Gemma is multimodal, so it reads the menu photo directly and I don't need a separate OCR step. The model is 3.3 GB and runs fine on an ordinary laptop. The browser shrinks photos to 1280px first, which keeps inference fast.
Structured output, not free text. Ollama's format parameter holds Gemma to a JSON schema. Every dish comes back with a verdict of exactly safe, ask or avoid, plus a reason and an ask_server question. Temperature is 0, and the prompt says "when unsure, choose 'ask'."
A safety net that doesn't trust the model. This is the part I care about most. The 4B model is good, but "good" isn't enough for an allergy. In testing it rated Garlic Butter Scallops and Tom Yum Goong (with prawns) below "avoid" for someone with a shellfish allergy.
So after Gemma answers, a deliberately dumb keyword pass runs over each dish's menu text:
def safety_net(dishes, profile):
"""Never trust the model alone: any dish whose text names an allergen keyword is forced to 'avoid'."""
for d in dishes:
text = f"{d.get('name', '')} {d.get('menu_text', '')}".lower()
hits = [a for a, words in profile["allergens"].items() if any(w.lower() in text for w in words)]
if hits and d.get("verdict") != "avoid":
d["verdict"] = "avoid"
d["reason"] = f"Menu text mentions {', '.join(hits)}. " + d.get("reason", "")
d["flagged_by"] = "keyword safety net"
if d.get("verdict") not in RANK:
d["verdict"] = "ask" # unknown verdict = don't claim safe
return sorted(dishes, key=lambda d: -RANK[d["verdict"]])
The model handles the nuance: sauces, cuisines that often use peanuts, cross-contact. The keywords guarantee the obvious cases. The profile maps each allergen to the words it hides behind (peanuts: satay, groundnut, arachis…, tree nuts: pesto, praline, marzipan…). The UI labels a dish the net overrode, so she can see why. The code can only make a verdict stricter, never more relaxed.
One app, two places to run it. If GEMINI_API_KEY is set, the same code calls hosted Gemma 4 (gemma-4-26b-a4b-it) through Google's Gemini API. That's how it runs on Render's free tier, which has no GPU. A render.yaml blueprint with no build step deploys it, and a password protects the URL. Without the key, it talks to local Ollama. It's the same prompt and the same safety net either way.
Hosted Gemma has no JSON mode, so the prompt includes the schema and the code takes the first {...} block out of the reply. Gemma 4 can include its "thoughts" in the reply too, and those get filtered out before parsing.
A voice from ElevenLabs. The verdicts become one short spoken summary: what to avoid, then each "ask" dish with its question, then what's safe. That goes to ElevenLabs' eleven_flash_v2_5 model, chosen for low latency. With no key, or offline, the button falls back to the browser's built-in speechSynthesis, so it always works.
Things that bit me deploying to Render:
- My health check pointed at a password-protected path, so it got a 401 every time and the deploy never went live. The fix was an unauthenticated
/healthz. - Render's startup probe sends
HEAD /, which Python'shttp.serverrejects with 501 unless you adddo_HEAD. - Google removed Gemma 3 from the Gemini API partway through, which caused a 404. The fix was switching to Gemma 4 and showing the provider's real error message instead of a bare "HTTP Error 404".
Why Does Open Innovation Matter?
- Her health data can stay with her. A list of someone's life-threatening allergies is medical information. With an open-weight model running locally, it never leaves the laptop. There's no vendor, no retention policy to read, and no account to make.
- Private when it matters, convenient when it doesn't. The Render version is the convenient one. Because Gemma is open-weight, the same model family also runs on hardware she controls. With a closed model, that choice wouldn't exist.
- It works where restaurants are. Basements, patios and bad signal don't matter when inference runs on the device she's carrying.
- It's free to run. Locally there's no API key and no per-photo bill, so she can check every menu without thinking about it.
-
I can swap the model with one setting.
MODEL=gemma3:12bgives more accuracy on a bigger machine, and the hosted version moved to Gemma 4 with a one-line change. - I control the whole pipeline. Because I own the inference call, I could enforce a JSON schema, set temperature to 0 and put my own deterministic check after the model. A closed chatbot gives you an answer and asks you to trust it, and for an allergy I won't.
My Agent Session
I built this with Claude Code. The session shows the whole path: picking the idea for cost and simplicity, the first test where Gemma missed the shellfish dishes (which is why the safety net exists), and the three Render deploy failures being debugged from real logs.
Prize Categories
- Best Use of Gemma
- Best Use of Render
- Best Use of ElevenLabs
- Best Use of Entire
SafePlate is a second pair of eyes, not a replacement for asking staff or carrying an EpiPen.
Top comments (0)