Интеграция Whisper API для транскрибации в мобильном приложении
Whisper — не просто «отправь аудио, получи текст». У API есть конкретные ограничения, которые требуют подготовки на стороне клиента: 25 МБ на файл, поддержка только определённых кодеков, отсутствие стриминга, синхронный ответ. Если не учесть эти детали на этапе архитектуры, интеграция превращается в серию хотфиксов.
Лимиты и как с ними жить
25 МБ — жёсткий лимит эндпоинта 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()
// Append file
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)!)
// Append model and language
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 синхронизации текста с аудио — отдельный этап.







