Интеграция OpenAI TTS для генерации речи в мобильном приложении
OpenAI TTS — самый простой в интеграции провайдер синтеза речи с хорошим качеством. Один эндпоинт, шесть голосов, два формата запроса (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 дней.







