The AY-Robots policy comparison table showing GR00T N1.7, GR00T N1.5, Pi0.5, SmolVLA and ACT with parameters, GPU tier, inference latency and minimum episodes
Voice controlSpeech recognitionLanguage conditioningVLALeRobotSO-100

Voice Control for Robot Arms: Whisper in Front of a Policy

AY-Robots ResearchAugust 23, 202625 min read

Speech recognition turns audio into the same string that --task already carries. Here is how to build the pipeline with Whisper, and why a one-instruction dataset makes it useless.

The demo is always the same. Somebody leans towards a laptop, says "pick up the red block", and a SO-100 arm reaches out and picks up the red block. It looks like the arm understood English. It did not. What happened is that a speech recogniser turned about two seconds of audio into a string, and that string was handed to a policy through exactly the same input slot that a command-line flag fills. If you had typed the sentence instead, the arm would have done the identical thing.

That distinction is the whole subject. Speech recognition is a text producer sitting in front of a language-conditioned policy. It is cheap, it is well understood, and it works. What it cannot do is give the policy a vocabulary it never learned, or make it care about words it was trained to ignore. This article covers both halves honestly: how to build the voice front end with software you can install today, and what remains broken on the other side of it.

What you need to know before you wire a microphone to a robot

  • Speech recognition emits a string. That string lands in the same field that lerobot-rollout --task already fills, and that lerobot-record --dataset.single_task wrote into your dataset. Nothing else about the policy changes.
  • Of the five policies this platform trains, ACT has no language input at all. The LeRobot docs annotate the --task flag on an ACT rollout with the comment "can be skipped for ACT".
  • lerobot-record stamps one task string onto every frame of a session. A dataset recorded the ordinary way contains exactly one instruction, so a policy trained on it has nothing to condition on and a perfect transcript changes nothing.
  • Whisper turbo is 809 M parameters, wants about 6 GB of VRAM and runs at roughly 8x the speed of large. Streaming wrappers around Whisper report about 3.3 s of latency on unsegmented long-form speech.
  • Speech is a start-of-episode cost, not a per-step cost. Per-step policy inference on this platform runs 20 ms (ACT) to 485 ms (Pi0.5). Do not put the recogniser inside the control loop.
  • Language following is a property of the training run, not of the microphone. NVIDIA reports 93.3 percent language following for GR00T N1.5 against 46.6 percent for N1 on the same real GR-1 task. No ASR upgrade moves that number.

What the policy actually receives when you talk to it

Audio never reaches the policy. Every vision-language-action model on this platform takes an image stack, a proprioceptive state vector and a text instruction. The instruction is a short plain-language sentence, and the model has its own opinion about how much attention to pay to it. The five trainable policies differ enormously here, and the difference decides whether voice control is even meaningful for your setup.

PolicyTakes a language input?How the text gets inPer-step inference
ACTNoThe --task flag is accepted and ignored. The LeRobot ACT page marks it "can be skipped for ACT".20 ms
SmolVLAYesA natural language instruction, one of three inputs alongside camera views and sensorimotor state.245 ms
GR00T N1.5Yesannotation.human.task_description in the dataset, an integer index into meta/tasks.jsonl.165 ms
GR00T N1.7YesSame dataset key. At inference the text arrives as observation["language"]["task"], shape (B, 1).152 ms
Pi0.5YesLanguage commands, trained jointly with object detections and semantic subtask prediction.485 ms

The per-step numbers come from the policy comparison. They matter later, when we work out where the recogniser can and cannot sit. For now the important column is the second one. If you trained ACT, there is no language input to speak into. You can build a beautiful voice pipeline and the arm will do the one thing it knows, every time, regardless of what you said.

python
# GR00T N1.7 policy input, from getting_started/policy.md in NVIDIA/Isaac-GR00T
observation = {
    "video": {...},              # camera frames
    "state": {...},              # joint positions
    "language": {
        "task": [["pick up the cube"]],   # shape (B, 1), list of lists of strings
    },
}

