---
title: Text to speech
description: Turn text into speech with Qwen3-TTS and ZONOS2 through OpenAI's speech API — preset voices, voices designed from a description, voice cloning from a short clip, emotions, 10 to 40+ languages — priced per minute of audio.
---

Lovely Router serves OpenAI's speech endpoint, `POST /v1/audio/speech`, with open-weight voice models on our own GPUs. Point the OpenAI SDK at `https://lovelyrouter.com/v1`, use your Lovely Router key, and you get the audio back in the response. On top of OpenAI's fields you can design a voice from a description, clone a voice from a short recording, pick the language and, on ZONOS2, set an emotion. Every field is in the [Audio endpoints](/docs/api/audio) reference.

## Models {#models}

| Model | Price | Best for | Languages |
| --- | --- | --- | --- |
| `qwen3-tts` | $0.036 per minute | Voices designed from a description, 9 preset voices with style instructions, cloning | 10: Chinese, English, Japanese, Korean, German, French, Russian, Portuguese, Spanish, Italian |
| `zonos2` | $0.036 per minute | Close voice clones, emotions (happy, sad, angry, surprised) | 40+, best in English, Chinese and Japanese |

Both are billed by the second of audio they make, at least one second per request. Qwen3-TTS is licensed under Apache-2.0 by Alibaba's Qwen team; ZONOS2 under Apache-2.0 by Zyphra.

Both models run on [hosted GPUs](/docs/models/hosted) that sleep when nobody uses them: the first request after an idle spell waits about 45 seconds (`qwen3-tts`) or up to 2 minutes (`zonos2`) while the model loads. After that, Qwen3-TTS speaks about twice as fast as real time, and ZONOS2 about as fast as real time per request, faster when several run at once.

## Speak {#speak}

```bash title="cURL"
curl https://lovelyrouter.com/v1/audio/speech \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen3-tts",
    "input": "Welcome back! Your order has shipped and should arrive on Thursday.",
    "voice": "serena",
    "instructions": "Warm and cheerful, a little fast"
  }' \
  --output welcome.mp3
```

```python title="Python"
from openai import OpenAI

client = OpenAI(base_url="https://lovelyrouter.com/v1", api_key="YOUR_API_KEY")
with client.audio.speech.with_streaming_response.create(
    model="qwen3-tts",
    input="Welcome back! Your order has shipped and should arrive on Thursday.",
    voice="serena",
    instructions="Warm and cheerful, a little fast",
) as response:
    response.stream_to_file("welcome.mp3")
```

```js title="Node.js"
import fs from 'node:fs';
import OpenAI from 'openai';

const client = new OpenAI({ baseURL: 'https://lovelyrouter.com/v1', apiKey: process.env.LOVELY_API_KEY });
const audio = await client.audio.speech.create({
  model: 'qwen3-tts',
  input: 'Welcome back! Your order has shipped and should arrive on Thursday.',
  voice: 'serena',
  instructions: 'Warm and cheerful, a little fast',
});
fs.writeFileSync('welcome.mp3', Buffer.from(await audio.arrayBuffer()));
```

The response is the audio itself (MP3 unless you set `response_format`), with the length in seconds in the `x-lovely-audio-seconds` header. Texts up to 4,096 characters; a long text is spoken sentence by sentence in the same voice.

## Voices {#voices}

`voice` takes one of the model's preset voices, a [saved voice](#clone-a-voice)'s id, or an OpenAI voice name, which maps to the closest preset so code written for OpenAI works unchanged.

| `qwen3-tts` voice | Sound | Native language |
| --- | --- | --- |
| `vivian` | Bright, slightly edgy young woman | Chinese |
| `serena` | Warm, gentle young woman | Chinese |
| `uncle_fu` | Seasoned man, low and mellow | Chinese |
| `dylan` | Youthful man, clear and natural | Chinese (Beijing dialect) |
| `eric` | Lively man, slightly husky | Chinese (Sichuan dialect) |
| `ryan` | Dynamic man with strong rhythm | English |
| `aiden` | Sunny American man | English |
| `ono_anna` | Playful, light young woman | Japanese |
| `sohee` | Warm woman, rich in emotion | Korean |

