Biologists want a number out of a scan, not a picture: how big is the tumor, how much did it grow. ScanSpeak turns a plain-English request into actions in a 3D viewer. The model only picks the actions, and the tools produce every number, so it can't make up a measurement. Then I measured how to make small local models reliable at this.
Step through a real session on mouse M37, one the tumor model never trained on. Each step shows the exact tool calls the model chose, the reply built from the tools' output, and what the viewer looked like right after.
96 requests a preclinical imaging scientist might type, run through 5 local models and 5 ways of getting structured output. These are the real recorded outputs, scored by replaying both the model's calls and the correct calls on a simulated viewer, then comparing the final state and every number reported.
Grammar-constrained decoding guarantees every call is valid, and it did: zero invalid calls. It still came out about 11 points behind the models' built-in tool calling, because when the output is a JSON array, models pad it with actions nobody asked for. The built-in format's own failures were mostly type slips, like "False" in quotes. A 30-line schema repair fixes those with no model involved.
One run at temperature 0. With 72 and 24 requests, differences of a few points between models aren't meaningful. The schema repair and a "minimal calls" prompt rule were designed after looking at failures, so both were checked on the 24 held-out requests written beforehand. The repair held up. The prompt rule didn't, and it's reported in the repo as a negative result.
load_scan, segment, measure, compare_timepoints, set_window, set_layout, focus_on… Small enough for a 1.5B model to read the whole list in its prompt.
Ollama gets the tool list plus the viewer state, like which scan is open and what's already segmented. It returns tool calls and nothing else.
Fix types the schema declares, like strings to booleans, numbers or enum spellings. Anything still invalid is rejected, not guessed at.
A 3D U-Net segments the tumor, voxel counts times real voxel size give volumes, and the reply is built only from those outputs. The same backend drives NiiVue in a browser or 3D Slicer.
The repo has the agent, the NiiVue web app, a 3D Slicer module, the 96-request benchmark with its scorer and analysis, and the tumor model, which comes from System 06. The scans are the public TumSeg database (CC-BY).
# a local model with tool calling ollama pull qwen2.5:3b # the viewer + agent at http://127.0.0.1:8417 python3 -m scanspeak.server # the benchmark: needs only Ollama, no scans python3 -u bench/run_bench.py && python3 bench/analyze.py --repair