NateRunsA-LIST/alist-food-text-qwen35
A-LIST food-text extraction — Qwen3.5-2B + LoRA (GGUF)
On-device food-diary nutrition extraction for the A-LIST iOS app (Locomo mode). Runs in llama.cpp (qwen35 arch — needs b10369 or newer).
Files
Adapter lineage
- Dataset:
food-text-unified:v1(66.5k scrubbed food-diary rows, teacherfood_text_v1) - Recipe:
qlora_r64_sft(3ep), converted from PEFT safetensors with llama.cppconvert_lora_to_gguf.py(--outtype f16) - Eval (1,500-row frozen eval, left-pad, production post-processing): calories 0.761 · protein 0.856 · carbs 0.775 · fat 0.769 · fiber 0.939 · sugar 0.856 · sat-fat 0.873 · water 0.903 · GI±10 0.905 · micros mean 0.862 · parse_fail 0/1500
- Ladder on the same eval: LFM2.5-shipped 0.397 → MiniLM encoder e2e 0.591 → LoRA r32 0.701 → this adapter 0.761 (teacher ceiling 0.976)
Prompt contract (SFT student inference contract)
The adapter is conditioned on this exact raw-text prompt — no chat template, no `<think>` block (the long food_text_v1 teacher prompt labeled the corpus but is NOT the inference prompt):
Extract nutrition from this food diary entry. Estimate typical portions when unstated; copy user-stated numbers verbatim and list those fields in user_stated_fields. Output only JSON.
Entry: {entry}
###JSONIt replies with a single JSON object: macros + sugar/saturatedfat, `waterml, glycemicindex`, 15 `micronutrients`, `userstatedfields`, `mealtitle`. Consumers should apply the dataset's post-processing (stated-calories reconcile, sugar/sat-fat subset invariant, calories derived 4/4/9 when unstated, GL derivation) — the eval numbers above are measured on post-processed outputs.
Both the base model and the fine-tune are Apache 2.0.
mmproj Q8_0 (2026-08-24)
mmproj-Qwen3.5-2B-Q8_0.gguf (365 MB, sha256 5eda1f66…) is the vision tower requantized to Q80 from the original Qwen3.5-2B weights — current for iOS (`qwenmmprojv2q8). On the frozen 215-product supplement eval every gated structured field holds within 1 point of the F16 numbers (micros improve, .862 vs .848). mmproj-Qwen3.5-2B-F16.gguf` stays for rollback.
v3pos — positional output contract (2026-09-12)
Same prompt as v2, byte for byte. The reply is {"n":[24 numbers],"s":[stated field names],"t":"meal title"} with n in this order: calories, protein, carbohydrates, fat, fiber, sugar, saturatedfat, waterml, glycemicindex, vitaminA, vitaminC, vitaminD, vitaminE, vitaminK, vitaminB6, vitaminB12, folate, calcium, iron, magnesium, zinc, potassium, selenium, omega3 (integers when integral, else ≤2 decimals). Consumers expand `n` back to the keyed fields (`lab/teacher/foodtextstudentv3pos.py` in ProtocolsBackend) and apply the same post-processing as v2. ~98 output tokens vs ~222 ⇒ 5.9 s vs 9.8 s per entry on an iPhone 16.
- Recipe:
lora_r64_bf16(2ep, lr 2e-4, bs 4x8)on the augmented train set (119,718 rows: v1 + 55,457 paraphrases + 4,995 hardcases); converted with llama.cppconvert_lora_to_gguf.py --outtype f16. - Eval (bf16, 1,500 frozen rows): calories 0.7587 · carbs 0.7527 · protein 0.8393 · micros 0.8644 · parse_fail 1/1500.
- Production Q4 artifact (llama.cpp, 1,000 reference rows): calories 0.739 · carbs 0.738 · micros 0.840 · parse_fail 0 — parity with the v2 artifact (0.748 / 0.735 / 0.821).
alist-food-lora-v2-f16.ggufstays: A-LIST 2.51 and 2.52 pin it by sha256. Do not modify or remove it.
Medication-label text adapter v1 (2026-09-13)
The OCR rung for the Medications protocol in signed-out mode: Apple Vision OCR text of a pharmacy sticker, compounding vial, Drug Facts panel, stock bottle or Supplement Facts panel in, one structured medication record out. Trained on medication-label-text:v1 (12k synthetic labels with exact targets, no patient data; checkpoint 600 of a 1-epoch mlx-lm run, picked on val loss). Same base, same target modules as the other adapters; never stack with the food or supplement adapter on one context.
Eval on the shipped stack (Q4KM base + this adapter, 1,827 rows whose drugs never appear in training): parsefail 0 · kind .999 · rxonly 1.000 · servingdesc .993 · frequencyunit .990 · dosesperday .989 · dosesperweek .999 · servingstrength .973 · strengthunit .951 · name .951 · all core fields .879. Zero-shot base: .000.
Prompt contract (medication_label_student_v1): Qwen3.5 chat template with thinking disabled, ONE user turn = instruction + OCR text (≤2000 chars), no prefill, greedy, stop <|im_end|>, max 400 tokens, cut the first balanced JSON object:
<|im_start|>user
Extract the medication or supplement from this label OCR text as one JSON object. Read amounts exactly as printed; null means not printed. Output only JSON.
{ocr_text}<|im_end|>
<|im_start|>assistant
<think>
</think>
Output keys in order: kind, name, brand, rxonly, form, route, servingdesc, servingstrength, strengthunit, dosesperday, dosesperweek, frequencyunit, ingredients[{name, amount, unit}]. Cadence is the canonical trio (`asdirected → doses_per_day 0; everyotherday` → 0.5; weekly → dosesperweek).