`zonos2` has three: `american_female`, `american_male` and `british_female`.

Every voice speaks every language its model supports, best in its native one. Without `voice`, the model picks a preset that suits `language` (on `qwen3-tts`: `vivian` for Chinese, `ono_anna` for Japanese, `sohee` for Korean, else `ryan`).

### Style instructions {#instructions}

On `qwen3-tts`, `instructions` steers how a preset or cloned voice speaks: emotion, pace, volume, a whisper, an accent. Write it as you would brief a voice actor, in any language:

```json
{ "model": "qwen3-tts", "voice": "uncle_fu", "input": "Gather round, and I'll tell you how it really happened.", "instructions": "Slow and conspiratorial, half whispering" }
```

### Design a voice {#design-a-voice}

Leave `voice` out and describe the voice in `instructions` — age, gender, timbre, pace, mood, accent — and `qwen3-tts` makes a new voice for it:

```json
{ "model": "qwen3-tts", "input": "Ahoy! Lower the sails, we're coming into port.", "instructions": "A gravelly old pirate captain with a booming voice, amused and a little drunk" }
```

A designed voice comes out a little different each time. To keep one, save the result as a [voice](#clone-a-voice) and use its id from then on.

### Emotions (ZONOS2) {#emotions}

`zonos2` takes `emotion` instead of instructions: one of `happy`, `sad`, `angry`, `surprised`, or weights from 0 to 1, such as `{"happy": 0.7, "surprised": 0.3}`. The speaker's voice stays the same; only the delivery changes.

## Clone a voice {#clone-a-voice}

Both models clone a voice from a short recording: 3 to 15 seconds of one person speaking clearly, without music. Save it once and use its id as `voice`:

```bash title="cURL"
curl https://lovelyrouter.com/v1/audio/voices \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -F name="Narrator" \
  -F transcript="Thanks for listening. See you next week on the show." \
  -F consent=true \
  -F file=@narrator.wav
```

The answer is the [voice object](/docs/api/audio#the-voice-object). Then speak with its id:

```bash title="cURL"
curl https://lovelyrouter.com/v1/audio/speech \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "qwen3-tts", "voice": "voice_8f2c4a1e9b7d3f60a5c2e1d4", "input": "Today we talk about the history of tea."}' \
  --output episode.mp3
```

- `transcript` is what the clip says. Qwen3-TTS clones more closely with it; without it, it matches the voice's sound only. ZONOS2 doesn't need it.
- `consent: true` is required: you confirm the voice is yours or you have the speaker's permission to clone it, and you are responsible for how you use it. Don't clone voices to deceive or impersonate people.
- Saved voices are free, private to your account and kept until you delete them, up to 100 per account. WAV, MP3, M4A, OGG/Opus, FLAC or WebM up to 10 MB.
- A saved voice works with both models, in every language they speak, and with `instructions` on `qwen3-tts`.

For a one-off clone, send the clip with the request instead: `reference_audio` (an `https` URL, a `data:` URL or base64), `reference_text` and `consent: true`.

## Output {#output}

| `response_format` | What you get |
| --- | --- |
| `mp3` (default) | MP3, 128 kbps |
| `opus` | Ogg Opus, 64 kbps |
| `aac` | AAC (ADTS) |
| `flac` | FLAC |
| `wav` | WAV, 16-bit |
| `pcm` | Raw 16-bit little-endian samples, 24 kHz mono (as OpenAI's) |

Qwen3-TTS makes 24 kHz audio and ZONOS2 44.1 kHz. `speed` (0.25 to 4.0) changes the pace without changing the pitch; you pay for the seconds you get back.

## Pricing and limits {#pricing}

- $0.036 per minute of audio on both models, billed by the second ($0.0006 per second), at least one second per request. A request that fails is not charged.
- Requests need a positive balance; there is no hold. Calls appear on your [Logs](/app/logs) page with the seconds made.
- Input up to 4,096 characters, instructions up to 1,000.
- No streaming: the audio comes back when it is complete. `stream_format: "sse"` returns `400`.
- Speech models answer only `/v1/audio/speech`. Sent to Chat Completions, they return `404` with a pointer here. They are served in the `creative-hosted` [channel group](/docs/models/channel-groups).
