Уявіть: користувач натискає кнопку відтворення у вашому додатку для аудіокниг. Між натисканням і першим звуком проходить 3–4 секунди. Це не просто затримка — це втрата користувача. Ми вирішували цю проблему в проєкті аудіогіда: перейшли на ElevenLabs з WebSocket-стрімингом та дисковим кешем, знизивши час першої відповіді з 2,8 с до 350 мс. Різниця у 8 разів — і користувачі залишаються. У цій статті я розповім, як інтегрувати AI TTS з нульовими затримками, використовуючи стріминг, кешування та правильний вибір провайдера.
Як вибрати AI TTS провайдера для вашого проєкту?
Вибір провайдера визначає якість та затримку. Порівняємо основних гравців:
| Провайдер | Голоси | Українська/російська мова | Стріминг |
|---|---|---|---|
| OpenAI TTS | 6 (alloy, echo, fable, onyx, nova, shimmer) | Добре | Так |
| ElevenLabs | Велика бібліотека, voice cloning | Відмінно | WebSocket |
| Yandex SpeechKit | 10+ російськомовних (Альона, Філіп) | Найкращий | REST/gRPC |
| Системний TTS (iOS/Android) | Залежить від пристрою | Середньо | Ні |
Для російськомовного проєкту рекомендуємо Yandex SpeechKit — найкращі голоси та підтримка SSML. Для глобального — OpenAI TLS з балансом якості та ціни. 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 у ваш додаток — ми забезпечимо низькі затримки та природне звучання.







