Deploy a production-ready Text-to-Speech API using Dia2 in minutes—no powerful GPU hardware required. Clone voices from short audio samples and generate natural-sounding speech with just a few API calls, including real-time streaming audio.
This project bundles a custom Dia2 TTS build (with streaming support) in a simple REST API deployed on Modal's serverless infrastructure.
/v1/audio/speech endpointpip install modal
modal setup
This opens your browser to log in and authenticates your machine with Modal.
git clone https://github.com/khariha/dia2-easy-tts.git
cd dia2-easy-tts
# Deploy to Modal (first time takes ~5 minutes to build image and download model weights)
modal deploy main.py
A successful deployment will show:
✓ App deployed! 🎉
└── 🔨 Created web function dia2 => https://your-app-name--dia2-easy-tts-dia2.modal.run
# Health check
curl https://your-app-name--dia2-easy-tts-dia2.modal.run/health
# Generate speech (non-streaming)
curl -X POST https://your-app-name--dia2-easy-tts-dia2.modal.run/v1/audio/speech \
-F 'script=[S1]Hello, this is a test of the Dia2 TTS system.' \
-o output.wav
Or run the included test script (update BASE_URL first):
pip install requests sounddevice soundfile numpy
python test.py
POST /v1/audio/speechGenerate speech from a dialogue script.
Form parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
script | string | required | Text with [S1]/[S2] speaker tags |
speaker1_audio | file | — | WAV file to clone voice for speaker 1 |
speaker2_audio | file | — | WAV file to clone voice for speaker 2 |
stream | bool | false | Stream audio chunks as they are generated |
chunk_frames | int | 1 | Frames per streaming chunk (~80ms each) |
cfg_scale | float | 2.0 | Classifier-free guidance scale |
cfg_filter_k | int | 50 | Top-k filter for CFG logits |
text_temperature | float | 0.6 | Sampling temperature for text tokens |
text_top_k | int | 50 | Top-k sampling for text tokens |
audio_temperature | float | 0.8 | Sampling temperature for audio tokens |
audio_top_k | int | 50 | Top-k sampling for audio tokens |
include_prefix | bool | false | Include prefix audio in the output |
use_cuda_graph | bool | true | Enable CUDA graph acceleration |
Returns: WAV audio file (streaming or complete)
GET /healthCheck service status and GPU availability.
Returns:
{
"status": "healthy",
"service": "Dia2 Easy TTS",
"gpu_info": {
"torch_info": {
"cuda_available": true,
"device_name": "NVIDIA A100-SXM4-40GB"
}
}
}
curl -X POST https://your-app-name--dia2-easy-tts-dia2.modal.run/v1/audio/speech \
-F 'script=[S1]Your text here. [S2]With multiple speakers.' \
-o output.wav
curl -X POST https://your-app-name--dia2-easy-tts-dia2.modal.run/v1/audio/speech \
-F 'script=[S1]Hello world' \
-F 'stream=true' \
-o output.wav
curl -X POST https://your-app-name--dia2-easy-tts-dia2.modal.run/v1/audio/speech \
-F 'script=[S1]Hello [S2]World' \
-F 'speaker1_audio=@/path/to/voice1.wav' \
-F 'speaker2_audio=@/path/to/voice2.wav' \
-F 'include_prefix=false' \
-o output.wav
import requests
BASE_URL = "https://your-app-name--dia2-easy-tts-dia2.modal.run"
script = """[S1] Welcome to our podcast.
[S2] Thanks for having me!
[S1] Let's dive right in."""
# Non-streaming
response = requests.post(
f"{BASE_URL}/v1/audio/speech",
data={"script": script},
timeout=300,
)
with open("output.wav", "wb") as f:
f.write(response.content)
# Streaming
with requests.post(
f"{BASE_URL}/v1/audio/speech",
data={"script": script, "stream": "true"},
stream=True,
timeout=300,
) as response:
with open("output.wav", "wb") as f:
for chunk in response.iter_content(chunk_size=None):
if chunk:
f.write(chunk)
// Non-streaming
const formData = new FormData();
formData.append('script', '[S1]Hello from JavaScript!');
const response = await fetch('https://your-app-name--dia2-easy-tts-dia2.modal.run/v1/audio/speech', {
method: 'POST',
body: formData,
});
const blob = await response.blob();
// Streaming
formData.append('stream', 'true');
const streamResponse = await fetch('...', { method: 'POST', body: formData });
const reader = streamResponse.body.getReader();
// pipe reader to Web Audio API or MediaSource for real-time playback
Scripts use speaker tags to indicate who is speaking:
[S1] — Speaker 1 (uses speaker1_audio if provided)[S2] — Speaker 2 (uses speaker2_audio if provided)[S1] It was a dark and stormy night.
[S2] The wind howled through the trees.
[S1] Sarah pulled her coat tighter as she walked.
Key constants at the top of main.py:
DIA2_MODEL_REPO = "nari-labs/Dia2-2B" # switch to Dia2-1B for faster/cheaper
DIA2_GPU_TYPE = "A100"
DIA2_MAX_CONTAINERS = 1 # max concurrent requests
Add min_containers=1 to the @app.function decorator to keep a warm container and eliminate cold starts (~38s), at the cost of continuous GPU charges.
The first deploy builds the image and downloads ~4GB of model weights. This is cached — subsequent deploys take ~30 seconds.
The first request after a period of inactivity loads the model into GPU memory. Set min_containers=1 in main.py to keep a container warm.
For long scripts, increase the client timeout:
requests.post(url, data=data, timeout=600)
modal app list # find your app ID
modal app stop <app-id>
8 commits
Python
100.0%
Deploy a production-ready Text-to-Speech API using Dia2 in minutes—no powerful GPU hardware required. Clone voices from short audio samples and generate natural-sounding speech with just a few API calls, including real-time streaming audio.
This project bundles a custom Dia2 TTS build (with streaming support) in a simple REST API deployed on Modal's serverless infrastructure.
/v1/audio/speech endpointpip install modal
modal setup
This opens your browser to log in and authenticates your machine with Modal.
git clone https://github.com/khariha/dia2-easy-tts.git
cd dia2-easy-tts
# Deploy to Modal (first time takes ~5 minutes to build image and download model weights)
modal deploy main.py
A successful deployment will show:
✓ App deployed! 🎉
└── 🔨 Created web function dia2 => https://your-app-name--dia2-easy-tts-dia2.modal.run
# Health check
curl https://your-app-name--dia2-easy-tts-dia2.modal.run/health
# Generate speech (non-streaming)
curl -X POST https://your-app-name--dia2-easy-tts-dia2.modal.run/v1/audio/speech \
-F 'script=[S1]Hello, this is a test of the Dia2 TTS system.' \
-o output.wav
Or run the included test script (update BASE_URL first):
pip install requests sounddevice soundfile numpy
python test.py
POST /v1/audio/speechGenerate speech from a dialogue script.
Form parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
script | string | required | Text with [S1]/[S2] speaker tags |
speaker1_audio | file | — | WAV file to clone voice for speaker 1 |
speaker2_audio | file | — | WAV file to clone voice for speaker 2 |
stream | bool | false | Stream audio chunks as they are generated |
chunk_frames | int | 1 | Frames per streaming chunk (~80ms each) |
cfg_scale | float | 2.0 | Classifier-free guidance scale |
cfg_filter_k | int | 50 | Top-k filter for CFG logits |
text_temperature | float | 0.6 | Sampling temperature for text tokens |
text_top_k | int | 50 | Top-k sampling for text tokens |
audio_temperature | float | 0.8 | Sampling temperature for audio tokens |
audio_top_k | int | 50 | Top-k sampling for audio tokens |
include_prefix | bool | false | Include prefix audio in the output |
use_cuda_graph | bool | true | Enable CUDA graph acceleration |
Returns: WAV audio file (streaming or complete)
GET /healthCheck service status and GPU availability.
Returns:
{
"status": "healthy",
"service": "Dia2 Easy TTS",
"gpu_info": {
"torch_info": {
"cuda_available": true,
"device_name": "NVIDIA A100-SXM4-40GB"
}
}
}
curl -X POST https://your-app-name--dia2-easy-tts-dia2.modal.run/v1/audio/speech \
-F 'script=[S1]Your text here. [S2]With multiple speakers.' \
-o output.wav
curl -X POST https://your-app-name--dia2-easy-tts-dia2.modal.run/v1/audio/speech \
-F 'script=[S1]Hello world' \
-F 'stream=true' \
-o output.wav
curl -X POST https://your-app-name--dia2-easy-tts-dia2.modal.run/v1/audio/speech \
-F 'script=[S1]Hello [S2]World' \
-F 'speaker1_audio=@/path/to/voice1.wav' \
-F 'speaker2_audio=@/path/to/voice2.wav' \
-F 'include_prefix=false' \
-o output.wav
import requests
BASE_URL = "https://your-app-name--dia2-easy-tts-dia2.modal.run"
script = """[S1] Welcome to our podcast.
[S2] Thanks for having me!
[S1] Let's dive right in."""
# Non-streaming
response = requests.post(
f"{BASE_URL}/v1/audio/speech",
data={"script": script},
timeout=300,
)
with open("output.wav", "wb") as f:
f.write(response.content)
# Streaming
with requests.post(
f"{BASE_URL}/v1/audio/speech",
data={"script": script, "stream": "true"},
stream=True,
timeout=300,
) as response:
with open("output.wav", "wb") as f:
for chunk in response.iter_content(chunk_size=None):
if chunk:
f.write(chunk)
// Non-streaming
const formData = new FormData();
formData.append('script', '[S1]Hello from JavaScript!');
const response = await fetch('https://your-app-name--dia2-easy-tts-dia2.modal.run/v1/audio/speech', {
method: 'POST',
body: formData,
});
const blob = await response.blob();
// Streaming
formData.append('stream', 'true');
const streamResponse = await fetch('...', { method: 'POST', body: formData });
const reader = streamResponse.body.getReader();
// pipe reader to Web Audio API or MediaSource for real-time playback
Scripts use speaker tags to indicate who is speaking:
[S1] — Speaker 1 (uses speaker1_audio if provided)[S2] — Speaker 2 (uses speaker2_audio if provided)[S1] It was a dark and stormy night.
[S2] The wind howled through the trees.
[S1] Sarah pulled her coat tighter as she walked.
Key constants at the top of main.py:
DIA2_MODEL_REPO = "nari-labs/Dia2-2B" # switch to Dia2-1B for faster/cheaper
DIA2_GPU_TYPE = "A100"
DIA2_MAX_CONTAINERS = 1 # max concurrent requests
Add min_containers=1 to the @app.function decorator to keep a warm container and eliminate cold starts (~38s), at the cost of continuous GPU charges.
The first deploy builds the image and downloads ~4GB of model weights. This is cached — subsequent deploys take ~30 seconds.
The first request after a period of inactivity loads the model into GPU memory. Set min_containers=1 in main.py to keep a container warm.
For long scripts, increase the client timeout:
requests.post(url, data=data, timeout=600)
modal app list # find your app ID
modal app stop <app-id>
8 commits
Python
100.0%