Інтеграція Deepgram для real-time трансгрипції в мобільних додатках
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-буфер та оновлювати його на місці.
Параметри Nova-2, які змінюють якість
utterance_end_ms: 1000 — Deepgram сам фіналізує utterance після 1 секунди тишини. Корисно для диктовки без явних команд «стоп».
diarize: true — розділення по спікерам, додає speaker в кожне слово.
punctuate: true — автопунктуація. Без неї текст йде без точок та ком.
smart_format: true — форматує числа, дати, телефони. «двадцять п'ять березня» → «25 березня».
Сроки
Базова інтеграція WebSocket + AudioRecord/AVAudioEngine + вивід тексту — 4–7 днів. Додавання діаризації, обробки переключення мережі (reconnect), фонового режиму, експорту результату — 8–14 днів.







