From nothing to a working call in about ten minutes.
- A machine running Docker with compose (a NAS is fine — that is where this was built).
- A running SUB/WAVE station. Talk Wave is its companion phone line, not a standalone radio.
- One LLM API key (OpenAI, Anthropic, Google, DeepSeek, OpenRouter, Requesty, Vercel AI Gateway) — or your own box, which needs none: Ollama, an OpenAI-compatible server, or the station's locca. Speech-to-text ships in the box; everything else has a working default. Which model and voice actually carry a call: what to run.
curl -fsSL https://raw.githubusercontent.com/mrain1p/Talk-Wave/main/install.sh | bashFetches the stack, generates the LiveKit secret, detects your LAN address, prepares data/, and starts everything — then tells you the address to open. It refuses to touch a folder that already holds a deployment. If you'd rather see every move, the same steps by hand:
Make a folder, grab four files from this repo — docker-compose.yaml, Caddyfile, .env.example and livekit.example.yaml — and create one empty directory:
mkdir talk-wave && cd talk-wave
wget https://raw.githubusercontent.com/mrain1p/Talk-Wave/main/docker-compose.yaml
wget https://raw.githubusercontent.com/mrain1p/Talk-Wave/main/Caddyfile
wget -O .env https://raw.githubusercontent.com/mrain1p/Talk-Wave/main/.env.example
wget -O livekit.yaml https://raw.githubusercontent.com/mrain1p/Talk-Wave/main/livekit.example.yaml
mkdir data && chown -R 1000:1000 ./data && chmod -R u+rwX ./dataThe
chownmatters. The services run as uid 1000, and adata/they can't read means no setup ask and a locked panel. The login gate and the logs name it — but never seeing it is better.
Then two edits:
livekit.yaml— paste a fresh secret underkeys:(the file shows the one-line generator). The app reads the keypair from here too, so it lives in one file..env— setHOST_IPto this machine's LAN address (it drives LiveKit's advertised media address, the browser URL, and the webhook callback), and setSUBWAVE_STREAM_URLto the station's publichttps://stream. Don't skip the stream URL: left blank it derives a plain-http URL that browsers silently block as mixed content, and the caller hears no station.
The docker-compose.yaml those files feed runs the four services — LiveKit for the call media, the worker (the DJ itself), the web half (tokens, widget, panel), and the bundled Caddy TLS door, whose Caddyfile carries the widget route and the /rtc WebSocket route.
That is the whole configuration surface on disk. Everything else — model, voice, permissions, the lot — is set later in the settings panel and applies to the next call without a restart.
docker compose up -dOpen https://<HOST_IP>:8443. The first visit shows a one-time certificate screen (self-signed TLS — HTTPS is required for the microphone); proceed past it, then:
- Set the admin password — the page asks before anything else, because until one exists the panel is open to whoever can reach it and the line itself answers nobody: no calls, texts or voicemail until the password is set.
- Open the settings (the gear), go to Configuration, point SUB/WAVE Station at your station and add your LLM key under Brains.
- Run the pipeline check (Diagnostics page) — twelve stages that walk every leg of a real call in order and name the first thing that would break.
- Press Call.
Everything the deployment owns is in that one folder, and only data/ ever changes — the app fills it as you use things (settings, keys, transcripts, uploads). Backing up = copying data/ + .env + livekit.yaml. The two Docker volumes hold only re-downloadable state.
- A real domain and a trusted certificate (no certificate screen): networking — the TLS front door.
- Your own reverse proxy instead of the bundled Caddy: replicate both Caddyfile routes — the widget and
/rtc— or calls connect with no audio; networking has the details. - Callers from outside your network: networking — one router rule plus one compose line.
- Every setting explained: settings. Hardening: security. When a call goes wrong: troubleshooting.