A high-performance Text-to-Speech (TTS) system with a custom inference engine, providing an OpenAI-compatible API for fast, streaming speech generation with speaker embedding support.
/v1/audio/speech endpointFastAPI Server (OpenAI-compatible endpoint)
|
KaniTTS Custom Inference Engine (CUDA graphs + Triton kernels)
|
Token-level Streaming + NeMo NanoCodec Decoder
|
Output: WAV / PCM / Server-Sent Events
The system uses:
nineninesix/kani-tts-2-pt | nineninesix/kani-tts-2-ennvidia/nemo-nano-codec-22khz-0.6kbps-12.5fps (12.5fps, 4 codebooks)cd <your_project_dir>
python -m venv venv
source venv/bin/activate
pip install fastapi uvicorn scipy
pip install "nemo-toolkit[tts]==2.4.0"
pip install "transformers==4.57.1"
pip install triton
Known issues
nemo-toolkit[tts] requires transformers==4.53, but this project requires transformers==4.57.1 for model compatibility. Install nemo-toolkit first, then upgrade transformers.
nemo-toolkit[tts] requires ffmpeg. Install it with apt install ffmpeg if not already present.
For Blackwell GPUs nemo-toolkit[tts]==2.5.1 works too.
python server.py
The server will start on http://localhost:8000 and automatically download the required models on first run.
curl http://localhost:8000/health
curl -X POST http://localhost:8000/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"input": "Hello, this is a test of the text to speech system.",
"voice": "speaker_1",
"response_format": "wav"
}' \
--output speech.wav
curl -X POST http://localhost:8000/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"input": "This will be streamed in real-time as audio chunks.",
"voice": "speaker_3",
"stream_format": "sse"
}'
You can test the API with open-audio, a Next.js frontend that connects to this server out of the box.
/v1/audio/speechOpenAI-compatible endpoint for text-to-speech generation.
{
"input": "Text to convert to speech",
"model": "tts-1",
"voice": "speaker_1",
"response_format": "wav",
"stream_format": null,
"max_chunk_duration": 30.0,
"silence_duration": 0.2
}
| Field | Type | Default | Description |
|---|---|---|---|
input | string | required | Text to convert to speech |
model | string | "tts-1" | OpenAI compatibility field (no effect on model selection) |
voice | string | "speaker_1" | Speaker name matching a .pt file in speakers/. Use "random" to skip speaker embedding. |
response_format | string | "wav" | "wav" or "pcm" |
stream_format | string | null | null for complete file, "sse" for streaming |
max_chunk_duration | float | 30.0 | Max seconds per chunk in long-form mode |
silence_duration | float | 0.2 | Silence between chunks in long-form mode |
Speaker embeddings are stored as .pt files in the speakers/ directory:
speaker_1 through speaker_10To add a new voice, place a 128-dimensional speaker embedding tensor as a .pt file in speakers/. All embeddings are loaded at server startup.
Non-Streaming (stream_format is null):
wav - Complete WAV file (default)pcm - Raw PCM audio with metadata headers (X-Sample-Rate, X-Channels, X-Bit-Depth)Streaming (stream_format: "sse"):
data: {"type": "speech.audio.delta", "audio": "<base64_pcm_chunk>"}
data: {"type": "speech.audio.delta", "audio": "<base64_pcm_chunk>"}
data: {"type": "speech.audio.done", "usage": {"input_tokens": 25, "output_tokens": 487, "total_tokens": 512}}
/healthReturns server and model status.
{
"status": "healthy",
"tts_initialized": true
}
For texts estimated to take more than 40 seconds to speak, the system automatically:
Control long-form behavior:
{
"input": "Very long text...",
"voice": "speaker_1",
"max_chunk_duration": 30.0,
"silence_duration": 0.2
}
Key configuration parameters in config.py:
# Audio Settings
SAMPLE_RATE = 22050
CHUNK_SIZE = 25 # Frames per streaming chunk
LOOKBACK_FRAMES = 15 # Context frames for decoding
# Generation Parameters
TEMPERATURE = 1.0
TOP_P = 0.95
REPETITION_PENALTY = 1.1
MAX_TOKENS = 3000
# Long-Form Settings
LONG_FORM_THRESHOLD_SECONDS = 40.0
LONG_FORM_CHUNK_DURATION = 30.0
LONG_FORM_SILENCE_DURATION = 0.2
# BemaTTS
TOKENS_PER_FRAME = 4
AUDIO_STEP = 1.0
USE_LEARNABLE_ROPE = True
SPEAKER_EMB_DIM = 128
USE_CUDA_GRAPHS = True
# Models
MODEL_NAME = "nineninesix/kani-tts-2-pt"
CODEC_MODEL_NAME = "nvidia/nemo-nano-codec-22khz-0.6kbps-12.5fps"
The system uses 128-dimensional speaker embeddings for voice identity. Embeddings are pre-saved as PyTorch tensors in speakers/.
nineninesix/speaker-emb-tbr).pt file:import torch
embedding = torch.tensor([...]) # 128-dim vector
torch.save(embedding, './speakers/my_speaker.pt')
"voice": "my_speaker"├── server.py # FastAPI application and main entry point
├── config.py # Configuration and constants
├── speakers/ # Pre-saved speaker embedding .pt files
│ ├── speaker_1.pt
│ ├── speaker_2.pt
│ └── ...
├── audio/
│ ├── __init__.py
│ └── streaming.py # Streaming audio writer with sliding window decoder
├── generation/
│ ├── __init__.py
│ ├── kani_generator.py # Async wrapper around custom inference engine
│ └── chunking.py # Text splitting for long-form generation
└── kani_tts/ # Custom inference engine
├── __init__.py
├── api.py # Simple KaniTTS API (for standalone use)
├── core.py # TTSConfig, NemoAudioPlayer, KaniModel
├── model.py # BemaTTS model with frame-level position encoding
├── inference_engine.py # Optimized decode loop with CUDA graphs
├── optimized_decode.py # Fused decoder operations
├── triton_kernels.py # Fused RMSNorm, SiLU-mul, RoPE kernels
├── static_cache.py # Static KV cache for CUDA graph compatibility
├── context.py # Thread-local context for CUDA graph capture
└── speaker_embedder.py # WavLM-based speaker embedding extraction
{"max_chunk_duration": 20.0}
LOOKBACK_FRAMES = 20
Models are automatically downloaded from HuggingFace on first run. If downloads fail:
python -c "
from transformers import AutoTokenizer, AutoModelForCausalLM
AutoTokenizer.from_pretrained('nineninesix/kani-tts-2-pt')
AutoModelForCausalLM.from_pretrained('nineninesix/kani-tts-2-pt')
"
The code in this repository is licensed under the Apache License 2.0. See LICENSE for details.
Model weights and codecs are subject to their own licenses:
For issues, questions, or feature requests, please open an issue on GitHub or Discord
1 commits
Python
100.0%
A high-performance Text-to-Speech (TTS) system with a custom inference engine, providing an OpenAI-compatible API for fast, streaming speech generation with speaker embedding support.
/v1/audio/speech endpointFastAPI Server (OpenAI-compatible endpoint)
|
KaniTTS Custom Inference Engine (CUDA graphs + Triton kernels)
|
Token-level Streaming + NeMo NanoCodec Decoder
|
Output: WAV / PCM / Server-Sent Events
The system uses:
nineninesix/kani-tts-2-pt | nineninesix/kani-tts-2-ennvidia/nemo-nano-codec-22khz-0.6kbps-12.5fps (12.5fps, 4 codebooks)cd <your_project_dir>
python -m venv venv
source venv/bin/activate
pip install fastapi uvicorn scipy
pip install "nemo-toolkit[tts]==2.4.0"
pip install "transformers==4.57.1"
pip install triton
Known issues
nemo-toolkit[tts] requires transformers==4.53, but this project requires transformers==4.57.1 for model compatibility. Install nemo-toolkit first, then upgrade transformers.
nemo-toolkit[tts] requires ffmpeg. Install it with apt install ffmpeg if not already present.
For Blackwell GPUs nemo-toolkit[tts]==2.5.1 works too.
python server.py
The server will start on http://localhost:8000 and automatically download the required models on first run.
curl http://localhost:8000/health
curl -X POST http://localhost:8000/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"input": "Hello, this is a test of the text to speech system.",
"voice": "speaker_1",
"response_format": "wav"
}' \
--output speech.wav
curl -X POST http://localhost:8000/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"input": "This will be streamed in real-time as audio chunks.",
"voice": "speaker_3",
"stream_format": "sse"
}'
You can test the API with open-audio, a Next.js frontend that connects to this server out of the box.
/v1/audio/speechOpenAI-compatible endpoint for text-to-speech generation.
{
"input": "Text to convert to speech",
"model": "tts-1",
"voice": "speaker_1",
"response_format": "wav",
"stream_format": null,
"max_chunk_duration": 30.0,
"silence_duration": 0.2
}
| Field | Type | Default | Description |
|---|---|---|---|
input | string | required | Text to convert to speech |
model | string | "tts-1" | OpenAI compatibility field (no effect on model selection) |
voice | string | "speaker_1" | Speaker name matching a .pt file in speakers/. Use "random" to skip speaker embedding. |
response_format | string | "wav" | "wav" or "pcm" |
stream_format | string | null | null for complete file, "sse" for streaming |
max_chunk_duration | float | 30.0 | Max seconds per chunk in long-form mode |
silence_duration | float | 0.2 | Silence between chunks in long-form mode |
Speaker embeddings are stored as .pt files in the speakers/ directory:
speaker_1 through speaker_10To add a new voice, place a 128-dimensional speaker embedding tensor as a .pt file in speakers/. All embeddings are loaded at server startup.
Non-Streaming (stream_format is null):
wav - Complete WAV file (default)pcm - Raw PCM audio with metadata headers (X-Sample-Rate, X-Channels, X-Bit-Depth)Streaming (stream_format: "sse"):
data: {"type": "speech.audio.delta", "audio": "<base64_pcm_chunk>"}
data: {"type": "speech.audio.delta", "audio": "<base64_pcm_chunk>"}
data: {"type": "speech.audio.done", "usage": {"input_tokens": 25, "output_tokens": 487, "total_tokens": 512}}
/healthReturns server and model status.
{
"status": "healthy",
"tts_initialized": true
}
For texts estimated to take more than 40 seconds to speak, the system automatically:
Control long-form behavior:
{
"input": "Very long text...",
"voice": "speaker_1",
"max_chunk_duration": 30.0,
"silence_duration": 0.2
}
Key configuration parameters in config.py:
# Audio Settings
SAMPLE_RATE = 22050
CHUNK_SIZE = 25 # Frames per streaming chunk
LOOKBACK_FRAMES = 15 # Context frames for decoding
# Generation Parameters
TEMPERATURE = 1.0
TOP_P = 0.95
REPETITION_PENALTY = 1.1
MAX_TOKENS = 3000
# Long-Form Settings
LONG_FORM_THRESHOLD_SECONDS = 40.0
LONG_FORM_CHUNK_DURATION = 30.0
LONG_FORM_SILENCE_DURATION = 0.2
# BemaTTS
TOKENS_PER_FRAME = 4
AUDIO_STEP = 1.0
USE_LEARNABLE_ROPE = True
SPEAKER_EMB_DIM = 128
USE_CUDA_GRAPHS = True
# Models
MODEL_NAME = "nineninesix/kani-tts-2-pt"
CODEC_MODEL_NAME = "nvidia/nemo-nano-codec-22khz-0.6kbps-12.5fps"
The system uses 128-dimensional speaker embeddings for voice identity. Embeddings are pre-saved as PyTorch tensors in speakers/.
nineninesix/speaker-emb-tbr).pt file:import torch
embedding = torch.tensor([...]) # 128-dim vector
torch.save(embedding, './speakers/my_speaker.pt')
"voice": "my_speaker"├── server.py # FastAPI application and main entry point
├── config.py # Configuration and constants
├── speakers/ # Pre-saved speaker embedding .pt files
│ ├── speaker_1.pt
│ ├── speaker_2.pt
│ └── ...
├── audio/
│ ├── __init__.py
│ └── streaming.py # Streaming audio writer with sliding window decoder
├── generation/
│ ├── __init__.py
│ ├── kani_generator.py # Async wrapper around custom inference engine
│ └── chunking.py # Text splitting for long-form generation
└── kani_tts/ # Custom inference engine
├── __init__.py
├── api.py # Simple KaniTTS API (for standalone use)
├── core.py # TTSConfig, NemoAudioPlayer, KaniModel
├── model.py # BemaTTS model with frame-level position encoding
├── inference_engine.py # Optimized decode loop with CUDA graphs
├── optimized_decode.py # Fused decoder operations
├── triton_kernels.py # Fused RMSNorm, SiLU-mul, RoPE kernels
├── static_cache.py # Static KV cache for CUDA graph compatibility
├── context.py # Thread-local context for CUDA graph capture
└── speaker_embedder.py # WavLM-based speaker embedding extraction
{"max_chunk_duration": 20.0}
LOOKBACK_FRAMES = 20
Models are automatically downloaded from HuggingFace on first run. If downloads fail:
python -c "
from transformers import AutoTokenizer, AutoModelForCausalLM
AutoTokenizer.from_pretrained('nineninesix/kani-tts-2-pt')
AutoModelForCausalLM.from_pretrained('nineninesix/kani-tts-2-pt')
"
The code in this repository is licensed under the Apache License 2.0. See LICENSE for details.
Model weights and codecs are subject to their own licenses:
For issues, questions, or feature requests, please open an issue on GitHub or Discord
1 commits
Python
100.0%