Sikia interviewer v8 (LoRA adapter, card format 12n)
A LoRA adapter on stock Gemma 4 E2B that reads a respondent's reply during an offline survey interview and
returns one short JSON reply. It follows the Sikia offline interview design: the app leads the interview
(question order, skip logic, repeat rounds, follow-ups, everything that is spoken) and the model only
listens. Each call gets the standing instructions (instructions.txt) and one short card.
Versions. v8 is trained like v4 and v6c (same base model, card format, instructions and settings) on wider data. It follows v7 (name lists) and adds free-text names, spoken numbers, negation, and "nothing else" replies. v4 remains the version in use until a later version passes Sikia's relevance test.
Status: research pilot. Use it together with the app's checks (below): several of the gains come from the model and the app together. Sikia's relevance test (off-topic replies in realistic interviews) is pending. Don't use it for real data collection yet.
What it does
| Card | Returns |
|---|---|
| Question (any form question: single or multiple choice, whole number, decimal, date, text) | {"act", "value", "say"}: act is answer, clarify, skip_request, impatient, correction (an earlier answer) or off_topic; say is always empty |
CURRENT: yes/no (another round? skip this question?) |
{"act": "yes"/"no"/"clarify", "say": ""} |
TASK: list items (split a spoken list) |
{"items": [...]} in the respondent's words |
TASK: probe (follow-up check on a researcher's goal) |
{"new_items": [...], "goal_met": bool, "follow_up": ""} |
The app counts the goal's target itself and stops a follow-up when nothing new is said, so it does not use the
model's goal_met. Follow-up questions are worded by the app; follow_up stays empty.
What is new since v6c
- Names that are not in the list. On questions whose options are names (regions, districts, villages, schools, markets, groups), v6c picked the nearest option for a name that was not there ("Tanga" became Kagera). v8 says the name is not in the list (clarify), or picks "Other" when the list has it, including for kinds of list it never trained on.
- Free-text names that are not names. "45", "jana" or "ndiyo" as a farmer's name, or a place when a person's name was asked, now get a re-ask.
- Spoken numbers. "elfu mbili na mia nane na nane" is 2808.
- Negation. "sijayaona, iko vizuri tu" to "is there any pest or disease?" is no.
- Yes or no with more said. "Ndiyo, na kuku pia" is yes.
- "Nothing else" in follow-up checks. "hakuna", "sina kingine" give no new point; "hakuna maji ya kutosha" is a point.
The app's checks
The adapter is meant to run with slmlib/cards.py and slmlib/controller.py in the project code. They do four
things the model cannot do alone: check a chosen option against the name actually said (ground_choice), check that
a free-text answer is the kind of name asked for (check_text), read spoken numbers and overrule a disagreeing
number (check_number), and count a follow-up goal's target. Scores below are shown for the model alone and with
these checks (eval/score_v12.py --app-check).
Example
LANG: sw
CURRENT: q=top_constraints type=select_multiple "Changamoto kubwa ni zipi?"
OPTIONS: 1=Ukame 2=Wadudu 3=Magonjwa 4=Udongo duni 5=Soko 6=Mbegu duni 7=Hakuna
CONTEXT: -
RESPONDENT SAID: "Wadudu ni wengi, na pia soko halina bei nzuri."
{"act": "answer", "value": "2 5", "say": ""}
Cards must match the training format byte for byte. slmlib/cards.py writes every card and applies the app's
post-processing.
Try it
Accept the Gemma licence on Hugging Face first, and use transformers 5.17 or later (earlier 5.x versions compute Gemma 4 E2B differently with the cache on, and the adapter then has no effect).
import torch
from huggingface_hub import hf_hub_download
from peft import PeftModel
from transformers import AutoModelForImageTextToText, AutoProcessor
BASE, ADAPTER = "google/gemma-4-E2B-it", "smutuvi/sikia-listener-v8-gemma4-e2b-lora"
proc = AutoProcessor.from_pretrained(BASE)
model = AutoModelForImageTextToText.from_pretrained(BASE, dtype=torch.bfloat16, device_map="auto")
model = PeftModel.from_pretrained(model, ADAPTER).eval()
system = open(hf_hub_download(ADAPTER, "instructions.txt"), encoding="utf-8").read().strip()
card = """LANG: sw
CURRENT: q=village type=select_one "Chagua kijiji:"
OPTIONS: 1=Kagera 2=Misenyi
CONTEXT: -
RESPONDENT SAID: "Tanga." """
msgs = [{"role": "system", "content": system}, {"role": "user", "content": card.strip()}]
prompt = proc.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True, enable_thinking=False)
enc = proc.tokenizer(prompt, return_tensors="pt", add_special_tokens=False).to(model.device)
out = model.generate(**enc, max_new_tokens=80, do_sample=False)
print(proc.tokenizer.decode(out[0, enc["input_ids"].shape[1]:], skip_special_tokens=True))
# expected: {"act": "clarify", "value": null, "say": ""} (the name is not in the list)
With the project code, a whole interview on any form, typed or spoken:
python demo/form_interview.py --adapter smutuvi/sikia-listener-v8-gemma4-e2b-lora --form bean_tricot # terminal
python demo/form_interview.py --adapter smutuvi/sikia-listener-v8-gemma4-e2b-lora --form clinic --lang en
python demo/form_app.py # browser; lists this adapter
Speech is transcribed separately (the base model with the adapter switched off, or the ndizi speech model): one skill per adapter.
Training data
10,943 cards; the data itself is not published (it contains what respondents said).
| Source | Cards | |
|---|---|---|
| Real bean-trial replies (Swahili), question and follow-up cards | 2,095 | labels hand-checked on the test set, corrected in training |
| Drafted interviews on 7 topics (Swahili, English) | 3,876 | free text follows the core-answer rule |
| Sikia practice interviews on 14 real forms (Swahili, English) | 2,111 | all reply types, weak-spot sets, and off-topic replies |
| Real Sikia replies on other forms (Swahili) | 138 | dev interviews only, usable labels only |
| Written rows for open comments and numbers (English, Swahili) | 702 | from v6c |
| Written rows: name lists | 842 | 7 kinds (region, district, village, variety, school, market, group) in Swahili and English; Colombian departamentos and municipios in Spanish |
| Written rows: free-text names | 614 | person and place questions; replies that are names, or numbers, dates, yes/no, another answer, or the wrong kind of name |
| Written rows: v7 review fixes | 565 | spoken numbers, negation, yes/no with detail, "nothing else" against "hakuna X" in follow-ups |
The written rows (2,021 in total for v7 and v8) are synthetic and need a native-speaker check; the names in them are invented or common place and person names, none from Sikia's relevance or held-out forms. Nothing comes from Sikia's 4 held-out forms or from the forms in Sikia's relevance test.
Evaluation
Test cards: the v6c test (the same 1,872 cards for every version) plus new groups for names (290), free-text names (233) and the v7 review fixes (202), 2,597 in all. v6c and v7 were scored on the same cards; groups that did not exist for v6c are marked n/a.
Model alone, compared on the same cards:
| v6c | v7 | v8 | |
|---|---|---|---|
| Real Sikia replies (118): act right | 0.966 | 0.958 | 0.966 |
| Real Sikia: misread as refusal, correction or off-topic | 0.008 | 0.025 | 0.017 |
| Real bean speech: act right | 0.974 | 0.974 | 0.963 |
| Real bean speech: wrong saves | 0.233 | 0.233 | 0.254 |
| Real bean speech: follow-up points, word overlap | 0.722 | 0.729 | 0.741 |
| Unseen forms: single-choice value | 1.0 | 0.958 | 1.0 |
| Unseen forms: yes/no | 1.0 | 0.985 | 1.0 |
| Names not in the list: clarify or "Other" | 0.017 | 0.966 | 1.0 |
| Name lists, a kind never in training | 0.75 | 1.0 | 1.0 |
| Wrong saves on name lists | 0.241 | 0.003 | 0.0 |
New groups (v7 against v8; v6c was not scored on them):
| v7 | v8 | v8 + app checks | |
|---|---|---|---|
| Free-text names: number / date / yes-no given as a name | 0.13 / 0.08 / 0.64 | 1.0 / 0.96 / 1.0 | 1.0 / 1.0 / 1.0 |
| Free-text names: another answer ("maharage") | 0.0 | 0.90 | 0.95 |
| Free-text names: a place for a person | 0.0 | 1.0 | 1.0 |
| Free-text names: a kind never in training | 0.48 | 0.95 | 0.97 |
| Free-text names: wrong saves | 0.24 | 0.03 | 0.02 |
| Spoken numbers | 0.81 | 0.97 | 1.0 |
| Negation | 0.82 | 0.89 | 0.89 |
| Yes or no with detail | 0.77 | 0.92 | 0.92 |
| "Nothing else" in follow-up checks | 0.83 | 0.90 | 0.90 |
| "Hakuna X" counted as a point | 0.73 | 0.80 | 0.80 |
| Test questions of the v7 fixes on forms not in training | 0.81 | 0.92 | 0.94 |
| All test cards: act right | 0.935 | 0.991 | 0.993 |
The name-list and free-text groups are synthetic and were written to match the rules; the numbers show that the model and the app agree with them, and that the rules carry over to kinds of names not in training. They do not show how well real respondents' replies are handled. Differences of 1 to 3 cards are within run-to-run noise (one seed was trained for v8).
Confidence. eval/confidence.py scores every reply, and eval/confidence_report.py picks a cut-off on the
validation cards (a wrong save counted as 3 re-asks) and reports the test. The app saves an answer when the
confidence is at or above the cut-off and asks again below it. Per 100 cards whose correct reply is an answer, using
the probability of the reply the model wrote (cut-off 0.6):
| Group | save everything: right / wrong / re-ask | with the cut-off: right / wrong / re-ask |
|---|---|---|
| All test cards | 94.6 / 5.4 / 0.5 | 93.7 / 2.8 / 3.8 |
| Real Sikia replies | 87.3 / 9.3 / 3.4 | 87.3 / 1.7 / 11.0 |
| Real bean speech | 74.9 / 26.8 / 0.6 | 68.7 / 12.8 / 20.1 |
| Unseen forms | 98.3 / 1.7 / 0.0 | 97.9 / 1.4 / 0.7 |
| Free-text names | 97.1 / 6.7 / 1.0 | 97.1 / 4.8 / 1.0 |
| Names, seen forms | 100 / 0 / 0 | 100 / 0 / 0 |
The cut-off trades lost data for re-asks: on all test cards, 0.8 gives 1.5 wrong saves and 6.4 re-asks, 0.9 gives 0.7 and 9.7. Expected calibration error is 0.014. Of the 1,401 answers with confidence 0.9 or more, 99.2% are right; below 0.8, only a third to two thirds are. Scoring each allowed reply against the others (choice probability) is less useful: it is over-confident (ECE 0.044), changes the answer on 4 of 2,102 cards, and does not cut wrong saves on real bean speech. Most test cards are easy, so the "all" row is dominated by cards at 0.9 or more; the real Sikia and real bean rows say more about real use. Real bean wrong saves are mostly options added or dropped on multi-choice answers, and a re-ask costs one in five answers there.
Known weak spots:
- Real bean multi-choice answers: options added or dropped (wrong saves 0.254); some of the keys look doubtful and need a review. v8 is level with v7 here (28 cards better, 23 worse).
- Negation on single-choice questions about health ("Wote ni wazima" to "have your animals been sick?") is sometimes read the wrong way round; the training rows covered crop questions only.
- Trailing-off refusals ("Sitaki…") on multi-choice questions were saved as an option once.
- Free text on real Sikia speech (the benchmark keys often summarise).
- Off-topic replies that answer a different question (Sikia's relevance test pending).
Training procedure
LoRA (PEFT) on the language model's linear layers only; the vision and audio towers are untouched. Loss on
the JSON reply only. Rank 16, alpha 32, 3 epochs, effective batch 16 (2 × 8), bf16, one GPU, transformers 5.17,
seed 42. Settings in train_config.json.