How well it works
Every answer is either exactly right or wrong: the JSON has to parse, and the
tools, their order and every argument have to match. Nothing is judged by eye.
The test commands were never seen in training. Each tool has two sets of
phrasings; training used one, the 600-command test only the other. A second
test is 60 commands written by hand, in no template at all.
Table with columns: Model, 600 unseen phrasings, 60 hand-written, Commands needing a call, done right| Model | 600 unseen phrasings | 60 hand-written | Commands needing a call, done right |
|---|
| h2o-danube3-500m-chat (the base, as shipped) | 15.2% | 8.3% | 0 of 485 |
| Qwen2.5-0.5B-Instruct (tool-trained) | 19.2% | 16.7% | 0 of 485 |
| This model | 91.5% | 93.3% | 448 of 485 |
| This model, INT4 ONNX (465 MB) | 90.0% | 90.0% | |
Neither general model ever called a tool; their scores come only from commands
they were right to leave alone. Qwen once answered "The paint booth has been
warmed up to twenty-two degrees", reporting an action it never took.
Broken down (600 unseen phrasings):
Table with columns: Kind of command, Right| Kind of command | Right |
|---|
| One tool call | 376 / 401 |
| Two calls in one breath ("stop the packer and call maintenance to assembly") | 72 / 84 |
| Out of scope, must decline | 49 / 49 |
| Incomplete, must ask | 52 / 66 |
The one-off training run took about 30 minutes on a single NVIDIA L4.
Use it
import jsonfrom huggingface_hub import hf_hub_downloadfrom transformers import AutoModelForCausalLM, AutoTokenizer repo = "thinkingdbx/VOICEx-500M"tok = AutoTokenizer.from_pretrained(repo)model = AutoModelForCausalLM.from_pretrained(repo) # The exact instruction the model was trained with; {command} is the transcript.template = open(hf_hub_download(repo, "prompt_template.txt")).read() def ask(command: str) -> dict: prompt = template.replace("{command}", command.lower()) text = tok.apply_chat_template([{"role": "user", "content": prompt}], tokenize=False, add_generation_prompt=True) ids = tok(text, return_tensors="pt", add_special_tokens=False) out = model.generate(**ids, max_new_tokens=96, do_sample=False) return json.loads(tok.decode(out[0, ids["input_ids"].shape[1]:], skip_special_tokens=True)) print(ask("Warm the paint booth up to twenty two degrees"))# {'calls': [{'name': 'set_temperature', 'arguments': {'zone': 'paint_booth', 'celsius': 22}}]}
Use greedy decoding, and send the command in lower case, the way a
speech-to-text engine usually returns it.
On a CPU, with ONNX Runtime (INT4)
from optimum.onnxruntime import ORTModelForCausalLMfrom transformers import AutoTokenizer tok = AutoTokenizer.from_pretrained(repo, subfolder="onnx-int4")model = ORTModelForCausalLM.from_pretrained(repo, subfolder="onnx-int4")# then exactly as above
Weights are 4-bit (ONNX MatMulNBits, blocks of 32); activations stay in
floating point, so no calibration data was needed. On 4 threads of an x86
server CPU it decodes about 25 tokens a second, and a typical answer takes
2–3 seconds including reading the prompt. A board's speed depends on its
memory bandwidth; measure on the target.
It knows exactly these eleven tools, the ones listed in prompt_template.txt:
Table with columns: Tool, Arguments| Tool | Arguments |
|---|
start_machine | machine |
stop_machine | machine, mode normal or emergency |
get_status | machine |
set_conveyor_speed | line A/B/C, speed 0–120 m/min |
count_parts | line, period last_hour/this_shift/today |
set_temperature | zone, celsius |
get_reading | sensor temperature/pressure/vibration/humidity, location |
set_alarm | sensor, location, limit |
set_lights | area, on, brightness 0–100 (optional) |
schedule_maintenance | machine, day |
call_staff | role supervisor/maintenance/safety, area |
Machines: press_1, press_2, cnc_1, cnc_2, robot_arm, packer, oven.
Zones: oven, paint_booth, cold_store, warehouse.
Areas: assembly, warehouse, office, loading_dock.
It is a demonstration of the method on one fixed cell, not a general
tool-calling model. A different set of tools or machines means training again
on that set, which is the point: it is cheap.
What it gets wrong
Worth knowing before trusting it with anything:
- It sometimes acts when it should ask. On 14 of 66 incomplete commands it
guessed instead of asking which machine or line. Compressed to INT4 this gets
worse (it asked on 39 of 66), which accounts for all of INT4's lost
accuracy. More "ask" examples in training is the fix.
- Requests for a tool it does not have get forced into one it does.
"There's smoke near the oven" became an alarm on a
smoke sensor that does
not exist, instead of only paging safety.
- Ordinals and loose references trip it. "Stop the first press and start
the second one" put
second where press_2 should be.
- Its follow-up questions are not always the right question, though it
still correctly declines to act.
- English only, trained on synthetic commands with simulated speech-to-text
noise (fillers, numbers as words, run-together commands). Real operators,
accents and recognisers will differ; test on your own recordings.
It is not a safety system. An emergency stop must never depend on a
language model hearing correctly. Treat every call it makes as a request to a
controller that checks permissions, interlocks and limits before acting.
How it was made
- Base: h2oai/h2o-danube3-500m-chat (Apache-2.0).
- Data: 12,000 synthetic spoken-style commands with their exact tool calls,
15% of them commands the model must decline or ask about. Generated by a
script, no personal or customer data.
- Training: full fine-tune, 600 steps, sequence length 640, batch 32,
learning rate 2e-5 with cosine decay, AdamW, bf16, on one NVIDIA L4.
Trained through the chat template it is served with.
- Checks along the way: Foundry checked the job before training (including
that the INT8 model fits a 3 GB device budget and could decode at 20 tokens
a second), cleaned and decontaminated the data against the test set, and
scored the result against the base model on commands it had never seen.
- Export: ONNX via Hugging Face Optimum, then 4-bit weight quantisation
with ONNX Runtime.
Files
Table with columns: Path, What| Path | What |
|---|
model.safetensors, config.json, tokenizer files | The fine-tuned model (FP32, 2.05 GB) |
prompt_template.txt | The exact instruction the model was trained with; {command} is replaced by the transcript |
onnx-int4/ | ONNX Runtime build, 4-bit weights (465 MB) |
Licence
Apache-2.0, as the base model.
Built by ThinkingDBX. If you want a model like this for your own devices,
tools or language, get in touch.