action, info = policy.get_action(observation)
# action has shape (B, T, D); the base checkpoint predicts an action horizon of 40 steps
The language input to GR00T is a list of lists of strings. That is the entire interface a voice front end has to fill.
bash
# LeRobot deploys the same string through a flag
lerobot-rollout \
  --strategy.type=base \
  --policy.path=${HF_USER}/my_policy \
  --robot.type=so100_follower \
  --robot.port=/dev/ttyACM1 \
  --robot.cameras="{ up: {type: opencv, index_or_path: /dev/video10, width: 640, height: 480, fps: 30}}" \
  --task="Put lego brick into the transparent box" \
  --duration=60
lerobot-rollout, from the LeRobot imitation-learning guide. The SmolVLA page repeats the same command with a note next to --task: use the same task description you used in your dataset recording.
ACT will accept your sentence and throw it away

The LeRobot ACT documentation shows --task="Your task description" in the rollout command with the inline comment # can be skipped for ACT. ACT's inputs are RGB frames, joint positions and a latent style variable set to zero at inference. There is no text encoder in it. If voice control is a requirement, ACT is off the table before you write a line of audio code. Compare it against a language-conditioned model on ACT versus SmolVLA before you commit to a training run.

The one-task dataset problem, which is where most voice projects die

Here is the failure that costs people a weekend. They record a LeRobot dataset the standard way, fine-tune SmolVLA, wire up Whisper, and discover the arm performs the same motion whatever they say. The transcript is perfect. The policy is trained. Nothing is broken. The dataset simply never contained more than one instruction.

Look at what the recorder actually writes. The dataset format stores a per-frame task, but the recording script fills it from a single flag that does not change for the duration of the session. What makes those episodes worth training on at all is a separate subject, covered in collecting high-quality VLA training data. Here we care about one field only: the one that carries the words.

bash
lerobot-record \
    --robot.type=so101_follower \
    --robot.port=/dev/tty.usbmodem585A0076841 \
    --robot.id=my_awesome_follower_arm \
    --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 1920, height: 1080, fps: 30}}" \
    --teleop.type=so101_leader \
    --teleop.port=/dev/tty.usbmodem58760431551 \
    --teleop.id=my_awesome_leader_arm \
    --dataset.repo_id=${HF_USER}/record-test \
    --dataset.num_episodes=5 \
    --dataset.single_task="Grab the black cube"
The flag is called single_task and it means it. Defaults elsewhere in the same recorder: 60 s per episode, 60 s reset, 50 episodes.
python
# src/lerobot/scripts/lerobot_record.py
frame = {**observation_frame, **action_frame, "task": single_task}
dataset.add_frame(frame)
src/lerobot/scripts/lerobot_record.py, the two lines that write the task. Every frame of every episode in the session gets the same string; there is no per-episode override in the recording loop.

The result is a set of episodes whose task table has exactly one row. GR00T's data preparation guide shows what a multi-instruction table looks like by contrast, and the shape makes the point on its own.

json
// demo_data/cube_to_bowl_5/meta/tasks.jsonl in NVIDIA/Isaac-GR00T
{"task_index": 0, "task": "cube into yellow bowl"}
{"task_index": 1, "task": "cube into green bowl"}
Two rows, two behaviours, and a word that distinguishes them. This is the minimum condition for language to mean anything.
One row in tasks.jsonl means language conditioning is mathematically impossible

If every frame the model ever saw carried the same instruction, the instruction carries zero information about the correct action. Gradient descent will happily learn to ignore it, because ignoring it costs nothing on the training loss. This is not a bug in the policy and it is not something a better microphone fixes. Check before you train: count the distinct rows in meta/tasks.jsonl. If the answer is 1, voice control is decoration. See loss falls, policy does nothing for the neighbouring failure mode.

The AY-Robots policies comparison table listing GR00T N1.7, GR00T N1.5, Pi0.5, SmolVLA and ACT with parameter counts, GPU tier, inference latency and minimum episodes
The five trainable policies side by side. The language question is not in this table, which is exactly why people get caught: ACT looks like the cheap sensible default until you want to talk to it.

The voice front end, component by component

