Skip to content

Deploy an ASR Pipeline on AirStack Edge

This tutorial shows you how to build and deploy an automatic speech recognition (ASR) ensemble pipeline on an AirStack Edge device. The example pipeline receives raw IQ samples, demodulates GMRS narrowband FM audio with GNU Radio blocks, transcribes the audio with Voxtral, and formats the transcript for downstream consumers.

Edge ASR Pipeline

This workflow covers:

  • using GNU Radio blocks in a Triton Python backend model
  • using custom conda environments for Triton Python backend models
  • chaining multiple Triton models together into an ensemble model
  • uploading and enabling models in the AirStack Edge Client UI
  • testing an ensemble model with a raw bytes file using the UI

Background

An ensemble model is a model pipeline that Triton Inference Server schedules as one logical application. Instead of asking a client to call each stage manually, the ensemble configuration maps the output tensors from one model into the input tensors of the next model. For more information on ensemble models, see the application note here.

The gmrs_voxtral_pipeline example uses three stages:

  1. fm_gmrs_demod: a Python backend model that wraps a GNU Radio GMRS Demodulator
  2. voxtral-mini-stt: a Python backend model that uses the Voxtral ASR model. This model is a Triton wrapper for the INT4 quantized Voxtral Mini 4B Realtime model available here
  3. voxtral_text_batcher: a Python backend post-processor that batches transcript text with radio metadata and returns a JSON string.

This ensemble gives you a single deployable application for the full RF-to-text workflow. Each model can be swapped or reused for different ASR pipelines.

GNU Radio Triton Models

Triton's Python backend allows you to wrap GNU Radio or other DSP code to run inside the same inference pipeline as the ASR model.

In this ensemble, fm_gmrs_demod is a Triton Python model that embeds a GNU Radio flowgraph. The model receives AirStack Edge IQ samples as a flat TYPE_FP32 tensor, views the data as complex64, pushes the complex samples into a custom NumPy-backed GNU Radio source block, and reads demodulated audio from a custom sink block. Between those two blocks, the flowgraph uses standard GNU Radio DSP blocks for filtering, squelch, and narrowband FM demodulation. You can use GNU Radio blocks for operations such as:

  • channel filtering and decimation
  • squelch and signal gating
  • FM, AM, or digital demodulation
  • resampling into an ASR-compatible audio rate
  • protocol-specific cleanup before inference

To build your own GNU Radio Triton model, reuse the source block that accepts a NumPy array from the Triton request, the sink block that stores the flowgraph output as a NumPy array, and keep the GNU Radio top block alive between execute calls. Keeping the flowgraph initialized avoids rebuilding DSP state for every inference request.

Custom Conda Environments

Triton's Python backend runs inside the inference-server container, but real applications often need packages beyond the container defaults. Additional dependencies can be packaged into a relocatable Conda environment with conda-pack and included in the model archive.

Each Python backend model that needs the environment includes the packed archive and points Triton to it with EXECUTION_ENV_PATH in config.pbtxt:

parameters [
  {
    key: "EXECUTION_ENV_PATH"
    value: { string_value: "$$TRITON_MODEL_DIRECTORY/conda-env.tar.gz" }
  }
]

When Triton loads the model, it unpacks and uses that environment for the model instance. This keeps model-specific dependencies separate from the base inference server and makes the model archive more portable between systems. Note: this will cause your model load to be slower as it unpacks the ennvironment.

Build the packed environment in a container that matches the runtime as closely as possible. For this tutorial, the optional rebuild path uses nvcr.io/nvidia/tritonserver:24.08-py3-igpu, the same Triton image used by AirStack Edge on an Orin NX. You can build it natively on ARM64 or from an x86 host with QEMU ARM64 emulation but native ARM64 will be much faster.

Prerequisites

Before starting, make sure you have:

  • an AirStack Edge device with the inference server installed and running
  • access to the Client UI at https://<hostname>:<port>/ui/
  • a bearer token for the device
  • enough free storage for the ASR model weights and inference archives

