Your Bon knows what you eat.
A Bon is a Swiss till receipt. Bon·Appétit reads yours with Apertus, Switzerland's open language model, and shows what your household ate this week on the Swiss food pyramid. It all happens on your own computer.
Apache-2.0 on GitHub. The method and results are in the report.
Receipts are written for the till.
Shops cut product names to fit a narrow paper roll, in the shop's language, with house-brand codes in front. The Migros Cumulus export uses the same names and has no barcodes, so there is nothing to look up.
What a receipt says
Five lines from two shops, exactly as printed: M-BUDG, QP and PRIX GAR. are house brands, and RUEBLI is Swiss German for carrots.
What Bon·Appétit reads
Each line becomes a food from the federal Swiss Food Composition Database (FSVO). When no entry fits, the line waits for you instead of being guessed.
Search finds candidates. Apertus picks and checks.
Search proposes foods from the Swiss food table. Apertus then works on each line in three steps: it reads the line, picks one candidate by its number or answers 0, and cross-checks the pick. The cross-check cut the typical error from 123 to 102 kcal per 100 g. Every number you see comes from the federal record.
Apertus reads the shorthand.
Apertus 8B works on each line three times. First it writes the line out in plain English, names the language, says whether it is food and gives a generic name in the style of the German food table. Then it sees a short list of candidates and answers with the number of the one that fits, or 0. Last, it estimates the product per 100 g. That estimate is never shown: it only decides which of the top eight candidates from the federal food table fits best, with a head start for its own pick.
The candidates come from all 1,145 foods in the federal food table, found by lexical token matching plus Swiss multilingual embeddings derived from Apertus by Andreas Martin. A line that gets 0 waits for you.
A food layer checks every pick.
Before a link counts, plain rules look at it. A line sold in litres cannot be a solid like cheese or chocolate, and the pack size has to make sense for the food. Bags and detergent are set aside as non-food.
A Swiss glossary adds words like Rüebli, Nüsslisalat and Peperoni, plus common French and Italian food words, to the search and to the pick prompt. Quantities come from the line itself, converted by code.
Air-gapped by default.
A year of shopping says a lot about health, religion and money. Under the revised Swiss data protection act that is sensitive personal data, so it stays on your machine.
make airgap puts the app and a llama.cpp model server on a Docker network marked internal, with no route out. Before it starts, the app tries four outside hosts and refuses to run if any of them answers. There is no account and no telemetry. If you would rather use a Swiss cloud, point LLM_BASE_URL at it.
The parts that make it usable.
Migros CSV and Coop PDF
Import the Cumulus CSV export from Migros, or the text of a Coop PDF receipt.
Four languages
Receipts in German, French and Italian. The weekly suggestion in those three and in English.
Fixes for open data
A simple calorie sanity check (4 kcal per gram of protein and carbohydrate, 9 per gram of fat) flags 3,035 of 86,517 Swiss products on Open Food Facts (3.5%) whose printed calories contradict their macronutrients; 433 look like kJ typed as kcal. The list is in eval/off_energy_flags.csv.
Try it on this page.
Pick a sample receipt, set how many people share the shopping and choose a language for the suggestions. Everything below recalculates in your browser.
Loading sample receipts.
Apertus on a laptop takes about 30 to 50 seconds per line. The household slider below sets how many adults share the shopping.
What Apertus taught us.
Apertus read RUEBLI as kale.
The 8B model read RUEBLI, Swiss German for carrot, as kale and once as flatbread. A short glossary of Swiss food words now goes into the search and the prompt.
Spelling instructions slip.
Asked for Swiss spelling, it still wrote ß in German suggestions. Code converts it to ss.
A strict grammar made it worse.
A full JSON schema grammar made each call about four times slower and made the model answer "no match" for almost every line. Plain JSON mode, with the schema checked in code, fixed both.
It rarely says it is sure.
So the app does not ask it for a confidence score. It chooses a list number, or 0.
Tested against printed labels.
Thousands of Swiss products on Open Food Facts carry the nutrition values printed on their packs. We hide the label, link the product from its name or from a receipt-style truncation of it, and compare the estimate with what the pack says.
Off by more than 100 kcal per 100 g
Share of products, lower is betterShare of products whose estimate is off by more than 100 kcal per 100 g.
Error against the printed label
Results are being computed| Method | Typical kcal error per 100 g | Off by over 100 kcal | Sugar error | Salt error | kcal within 20% | Food group right | Left for you | n |
|---|
Baselines.
Fuzzy string matching, the way a 2018 hackathon app did it. Embeddings alone. Apertus alone, asked to guess the nutrients with no database. Then Apertus pick only, and Bon·Appétit with the cross-check.
The best possible row is the score a perfect link to a generic food could reach. The federal food table holds generic foods and the packs are specific brands, so even a perfect link has some error.
Limits.
Purchases are not intake. Food gets shared, thrown away or eaten outside the house, and a receipt shows none of that.
The rules that turn product names into receipt-style truncations are synthetic, and the report documents them. Federal food table values are generic, so a brand's recipe can differ.
Prior work.
TooSweet, from the Open Food Hackdays in 2018, ranked Cumulus purchases by sugar. ETH Zurich's FoodCoach and Receipt2Nutrition research studied nutrition from shopping data before us.
Bon·Appétit adds a local model that can abstain, and a check layer behind it.
of 86,517 Swiss products on Open Food Facts list calories that contradict their own fat, carbohydrate and protein values. 433 look like kilojoules typed as kilocalories.
Energy check: 4 kcal per gram of protein and carbohydrate, 9 per gram of fat, 2 per gram of fibre; flagged when printed and computed energy differ by more than 25% and 25 kcal per 100 g. Alcoholic drinks skipped.
The list, for Open Food Facts volunteersHouseholds first, then the people who help them.
You already hold the purchase data. Bon·Appétit turns it into a nutrition picture without sending it to a third party and without a per-request fee.
Households
See what a week of shopping adds up to, privately, with swaps you can make in the same shop.
Dietitians and home care
Start from what a client actually bought, with their consent, instead of a food diary written from memory.
Insurer prevention programmes
Offer members a weekly view that stays on their own computer, so the receipts never reach the insurer.
Retailers' loyalty apps
Run the linker next to the purchase data a loyalty programme already holds, with the Swiss pyramid as the reference.
Cantonal health promotion
Use the open code in nutrition programmes in German, French or Italian speaking cantons.
Researchers
Reuse the linker module and the label-based evaluation to study shopping data and diet.
Three commands on your own computer.
You need git and Docker. Everything runs in a container on your machine.
$ git clone https://github.com/kinnuworks/bon-appetit $ cd bon-appetit $ make run
make airgap runs the app with no route out. make eval re-scores the stored predictions and checks their SHA-256.
$ export LLM_NAME=swiss-ai/Apertus-v1.5-8B $ export LLM_BASE_URL=https://your-swiss-endpoint/v1 $ export LLM_API_KEY=your-key $ make run
With these set, receipt lines go to that endpoint instead of the local model. Pick a provider that runs in Switzerland under Swiss law.
Honest answers.
Is this medical advice?
No. It describes what a household bought and compares that with the Swiss dietary recommendations. For a diagnosis or a diet plan, talk to a doctor or a registered dietitian.
Does anything leave my computer?
Not with make airgap. The app and the model server sit on a Docker network marked internal, with no route out, and the app refuses to start if any of four outside hosts answers. There is no account and no telemetry. If you set LLM_BASE_URL, the receipt lines go to that endpoint, so choose one you trust.
Why not just use barcodes?
The Migros Cumulus export has no barcodes, and a paper receipt does not print them. The shortened product name is all there is to work with.
What about Coop?
Coop PDF receipts are read as text. Lines with Coop house brands such as Naturaplan, Qualité & Prix and Prix Garantie go through the same reader as Migros lines.
What if it gets an item wrong?
Every line shows the food from the federal food table it was linked to, so a wrong link is easy to spot. The demo on this page keeps the real mistakes, such as iced tea linked to lemon juice. When no candidate fits, Apertus answers 0 and the line waits for you, and the food layer rejects links whose unit or pack size makes no sense.
Which model runs it?
Apertus v1.5 8B, running locally; the air-gapped setup serves it with llama.cpp. It works on each line in three steps: it reads the line, picks a candidate by its number, and cross-checks the pick against its own estimate per 100 g, which is never shown. It never produces a nutrient value: code computes every quantity from the FSVO database.
Can I use it in French or Italian?
Yes. It reads receipts in German, French and Italian, and writes the weekly suggestion in German, French, Italian or English.
How accurate is it?
See the evidence section. Estimates are measured against the nutrition labels printed on Swiss products on Open Food Facts, next to simpler baselines and a best possible row that shows how close a perfect link to a generic food could get. What a household buys is still not exactly what it eats.
Read last week's shopping. On your own computer.
Open source, in four languages, with no account to make.