Assume you have done the dataset work and your policy genuinely responds to different instructions. The audio side is now a well-trodden four-stage pipeline: gate on speech, optionally gate on a wake word, transcribe, then map the transcript onto a string the policy has actually seen. Every piece below is open source and was checked against its repository for this article.

  1. 1
    Capture 16 kHz mono audio

    Everything downstream expects 16 kHz. Whisper resamples internally, Silero VAD supports 8000 Hz and 16000 Hz only, and openWakeWord wants frames in multiples of 80 ms, which is 1280 samples at 16 kHz. Pick 16 kHz once and stop converting.

    bash
    pip install sounddevice numpy
    # 1280 samples at 16 kHz = 80 ms, the openWakeWord frame size
  2. 2
    Gate on voice activity so the recogniser never sees silence

    Silero VAD is about two megabytes as a JIT model and processes a 30 ms chunk in under 1 ms on a single CPU thread, under an MIT licence. This step is not an optimisation, it is a correctness fix: Whisper invents text when handed silence.

    python
    from silero_vad import load_silero_vad, read_audio, get_speech_timestamps
    
    model = load_silero_vad()
    wav = read_audio('utterance.wav')
    speech_timestamps = get_speech_timestamps(wav, model, return_seconds=True)
  3. 3
    Add a wake word if the robot shares a room with humans

    openWakeWord suggests a false-accept rate below 0.5 per hour with a false-reject rate below 5 percent as reasonable in practice, and reports that a single core of a Raspberry Pi 3 can run 15 to 20 of its models simultaneously in real time. That budget matters when the same machine is also driving servos.

    python
    import openwakeword
    from openwakeword.model import Model
    
    openwakeword.utils.download_models()
    model = Model(wakeword_models=["path/to/model.tflite"])
    prediction = model.predict(frame)   # frame = 80 ms of 16 kHz audio
  4. 4
    Transcribe the gated segment

    faster-whisper is the pragmatic default on a machine with a GPU. Its own benchmark on large-v2 reports 1m03s at fp16 against 2m23s for the reference openai/whisper implementation, and 59s at int8 with VRAM down from 4708 MB to 2926 MB.

    python
    from faster_whisper import WhisperModel
    
    model = WhisperModel("large-v3", device="cuda", compute_type="float16")
    segments, info = model.transcribe(
        "utterance.wav",
        beam_size=5,
        vad_filter=True,
        vad_parameters=dict(min_silence_duration_ms=500),
    )
    text = " ".join(s.text for s in segments).strip()
  5. 5
    Snap the transcript onto a task string the policy has seen

    This is the step people skip and it is the one that decides whether the system works. Do not pass the raw transcript to the policy. Match it against the exact strings in meta/tasks.jsonl and refuse anything that does not match closely enough. A policy fed an unseen phrasing is out of distribution, and out of distribution means it does something, just not what you asked.

    python
    import json, difflib
    
    TASKS = [json.loads(l)["task"]
             for l in open("meta/tasks.jsonl") if l.strip()]
    
    def snap(transcript, cutoff=0.6):
        hit = difflib.get_close_matches(
            transcript.lower().strip(".!? "),
            [t.lower() for t in TASKS], n=1, cutoff=cutoff)
        if not hit:
            return None          # say "I do not know that command" and stop
        return TASKS[[t.lower() for t in TASKS].index(hit[0])]
  6. 6
    Hand the canonical string to the running policy

    For LeRobot policies that is the --task value on the rollout, or the task field of the observation if you drive the policy from your own loop. For GR00T it is the language.task entry. Set it once at the start of an episode; do not rewrite it every control step.

    bash
    TASK=$(python voice_listen.py)   # prints one canonical task string, or exits non-zero
    
    lerobot-rollout \
      --strategy.type=base \
      --policy.path=${HF_USER}/my_policy \
      --robot.type=so100_follower \
      --robot.port=/dev/ttyACM1 \
      --task="$TASK" \
      --duration=60