Step 1: Download the Inference Tarballs

Packaged tarballs for this tutorial can be found here. You can use the pre packaged files as is or unpack and re build your own using the source code.

wget https://archive.deepwavedigital.com/triton-models/fm_gmrs_demod.tar.gz
wget https://archive.deepwavedigital.com/triton-models/gmrs_voxtral_pipeline.tar.gz
wget https://archive.deepwavedigital.com/triton-models/text_batcher.tar.gz
wget https://archive.deepwavedigital.com/triton-models/voxtral-mini-stt.tar.gz

Step 2: Upload and Enable the Models in the Client UI

Open the Client UI located at https://<hostname>:<port>/ui/.

Enter your bearer token on the Connect to Device screen.

Then upload the models:

  1. Open the Apps tab.
  2. Click Upload.
  3. Select your fm_gmrs_demod.tar.gz download.
  4. Repeat for voxtral-mini-stt.tar.gz, voxtral_text_batcher.tar.gz, and gmrs_voxtral_pipeline.tar.gz. Note: the voxtral-mini upload will take time to upload
  5. Enable the models and wait for each model status to show READY. Note: the voxtral-mini model will be slow to load as it unpacks the custom conda environtment.

If a model does not reach READY, check the Console panel in the UI first. For lower-level errors, log in to the device and inspect the inference server container:

docker logs airstack-edge-inference-server

Step 3: Test with a Raw Bytes File

The UI Verify buttons allow you to inject a file directly into the model without opening a live receiver stream. This is the fastest way to verify that the model loads, accepts the expected input format, and returns transcript JSON. The test file must match the expected input type of your model in bytes. Providing sigmf metadata is optional.

For this example, the fm_gmrs_demod configuration expects RF data sampled at 1.92e6 samples per second and produces 16 kHz audio for Voxtral. Use a recording that matches the sample rate and contains a GMRS/NBFM voice signal inside the demodulator passband. A sample text file can be found here

To run the test:

  1. Open the Apps tab.
  2. Select gmrs_voxtral_pipeline.
  3. Click the sigmf button in the Verify column for the model.
  4. Choose the sigmf files from the test file link above or another compatible sigmf recording.
  5. Select verify.

Note: This verification can be slow to run due to the sample rate of the recorded file.

The expected response is a JSON string from text_batcher, for example:

{
   "documents":[
      {
         "document":"Here's a sample file for you.",
         "doc_index":2,
         "doc_metadata":{
            "streamId":"gmrs-462637500",
            "doc_id":"nomad-7310-gmrs-462637500",
            "timestamp":"2026-05-28T13:32:00.289640Z",
            "radio_metadata":{
               "annotations":[],
               "captures":[
                  {
                     "core:datetime":"2026-05-28T13:24:31.859Z",
                     "core:frequency":462637500,
                     "core:sample_start":0
                  }
               ],
               "global":{
                  "antenna:agc":true,
                  "antenna:clock_source":"Internal",
                  "antenna:gain":0,
                  "core:author":"nomad-7310",
                  "core:datatype":"cf32_le",
                  "core:description":"Deepwave AirStack Edge Recording",
                  "core:recorder":"Edge v1.0.0",
                  "core:sample_rate":1920000,
                  "core:version":"v1.0.0",
                  "gps:altitude":67.19999694824219,
                  "gps:latitude":39.9503641,
                  "gps:longitude":-75.1654439,
                  "gps:mode":"3d"
               }
            }
         }
      }
   ]
}

If the response contains an empty documents list, the model may be waiting for more audio before it emits a transcript.

Step 4: Run Against a Live Receiver Stream

