Real-time translated video calls. Up to 4 people talk face-to-face over P2P WebRTC, each in their own language — speech is transcribed, translated into every participant's language in parallel, and shown as live subtitles on each speaker's video. Includes an auto-translated text chat.
Each peer (browser)
├─ camera + mic ──► WebRTC mesh ──► other peers hear/see you directly (P2P)
└─ same mic track ──► MediaRecorder (webm/opus, 250ms) ──binary WS──► Axum server
└─► per-speaker Deepgram WS (STT)
interim → subtitle_interim (broadcast)
final → Groq fan-out → subtitle_final
{ it, en, es… } → each peer picks its lang
WebRTC signaling (offer/answer/ice) and chat are relayed by the server.
- 📹 P2P video calls — WebRTC full mesh, up to 4 peers (server never touches media).
- 🌍 Live translated subtitles — each utterance is transcribed and translated into every language in the room in parallel, shown on the speaker's video cell in your language.
- 💬 Auto-translated chat — messages arrive in your language, original shown below.
- 😀 Emoji reactions — send quick emoji reactions (👍 ❤️ 😂 👏 🎉 🔥 ...) that float over the speaker's video.
- ✋ Hand raise — raise your hand like in Google Meet to signal you want to speak.
- 🎚️ Controls — mute mic, camera on/off, speak-translations (TTS), hand raise, chat, leave.
- 🏠 Lobby — public rooms list their online members; tap to join. Rooms can be public or private.
- 🎛️ Pre-join — camera preview + camera/mic device selectors before entering.
- 🌐 Localized UI — all 8 supported languages, auto-detected from the browser (fallback English).
- 📱 Mobile-first — responsive video grid, chat as a bottom-sheet drawer.
Supported languages: Italian, English, Spanish, French, German, Portuguese, Japanese, Chinese.
| Layer | Tech |
|---|---|
| Backend | Rust — Axum 0.8 + Tokio (WS relay + signaling) |
| Video/Audio | WebRTC mesh (P2P), STUN-only |
| STT | Deepgram Nova-2 streaming WebSocket |
| Translation | Groq openai/gpt-oss-20b (parallel fan-out) |
| Frontend | Astro 5 + vanilla TypeScript modules (src/scripts/) |
| TTS | Browser SpeechSynthesis API |
Audio/video flows peer-to-peer; the server only relays signaling, runs STT, fans
out translations, and relays chat. Rooms are ephemeral (in-memory DashMap, no DB).
Peers connect to GET /ws?room=..&lang=..&name=..&id=..&public=.. and exchange JSON
text frames (audio is sent as binary frames):
- Client → server:
start/stop(speaking session),offer/answer/ice(WebRTC, relayed toto),chat,mute_audio/mute_video,emoji(reaction),hand_raise(toggle). - Server → client:
room_joined(your id + existing peers),peer_joined,peer_left,room_full, relayedoffer/answer/ice(withfrom),chat_message(with atranslationsmap),peer_muted,emoji_reaction,hand_raised,subtitle_interim,subtitle_final(with atranslationsmap). GET /rooms— lobby (public rooms + online members).GET /health— health check.
Existing peers initiate the WebRTC offer toward a newcomer (avoids offer glare).
- Rust (stable) + Cargo · Node 18+ + npm
- API keys:
DEEPGRAM_API_KEY(Nova-2 STT) andGROQ_API_KEY(GPT-OSS translation)
cp server/.env.example server/.env # add your keysServer (port 3001): cd server && cargo run
Client (port 4321): cd client && npm install && PUBLIC_WS_HOST=localhost:3001 npm run dev
Open http://localhost:4321 in two tabs (or two devices on your LAN), pick a language in each, join the same room, and you're on a translated call.
HTTPS:
getUserMediaand WebRTC need a secure context.localhostis exempt for dev; use HTTPS for LAN/remote.
cp server/.env.example server/.env
docker compose up --build # client :4321 · server :3001Frontend and backend deploy separately — Vercel is serverless and cannot host the persistent WebSocket relay, so the Rust server runs on Railway.
- New Project → Deploy from GitHub → this repo. Service Root Directory =
server(usesserver/Dockerfile+server/railway.toml,/healthhealthcheck). - Variables:
DEEPGRAM_API_KEY,GROQ_API_KEY(Railway injectsPORT). Optional ops:LOG_FORMAT=json(structured logs), andBETTERSTACK_SOURCE_TOKEN(+ optionalBETTERSTACK_INGEST_URL) to ship logs to Better Stack (spec 0063, off unless set). - Deploy, copy the public domain. Railway deploys are manual:
railway upfromserver/.
- Import this repo. Root Directory =
client(Astro auto-detected). - Env
PUBLIC_WS_HOST= your Railway domain (host only, no protocol). - Deploy.
Pushes to main auto-deploy the frontend; the Railway backend is deployed manually.
Production WebRTC: this uses STUN only (~85% of NATs connect). For reliable connectivity across symmetric NATs, add a TURN server to the
ICE_SERVERSlist inclient/src/scripts/webrtc.ts. Also restrictCorsLayer::permissive()to your origin.
- Uptime + alerts: a GitHub Actions cron (
.github/workflows/uptime.yml) pings the server/healthand the client, and scrapes/metricsto alert on 5xx error rate and p95 latency (job failure → owner email). Railway auto-restarts on crash. - External monitors: Better Stack uptime monitors, provisioned reproducibly from
infra/betterstack/(monitors.json+setup-monitors.mjs). - Metrics: Prometheus
GET /metrics(request totals by status class, latency histogram, live room/peer gauges) — spec 0058. - Logs: canonical JSON logs with request IDs (spec 0050); optional app-side shipping
to Better Stack Logs (spec 0063) when
BETTERSTACK_SOURCE_TOKENis set.
The backend (:3001) must be running with real keys for the network-backed tests
(chat/subtitles) — they're skipped if DEEPGRAM_API_KEY / GROQ_API_KEY are absent.
Server — Rust unit + integration tests (lifecycle / signaling / max-4 / mute need no APIs; chat + audio drive real Deepgram/Groq):
cd server
cargo test
# coverage (rustup toolchain):
rustup run stable cargo llvm-cov test --summary-only # ~86% linesClient — Playwright e2e (home/lobby, pre-join toggles, WebRTC video, translated chat,
subtitles, controls, room-full) with V8 coverage mapped to src/scripts/*.ts:
cd client
npm run test:e2e # builds an instrumented bundle, serves it, runs e2e
# → prints "client script coverage: ~88% lines"; HTML report in client/coverage/Multi-party subtitle pipeline (standalone, no browser) — scripts/pipeline-test.mjs
connects three peers (it/en/es), each speaks, asserting the subtitle_final fan-out:
say -v Alice -o it.aiff "Ciao a tutti, come va oggi?"
ffmpeg -y -i it.aiff -ac 1 -ar 16000 -c:a libopus -b:a 32k -f webm -live 1 it.webm
node scripts/pipeline-test.mjs it.webmCoverage: server ≈ 86% lines, client ≈ 88% lines (both ≥ 85%).
server/ Rust/Axum relay
src/{main,config,protocol,rooms,deepgram,groq,translator}.rs
Dockerfile · railway.toml · .env.example
client/ Astro 5 SPA
src/pages/index.astro screens + styles
src/scripts/{app,webrtc,audio-capture,chat,i18n}.ts
src/layouts/Base.astro
scripts/ pipeline-test.mjs · docker-compose.yml · LICENSE (PolyForm Shield 1.0.0)
- Deepgram input: send
container=webmonly and let Deepgram auto-detect Opus/sample rate from the header (explicitencoding/sample_ratebreak container demuxing). - Dual audio path: the same mic track feeds WebRTC (peers hear you live) and a MediaRecorder (server STT) — a MediaStreamTrack supports multiple consumers.
- The Groq model id lives in
server/src/groq.rs. Deepgram auth usesToken <key>, GroqBearer <key>.
Source-available under the PolyForm Shield License 1.0.0 © 2026 Alessandro Micelli. You may read, modify and use the code for any purpose except providing a product or service that competes with VoxTranslate. This is not an OSI "open source" license.