Інтеграція OpenAI TTS для генерації мови в мобільному додатку
OpenAI TTS — найпростіший в інтеграції провайдер синтезу мови з хорошою якістю. Один endpoint, шість голосів, два формати запиту (REST та streaming), підтримка 57 мов. Головний нюанс — правильно організувати кешування та стріммове воспроизведення, інакше затримка 1–3 секунди перед першим словом буде дратувати користувачів.
API та параметри
POST https://api.openai.com/v1/audio/speech
Authorization: Bearer {api_key}
Content-Type: application/json
{
"model": "tts-1-hd",
"input": "Ваш текст тут",
"voice": "nova",
"response_format": "mp3",
"speed": 1.0
}
Моделі:
-
tts-1— швидше, дещо гірша якість, дешевше ($15/млн символів) -
tts-1-hd— вища якість, повільніше ~30%, дорожче ($30/млн символів)
Голоси: alloy (нейтральний), echo (м'ягкий чоловічий), fable (британський), onyx (глибокий чоловічий), nova (жвавий жіночий), shimmer (спокійний жіночий).
Для російської мови nova та shimmer звучать найнатуральніше.
speed: 0.25–4.0. За умовчанням 1.0. Значення вище 1.3 починають ламати просодію.
Реалізація без стрімінгу (для коротких текстів)
// iOS: завантаження та воспроизведення
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 = TTSSpeechRequest(model: "tts-1", input: text, voice: voice, responseFormat: "mp3")
request.httpBody = try JSONEncoder().encode(body)
let (data, _) = try await URLSession.shared.data(for: request)
audioPlayer = try AVAudioPlayer(data: data)
audioPlayer?.play()
}
Для коротких фраз (до 100 символів) на tts-1, затримка ~300–500 мс — прийнятливо без стрімінгу. Для довгих текстів потрібен стріммінг.
Стріммове воспроизведення на Android
class OpenAITTSStreamer(private val apiKey: String, private val context: Context) {
private val exoPlayer = ExoPlayer.Builder(context).build()
fun speak(text: String, voice: String = "nova") {
val requestBody = JSONObject().apply {
put("model", "tts-1")
put("input", text)
put("voice", voice)
put("response_format", "mp3")
}.toString().toRequestBody("application/json".toMediaType())
// Використовуємо OkHttp як DataSource через кастомний MediaSource
val call = OkHttpClient().newCall(
Request.Builder()
.url("https://api.openai.com/v1/audio/speech")
.header("Authorization", "Bearer $apiKey")
.post(requestBody)
.build()
)
call.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
// Пишемо потік у тимчасовий файл, одночасно починаємо воспроизведення
val tempFile = File(context.cacheDir, "tts_${System.currentTimeMillis()}.mp3")
response.body!!.byteStream().use { input ->
tempFile.outputStream().use { output ->
val buffer = ByteArray(8192)
var bytes: Int
var firstChunk = true
while (input.read(buffer).also { bytes = it } != -1) {
output.write(buffer, 0, bytes)
if (firstChunk && tempFile.length() > 32768) {
firstChunk = false
// Починаємо воспроизведення після першіх 32 KB
Handler(Looper.getMainLooper()).post {
exoPlayer.setMediaItem(MediaItem.fromUri(tempFile.toUri()))
exoPlayer.prepare()
exoPlayer.play()
}
}
}
}
}
}
override fun onFailure(call: Call, e: IOException) { /* обробка помилки */ }
})
}
}
ExoPlayer підтримує воспроизведення з файла, який ще пишеться — ProgressiveMediaSource читає дані по мері їхнього надходження. Затримка до першого звуку — 400–700 мс.
Кеш
// iOS: кеш синтезованого аудіо
class TTSCache {
private let cacheURL: URL
init() {
cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
.appendingPathComponent("tts_cache")
try? FileManager.default.createDirectory(at: cacheURL, withIntermediateDirectories: true)
}
func key(text: String, voice: String) -> String {
let input = "\(text)|\(voice)"
return SHA256.hash(data: Data(input.utf8)).hexString
}
func get(_ key: String) -> Data? {
let url = cacheURL.appendingPathComponent(key + ".mp3")
return try? Data(contentsOf: url)
}
func set(_ key: String, data: Data) {
let url = cacheURL.appendingPathComponent(key + ".mp3")
try? data.write(to: url)
}
}
Перед кожним TTS-запитом — перевіряємо кеш. Попадання в кеш = миттєве воспроизведення. Для UI-фраз додатка (привіт, підказки) — предгенеруй аудіо при першому запуску та кешуй назавжди.
Обробка довгих текстів
OpenAI TTS приймає до 4096 символів за запрос. Для довгих текстів — разбивка по реченнях:
func splitBySentences(_ text: String, maxLength: Int = 1000) -> [String] {
var chunks: [String] = []
var current = ""
for sentence in text.components(separatedBy: CharacterSet(charactersIn: ".!?\n")) {
let trimmed = sentence.trimmingCharacters(in: .whitespaces)
if trimmed.isEmpty { continue }
if current.count + trimmed.count > maxLength {
if !current.isEmpty { chunks.append(current) }
current = trimmed
} else {
current += (current.isEmpty ? "" : ". ") + trimmed
}
}
if !current.isEmpty { chunks.append(current) }
return chunks
}
Синтезуємо куски паралельно через TaskGroup, воспроизводим послідовно — загальна затримка менша, ніж при послідовній обробці.
Сроки
REST-інтеграція з кешем на одній платформі — 3–4 дні. Стріммове воспроизведення + разбивка довгих текстів + UI управління голосом — 7–10 днів.