After the raw file test works, use the same model with a live receive stream.

  1. Set the master_clock_rate to 61.44e6 and click Enable Device in the device sidebar.
  2. Set data_type to c_f32 and select your desired channel.
  3. Open the Process tab.
  4. Configure the receiver with a sample rate of 1.92e6 and choose your desired frequency based on your GMRS channel number.
  5. Open the stream using the Setup Stream button on the Receiver card.
  6. Select gmrs_voxtral_pipeline for your model name.
  7. Set your desired number of samples, ideally a number large enough to get a few seconds of audio: sample rate * seconds of audio (6 is suggested to start)
  8. Use single mode for one-shot validation or continuous mode for live transcription. For continuous mode, register a webhook or select the discard option
  9. Click Start.

For continuous operation, the final model output is posted through the selected webhook. The text_batcher output is already formatted as JSON, so register the webhook with JSON content type. For debugging, register the 'discard' webhook and monitor the debug tab on the client UI.

Optional Live Testing

If you have access to a GMRS radio you could now test your system with live data. Attach an antenna to your specified channel on your AirStack Edge radio. Tune your walkie talkie to the specified frequency and monitor the debug tab on the client UI or your webhook responses to see your transcripts begin once you begin talking.

Extensions

This tutorial demonstrates RF preprocessing, ASR, and postprocessing as separate Triton models connected by an ensemble. You can replace or add stages without changing the AirStack Edge workflow. The client still uploads models, enables the ensemble, and runs a single application.

Some potential extensions to this pipeline may be:

  • Batched Preprocessing: Live RF streams often contain long quiet periods, partial transmissions, or short audio chunks that are not useful by themselves. A preprocessing model can accumulate samples until it sees enough energy or voice activity, then emit a larger audio window to voxtral-mini. This reduces empty ASR requests and gives the model more context. For radio systems with push-to-talk behavior, this stage can also detect transmission boundaries and flush the ASR model when a transmission ends.

  • Event Driven Alerts: The current text_batcher groups transcript text and metadata into JSON. You could replace or extend it with a keyword filter that only emits output when certain words, call signs, locations, or phrases are heard. For example, the postprocessor could suppress routine traffic but post a webhook when a transcript contains an alert word, a unit identifier, or a configured phrase list. This type of model would use the decoupled Triton feature found here

  • Different Input Signals: Different signals can be supported by swapping the first model in the ensemble. The GMRS example uses narrowband FM demodulation, but the same pattern can support other front ends:

    • broadcast FM demodulation before ASR
    • AM voice demodulation
    • P25 or other digital voice decode before transcription or classification
    • channelizer output where one wideband capture is split into multiple narrowband ASR pipelines

For larger systems, you can also build multiple ensembles that share common stages. For example, one ensemble can run fm_gmrs_demod into voxtral-mini, while another uses a different demodulator but the same ASR and postprocessor. This keeps deployment modular: update the ASR model once, then reuse it across several RF pipelines.

Optional: Building a Packed Environment

If you have a model that requires a custom conda environment the run command below is a good start to create a tarball for use.

docker run --rm --platform linux/arm64 \
  --network=host \
  -v "$PWD:/workspace" \
  -w /workspace \
  nvcr.io/nvidia/tritonserver:24.08-py3-igpu \
  bash -lc '
    apt-get update &&
    apt-get install -y --no-install-recommends bzip2 ca-certificates git wget &&
    wget -q https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-aarch64.sh -O /tmp/miniforge.sh &&
    bash /tmp/miniforge.sh -b -p /opt/conda &&
    export PATH=/opt/conda/bin:$PATH &&
    mamba env create -f YOUR_CONDA_ENV_FILE.yml &&
    source /opt/conda/etc/profile.d/conda.sh &&
    conda activate YOUR_CONDA_ENV &&
    conda-pack -n YOUR_CONDA_ENV -o /workspace/YOUR_CONDA_ENV.tar.gz
  '

From an x86 host, enable ARM64 emulation first:

docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

Then run the same docker run --platform linux/arm64 command. The QEMU path is useful for CI and developer workstations, but it is slower than building on native ARM64. Test the rebuilt packed environment on the target device before creating new release tarballs.