Etiquette 1 (8B)
Reads a nutrition table as well as a frontier model, on one consumer GPU, offline, for a tenth of a cent a pack.
Etiquette reads food packaging. Give it the photos of a product and it returns one JSON object with what is printed on the pack: the product's name per language, the generic (legal) name, brand, net quantity, the nutrition table row by row as printed (value, unit, qualifier), the basis of the table, the ingredient list per language, allergen statements, certification and origin marks, packaging and recycling text, and a tag per photo saying what that photo shows.
It reads. It does not estimate, derive or complete. A field that is not printed and legible in the photos is left
out, and a row printed as < 0.5 g is returned as < 0.5 g. Conversions (kJ to kcal, sodium to salt, per-serving
to per-100 g) belong in your code, where they can be checked.
Étiquette is French for "label".
Who made it and why
Etiquette is trained by FoodClock, a food-tracking service built on Open Food Facts data. Open Food Facts gave us the photos and the catalogue; this model is our contribution back. Weights and prompt are Apache-2.0. Use it to read labels into your own database, to check contributions, or to build on.
What it returns
The exact prompt is in prompt.txt and the system message in system.txt. Send them
unchanged with the photos; the model was trained on that wording. Fields, in order:
| field | shape |
|---|---|
kind |
"food" or "not-food" (always present) |
product_name |
{language: text} - the name the front presents the product under; ISO 639-1 keys, und for a coined name |
generic_name |
{language: text} - the descriptive or legal denomination, usually by the ingredient list |
brand, quantity |
strings, as printed |
nutriments |
{row: {"value": number, "unit": "kJ"|"kcal"|"g"|"mg"|"µg", "qualifier": "<"|"≤"|">"}} for the rows printed: energy_kj, energy_kcal, fat, saturated_fat, carbohydrates, sugars, fiber, proteins, salt, sodium |
basis |
{"per": "100g"|"100ml"|"serving"|..., "header": the printed column header, "size": the serving size when printed} |
minerals_per_l |
{name: number} for mineral waters |
allergens_printed, may_contain_printed |
the printed statements |
marks, marks_printed |
certification, quality and origin marks |
packaging |
{"materials", "recycling_codes", "text"} |
ingredients |
{language: the printed list} |
photos |
one entry per photo, in order: {"shows": ["front" | "nutrition" | "ingredients" | "allergens" | "marks" | "packaging" | "other", ...]} |
A field is present only when it is printed and legible in the photos you sent. Values are numbers, not strings; text is verbatim, in the script and case of the pack.
How well it reads
Numbers from the exam that gated this release (run 13). Printed truth: 100 products drawn from Open Food Facts, about half of them Moroccan packs (Arabic and French on the same pack) and the rest from France, the UK, the US, India, Spain, Belgium, Switzerland, Greece, the Balkans and elsewhere, with ingredient lists in 12 languages; every field confirmed by a person against the photos, with "not printed" recorded as a value of its own. The model sees only the photos the person ticked, so every judgement is about reading. Score = fields agreeing with the person, over all judgements; a right text filed under the wrong field or language key (a filing disagreement, 40 of 981 judgements) counts as not agreeing in that headline and is left out of the per-field percentages. The cards, this model's replies and the scorer are published as FoodClockLabs/etiquette-printed-truth, so the number can be re-run without a GPU.
| field | agrees with the person | invented, of the field's judgements |
|---|---|---|
| all judged fields | 83.0% | 42 of 981 judgements across 100 products |
| energy (kcal) | 97.0% | 1 of 100 |
| salt | 98.0% | 2 of 100 |
| sodium | 96.0% | 0 of 100 |
| basis of the table | 94.0% | 0 of 100 |
| net quantity | 86.0% | 12 of 100 |
| product name | 79.0% | 1 of 100 |
| brand | 78.0% | 5 of 100 |
| front name per language | 78.8% | 6 of 131 |
| generic name per language | 76.1% | 15 of 150 |
A second hundred we never look at: because the exam above is public and was read many times while the model was tuned, a second set of 100 products was drawn the same way and kept private (99 of them with photos on file). Its printed truth was recorded from the photos by a frontier-model reader under the same written brief and accepted by FoodClock, not confirmed field by field by a person. The model is scored on it in totals only, and nobody reads its cards during development. Run 13 on that set: 81.0%; 22 of 988 judgements across 99 products invented.
The same exam, sat by frontier models. Each model below received Etiquette's own system message and prompt, the same ticked photos of the same 100 products (1,568 px, at most six per product), one call per product, and its reply was parsed and scored by the same code. Nothing else differs. Columns: agreement over all judged fields; invented values of all judgements; per-field agreement in %.
| model | all fields | invented | kcal / salt / sodium | basis | net quantity | product name | brand | cost of the 100 products |
|---|---|---|---|---|---|---|---|---|
| Etiquette 1 (8B) | 83.0% | 42 of 981 | 97 / 98 / 96 | 94 | 86 | 79 | 78 | one consumer GPU, no API |
| Claude Sonnet 5.5 | 87.4% | 21 of 975 | 96 / 98 / 95 | 89 | 93 | 88 | 91 | $3.28 in API fees |
| Claude Opus 5.5 | 90.8% | 10 of 964 | 95 / 95 / 95 | 89 | 93 | 89 | 92 | $18.35 in API fees |
Read it plainly: on the rows a food database lives on (energy, salt, sodium) the 8B model and the frontier models are the same model to within a card or two, and Etiquette reads the basis of the table better; on names, brands and net quantities the frontier models read better and invent less. The difference is everything else: a frontier call costs about three cents a product, Etiquette about a tenth of a cent in electricity on a GPU that is already paid for; the photos never leave the machine; and the weights are a file, so this exam can be re-run on the same bytes next year, which no API model can promise.
Catalogue exam: 100 further products scored against their Open Food Facts record (which has its own errors): 87.8% agreement over nutrition, names, quantity and ingredients. Photo tags: recall 100.0% front, 98.0% nutrition table, 89.9% ingredient list over 100 products.
An "invented" value is one the model returned where the person recorded "not printed". The two pages of the exam that matter most for a database - energy, salt and sodium - sit at 95-98% with 0-2 inventions each.
Known limits
- Coined names it has never seen ("Tagada", "Coke Original Taste") are sometimes left out of
product_name. - Some Arabic fronts are read into the Latin-script name only.
- A serving size or piece count printed prominently is sometimes returned as the net quantity (about 1 in 10 packs).
- On packs that print the ingredients in several scripts, the list is occasionally returned in the other language.
- Long ingredient lists in Arabic can run to the token limit; allow 2,048 new tokens.
- Trained on photos from Open Food Facts: phone photos of real packs, often at an angle, sometimes blurred. It reads flat scans well too, but it has not been trained on them.
Training
- Base: Qwen/Qwen3-VL-8B-Instruct, LoRA rank 32 on the language model, learning rate 1e-4, 2 epochs, 3,566 optimizer steps, images up to 1,568 px on the long side, bf16 on one RTX PRO 6000.
- Data: 14,657 products from Open Food Facts (14,270 train, 387 validation), 53,228 photos. Targets were produced by a pipeline of open local vision models cross-checked against the printed text and the catalogue, with each field weighted by how well it was verified; unverifiable fields were masked, not guessed. Every product in any of our exams was excluded from training. Products are from 60 languages of packaging.
- The merged weights are what is published; no adapter is needed.
Running it
import json, torch
from PIL import Image
from transformers import AutoProcessor, AutoModelForImageTextToText
repo = "FoodClockLabs/Etiquette-1-8B"
processor = AutoProcessor.from_pretrained(repo, min_pixels=256*28*28, max_pixels=1568*1568)
model = AutoModelForImageTextToText.from_pretrained(repo, torch_dtype=torch.bfloat16, attn_implementation="sdpa").to("cuda").eval()
system = open("system.txt").read(); prompt = open("prompt.txt").read()
photos = [Image.open(p) for p in ["front.jpg", "back.jpg"]]
msgs = [{"role": "system", "content": system},
{"role": "user", "content": [{"type": "image"} for _ in photos] + [{"type": "text", "text": prompt}]}]
text = processor.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
inputs = processor(text=[text], images=photos, return_tensors="pt").to("cuda")
out = model.generate(**inputs, max_new_tokens=2048, do_sample=False, repetition_penalty=1.05)
reply = processor.batch_decode(out[:, inputs["input_ids"].shape[1]:], skip_special_tokens=True)[0]
print(json.loads(reply[reply.index("{"):reply.rindex("}")+1]))
Send the photos in the order you want the photos tags back. Greedy decoding; a repetition penalty of 1.05 keeps
long ingredient lists from looping. Memory grows with the number and resolution of photos; lower max_pixels on
smaller cards.
On a laptop (GGUF). Quantized builds for llama.cpp, Ollama and LM Studio, with the vision projector (mmproj)
needed for photos, are published by the hub's quantizer at
mradermacher/Etiquette-1-8B-GGUF. Three steps:
# 1. the weights: a quant (Q8_0 reads like the full model; Q4_K_M fits an 8 GB laptop) and the vision projector
huggingface-cli download mradermacher/Etiquette-1-8B-GGUF Etiquette-1-8B.Q8_0.gguf Etiquette-1-8B.mmproj-f16.gguf --local-dir .
# 2. serve with llama.cpp (any recent build); keep -c 40960: six photos are ~14k visual tokens
llama-server -m Etiquette-1-8B.Q8_0.gguf --mmproj Etiquette-1-8B.mmproj-f16.gguf -c 40960 -ngl 999 --jinja --port 8080
# 3. read a pack (this repository's run_gguf.py, standard library only)
curl -LO https://huggingface.co/FoodClockLabs/Etiquette-1-8B/resolve/main/run_gguf.py
python3 run_gguf.py front.jpg back.jpg nutrition.jpg
run_gguf.py fetches system.txt and prompt.txt from this repository, sends the photos before the
prompt text - the order the model was trained on; the other order costs about ten points on the exam - with the
settings the numbers below were measured with (temperature 0.1, top-k 20, repeat penalty 1.05, 3,000 tokens), and
prints the JSON. If you call the server yourself, keep that order and those settings.
The same exam, quantized. The public hundred, the same ticked photos, the same prompt, the same scorer; the GGUF rows served by llama.cpp with the f16 vision projector. Read it as two steps: llama.cpp's path costs about two points at full precision (its image preprocessing is not transformers'); 8-bit adds nothing to that; 4-bit about one point more.
| weights | all fields | invented | kcal / salt / sodium | basis | net quantity | product name | brand | file | runs on |
|---|---|---|---|---|---|---|---|---|---|
| BF16 (this repository) | 83.0% | 42 of 981 | 97 / 98 / 96 | 94 | 86 | 79 | 78 | 17.5 GB | one 24 GB GPU |
| F16 (GGUF) | 81.0% | 40 of 992 | 96 / 99 / 95 | 94 | 87 | 79 | 79 | 16.4 GB | a 24 GB GPU; llama.cpp at full precision |
| Q8_0 (GGUF) | 81.1% | 38 of 993 | 96 / 99 / 95 | 94 | 87 | 79 | 80 | 8.8 GB | any 12 GB GPU, or a Mac with 16 GB |
| Q4_K_M (GGUF) | 79.9% | 47 of 999 | 96 / 98 / 94 | 95 | 85 | 76 | 78 | 5.0 GB | a laptop GPU with 8 GB, or a Mac with 8 GB |
Licence and attribution
| component | holder | licence |
|---|---|---|
Etiquette 1 weights (model.safetensors), prompt.txt, system.txt, foodclock_exam.json, this card |
Copyright 2026 FoodClock (FoodClock Labs) | Apache-2.0 - see LICENSE and NOTICE |
| Base model Qwen3-VL-8B-Instruct, of which this is a merged LoRA fine-tune; tokenizer, processor and chat template files | Copyright 2025 Alibaba Cloud | Apache-2.0 |
| Training data: product records and photos | Open Food Facts and its contributors | database ODbL 1.0, contents DbCL 1.0, photos CC BY-SA 3.0 |
| The exam: FoodClockLabs/etiquette-printed-truth | FoodClock Labs 2026 | CC BY-SA 4.0 |
Contains information from Open Food Facts, which is made available here under the Open Database License (ODbL). No photo is distributed in this repository; the model was trained on photos that remain on Open Food Facts under their contributors' licence. Open Food Facts is a trademark of the Open Food Facts association; FoodClock Labs is not affiliated with or endorsed by it.
What this asks of you: keep the LICENSE and NOTICE files with any copy or derivative of the weights (Apache-2.0);
credit Alibaba Cloud's Qwen3-VL as the base model; and if you publish readings from this model into a database, credit
Open Food Facts as the source of the photos your readings come from, as we do. Nothing in these licences restricts
commercial use.
The model reads what is printed. It is not a substitute for the pack in front of you, and nothing it returns is dietary or medical advice.
- Downloads last month
- 51