AI voice engine: use built-in voices or clone a voice from a short recording, then generate speech with Pocket TTS (Kyutai). English only; CPU-optimized, no GPU required.
Option A — Docker (one service, recommended)
Backend serves the main UI and the built React app at /preview. No separate frontend process.
# Optional: include React app in image (from project root)
cd preview-react && npm install && npm run build && cd ..
docker build -t kani-tts .
docker run -p 7862:7862 -v kani-voice_storage:/app/voice_storage --env-file .env kani-tts
Use --env-file .env so the container gets ANTHROPIC_API_KEY, DEEPGRAM_API_KEY, HF_TOKEN, etc. from your project root .env.
npm run build in preview-react before docker build; otherwise the legacy preview HTML is used).Option B — Docker Compose (API + optional Redis)
Same as above, with optional Redis for async clone/narrate:
docker compose up -d app
# Optional: start Redis + Celery worker (set CELERY_BROKER_URL=redis://redis:6379/0 in app env):
# docker compose --profile celery up -d redis
# docker compose run --rm -e CELERY_BROKER_URL=redis://redis:6379/0 app celery -A celery_app worker --loglevel=info
Option C — Local: backend only (single process)
One Python process serves everything (including /preview if you’ve built the React app).
# From project root, with venv activated
pip install -r requirements-core.txt && pip install -r requirements-server.txt && pip install -r requirements-rag.txt
python server.py
cd preview-react && npm run build)Option D — Local: backend + React dev server (two processes)
Backend for API; Vite dev server for React with hot reload. Use when actively developing the frontend.
pip install -r requirements-core.txt && pip install -r requirements-server.txt && pip install -r requirements-rag.txt
python server.py
cd preview-react && npm install && npm run dev
/api, /adventure, /tts, /voices, /ws, etc. to http://localhost:7862)If the frontend is on a different origin, set CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 in the backend env.
# From project root, with venv activated
pip install -r requirements-core.txt && pip install -r requirements-server.txt
# PDF parsing and RAG/Co-DM: pip install -r requirements-rag.txt
python server.py
Python 3.10–3.14, PyTorch 2.5+. Pocket TTS runs on CPU by default and does not require a GPU.
If pip reports "resolution-too-deep": Install in order one package at a time:
pip install torch>=2.5.0
pip install "soundfile>=0.13.0"
pip install pocket-tts
pip install fastapi uvicorn slowapi "gradio>=6.6.0"
python server.py
PORT env var)./preview)/preview now serves a React app build when available (fallback: legacy static/index.preview.html).
# One-time install
cd preview-react
npm install
# Build into static/preview-react (served by FastAPI at /preview)
npm run build
# Optional local dev server for frontend iteration
npm run dev
After npm run build, open http://localhost:7862/preview.
| Variable | Default | Description |
|---|---|---|
SERVER_NAME | 0.0.0.0 | Bind address |
PORT | 7862 | Server port (override with env var) |
VOICE_STORAGE_PATH | ./voice_storage | Directory for cloned voice files (.safetensors) and metadata |
API_KEYS | (empty) | Comma-separated API keys; header X-API-Key |
REQUIRE_API_KEY | (unset) | Set to 1/true/yes to require key for TTS/clone |
Optional features (see config.py for full list):
| Variable | Description |
|---|---|
VOICE_STORAGE_BACKEND | local or s3; use S3 for multi-instance or durability |
VOICE_STORAGE_BUCKET | S3 bucket name when backend is s3 |
DATABASE_URL | SQLite or PostgreSQL URL for voice metadata (e.g. sqlite:///voice_metadata.db) |
CELERY_BROKER_URL | Redis URL to enable async clone (returns job_id; poll GET /jobs/{job_id}) |
CORS_ORIGINS | Comma-separated origins for CORS (empty = same-origin only) |
ADMIN_API_KEY | When set, DELETE /admin/voices/{voice_id} with header X-Admin-Key for take-down |
ABUSE_CLONE_PER_IP_PER_HOUR | Max clones per IP per hour (0 = disable) |
RATE_LIMIT_GLOBAL, RATE_LIMIT_TTS, RATE_LIMIT_CLONE | e.g. 60/minute; empty = no limit |
HF_TOKEN | Hugging Face token for voice cloning (gated model). Optional if you run hf auth login first — then the cached token is used. Otherwise create at hf.co/settings/tokens, request access at hf.co/kyutai/pocket-tts, and set HF_TOKEN=hf_... in .env (no spaces/quotes). |
{"status":"ok","service":"kani-tts"}.{"require_api_key": true}.language_tags (e.g. ["en"]) and preset_voices (e.g. ["alba", "marius", ...]).max_narrate_chars, max_narrate_chunks.text, language_tag (ignored; English only), voice_id (preset name or cloned voice ID), optional temperature, top_p, repetition_penalty; optional file reference_audio for one-off clone. Returns WAV.audio (file), optional name, consent_scope, faction; returns voice_id or (when Celery enabled) job_id.voice_id.text, voice_id (preset or cloned), optional language_tag, chunk_by, max_chars; returns WAV.X-Admin-Key when ADMIN_API_KEY is set).Full request/response schemas: http://localhost:7862/docs (or your host/port).
voice_id set to the preset name (e.g. alba) for TTS or narrate without cloning..safetensors file and appears in the voice list.pip install -r requirements-dev.txt
pytest tests/ -v
By default, slow tests (POST /tts, which loads the model) are skipped. To run them: pytest tests/ -v -m slow.
Docker: Build and run the API (default port 7862). Pass your .env so the container has API keys (e.g. ANTHROPIC_API_KEY, DEEPGRAM_API_KEY, HF_TOKEN):
docker build -t kani-tts .
docker run -p 7862:7862 -v kani-voice_storage:/app/voice_storage --env-file .env kani-tts
Docker Compose: API + Redis (for optional async clone/narrate):
docker compose up -d app
# With Redis and Celery worker (set CELERY_BROKER_URL=redis://redis:6379/0 in app env):
# docker compose --profile celery up -d redis && docker compose run -e CELERY_BROKER_URL=redis://redis:6379/0 app celery -A celery_app worker --loglevel=info
Env: Set PORT, VOICE_STORAGE_PATH (or use a volume), and optionally API_KEYS, REQUIRE_API_KEY, DATABASE_URL, CELERY_BROKER_URL, CORS_ORIGINS (see Config table). For production, back up voice_storage and your database (SQLite file or PostgreSQL).
The web UI is a GM-focused voice studio; the same API can be called from your TTRPG app (VTT, companion app, or bot).
Auth: If the server has REQUIRE_API_KEY=1, send X-API-Key: <your-key> (or Authorization: Bearer <key>) on every request. Voices are scoped per key when API_KEYS and a DB are configured.
Endpoints and example payloads:
POST /voices/clone — form: audio (file), optional name, consent_scope (e.g. tts or commercial), faction. Returns voice_id or (with Celery) job_id; poll GET /jobs/{job_id} until done.GET /voices/list — returns cloned voices. Use GET /voices for preset voice names.POST /tts — form: text, optional voice_id (preset name or cloned ID), temperature, top_p, repetition_penalty. Returns WAV bytes.POST /tts/narrate — JSON: { "text": "...", "voice_id": "alba", "language_tag": "en", "chunk_by": "sentence", "max_chars": 500 }. Returns WAV. For long scripts, use "async": true when Celery is configured; then poll GET /jobs/{job_id} and fetch WAV from GET /jobs/{job_id}/result.Example (preset voice then clone):
# Speak with built-in voice
curl -X POST http://localhost:7862/tts -F "text=You approach the gates." -F "voice_id=alba" --output out.wav
# Clone a voice (after uploading audio)
curl -X POST http://localhost:7862/voices/clone -F "audio=@sample.wav" -F "name=Dragon Queen" -H "X-API-Key: YOUR_KEY"
# Then speak as that voice
curl -X POST http://localhost:7862/tts -F "text=You approach the gates." -F "voice_id=VOICE_ID" -H "X-API-Key: YOUR_KEY" --output out.wav
.safetensors and appear in the voice dropdown with built-in presets.job_id; the UI polls until the job completes and then shows the new voice_id.PDF parsing requires 'pymupdf4llm'
Install RAG dependencies: pip install -r requirements-rag.txt. With Docker, the image already includes them; if you see this in a container, rebuild the image (docker build -t kani-tts .) and run again.
404 on GET /api/campaigns or POST /adventure/ai-parse
The server build you’re running doesn’t include these routes. Rebuild and restart so the process uses the latest code (e.g. docker build -t kani-tts . then docker run ... again, or restart python server.py after pulling). Ensure DATABASE_URL is set (e.g. sqlite:///./voice_metadata.db) so /api/campaigns can store data.
403 on WebSocket /ws/audio
Usually the same cause: an older server build that rejects the WebSocket. Rebuild/restart the server. If the frontend is on a different origin (e.g. Vite at http://localhost:5173 and API at http://localhost:7862), set CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 in the server env and restart.
Bind for 0.0.0.0:7862 failed: port is already allocated
Something is already using port 7862. Stop it, then run the container again. Examples:
docker stop kani-tts-app (or the name from docker ps).python server.py in that terminal.docker run -p 7863:7862 -v kani-voice_storage:/app/voice_storage --env-file .env kani-tts and use http://localhost:7863.37 commits
HTML
42.1%
Python
36.2%
JavaScript
18.8%
CSS
2.8%
AI voice engine: use built-in voices or clone a voice from a short recording, then generate speech with Pocket TTS (Kyutai). English only; CPU-optimized, no GPU required.
Option A — Docker (one service, recommended)
Backend serves the main UI and the built React app at /preview. No separate frontend process.
# Optional: include React app in image (from project root)
cd preview-react && npm install && npm run build && cd ..
docker build -t kani-tts .
docker run -p 7862:7862 -v kani-voice_storage:/app/voice_storage --env-file .env kani-tts
Use --env-file .env so the container gets ANTHROPIC_API_KEY, DEEPGRAM_API_KEY, HF_TOKEN, etc. from your project root .env.
npm run build in preview-react before docker build; otherwise the legacy preview HTML is used).Option B — Docker Compose (API + optional Redis)
Same as above, with optional Redis for async clone/narrate:
docker compose up -d app
# Optional: start Redis + Celery worker (set CELERY_BROKER_URL=redis://redis:6379/0 in app env):
# docker compose --profile celery up -d redis
# docker compose run --rm -e CELERY_BROKER_URL=redis://redis:6379/0 app celery -A celery_app worker --loglevel=info
Option C — Local: backend only (single process)
One Python process serves everything (including /preview if you’ve built the React app).
# From project root, with venv activated
pip install -r requirements-core.txt && pip install -r requirements-server.txt && pip install -r requirements-rag.txt
python server.py
cd preview-react && npm run build)Option D — Local: backend + React dev server (two processes)
Backend for API; Vite dev server for React with hot reload. Use when actively developing the frontend.
pip install -r requirements-core.txt && pip install -r requirements-server.txt && pip install -r requirements-rag.txt
python server.py
cd preview-react && npm install && npm run dev
/api, /adventure, /tts, /voices, /ws, etc. to http://localhost:7862)If the frontend is on a different origin, set CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 in the backend env.
# From project root, with venv activated
pip install -r requirements-core.txt && pip install -r requirements-server.txt
# PDF parsing and RAG/Co-DM: pip install -r requirements-rag.txt
python server.py
Python 3.10–3.14, PyTorch 2.5+. Pocket TTS runs on CPU by default and does not require a GPU.
If pip reports "resolution-too-deep": Install in order one package at a time:
pip install torch>=2.5.0
pip install "soundfile>=0.13.0"
pip install pocket-tts
pip install fastapi uvicorn slowapi "gradio>=6.6.0"
python server.py
PORT env var)./preview)/preview now serves a React app build when available (fallback: legacy static/index.preview.html).
# One-time install
cd preview-react
npm install
# Build into static/preview-react (served by FastAPI at /preview)
npm run build
# Optional local dev server for frontend iteration
npm run dev
After npm run build, open http://localhost:7862/preview.
| Variable | Default | Description |
|---|---|---|
SERVER_NAME | 0.0.0.0 | Bind address |
PORT | 7862 | Server port (override with env var) |
VOICE_STORAGE_PATH | ./voice_storage | Directory for cloned voice files (.safetensors) and metadata |
API_KEYS | (empty) | Comma-separated API keys; header X-API-Key |
REQUIRE_API_KEY | (unset) | Set to 1/true/yes to require key for TTS/clone |
Optional features (see config.py for full list):
| Variable | Description |
|---|---|
VOICE_STORAGE_BACKEND | local or s3; use S3 for multi-instance or durability |
VOICE_STORAGE_BUCKET | S3 bucket name when backend is s3 |
DATABASE_URL | SQLite or PostgreSQL URL for voice metadata (e.g. sqlite:///voice_metadata.db) |
CELERY_BROKER_URL | Redis URL to enable async clone (returns job_id; poll GET /jobs/{job_id}) |
CORS_ORIGINS | Comma-separated origins for CORS (empty = same-origin only) |
ADMIN_API_KEY | When set, DELETE /admin/voices/{voice_id} with header X-Admin-Key for take-down |
ABUSE_CLONE_PER_IP_PER_HOUR | Max clones per IP per hour (0 = disable) |
RATE_LIMIT_GLOBAL, RATE_LIMIT_TTS, RATE_LIMIT_CLONE | e.g. 60/minute; empty = no limit |
HF_TOKEN | Hugging Face token for voice cloning (gated model). Optional if you run hf auth login first — then the cached token is used. Otherwise create at hf.co/settings/tokens, request access at hf.co/kyutai/pocket-tts, and set HF_TOKEN=hf_... in .env (no spaces/quotes). |
{"status":"ok","service":"kani-tts"}.{"require_api_key": true}.language_tags (e.g. ["en"]) and preset_voices (e.g. ["alba", "marius", ...]).max_narrate_chars, max_narrate_chunks.text, language_tag (ignored; English only), voice_id (preset name or cloned voice ID), optional temperature, top_p, repetition_penalty; optional file reference_audio for one-off clone. Returns WAV.audio (file), optional name, consent_scope, faction; returns voice_id or (when Celery enabled) job_id.voice_id.text, voice_id (preset or cloned), optional language_tag, chunk_by, max_chars; returns WAV.X-Admin-Key when ADMIN_API_KEY is set).Full request/response schemas: http://localhost:7862/docs (or your host/port).
voice_id set to the preset name (e.g. alba) for TTS or narrate without cloning..safetensors file and appears in the voice list.pip install -r requirements-dev.txt
pytest tests/ -v
By default, slow tests (POST /tts, which loads the model) are skipped. To run them: pytest tests/ -v -m slow.
Docker: Build and run the API (default port 7862). Pass your .env so the container has API keys (e.g. ANTHROPIC_API_KEY, DEEPGRAM_API_KEY, HF_TOKEN):
docker build -t kani-tts .
docker run -p 7862:7862 -v kani-voice_storage:/app/voice_storage --env-file .env kani-tts
Docker Compose: API + Redis (for optional async clone/narrate):
docker compose up -d app
# With Redis and Celery worker (set CELERY_BROKER_URL=redis://redis:6379/0 in app env):
# docker compose --profile celery up -d redis && docker compose run -e CELERY_BROKER_URL=redis://redis:6379/0 app celery -A celery_app worker --loglevel=info
Env: Set PORT, VOICE_STORAGE_PATH (or use a volume), and optionally API_KEYS, REQUIRE_API_KEY, DATABASE_URL, CELERY_BROKER_URL, CORS_ORIGINS (see Config table). For production, back up voice_storage and your database (SQLite file or PostgreSQL).
The web UI is a GM-focused voice studio; the same API can be called from your TTRPG app (VTT, companion app, or bot).
Auth: If the server has REQUIRE_API_KEY=1, send X-API-Key: <your-key> (or Authorization: Bearer <key>) on every request. Voices are scoped per key when API_KEYS and a DB are configured.
Endpoints and example payloads:
POST /voices/clone — form: audio (file), optional name, consent_scope (e.g. tts or commercial), faction. Returns voice_id or (with Celery) job_id; poll GET /jobs/{job_id} until done.GET /voices/list — returns cloned voices. Use GET /voices for preset voice names.POST /tts — form: text, optional voice_id (preset name or cloned ID), temperature, top_p, repetition_penalty. Returns WAV bytes.POST /tts/narrate — JSON: { "text": "...", "voice_id": "alba", "language_tag": "en", "chunk_by": "sentence", "max_chars": 500 }. Returns WAV. For long scripts, use "async": true when Celery is configured; then poll GET /jobs/{job_id} and fetch WAV from GET /jobs/{job_id}/result.Example (preset voice then clone):
# Speak with built-in voice
curl -X POST http://localhost:7862/tts -F "text=You approach the gates." -F "voice_id=alba" --output out.wav
# Clone a voice (after uploading audio)
curl -X POST http://localhost:7862/voices/clone -F "audio=@sample.wav" -F "name=Dragon Queen" -H "X-API-Key: YOUR_KEY"
# Then speak as that voice
curl -X POST http://localhost:7862/tts -F "text=You approach the gates." -F "voice_id=VOICE_ID" -H "X-API-Key: YOUR_KEY" --output out.wav
.safetensors and appear in the voice dropdown with built-in presets.job_id; the UI polls until the job completes and then shows the new voice_id.PDF parsing requires 'pymupdf4llm'
Install RAG dependencies: pip install -r requirements-rag.txt. With Docker, the image already includes them; if you see this in a container, rebuild the image (docker build -t kani-tts .) and run again.
404 on GET /api/campaigns or POST /adventure/ai-parse
The server build you’re running doesn’t include these routes. Rebuild and restart so the process uses the latest code (e.g. docker build -t kani-tts . then docker run ... again, or restart python server.py after pulling). Ensure DATABASE_URL is set (e.g. sqlite:///./voice_metadata.db) so /api/campaigns can store data.
403 on WebSocket /ws/audio
Usually the same cause: an older server build that rejects the WebSocket. Rebuild/restart the server. If the frontend is on a different origin (e.g. Vite at http://localhost:5173 and API at http://localhost:7862), set CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 in the server env and restart.
Bind for 0.0.0.0:7862 failed: port is already allocated
Something is already using port 7862. Stop it, then run the container again. Examples:
docker stop kani-tts-app (or the name from docker ps).python server.py in that terminal.docker run -p 7863:7862 -v kani-voice_storage:/app/voice_storage --env-file .env kani-tts and use http://localhost:7863.37 commits
HTML
42.1%
Python
36.2%
JavaScript
18.8%
CSS
2.8%