Skip to main content
Glama

voice-service

A self-hosted Voice API (speech-to-text + text-to-speech) with real auth and a small admin website, plus a thin MCP stdio proxy that lets MCP clients (bots, tools) call the API as native tools.

Monorepo, two deployables:

voice-service/
├── voice-api/     # FastAPI server: Voice REST API + admin web (dockerized, runs on the VPS)
├── mcp-proxy/     # MCP stdio proxy (client shim; pip-installable / vendored into bot images)
├── docs/          # design spec
├── docker-compose.yml
└── README.md

Architecture

 browser (admin) ──session cookie──▶ Admin Web ┐
                                                ├─(same FastAPI app)─▶ Voice API ─▶ SQLite + audio cache + Whisper
 bots / tools ──stdio──▶ MCP proxy ──Bearer─────┘

One container runs the FastAPI app serving both the Voice API and the admin web against one SQLite DB. The MCP proxy is a separate artifact that runs locally next to each client and translates tool calls into authenticated REST calls.

Related MCP server: MCP TTS Server

Voice API

All API endpoints except /audio, /healthz require Authorization: Bearer <api-key>.

Method

Path

Scope

Body / notes

POST

/transcribe

transcribe

{audio_base64, language?}{text, language} (Whisper, lazy-loaded, threadpool)

POST

/speak

speak

{text, lang?, engine?, voice?, style?}{url, cached, engine, voice, format}

GET

/voices

any key

Gemini voices + current default

GET

/audio/<sha>.ogg

none

serves a cached clip (opaque hashed name)

GET

/healthz

none

liveness + engine/model info

TTS engines. gTTS is the free default (one voice per language, no key). Gemini TTS is optional — enable it by pasting a Gemini key in the admin settings, then choose the engine mode: gtts, gemini_primary (Gemini first, gTTS fallback), or gemini_fallback (gTTS first, Gemini on failure). voice and style apply to Gemini only (30 prebuilt voices + natural-language style); gTTS ignores them. All output is transcoded by ffmpeg to Ogg/Opus so it renders as a Telegram voice bubble. /speak returns a URL, never base64.

Audio cache. Content-addressed: sha256(text + lang + engine + voice + style). A cache hit returns instantly with no engine/ffmpeg work. gTTS clips are keyed with empty voice/style so they are shared across requests.

Admin website

Session-cookie login (Starlette SessionMiddleware), CSRF on all POST forms, login rate-limit, forced password change on first login.

  • First boot seeds an admin user with a random password, written to /data/admin-password.txt (0600) and logged once; must_change=1.

  • /keys — create (full key shown once), enable/disable, delete API keys per bot.

  • /settings — Gemini key (write-only), engine mode, Whisper model, default voice + style, with a preview button.

  • /dashboard — per-key usage (requests, STT seconds, TTS chars) over 7 / 30 days.

Run locally

cd voice-service
python3.12 -m venv .venv && .venv/bin/pip install -r voice-api/requirements.txt

# ffmpeg must be on PATH (brew install ffmpeg / apt install ffmpeg)
mkdir -p /tmp/voice/{data,models,audio}
cd voice-api
DATA_DIR=/tmp/voice/data MODELS_DIR=/tmp/voice/models AUDIO_DIR=/tmp/voice/audio \
  WHISPER_MODEL=tiny PUBLIC_BASE_URL=http://localhost:8770 PORT=8770 HOST=127.0.0.1 \
  VOICE_COOKIE_SECURE=0 \
  ../.venv/bin/python -m voice_api

The first-boot admin password is printed to the log and written to /tmp/voice/data/admin-password.txt. Open http://localhost:8770/login, change the password, then create an API key under API keys (or use the CLI):

DATA_DIR=/tmp/voice/data ../.venv/bin/python -m voice_api.seed --name mybot --scopes transcribe,speak

Smoke test:

KEY=vsk_...   # the key you created
curl -s -X POST localhost:8770/speak -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' -d '{"text":"xin chào","lang":"vi"}'

Run the tests:

.venv/bin/python -m pytest voice-api/tests -v

MCP proxy

pip install ./mcp-proxy        # or vendor voice_mcp_proxy into the bot image

# register with a client (env carries the URL + per-bot key):
VOICE_API_URL=https://voice.veasy.vn VOICE_API_KEY=vsk_... \
  claude mcp add --transport stdio voice -- python -m voice_mcp_proxy

Tools: transcribe, speak(text, lang?, engine?, voice?, style?), list_voices, voice_info.

Deploy (VPS behind a reverse proxy)

cp .env.example .env      # set PUBLIC_BASE_URL=https://voice.veasy.vn, WHISPER_MODEL, etc.
docker compose up -d --build
docker compose logs voice-api | grep 'First-boot admin'   # grab the password once

Point a reverse proxy (nginx/Caddy/Traefik) with HTTPS at 127.0.0.1:8770. Keep VOICE_COOKIE_SECURE=1 in production. Gemini key + engine mode are configured in the admin web (stored in the DB), not in env.

Maintenance

ActivitySlowing
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Provides text-to-speech conversion through a unified MCP interface, supporting both local Kokoro and cloud OpenAI TTS engines with streaming audio, voice selection, and customization via natural language instructions.
    7
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables speech-to-text and text-to-speech conversion using OpenAI-compatible APIs. Supports customizable models, voices, and output directories.
    GPL 3.0