ComponentPackageSizeCost the repo statesLicence
Voice activity detectionsilero-vadabout 2 MB (JIT)under 1 ms per 30 ms chunk, one CPU threadMIT
Wake wordopenwakewordmelspectrogram + shared embedding + small classifier15 to 20 models in real time on one Raspberry Pi 3 coreApache-2.0
ASR, GPUfaster-whisperlarge-v3 weightslarge-v2 fp16 beam 5: 1m03s, 4525 MB VRAMMIT
ASR, CPU or edgewhisper.cppbase 142 MiB on diskabout 388 MB memory for base; large is 2.9 GiB and about 3.9 GBMIT
ASR, streamingwhisper_streamingwraps faster-whisper or mlx-whisperabout 3.3 s latency, prefix confirmed by 2 agreeing iterationsMIT

Latency: a start-of-episode cost, not a control-loop cost

The most common design mistake is to treat the recogniser as part of the control loop. It is not, and it must not be. Inference latency on this platform runs from 20 ms per action step for ACT to 485 ms for Pi0.5. A streaming Whisper wrapper needs roughly 3.3 seconds to confirm a transcript. Those two numbers live in different worlds and should never share a loop.

StageTypical costWhere it belongsSource of the number
Wake word detection80 ms frame granularityalways on, separate threadopenWakeWord repo
Voice activity gatingunder 1 ms per 30 ms chunkalways on, same thread as capturesilero-vad repo
Whisper transcription, batch on a segmentseconds, scales with utterance lengthonce, before the episode startsfaster-whisper benchmark
Whisper transcription, streamingabout 3.3 s to confirmonce, before the episode startswhisper_streaming
Policy action step, ACT20 msinside the control loopAY-Robots policy data
Policy action step, GR00T N1.7152 msinside the control loopAY-Robots policy data
Policy action step, Pi0.5485 msinside the control loopAY-Robots policy data

Read the table the right way round. Three seconds of speech latency before the arm moves is fine, because a human said something and expects a pause. Three seconds inside an action chunk is a wrecked policy. So the architecture is: recognise, commit to a string, then start the rollout. If you want mid-episode interruption, implement it as a stop signal, not as a re-prompt. A stop can be a keyword spotter running at 80 ms granularity; a full re-transcription cannot keep up with a 20 ms loop and should not try.

Where remote inference makes this worse

On AY-Robots, /api/inference/pod provisions a cloud GPU that serves the policy and the local robot client talks to that endpoint. That works for slow pick-and-place and it does not work for fast reactive motion: the control loop is already 20 to 485 ms per action step, and public-internet round trips on top turn a working policy into a hesitant one. A voice front end does not change this either way, because it runs before the loop. But if you are already paying network latency per action step, adding a cloud ASR API on the same path is how a two second interaction becomes a five second one. Run the recogniser locally. See the client guide for where the pieces sit.

Choosing a Whisper, with the numbers the repos publish

Whisper is OpenAI's open-weight speech recognition model, and the repository publishes a sizing table that is the only guide you need for picking one. The table below is that table, read from the upstream README in August 2026. For the wider story of how language got into robot policies at all, the vision-language-action overview is the background reading.

SizeParametersMultilingual nameRequired VRAMRelative speed
tiny39 Mtinyabout 1 GBabout 10x
base74 Mbaseabout 1 GBabout 7x
small244 Msmallabout 2 GBabout 4x
medium769 Mmediumabout 5 GBabout 2x
large1550 Mlargeabout 10 GB1x
turbo809 Mturboabout 6 GBabout 8x

The turbo entry is worth understanding rather than just picking. The repository describes it as an optimised version of large-v3 that transcribes faster with a minimal degradation in accuracy, and the same README carries a blunt warning beside it: the turbo model is not trained for translation tasks, and it will return the original language even if you pass --task translate. For a fixed command vocabulary in one language that trade is almost free. If you ever need non-English speech turned into English, use one of the multilingual models instead.

bash
# reference implementation
pip install -U openai-whisper
whisper audio.wav --model turbo

# GPU, roughly 2x faster than the reference at fp16 on the repo's own benchmark
pip install faster-whisper

