A complete IVR (Interactive Voice Response) workflow built with Go, Asterisk ARI, Deepgram (STT + TTS), and Google Gemini (LLM). This project provides a fully automated conversational IVR system capable of:
- Recording the caller's request
- Transcribing speech β text
- Processing intent with Gemini
- Generating a spoken response via Deepgram TTS
- Playing the response back to the caller
This repository contains the first working MVP based on WAV file TTS output, with future support for RTP TTS streaming currently under development. It also ships a Vagrant + Tailscale setup that provisions the PBX VM for you.
- Incoming call enters a Stasis app
- System plays a welcome prompt
- User records a request
- The recording is transcribed using Deepgram STT
- The text is processed by Google Gemini (LLM)
- The LLM output is converted to audio via Deepgram TTS
- Asterisk plays the generated WAV file
This version uses file-based TTS instead of RTP streaming.
- Deepgram generates a Linear16 WAV file with an 8000 Hz sample rate
- The file is saved in a shared directory
- Asterisk retrieves and plays the file
- Ensures stability and avoids ARI ExternalMedia issues
Both:
- the caller recording, and
- the TTS response
are stored in the same folder, which is mounted as a Docker volume so both Asterisk and the Go app can access it.
Example (docker-compose):
/var/spool/asterisk/recording:/mnt/tts
- Ubuntu 22.04 + Asterisk 22 built from source, with ARI and a basic PJSIP endpoint
- The VM joins your Tailscale tailnet, so SIP softphones and debugging tools can reach it from any device on the tailnet
- Docker + Docker Compose installed, repository root mounted at
/vagrant
The stack includes:
- Go application (runs in a container on the VM, next to Asterisk)
- Shared mounted directory for recordings and TTS files
- Environment variable injection via
.env
This MVP is based on WAV playback. A more advanced version using RTP streaming through ARI ExternalMedia is being developed on a separate branch.
Caller (SIP softphone, e.g. over the tailnet)
β
Asterisk (native on the VM, Stasis app)
β ARI events + recording
Go IVR app (Docker container on the same VM)
β send audio β Deepgram STT
β text β Gemini LLM
β LLM output β Deepgram TTS (WAV file)
β saved to shared volume
Asterisk plays WAV file
Shared directory example:
/var/spool/asterisk/recording
ββ msg_<channelID>_<timestamp>.wav (caller recording)
ββ msg_<channelID>_<timestamp>_tts.wav (TTS response)
Code design notes
- Each incoming call is handled by a
CallSession(internal/ivr) that carries the per-call state and runs the steps in order: transcribe β generate reply β synthesize β play. internal/sttexposes its ownTranscriptionResult/Transcribe(), so the rest of the app does not depend on Deepgram's SDK types.internal/ariutilbuilds and validates the ARI connection from aConfigstruct, failing fast when a required variable is missing.
- Docker & Docker Compose (installed on the VM by the provisioning scripts)
- Asterisk 22+ with ARI enabled (provisioned by the Vagrant setup below)
- Vagrant + VirtualBox on the host (for the PBX VM)
- A Tailscale account and a reusable auth key
- Deepgram API key
- Google Gemini API key
.envfile configured (see below)mise(optional task runner used for the test tasks)
Guide: infra/README.md
The Vagrant setup provisions the PBX VM in this order: system dependencies (+ Docker) β Tailscale join β Asterisk build/install β Asterisk configuration (ARI, HTTP, PJSIP, dialplan, sounds).
Quick start, from the repository root:
cp env.example .env # then fill in the values
set -a && source .env && set +a # Vagrant reads variables from your shell, not from .env
cd infra/vagrant
vagrant upNotes:
set -amust be active while sourcing.env, otherwise the variables are not exported to thevagrantprocess and provisioning fails on missing variables.- Generate a reusable Tailscale auth key at https://login.tailscale.com/admin/settings/keys (reusable is handy if you destroy and recreate the VM).
- Ports 8088 (ARI), 4002 and 5060/udp are also forwarded to
localhost. - The prerecorded prompts in
infra/vagrant/assets/are copied into Asterisk's sounds directory during provisioning.
Register a softphone (Zoiper, Linphone, β¦) against the VM's Tailscale IP (tailscale ip -4 inside the VM), port 5060/UDP. The softphone device must be on the same tailnet.
- Default dev credentials:
1001/1001passβ override them withPJSIP_ENDPOINT_IDandPJSIP_PASSWORDbefore provisioning. - Dial the endpoint ID itself (
1001by default): plays a test prompt, useful to check SIP and audio. - Dial
6001: enters the Stasis app and starts the IVR.
Create a .env file in the project root (start from env.example):
# ------------------------------
# ARI
# ------------------------------
ARI_URL=http://localhost:8088/ari
ARI_WS_URL=ws://localhost:8088/ari/events
ARI_USERNAME=your_username
ARI_PASSWORD=your_password
ARI_APPLICATION_NAME=app_name_stasis
# ------------------------------
# DEEPGRAM
# ------------------------------
DEEPGRAM_API_KEY=your_deepgram_api_key
# ------------------------------
# GEMINI
# ------------------------------
GEMINI_API_KEY=your_gemini_api_key
# ------------------------------
# TAILSCALE (VM provisioning)
# ------------------------------
TAILSCALE_AUTHKEY=your_tailscale_authkey
TAILSCALE_HOSTNAME=pbx-server
β Notes:
ARI_USERNAME,ARI_PASSWORDandARI_APPLICATION_NAMEare used both by the provisioning scripts (written into Asterisk'sari.confand dialplan) and by the Go app, so they must be the same on both sides.ARI_URL/ARI_WS_URLkeeplocalhostin.env(host-side runs and integration tests). Inside the container,docker-compose.yamloverrides them withhost.docker.internal, which resolves to the VM throughextra_hosts..envis git-ignored β never commit it.
Asterisk runs natively on the VM; the Go app runs in a container next to it. From the host:
cd infra/vagrant
vagrant sshThen inside the VM:
cd /vagrant
docker compose up --buildThe Go app then:
- connects to Asterisk's ARI (
host.docker.internal:8088) - waits for Stasis events
- processes audio through STT β LLM β TTS
- writes WAV files to the shared folder (
/var/spool/asterisk/recording, mounted as/mnt/tts)
- Caller enters the Stasis app (dial
6001) - System plays the welcome message β
1record a request,0hang up - After recording β
1re-record,2listen back,3send the request,0hang up - The Go app fetches the recording through ARI, Deepgram transcribes it, Gemini generates a response, Deepgram creates a WAV file (a waiting sound plays meanwhile)
- Asterisk plays the TTS WAV back to the caller
- Afterwards β
1new recording,2listen to the request,3send,4replay the response,0hang up
Tasks are defined in mise.toml:
mise run test-unit # fast unit tests, no network (go test -short ./...)
mise run test-ariutil # ARI integration test β needs the VM running
mise run test-stt # Deepgram integration test β needs DEEPGRAM_API_KEY, consumes API quota
mise run stt-fixture # regenerate the STT test audio (needs espeak-ng); only when the test phrase changesIntegration tests are skipped in -short mode, and the integration scripts load .env automatically.
ari-stt-tts/
β
βββ docs/ <-- planning and implementation notes
β
βββ infra/
β βββ README.md <-- infrastructure guide
β βββ vagrant/
β βββ Vagrantfile
β βββ assets/ <-- prerecorded prompts, copied into Asterisk's sounds directory (/var/lib/asterisk/sounds/en) during provisioning
β βββ provisioning/
β βββ bootstrap.sh
β βββ dependencies.sh
β βββ network/ <-- tailscale.sh
β βββ asterisk/ <-- install.sh, configure.sh
β
βββ internal/
β βββ ai/ <-- gemini
β βββ ariutil/ <-- ARI client (config + connection)
β βββ externalmedia/ <-- about rtp (still in development)
β βββ ivr/ <-- call handling (handler, session, dtmf, sound, record)
β βββ stt/ <-- deepgram STT
β βββ tts/ <-- deepgram TTS
β
βββ scripts/ <-- helper scripts used by the mise test tasks
β
βββ Dockerfile
βββ docker-compose.yaml
βββ env.example <-- example env file (copy to .env)
βββ go.mod
βββ go.sum
βββ main.go
βββ mise.toml
βββ README.md
- STT uses Deepgram's REST pre-recorded API (the request is transcribed once the recording ends) β no live streaming yet
- RTP streaming not yet implemented (separate branch)
- No retry mechanism for ARI reconnect
- No multi-language support (English only for now)
- The
aiandttspackages are still being reworked
β STT β Gemini β TTS WAV β ARI event handling β Docker compose integration β Shared file-based workflow
Pull Requests are welcome!
Please branch from rtp.
This project is licensed under the MIT License. You are free to use, modify, distribute, and integrate this project into commercial or private software.
See the full license in the LICENSE file.