Configuration

September 9, 2026 ยท View on GitHub

Last updated: 2026-08-11

Docker Compose

Compose files are in docker/cpu, docker/gpu, docker/rocm. Run docker compose up from inside one.

image vs build

build: is active, image: is commented out:

services:
  kokoro-tts:
    # image: ghcr.io/remsky/kokoro-fastapi-gpu:${VERSION:-latest}
    build:
      context: ../..
      dockerfile: docker/gpu/Dockerfile.optimized

build: compiles from your checkout. Use it if you're changing anything under api/.

For the published image, uncomment image: and comment out build:. VERSION defaults to latest, or set the full tag (v included, same as docker-bake.hcl):

VERSION=v0.8.0 docker compose up

That VERSION comes from docker/gpu/.env, not the repo root (compose reads it next to the compose file). Separate from the app .env below.

No checkout needed? Skip compose, use docker run (see README).

Volume mounts shadow the image

volumes:
  - ../../api:/app/api
  - ../../web:/app/web

Gets you edits without a rebuild. Also means /app/api/src/models is your host dir, not the baked one, hence DOWNLOAD_MODEL=true in the compose env. docker run needs neither, nothing shadows the baked model.

Drop the mounts for the image as published. Keeping them with image: runs published deps against local source.

GPU file sets user: "1001:1001" to match appuser. On Linux, write failures to the mounts are usually host ownership.

Setting variables

Compose environment: block:

environment:
  - API_LOG_LEVEL=WARNING
  - DEFAULT_VOICE=bf_emma

Or the command line:

docker run --env API_LOG_LEVEL=WARNING -p 8880:8880 ghcr.io/remsky/kokoro-fastapi-cpu:latest

A .env at the repo root works, picked up by:

env_file:
  - path: ../../.env
    required: false

required: false so a fresh clone still starts without one.

environment: beats env_file. PYTHONPATH, DOWNLOAD_MODEL, API_LOG_LEVEL, and USE_GPU (GPU/ROCm) are pinned there, so setting those in .env does nothing. Edit the compose file instead.

Names are the field names from api/src/core/config.py, uppercased. Unrecognized keys in .env are ignored. Three rows below are process-only: read outside the settings object, so they work as env vars but not from .env.

Reference

API

VariableDefault
HOST0.0.0.0Bind address, :: for IPv6-only or dual-stack
PORT8880Bind port
API_TITLEKokoro TTS APIOpenAPI title
API_DESCRIPTIONAPI for text-to-speech generation using KokoroOpenAPI description
API_VERSIONfrom VERSIONOpenAPI version string
API_LOG_LEVELDEBUGloguru level, see Logging below (process-only)
WEB_CONCURRENCY1uvicorn worker processes. Inference is synchronous, so one process serves requests one at a time; each extra worker loads its own model copy (process-only)

Model & device

VariableDefault
USE_GPUtrueUse GPU if one is available
DEVICE_TYPEautoForce cuda, mps, or cpu
MODEL_DIR/app/api/src/modelsWhere model weights live, container path
VOICES_DIR/app/api/src/voices/v1_0Where voice packs live, container path
MODEL_REPO_IDhexgrad/Kokoro-82MFallback download source when MODEL_DIR is empty
DOWNLOAD_MODELtrueFetch weights at image build and container start (process-only)

Voices

VariableDefault
DEFAULT_VOICEaf_heartVoice used when a request omits one, preselected in the web player, warms the model at startup
DEFAULT_VOICE_CODEunsetOverride the language code normally taken from the voice name's first letter. Applies to every speaker, so a [voice:] dialogue mixing languages is forced onto this one
VOICE_WEIGHT_NORMALIZATIONtrueRescale combined voice weights to sum to 1
ALLOW_LOCAL_VOICE_SAVINGfalseLet combined voices be downloaded (/v1/audio/voices/combine) and tuned voices be saved into VOICES_DIR (/dev/tune save_voice)
ENABLE_VOICE_TAGStrueKill switch for [voice:] parsing and /dev/dialogue

Text processing

