-
Notifications
You must be signed in to change notification settings - Fork 0
Usage
Real, copy-paste recipes for the Free API. For the 5-step fast path see Quick Start; for tuning and common problems see Getting Good Results.
The whole API surface:
object Whisper {
suspend fun loadModel(context: Context, modelPath: String): WhisperModel
suspend fun loadModelFromAsset(context: Context, assetName: String): WhisperModel
suspend fun transcribe(model: WhisperModel, audioPath: String, config: WhisperConfig = WhisperConfig()): WhisperResult
fun releaseModel(model: WhisperModel)
fun getSystemInfo(): String
}
data class WhisperConfig(
val language: String = "auto", // ISO code ("en", "fr", …) or "auto"
val translate: Boolean = false, // translate to English
val threads: Int = 4,
val maxSegmentLength: Int = 0,
val printTimestamps: Boolean = true,
)
data class WhisperResult(val text: String, val segments: List<WhisperSegment>, val processingTimeMs: Long)
data class WhisperSegment(val startMs: Long, val endMs: Long, val text: String)Audio input can be WAV, MP3 or FLAC at any sample rate — it's decoded and resampled to 16 kHz mono internally.
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import dev.ffmpegkit.whisper.Whisper
import dev.ffmpegkit.whisper.WhisperConfig
import kotlinx.coroutines.launch
import java.io.File
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val modelPath = File(getExternalFilesDir("models"), "ggml-base.en.bin").absolutePath
val audioPath = File(getExternalFilesDir(null), "speech.wav").absolutePath
val model = Whisper.loadModel(this@MainActivity, modelPath)
val result = Whisper.transcribe(model, audioPath, WhisperConfig(language = "en"))
Log.i("Whisper", "Transcript: ${result.text}")
Whisper.releaseModel(model)
}
}
}Loading the model is expensive — do it once and keep it for the whole screen. A
ViewModel makes the lifecycle clean.
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import dev.ffmpegkit.whisper.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class TranscribeViewModel(app: Application) : AndroidViewModel(app) {
private var model: WhisperModel? = null
val state = MutableStateFlow<String>("idle")
fun prepare(modelPath: String) = viewModelScope.launch {
state.value = "loading model…"
try {
model = Whisper.loadModel(getApplication(), modelPath)
state.value = "ready"
} catch (e: WhisperException.ModelLoadException) {
state.value = "model error: ${e.message}"
}
}
fun transcribe(audioPath: String, language: String = "auto") = viewModelScope.launch {
val m = model ?: run { state.value = "model not loaded"; return@launch }
state.value = "transcribing…"
try {
val result = withContext(Dispatchers.Default) {
Whisper.transcribe(m, audioPath, WhisperConfig(language = language))
}
state.value = result.text
} catch (e: WhisperException) {
state.value = "error: ${e.message}"
}
}
override fun onCleared() {
model?.let { Whisper.releaseModel(it) } // free native memory
model = null
}
}Bundle the model in src/main/assets/models/ and load it directly (it's copied to
the cache dir automatically, because the native loader needs a real file path):
val model = Whisper.loadModelFromAsset(context, "models/ggml-base.en.bin")Bundling a model inflates your APK by 75 MB–500 MB. Prefer a first-run download into
getExternalFilesDir("models")for anything but the tiniest model.
WhisperResult.segments gives you time-stamped chunks — perfect for subtitles or
jumping to a point in the audio.
val result = Whisper.transcribe(model, audioPath, WhisperConfig(language = "en"))
println("Full text: ${result.text}")
println("Took ${result.processingTimeMs} ms")
result.segments.forEach { seg ->
val start = "%.1f".format(seg.startMs / 1000.0)
val end = "%.1f".format(seg.endMs / 1000.0)
println("[$start s – $end s] ${seg.text}")
}
// Build a naive SRT subtitle file
val srt = result.segments.mapIndexed { i, s ->
"${i + 1}\n${msToSrt(s.startMs)} --> ${msToSrt(s.endMs)}\n${s.text}\n"
}.joinToString("\n")
fun msToSrt(ms: Long): String {
val h = ms / 3_600_000; val m = (ms % 3_600_000) / 60_000
val s = (ms % 60_000) / 1000; val millis = ms % 1000
return "%02d:%02d:%02d,%03d".format(h, m, s, millis)
}// Auto-detect the language
Whisper.transcribe(model, path, WhisperConfig(language = "auto"))
// Force a language (more reliable when you know it)
Whisper.transcribe(model, path, WhisperConfig(language = "fr"))
// Transcribe AND translate to English in one pass
Whisper.transcribe(model, path, WhisperConfig(language = "auto", translate = true))
translate = truealways targets English. Multilingual models only (base,small, …) — the.enmodels are English-only.
Every failure is a typed WhisperException:
try {
val model = Whisper.loadModel(context, modelPath)
val result = Whisper.transcribe(model, audioPath)
// …
Whisper.releaseModel(model)
} catch (e: WhisperException.ModelLoadException) {
// model file missing or corrupt
} catch (e: WhisperException.InvalidAudioException) {
// audio missing or not decodable (not WAV/MP3/FLAC)
} catch (e: WhisperException.TranscriptionException) {
// whisper.cpp failed, or model was already released
}Record 16 kHz mono 16-bit PCM with AudioRecord and write a WAV the library can
read. Requires the RECORD_AUDIO permission in your app (request it at runtime).
import android.media.*
import java.io.File
import java.io.RandomAccessFile
import java.nio.ByteBuffer
import java.nio.ByteOrder
/** Records mono 16 kHz PCM WAV until [stop] returns true. Call off the main thread. */
fun recordWav(out: File, stop: () -> Boolean) {
val rate = 16000
val minBuf = AudioRecord.getMinBufferSize(
rate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT,
)
val rec = AudioRecord(
MediaRecorder.AudioSource.VOICE_RECOGNITION, // speech-tuned mic
rate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, minBuf,
)
val raf = RandomAccessFile(out, "rw").apply { setLength(0); write(ByteArray(44)) } // header placeholder
val buf = ShortArray(minBuf / 2)
var total = 0L
rec.startRecording()
while (!stop()) {
val n = rec.read(buf, 0, buf.size)
if (n > 0) {
val bytes = ByteBuffer.allocate(n * 2).order(ByteOrder.LITTLE_ENDIAN)
for (i in 0 until n) bytes.putShort(buf[i])
raf.write(bytes.array()); total += n * 2
}
}
rec.stop(); rec.release()
writeWavHeader(raf, total, rate); raf.close()
}
private fun writeWavHeader(raf: RandomAccessFile, dataLen: Long, rate: Int) {
fun le(v: Int, n: Int) = ByteArray(n) { ((v shr (8 * it)) and 0xFF).toByte() }
raf.seek(0)
raf.write("RIFF".toByteArray()); raf.write(le((36 + dataLen).toInt(), 4)); raf.write("WAVE".toByteArray())
raf.write("fmt ".toByteArray()); raf.write(le(16, 4)); raf.write(le(1, 2)); raf.write(le(1, 2))
raf.write(le(rate, 4)); raf.write(le(rate * 2, 4)); raf.write(le(2, 2)); raf.write(le(16, 2))
raf.write("data".toByteArray()); raf.write(le(dataLen.toInt(), 4))
}Then transcribe the file:
val wav = File(context.cacheDir, "recording.wav")
// … recordWav(wav) { userPressedStop } on a background thread …
val result = Whisper.transcribe(model, wav.absolutePath, WhisperConfig(language = "auto"))Real-time transcription (mic → text as you speak) with automatic silence detection is the Pro tier (
WhisperStreaming+WhisperVAD). See jokobee.com.
Log.i("Whisper", Whisper.getSystemInfo()) // NEON, threads, backend flagsUseful in a bug report to confirm NEON is active and the native library loaded.
Next: Getting Good Results (speed, noise, accuracy, hallucinations) · Model Download · Troubleshooting.