# CPU or edge, C++ with quantisation
cmake -B build && cmake --build build -j --config Release
sh ./models/download-ggml-model.sh base.en
./build/bin/whisper-cli -m models/ggml-base.en.bin -f samples/jfk.wav

# quantise to shrink the model further
./build/bin/quantize models/ggml-base.en.bin models/ggml-base.en-q5_0.bin q5_0
Three installs, three deployment situations. The whisper.cpp build lines take -DGGML_CUDA=1 or -DGGML_VULKAN=1 if you have an accelerator.
Cascaded speech to text to policy, against an end-to-end speech VLA
Why the cascade is the right default today
  • Every piece is swappable. Change the recogniser without retraining the policy, change the policy without touching audio code.
  • You can inspect the intermediate string. When the arm does the wrong thing you know immediately whether the transcript or the policy is at fault, which is most of the debugging value.
  • The snapping step gives you a hard vocabulary boundary. An end-to-end model has no equivalent refusal path.
  • It works with all three language-conditioned policies on this platform without any modification to the training pipeline.
  • You can test the policy half with no audio at all. Two text rollouts with different --task values tell you whether a voice front end is worth building, before you install a single audio package.
What the cascade costs you
  • Errors compound. A misheard word becomes a wrong task string, and the policy has no way to signal that it did not understand. It executes the nearest thing it knows.
  • Transcription throws away everything that is not words. Tone, urgency and who is speaking all disappear at the string boundary, so the robot cannot behave differently for different people.
  • Two models to keep in memory on an edge machine, on top of the policy.
  • The snapping step is a hand-written vocabulary. It does not generalise to phrasings you did not anticipate, which is a feature until a user says something reasonable and gets refused.

Make the recogniser deaf to everything that is not a command

Whisper's defaults are tuned for transcribing podcasts, not for a microphone that is open all day next to a servo bus. The reference CLI defaults, read from the source, are worth knowing before you tune anything: temperature 0 with a fallback increment of 0.2, beam size 5, best of 5, condition_on_previous_text True, no_speech_threshold 0.6, logprob_threshold -1.0 and compression_ratio_threshold 2.4. Two of those want changing for robot use.

bash
whisper utterance.wav \
  --model turbo \
  --language English \
  --condition_on_previous_text False \
  --no_speech_threshold 0.8 \
  --initial_prompt "pick up the red block. put the block in the bowl. open the gripper."
condition_on_previous_text False stops one bad segment poisoning the next. initial_prompt biases the decoder towards your command vocabulary; it is a prompt, not a grammar, so it nudges rather than constrains.
Whisper invents sentences out of silence, and it will command your robot with them

This is measured, not folklore. Careless Whisper: Speech-to-Text Hallucination Harms (ACM FAccT 2024) found that roughly 1 percent of audio transcriptions contained entire hallucinated phrases or sentences that did not exist in any form in the underlying audio, and that 38 percent of those hallucinations contained explicit harms. The effect was strongest for speakers with longer non-vocal stretches. A robot arm listening on an open microphone in a quiet lab is exactly the pathological case. Two defences, use both: gate every segment through a VAD so the recogniser never sees silence, and refuse any transcript that does not snap onto a known task string. Never let free text reach the policy.

What speech recognition cannot fix

Assume the transcript is perfect. Every remaining failure lives in the policy, and there are four of them worth naming, because each has a different fix and none of the fixes is audio.

SymptomActual causeWhat fixes it
Arm performs the same motion whatever you sayOne row in meta/tasks.jsonl, so language carried no information during trainingRecord additional sessions with different --dataset.single_task values, then retrain
Arm reaches for the frequently-seen object, not the named oneVision shortcut from dataset bias. The LIBERO-CF benchmark was built specifically to measure thisMore balanced object placement in the data; the paper's counterfactual guidance is an inference-time mitigation
"Grab the cube" works, "pick up the cube" does notPhrasing sensitivity. Semantically similar instructions can induce drastically different behavioursSnap transcripts to canonical strings, or augment the task table with paraphrases before training
Named object was never in the dataOut of distribution. The policy has no representation to ground the word ontoNew demonstrations. There is no prompt that substitutes for them

