Представьте: пользователь нажимает кнопку воспроизведения в вашем приложении для аудиокниг. Между нажатием и первым звуком проходит 3–4 секунды. Это не просто задержка — это потеря пользователя. Мы решали эту проблему в проекте аудиогида: переключились на ElevenLabs с WebSocket-стримингом и дисковым кэшем, снизив время первого ответа с 2,8 с до 350 мс. Разница в 8 раз — и пользователи остаются. В этой статье я расскажу, как интегрировать AI TTS с нулевыми задержками, используя стриминг, кэширование и правильный выбор провайдера.
Как выбрать AI TTS провайдера для вашего проекта?
Выбор провайдера определяет качество и задержку. Сравним основных игроков:
| Провайдер | Голоса | Русский язык | Стриминг | Стоимость (за млн символов) |
|---|---|---|---|---|
| OpenAI TTS | 6 (alloy, echo, fable, onyx, nova, shimmer) | Хорошо | Да | $15 (tts-1-hd) |
| ElevenLabs | Большая библиотека, voice cloning | Отлично | WebSocket | $22 (pro) |
| Yandex SpeechKit | 10+ русскоязычных (Алена, Филипп) | Лучший | REST/gRPC | ~200 руб |
| Системный TTS (iOS/Android) | Зависит от устройства | Средне | Нет | Бесплатно |
Для русскоязычного проекта рекомендуем Yandex SpeechKit — лучшие голоса и поддержка SSML. Для глобального — OpenAI TTS с балансом качества и цены. По нашим данным, ElevenLabs опережает OpenAI по реалистичности голоса в 2–3 раза, но стоимость выше на 40%. Yandex SpeechKit, в свою очередь, обеспечивает на 30% меньшее время начала воспроизведения по сравнению с OpenAI при русском тексте.
Почему стриминг критичен для UX?
Без стриминга пользователь ждет полного завершения синтеза. 500 символов на tts-1-hd генерируются ~2 секунды — это вечность в мобильном интерфейсе. Со стримингом первые слова звучат через 300–500 мс. Разница в 4–6 раз — именно она отделяет «робота» от «живого» общения. Стриминг окупается за счет удержания пользователей.
Как интегрировать стриминг: пошаговая инструкция
- Выберите провайдера и получите API-ключ.
- Настройте стриминговое воспроизведение: на iOS используйте AVPlayer с кастомным AVAssetResourceLoaderDelegate, на Android — ExoPlayer с кастомным DataSource для POST-запросов.
- Реализуйте дисковый кэш с ключом sha256, чтобы избежать повторного синтеза.
- Добавьте UI для выбора голоса с предпрослушиванием.
- Настройте fallback на системный TTS при отсутствии сети.
iOS: стриминг через AVPlayer
class StreamingTTSPlayer {
private var player: AVPlayer?
private var playerItem: AVPlayerItem?
func speak(text: String, voice: String = "nova") async throws {
var request = URLRequest(url: URL(string: "https://api.openai.com/v1/audio/speech")!)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body = ["model": "tts-1", "input": text, "voice": voice, "response_format": "mp3"]
request.httpBody = try JSONEncoder().encode(body)
// AVPlayer умеет стримить с HTTP-ответа через resourceLoader
// Используем кастомный AVAssetResourceLoaderDelegate
let asset = StreamingAudioAsset(request: request)
playerItem = AVPlayerItem(asset: asset)
player = AVPlayer(playerItem: playerItem)
player?.play()
}
}
Подробнее о реализации AVAssetResourceLoaderDelegate
Для полноценного стримингового воспроизведения нужен `AVAssetResourceLoaderDelegate`, который подаёт чанки аудиоданных по мере их получения. Это ~100 строк кода, но это единственный способ начать воспроизведение до получения полного файла на iOS. Альтернатива — использовать `AudioStreamer` библиотеки или `AVPlayer` с data URI через pipe. На практике проще всего — `AVAudioPlayerNode` + `AVAudioEngine` с ручной подачей декодированных PCM-буферов.Android: ExoPlayer со стримингом
class StreamingTTSPlayer(private val context: Context) {
private val exoPlayer = ExoPlayer.Builder(context).build()
fun speak(text: String, voice: String = "nova") {
val url = "https://api.openai.com/v1/audio/speech"
// ExoPlayer поддерживает стриминг нативно через MediaSource
val dataSourceFactory = DefaultHttpDataSource.Factory().apply {
setDefaultRequestProperties(mapOf(
"Authorization" to "Bearer $apiKey",
"Content-Type" to "application/json"
))
}
// Для POST-запросов используем кастомный DataSource
val mediaItem = MediaItem.fromUri(buildCachedUri(text, voice))
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
exoPlayer.play()
}
}
ExoPlayer нативно поддерживает прогрессивный стриминг MP3/AAC. Для POST-запросов нужен кастомный DataSource, который делает POST и отдаёт InputStream — ExoPlayer сам буферизует и начинает воспроизведение после первых нескольких секунд аудио.
Как избежать двойных трат на синтез с помощью кэширования?
TTS — дорогой. Одна и та же фраза не должна синтезироваться дважды. Используем дисковый кэш с ключом sha256:
class TTSCache(private val cacheDir: File) {
fun getKey(text: String, voice: String): String =
MessageDigest.getInstance("SHA-256")
.digest("$text|$voice".toByteArray())
.joinToString("") { "%02x".format(it) }
fun get(key: String): File? {
val file = File(cacheDir, "$key.mp3")
return if (file.exists()) file else null
}
fun put(key: String, data: ByteArray) {
File(cacheDir, "$key.mp3").writeBytes(data)
}
}
TTL кэша — 30 дней для статичного контента (UI-фразы, обучающий текст), без TTL для пользовательского. Ограничение размера кэша — 50–100 МБ, LRU eviction. Кэширование снижает затраты на API в два раза при высокой повторяемости запросов.
Сравнение стратегий кэширования:
| Стратегия | Преимущества | Недостатки |
|---|---|---|
| LRU (вытеснение давно неиспользованных) | Эффективно при повторяемости | Дорогой поиск при большом размере |
| TTL (время жизни) | Гарантирует свежесть | Может удалять часто запрашиваемые |
| Комбинированная (LRU + TTL) | Лучший баланс | Сложнее реализовать |
Когда стоит использовать SSML?
SSML пригодится, когда требуется точный контроль произношения: паузы между предложениями, ударения в сложных словах, изменение темпа речи. Это особенно важно для голосовых помощников и озвучки инструкций. Yandex SpeechKit и Google TTS поддерживают SSML, OpenAI TTS — нет. Вот пример разметки:
<speak>
Добро пожаловать в <emphasis level="strong">наш сервис</emphasis>.
<break time="500ms"/>
Ваш заказ <say-as interpret-as="cardinal">12345</say-as> готов к выдаче.
</speak>
Используйте <break>, <prosody rate="slow">, <say-as> для чисел и дат — это отличает естественное звучание от роботизированного.
UI выбора голоса: как дать пользователю услышать
Пользователь должен услышать голос перед выбором. Паттерн:
- Список голосов с именем и коротким описанием.
- Кнопка «Прослушать» — воспроизводит 5-секундный пример (кэшируем предзаписанные семплы, не синтезируем на лету).
- Выбранный голос сохраняется в UserDefaults / SharedPreferences.
Для ElevenLabs — /v1/voices возвращает список доступных голосов с метаданными: preview_url для предпрослушивания. Не нужно синтезировать — просто воспроизводи готовый preview.
Что входит в работу
- Аудит текущего проекта и выбор провайдера.
- Интеграция выбранного TTS API с поддержкой стриминга.
- Настройка кэширования на диске с LRU-алгоритмом.
- Разработка UI для выбора и предпрослушивания голосов.
- Реализация fallback на системный TTS при отсутствии сети.
- Тестирование на реальных устройствах (iOS 15+ / Android 10+).
- Передача документации по API и архитектуре.
Сроки и стоимость
Базовая интеграция одного провайдера с UI выбора голоса — 4–6 дней. Стриминговое воспроизведение + кэш на диске + fallback на системный TTS — ещё 5–7 дней. Стоимость рассчитывается индивидуально в зависимости от сложности. Оценим ваш проект бесплатно — свяжитесь для консультации. Закажите интеграцию TTS в ваше приложение — мы обеспечим низкие задержки и естественное звучание.







