Интеграция Deepgram для транскрибации в реальном времени в мобильном приложении
Deepgram Nova-2 — единственный провайдер с по-настоящему низкой задержкой на стриминге: медиана около 300 мс от конца фразы до текста. Whisper такого не умеет в принципе — он синхронный. Если задача «пользователь говорит — текст появляется на экране» с задержкой меньше секунды, это Deepgram.
Протокол подключения
Deepgram работает через WebSocket. Endpoint:
wss://api.deepgram.com/v1/listen?model=nova-2&language=ru&encoding=linear16&sample_rate=16000&channels=1&interim_results=true
Параметры критичны: encoding=linear16 означает сырой PCM 16-bit little-endian. Любой другой формат без явного указания кодека — риск 1008 Policy Violation. interim_results=true включает частичные результаты — именно они создают ощущение реального времени.
iOS: AVAudioEngine + URLSessionWebSocketTask
class DeepgramStreamer {
private var audioEngine = AVAudioEngine()
private var webSocket: URLSessionWebSocketTask?
func start() throws {
let session = URLSession(configuration: .default)
var request = URLRequest(url: URL(string: "wss://api.deepgram.com/v1/listen?model=nova-2&language=ru&encoding=linear16&sample_rate=16000&channels=1&interim_results=true")!)
request.setValue("Token \(apiKey)", forHTTPHeaderField: "Authorization")
webSocket = session.webSocketTask(with: request)
webSocket?.resume()
receiveLoop()
let inputNode = audioEngine.inputNode
let format = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 16000, channels: 1, interleaved: false)!
inputNode.installTap(onBus: 0, bufferSize: 4096, format: format) { buffer, _ in
guard let channelData = buffer.int16ChannelData else { return }
let frameLength = Int(buffer.frameLength)
let data = Data(bytes: channelData[0], count: frameLength * 2)
self.webSocket?.send(.data(data)) { _ in }
}
try audioEngine.start()
}
private func receiveLoop() {
webSocket?.receive { [weak self] result in
if case .success(let message) = result, case .string(let text) = message {
// Decode Deepgram JSON response
self?.handleTranscript(text)
}
self?.receiveLoop()
}
}
}
Важная деталь: AVAudioEngine.inputNode на iOS 16+ требует явного запроса микрофона через AVAudioSession.sharedInstance().requestRecordPermission. И обязательно AVAudioSession.setCategory(.record, mode: .measurement) — режим .measurement отключает AEC и AGC, которые могут исказить сигнал для транскрипции.
Android: AudioRecord + OkHttp WebSocket
class DeepgramStreamer(private val apiKey: String) {
private val client = OkHttpClient()
private var webSocket: WebSocket? = null
private var audioRecord: AudioRecord? = null
fun start(onTranscript: (String, Boolean) -> Unit) {
val request = Request.Builder()
.url("wss://api.deepgram.com/v1/listen?model=nova-2&language=ru&encoding=linear16&sample_rate=16000&channels=1&interim_results=true")
.header("Authorization", "Token $apiKey")
.build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onMessage(webSocket: WebSocket, text: String) {
val json = JSONObject(text)
val channel = json.getJSONObject("channel")
val alternatives = channel.getJSONArray("alternatives")
val transcript = alternatives.getJSONObject(0).getString("transcript")
val isFinal = json.getBoolean("is_final")
if (transcript.isNotEmpty()) onTranscript(transcript, isFinal)
}
})
val bufferSize = AudioRecord.getMinBufferSize(16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT)
audioRecord = AudioRecord(MediaRecorder.AudioSource.MIC, 16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize)
audioRecord?.startRecording()
Thread {
val buffer = ShortArray(bufferSize / 2)
while (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
val read = audioRecord!!.read(buffer, 0, buffer.size)
if (read > 0) {
val byteBuffer = ByteBuffer.allocate(read * 2).order(ByteOrder.LITTLE_ENDIAN)
buffer.take(read).forEach { byteBuffer.putShort(it) }
webSocket?.send(byteBuffer.array().toByteString())
}
}
}.start()
}
}
ByteOrder.LITTLE_ENDIAN — обязателен. Deepgram ожидает LE PCM. Если отправить BE, транскрипция будет работать, но с заметно худшим качеством.
Что делать с interim_results
Deepgram возвращает два типа сообщений: с is_final: false (interim) и is_final: true (финальный). Правильный паттерн UI:
- Interim отображаем серым или курсивом — пользователь видит, что идёт распознавание
- При получении
is_final: trueзаменяем все предыдущие interim этого utterance финальным текстом -
speech_final: trueозначает конец паузы — хороший момент для начала обработки фразы
Типичная ошибка — накапливать все interim как отдельные строки. Это приводит к дублированию. Надо хранить текущий interim-буфер и обновлять его in-place.
Параметры Nova-2, которые меняют качество
utterance_end_ms: 1000 — Deepgram сам финализирует utterance после 1 секунды тишины. Полезно для диктовки без явных команд «стоп».
diarize: true — разделение по спикерам, добавляет speaker в каждый word.
punctuate: true — автопунктуация. Без неё текст идёт без точек и запятых.
smart_format: true — форматирует числа, даты, телефоны. «двадцать пятое марта» → «25 марта».
Сроки
Базовая интеграция WebSocket + AudioRecord/AVAudioEngine + вывод текста — 4–7 дней. Добавление диаризации, обработки переключения сети (reconnect), фонового режима, экспорта результата — 8–14 дней.