The second row is the one that looks like a language failure and is not. It is the same mechanism behind a policy that only works in one setup: the model latched onto whatever in the image correlated with success, and the instruction was never the cheapest signal available to it. That row deserves a number. When Vision Overrides Language (Fang and colleagues, February 2026) introduced LIBERO-CF to test whether policies still follow the instruction when vision and language disagree. Their inference-time mitigation, Counterfactual Action Guidance, raises language following accuracy on under-observed tasks by 9.7 percent for Pi0.5 with a training-free strategy, and in real-world evaluation reduces counterfactual failures by 9.4 percent. Read those as evidence of how much headroom is being lost to visual shortcuts in the first place. The third row has a number too: Learning What to Say to Your VLA (Jeong, Swamy and Bajcsy, June 2026) reports that searching over how the instruction is worded improves base VLA performance by 24.7 percent in simulation and 65.0 percent in hardware, on frozen pretrained policies with the weights untouched.

The contrast that makes the training side concrete is NVIDIA's own. On the same real GR-1 task, GR00T N1.5 follows the language command 93.3 percent of the time against 46.6 percent for N1, and on the Language Table benchmark 93.2 percent against 52.8 percent. That gap was closed by freezing and improving the vision-language model and changing the adapter, not by improving anybody's microphone. If your policy ignores words, the fix is upstream of the audio pipeline every single time. Comparing the language-capable models is a better use of an afternoon than tuning Whisper.

The AY-Robots MCP server page listing the platform operations exposed as tools to AI agents
A different sense of natural language control: the MCP server exposes platform operations to an agent, so an assistant can start a training run or launch a session by being asked. That is language over the workflow, not over the arm's motion. The two are often conflated in demos.

This distinction is worth being pedantic about, because the two things look identical in a video. Natural language over the platform's operations is a solved problem: an agent reads a tool schema and calls it, the same operations exposed by the CLI. Natural language over the arm's motion is not solved. It is a property of your training data, and it degrades exactly as far as your data is unbalanced. A high-level planner sits between the two: it decomposes a spoken instruction into a sequence of skills and calls them in order, but every skill in that sequence still has to have been learned from demonstrations first.

Recording a dataset that language can steer

If you want voice control to be more than a demo, the work is in the recording session, not the audio stack. The goal is a task table with several rows and enough demonstrations behind each row that the model has to read the words to predict the actions. The SmolVLA guide's own reference dataset is 50 episodes across 5 cube positions, 10 per position, and notes that 25 episodes was not enough. Treat that as a floor per instruction, not per dataset.

  1. 1
    Design a command set where vision alone cannot disambiguate

    Two bowls of different colours in every frame, and the instruction picks one. If the yellow bowl is only ever present when the yellow instruction is given, you have rebuilt the shortcut and the model will use it.

  2. 2
    Record one session per instruction

    Run lerobot-record once per task string. The flag is single_task and it applies to the whole session, so distinct instructions mean distinct invocations.

    bash
    lerobot-record ... --dataset.repo_id=${HF_USER}/two-bowls \
      --dataset.num_episodes=25 --dataset.single_task="cube into yellow bowl"
    
    lerobot-record ... --dataset.repo_id=${HF_USER}/two-bowls --resume=true \
      --dataset.root=/path/to/local/two-bowls \
      --dataset.num_episodes=25 --dataset.single_task="cube into green bowl"
  3. 3
    Verify the task table before you spend money on a GPU

    Count the rows. This is a five second check that saves a four to twelve dollar training run and several hours.

    bash
    wc -l meta/tasks.jsonl
    cat meta/tasks.jsonl
    # expect one line per distinct instruction, not one line total
  4. 4
    Convert the format if you are training GR00T

    The GR00T data preparation guide states the reason outright: GR00T currently uses the LeRobot v2 data format because many upstream datasets (DROID, LIBERO, Bridge) are published in v2. A v3.0 dataset has to be converted down first, with the conversion script that ships in the repo, and a rejected dataset is one of the more common training-time surprises.

    bash
    # LeRobot v3.0 -> v2, the script shipped in NVIDIA/Isaac-GR00T
    python scripts/lerobot_conversion/convert_v3_to_v2.py --help
    
    # then add meta/modality.json. The language block, from examples/SO100/modality.json:
    #   "annotation": {
    #       "human.task_description": { "original_key": "task_index" }
    #   }
    # the parquet column annotation.human.task_description is an int index into meta/tasks.jsonl
  5. 5
    Fine-tune a language-capable policy

    SmolVLA on a 24 GB card is the cheapest way to test whether language conditioning took. The training docs describe the form and what the backend actually sends. Its minimum on this platform is 30 episodes and a run costs about 1 to 3 dollars. GR00T N1.7 and Pi0.5 need an A100 or H100 80 GB and land at about 4 to 12 dollars. The command below is the LeRobot doc's own example; the AY-Robots training form sends its own defaults instead.

    bash
    lerobot-train \
      --policy.path=lerobot/smolvla_base \
      --dataset.repo_id=${HF_USER}/two-bowls \
      --batch_size=64 \
      --steps=20000 \
      --output_dir=outputs/train/my_smolvla \
      --policy.device=cuda
  6. 6
    Test with text before you test with a microphone

    Run the rollout twice with the two task strings and nothing else changed. If the arm does the same thing both times, stop. The audio pipeline cannot help and you will waste a day proving it.

    bash
    lerobot-rollout --strategy.type=base --policy.path=${HF_USER}/my_policy \
      --robot.type=so100_follower --robot.port=/dev/ttyACM1 \
      --task="cube into yellow bowl" --duration=60
    
    lerobot-rollout --strategy.type=base --policy.path=${HF_USER}/my_policy \
      --robot.type=so100_follower --robot.port=/dev/ttyACM1 \
      --task="cube into green bowl" --duration=60
