Інтеграція Whisper API для трансгрипції в мобільних додатках
Whisper — це не просто «відправи аудіо, отримай текст». API має конкретні обмеження, які потребують підготовки на стороні клієнта: 25 МБ на файл, підтримка тільки певних кодеків, відсутність стрімінгу, синхронна відповідь. Якщо не врахувати ці деталі на етапі архітектури, інтеграція перетворюється на серію гарячих фіксів.
Ліміти та як з ними жити
25 МБ — жорсткий ліміт endpoint'а POST /v1/audio/transcriptions. Хвилина MP3 в 128 кbps займає ~1 МБ, значить ліміт — приблизно 25 хвилин. Для більшості голосових заміток хватає, для записів зустрічей — ні.
Рішення: нарізка на клієнті. На iOS — AVAssetExportSession з часовим діапазоном через AVAssetExportSession.timeRange. На Android — MediaExtractor + MediaMuxer для точної нарізки без перекодування, якщо вихідний кодек вже сумісний (AAC в MP4 — зазвичай так).
Кодек. API приймає mp3, mp4, mpeg, mpga, m4a, wav, webm. Важливо: контейнер, а не кодек всередину. .m4a з AAC — проходить. .m4a з ALAC — ні, отримаєш 400. На iOS після AVAssetExportSession з пресетом AVAssetExportPresetAppleM4A завжди буде AAC. На Android безпечніше конвертувати через MediaCodec в PCM → WAV, якщо не впевнений у джерелі.
Мова. Параметр language у форматі ISO-639-1 (ru, en, uk) прискорює трансгрипцію та зменшує помилки. Без нього Whisper витрачає час на детекцію мови та іноді помиляється на коротких фрагментах.
Реалізація на iOS (Swift)
struct WhisperService {
private let apiKey: String
private let session = URLSession.shared
func transcribe(audioURL: URL, language: String = "ru") async throws -> String {
var request = URLRequest(url: URL(string: "https://api.openai.com/v1/audio/transcriptions")!)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
let boundary = UUID().uuidString
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
var body = Data()
// Додаємо файл
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"audio.m4a\"\r\n".data(using: .utf8)!)
body.append("Content-Type: audio/m4a\r\n\r\n".data(using: .utf8)!)
body.append(try Data(contentsOf: audioURL))
body.append("\r\n".data(using: .utf8)!)
// Додаємо модель та мову
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"model\"\r\n\r\nwhisper-1\r\n".data(using: .utf8)!)
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"language\"\r\n\r\n\(language)\r\n".data(using: .utf8)!)
body.append("--\(boundary)--\r\n".data(using: .utf8)!)
request.httpBody = body
let (data, _) = try await session.data(for: request)
let response = try JSONDecoder().decode(TranscriptionResponse.self, from: data)
return response.text
}
}
Для файлів більше 25 МБ — перед викликом transcribe запускаємо AudioChunker.split(url:maxBytes:), отримуємо масив URL, запускаємо transcribe паралельно через TaskGroup, мержимо за порядком індексів.
Реалізація на Android (Kotlin)
suspend fun transcribe(file: File, language: String = "ru"): String {
val client = OkHttpClient.Builder()
.readTimeout(120, TimeUnit.SECONDS)
.build()
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", file.name, file.asRequestBody("audio/mp4".toMediaType()))
.addFormDataPart("model", "whisper-1")
.addFormDataPart("language", language)
.build()
val request = Request.Builder()
.url("https://api.openai.com/v1/audio/transcriptions")
.header("Authorization", "Bearer $apiKey")
.post(requestBody)
.build()
return withContext(Dispatchers.IO) {
client.newCall(request).execute().use { response ->
val json = response.body!!.string()
JSONObject(json).getString("text")
}
}
}
Зверни увагу: readTimeout — 120 секунд мінімум. Whisper на довгому файлі відповідає повільно. Дефолтні 10 секунд OkHttp гарантійно дадуть SocketTimeoutException.
Параметри, які часто ігнорують
response_format: verbose_json — повертає не просто текст, а сегменти з start, end, text. Це потрібно для синхронізації аудіо з текстом, пошуку по часу, субтитрів.
prompt — до 224 токенів контексту, який підказує моделі стиль та спеціальні слова. Передавай термінологію предметної області: «ТЗ, MVP, бекелог, Jira» для IT-мітингів, «ЕКГ, АД, анамнез» для медицини. Це реально знижує кількість помилок у спеціальних термінах.
temperature: 0 — детерміністичний вивід. Для продакшену краще, ніж дефолт.
Типові помилки при інтеграції
Завантаження Data(contentsOf:) цілком в пам'ять перед відправкою — на 100 МБ файлі це OOM на бюджетному Android. Використовуй file.asRequestBody() в OkHttp або InputStream-based upload в iOS через URLSession.uploadTask(withStreamedRequest:).
Відсутність retry-логіки. Whisper API періодично повертає 503 Service Unavailable при навантаженні. Експоненціальний backoff з 3 спробами закриває 99% випадків.
Зберігання API-ключа в коді або BuildConfig. Ключ повинен йти через бекенд — мобільний клієнт не повинен мати прямого доступу до OpenAI API в продакшені.
Сроки та процес
Базова інтеграція Whisper API (запис → трансгрипція → вивід тексту) на одній платформі — 3–5 днів. Додавання чанкування, verbose_json з мітками часу, retry-логіки, фонової обробки через WorkManager/BackgroundTasks — ще 5–8 днів. Мультиязичність та UI синхронізації тексту з аудіо — окремий етап.







