AI Text-to-Speech з вибором голосу в мобільному додатку
Системний TTS на iOS та Android — AVSpeechSynthesizer та TextToSpeech — вирішує базову задачу, але звучить роботизовано. AI TTS від ElevenLabs, OpenAI або Yandex SpeechKit — це голоси, які складно відрізнити від живих. Інтеграція вимагає продуманого кешування та стрімінгового воспроизведення, інакше затримка 2–4 секунди перед першим словом убиває UX.
Провайдери та їхні особливості
OpenAI TTS — 6 голосів (alloy, echo, fable, onyx, nova, shimmer), моделі tts-1 (швидка) та tts-1-hd (якісна). Підтримує стріммінг. Російський — добре. Вартість: $15/млн символів для tts-1-hd.
ElevenLabs — велика бібліотека голосів, voice cloning, multilingual v2. Стріммінг через WebSocket. Найкраще якість серед усіх провайдерів.
Yandex SpeechKit — найкращі російськомовні голоси, включаючи alena, filipp. REST або gRPC. SSML для управління інтонацією, паузами, наголосом.
Системний TTS — безплатно, офлайн, нульова затримка, але роботизовано. Хорош як fallback.
Стріммове воспроизведення
Найважливіше в реалізації TTS — не чекати повної відповіді. 500 символів тексту на tts-1-hd синтезується ~2 секунди. Зі стрімінгом користувач чує перші слова через 300–500 мс.
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, який подає чанки аудіоданих по мері їхнього отримання. Це ~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 — дорогий. Один і той же текст з тими ж налаштуваннями голосу не повинен синтезуватися двічі.
// Android: кеш на диску з ключем 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.
UI вибору голосу
Користувач повинен почути голос перед вибором. Паттерн:
- Список голосів з ім'ям та коротким описом
- Кнопка «Прослухати» — воспроизводит 5-секундний приклад (кешуємо предзаписані семпли, не синтезуємо на льоту)
- Вибраний голос зберігається в UserDefaults / SharedPreferences
Для ElevenLabs — /v1/voices повертає список доступних голосів з метаданими: preview_url для прослухування. Не потрібно синтезувати — просто воспроизводить готовий preview.
SSML для тонкої настройки
Yandex SpeechKit та Google TTS підтримують SSML:
<speak>
Добро пожаловать в <emphasis level="strong">наш сервис</emphasis>.
<break time="500ms"/>
Ваш заказ <say-as interpret-as="cardinal">12345</say-as> готов к выдаче.
</speak>
<break>, <prosody rate="slow">, <say-as> для чисел та дат — це те, що відрізняє природне звучання від роботизованого. OpenAI TTS SSML не підтримує — управління через <pause> в тексті або промпт-інструкції.
Сроки
Базова інтеграція одного провайдера з UI вибору голосу — 4–6 днів. Стріммове воспроизведення + кеш на диску + fallback на системний TTS — ще 5–7 днів.