The test that tells you everything in four minutes

Two rollouts, two instructions, identical scene. If the behaviour differs, you have a language-conditioned policy and a voice front end is worth building. If it does not, you have a single-task policy with an unused text input, and every hour spent on Whisper is an hour spent decorating. Do this before you write any audio code, and do it again after every retrain. It is the cheapest experiment in this whole article. Full walkthrough at run your first policy.

Two routes to a talking arm

You own every piece: the arm, the recording sessions, the GPU rental, the audio stack. Nothing here is exotic, but the pieces have to be assembled in the right order or you will debug the wrong half.

  1. Build or buy an SO-100 class arm and calibrate it. See the SO-100 setup guide.
  2. Design a command set of two to four instructions where the scene alone cannot disambiguate.
  3. Record one lerobot-record session per instruction, 25 to 50 episodes each. Verify meta/tasks.jsonl has one row per instruction.
  4. Fine-tune a language-capable policy: SmolVLA, GR00T N1.5, GR00T N1.7 or Pi0.5. Not ACT.
  5. Run two text rollouts with different --task values and confirm the behaviour actually differs.
  6. Only then: silero-vad for gating, openWakeWord if the room is noisy, faster-whisper for transcription, and a snapping function that refuses anything not in the task table.
bash
pip install faster-whisper silero-vad openwakeword sounddevice
pip install -e ".[smolvla]"       # in the lerobot checkout

# gate, transcribe, snap, launch
python voice_listen.py | xargs -I{} \
  lerobot-rollout --strategy.type=base \
    --policy.path=${HF_USER}/my_policy \
    --robot.type=so100_follower --robot.port=/dev/ttyACM1 \
    --task="{}" --duration=60
The 7.4 V trap, while you have the hardware open

SO-100 and SO-101 arms use Feetech STS3215 bus servos in the 7.4 V variant. A 12 V version of the same part number exists and feeding 12 V to a 7.4 V servo destroys it. This has nothing to do with voice control and everything to do with the fact that people wiring a new microphone tend to be rewiring the bench at the same time.

The short version

Speech recognition in front of a robot policy is a solved engineering problem with well-documented components and published numbers. Whisper and its faster reimplementations, a 2 MB voice activity detector, and an 80 ms wake word model will get you from a microphone to a canonical task string reliably, on hardware you already own, for the cost of an afternoon.