VariableDefault
TARGET_MIN_TOKENS175Chunker aims for at least this many tokens
TARGET_MAX_TOKENS250Chunker aims for at most this many
ABSOLUTE_MAX_TOKENS450Hard ceiling per chunk, model limit is 510
ENABLE_SSMLtrueKill switch for SSML translation, the /dev/ssml routes and ssml: true on the speech endpoints will 403 when off
SSML_MAX_DEPTH10Deepest SSML nesting translated, past it is a 400
MAX_PAUSE_DURATION_S60.0Ceiling for a single [pause:Ns] tag or SSML <break>, longer values are clamped
MAX_TOTAL_PAUSE_S300.0Ceiling for total pause silence per request, over it is a 400
MAX_INPUT_LENGTH1000000Ceiling for characters of text per request, over it is a 400
ADVANCED_TEXT_NORMALIZATIONtrueMaster switch for number/URL/email expansion before phonemizing; English only, per-request fields in Text normalization

Audio

VariableDefault
DEFAULT_VOLUME_MULTIPLIER1.0Global gain applied to generated audio
GAP_TRIM_MS1Base trim from each streaming chunk end
DYNAMIC_GAP_TRIM_PADDING_MS410Padding added back for dynamic gap trim
DYNAMIC_GAP_TRIM_PADDING_CHAR_MULTIPLIER{".": 1, "!": 0.9, "?": 1, ",": 0.8}Per-punctuation scaling of that padding, dict-valued so set it in .env rather than a shell

Web player & CORS

VariableDefault
ENABLE_WEB_PLAYERtrueServe the browser UI
WEB_PLAYER_PATHwebStatic file root for it
CORS_ENABLEDtrueSend CORS headers
CORS_ORIGINS["*"]Allowed origins, narrow this if the port is reachable beyond localhost

Temp files

VariableDefault
TEMP_FILE_DIRapi/temp_filesWhere return_download_link files are written
MAX_TEMP_DIR_SIZE_MB2048Prune temp files past this total
MAX_TEMP_DIR_AGE_HOURS1Prune temp files older than this
MAX_TEMP_DIR_COUNT3Keep at most this many temp files

Hardware

VariableDefault
ENABLE_MIOPENfalseKeep MIOpen enabled on ROCm. Off by default because MIOpen compiles a kernel per tensor shape, which negatively impacts performance as Kokoro hits it every request

Operational routes

VariableDefault
ENABLE_DEBUG_ENDPOINTSfalseExpose /debug/* host and process introspection
ALLOW_DEV_UNLOADfalseExpose /dev/model, POST /dev/unload, and POST /dev/reload
ENABLE_INNO_TUNERfalseExpose POST /dev/tune, see inno-tune.md
MODEL_AUTO_UNLOAD_TIMEOUT_SECONDS0.0Idle seconds before auto-unload; 0 disables auto-unload

Text normalization

normalization_options on the speech endpoints, per request. The normalizer runs for English only, the one language with a normalizer registered. Other languages go to the phonemizer as written, remove_emoji aside.

FieldDefault
normalizetrueMaster switch, off sends the text as written
url_normalizationtruehttps://test.org/path reads as https test dot org slash path
email_normalizationtrueuser@example.com reads as user at example dot com
phone_normalizationtrue555-123-4567 reads as spoken digits
caps_normalizationtrueARNE SAKNUSSEMM reads as two words instead of spelled out, FBI and US GDP still spelled
unit_normalizationfalse10KB reads as 10 kilobytes
optional_pluralization_normalizationtruefriend(s) reads as friends
replace_remaining_symbolstrueLeftover symbols read as words, & as and, @ as at
remove_emojifalseEmoji dropped instead of read by name, every language
{"input": "10KB at user@example.com", "voice": "af_heart", "normalization_options": {"unit_normalization": true, "remove_emoji": true}}

Logging

Global API loguru logging level can be set using the API_LOG_LEVEL environment variable. Defaults to DEBUG.

Docker

Modify the appropriate compose yml or append to command line.

docker run --env 'API_LOG_LEVEL=WARNING' ...

Direct via UV

Linux and macOS

export API_LOG_LEVEL=WARNING
./start-cpu.sh OR
./start-gpu.sh

Windows

$env:API_LOG_LEVEL = 'WARNING'
.\start-cpu.ps1 OR
.\start-gpu.ps1