Skip to content
LucQuebec edited this page Jul 5, 2026 · 1 revision

Usage — complete code examples

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.


1. Minimal example (Activity)

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)
        }
    }
}

2. Recommended: a ViewModel (load once, reuse, release on teardown)

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
    }
}

3. Load the model from assets

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.


4. Working with segments and timestamps

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)
}

5. Language detection and translation

// 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 = true always targets English. Multilingual models only (base, small, …) — the .en models are English-only.


6. Error handling

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
}

7. Recording audio to transcribe

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.


8. Check the runtime

Log.i("Whisper", Whisper.getSystemInfo())  // NEON, threads, backend flags

Useful 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.

Clone this wiki locally