The unsolved part is on the other side of the string, and it is not an audio problem. It is a data problem, measurable before you buy a microphone by counting the rows in a JSON Lines file. A policy trained on one instruction cannot be steered by words. A policy trained on unbalanced scenes will follow the picture instead of the sentence. And ACT has no text input at all, which is worth knowing before you pick it because it is otherwise the cheapest and fastest of the five. Fix the dataset, then wire up the microphone. In that order the whole thing takes a weekend. In the other order it takes a month and does not work.

The AY-Robots try page showing three ways to start without owning a robot: drive a real arm, compare models, rent a GPU
Three entry points that need no hardware. Comparing the five policies is the one that matters here, because the language question is decided at model selection.

Only three of the five policies can hear you

ACT has no language input. SmolVLA, GR00T N1.5, GR00T N1.7 and Pi0.5 do, at 245, 165, 152 and 485 ms per action step and very different GPU tiers. Compare them on parameter counts, latency, minimum episodes and dataset format before you record a single episode.

Compare the five policies

Frequently asked questions

Can I just talk to my SO-100 and have it do what I say?

Only within the set of instructions the policy was trained on. Speech recognition turns your sentence into a string, and that string goes into the same field as the --task flag. If your dataset contained one instruction, the policy learned one behaviour and will perform it whatever you say. If it contained several distinct instructions with balanced scenes behind them, then yes, spoken commands select between them. Nothing in the audio path adds capability the training run did not.

Does ACT support voice commands?

No. ACT takes RGB frames, joint positions and a latent style variable. The LeRobot documentation shows --task in the ACT rollout example with the comment that it can be skipped for ACT. It is about 80 M parameters and 20 ms per action step, which makes it the fastest and cheapest option on this platform, but the text input simply is not there. If you need language, use SmolVLA, GR00T N1.5, GR00T N1.7 or Pi0.5.

Which Whisper model should I run on the machine next to the arm?

For a fixed command vocabulary in one language, small or base through whisper.cpp is usually enough and costs about 388 MB of memory for base. If you have a GPU on that machine, faster-whisper with large-v3 at int8 is the better default; the repo benchmarks large-v2 at 59s and 2926 MB VRAM at int8 against 2m23s and 4708 MB for the reference implementation. Turbo, at 809 M parameters and about 6 GB VRAM, is the sweet spot when you want large-v3 quality with roughly 8x the speed and do not need translation.

Will speech recognition slow down my control loop?

Not if you architect it correctly. Recognition happens once before the rollout starts, then the confirmed string is fixed for the episode. Streaming Whisper wrappers report roughly 3.3 s of latency, which is unusable inside a loop that runs at 20 to 485 ms per action step. If you need to interrupt mid-episode, use a wake word spotter as a stop signal at 80 ms granularity rather than re-transcribing.

The transcript is correct but the arm does the wrong thing. Where do I look?

Reproduce it with the text rollout first, passing the exact transcript as --task. If the failure reproduces without a microphone in the loop, the audio stack is exonerated and the problem is the policy. Then check three things in order: how many distinct rows meta/tasks.jsonl has, whether the scene is disambiguated by vision alone rather than by the instruction, and whether the phrasing you spoke matches the phrasing in the dataset exactly. Research on counterfactual failures in VLAs shows models frequently pick the frequently-observed object regardless of the instruction.

How many different instructions do I need before language conditioning actually takes?

There is no published minimum, but two anchors help. The LeRobot SmolVLA guide's reference dataset is 50 episodes across 5 distinct cube positions, 10 episodes per position, and the authors note that a similar dataset with 25 episodes was not enough and led to bad performance. The Isaac-GR00T cube_to_bowl demo dataset carries two rows in meta/tasks.jsonl, which is the smallest table where a word can carry any information at all. Treat 25 to 50 episodes per instruction as the working floor and start with two or three instructions rather than ten. Remember the platform minimums on top of that: SmolVLA needs at least 30 episodes, ACT and both GR00T models at least 50, so even a two-instruction dataset is a 60 to 100 episode recording job.

Sources

Sources

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started