Інтеграція ElevenLabs для генерації мови в мобільному додатку
ElevenLabs — один з двох провайдерів з по-справжньому природно звучащою мультимовною мовою (другий — OpenAI TTS). Для російської мови ElevenLabs з моделлю eleven_multilingual_v2 видає результат, який люди регулярно приймають за живу мову. Інтеграція нетривіальна: у API є нюанси з форматами, стрімінгом та управлінням символьною квотою.
Базова інтеграція: REST
Мінімальний запрос на синтез:
POST https://api.elevenlabs.io/v1/text-to-speech/{voice_id}
xi-api-key: YOUR_KEY
Content-Type: application/json
{
"text": "Привіт, це тестовий текст",
"model_id": "eleven_multilingual_v2",
"voice_settings": {
"stability": 0.5,
"similarity_boost": 0.75,
"style": 0.0,
"use_speaker_boost": true
}
}
Відповідь — бінарний аудіофайл. За умовчанням mp3_44100_128, можна змінити через query-параметр output_format: pcm_16000, pcm_22050, pcm_24000, pcm_44100, mp3_22050_32, mp3_44100_64, mp3_44100_128, mp3_44100_192.
Для воспроизведення в мобільному додатку — mp3_44100_128. Для on-the-fly воспроизведення без зберігання — pcm_16000 з немедленною подачею в AudioTrack / AVAudioPlayerNode.
Стріммінг: WebSocket API
ElevenLabs підтримує два види стрімінгу: streaming HTTP (/v1/text-to-speech/{voice_id}/stream) та WebSocket (/v1/text-to-speech/{voice_id}/stream-input).
WebSocket — для діалогових додатків, де текст генерується по мері ответа LLM:
// iOS: стріммінг через URLSessionWebSocketTask
class ElevenLabsStreamPlayer {
private var webSocket: URLSessionWebSocketTask?
private var audioEngine = AVAudioEngine()
private var playerNode = AVAudioPlayerNode()
func connect(voiceId: String) {
let url = URL(string: "wss://api.elevenlabs.io/v1/text-to-speech/\(voiceId)/stream-input?model_id=eleven_multilingual_v2&output_format=pcm_16000")!
var request = URLRequest(url: url)
request.setValue(apiKey, forHTTPHeaderField: "xi-api-key")
webSocket = URLSession.shared.webSocketTask(with: request)
webSocket?.resume()
// Інічіалізація потока
let initMsg = #"{"text":" ","voice_settings":{"stability":0.5,"similarity_boost":0.75}}"#
webSocket?.send(.string(initMsg)) { _ in }
audioEngine.attach(playerNode)
audioEngine.connect(playerNode, to: audioEngine.mainMixerNode, format: nil)
try? audioEngine.start()
receiveAudio()
}
func sendText(_ chunk: String) {
let msg = "{\"text\":\"\(chunk)\"}"
webSocket?.send(.string(msg)) { _ in }
}
func flush() {
webSocket?.send(.string("{\"text\":\"\"}")) { _ in }
}
private func receiveAudio() {
webSocket?.receive { [weak self] result in
if case .success(.string(let text)) = result,
let data = text.data(using: .utf8),
let json = try? JSONDecoder().decode(AudioChunk.self, from: data),
let audioB64 = json.audio,
let audioData = Data(base64Encoded: audioB64) {
self?.enqueueAudio(audioData)
}
self?.receiveAudio()
}
}
private func enqueueAudio(_ data: Data) {
// PCM 16000 Hz, int16 → AVAudioPCMBuffer
let format = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 16000, channels: 1, interleaved: false)!
let frameCount = AVAudioFrameCount(data.count / 2)
guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCount) else { return }
buffer.frameLength = frameCount
data.withUnsafeBytes { ptr in
buffer.int16ChannelData?[0].update(from: ptr.bindMemory(to: Int16.self).baseAddress!, count: Int(frameCount))
}
playerNode.scheduleBuffer(buffer, completionHandler: nil)
if !playerNode.isPlaying { playerNode.play() }
}
}
Паттерн використання в діалоговому ассистенті: по мері отримання токенів від GPT — sendText(token), по завершенню ответа — flush(). Затримка до першого звуку — 200–400 мс.
Налаштування голосу
stability (0–1): чим вище, тим більш монотонно. 0.3–0.5 для живої мови, 0.8–1.0 для дикторського чтення.
similarity_boost (0–1): наскільки точно воспроізводить оригінальний тембр клонованого голосу. Занадто високе значення (>0.9) може додати артефакти.
style (0–1): тільки для eleven_multilingual_v2 та eleven_turbo_v2_5. Усилює емоціональність. 0 для нейтральної мови.
use_speaker_boost: true — улучшает четкість для синтезованих голосів (не клонів). Включай за умовчанням.
Моніторинг квоти
ElevenLabs рахує символи. GET /v1/user/subscription повертає character_count та character_limit. Додавай перевірку перед кожним запитом — якщо квота < довжини тексту, показуй помилку або пропонуй апгрейд.
suspend fun checkQuota(textLength: Int): Boolean {
val response = httpClient.get("https://api.elevenlabs.io/v1/user/subscription") {
header("xi-api-key", apiKey)
}.body<SubscriptionInfo>()
return (response.characterLimit - response.characterCount) >= textLength
}
Кешування
Один і той же текст з тими ж налаштуваннями голосу повинен синтезуватися один раз. Ключ кешу: sha256(text + voice_id + stability + similarity_boost + model_id). Зберігай файли у внутрішньому сховищі додатка з TTL 30 днів, LRU-вибросом при перевищенні 100 МБ.
Сроки
Базова інтеграція REST + воспроизведення — 2–3 дні. Стріммовий WebSocket з подачею токенів від LLM — 5–7 днів. Повний UI вибору голосу + кеш + моніторинг квоти — 10–14 днів.







