whisper.cpp Free code examples in C#, Python, Java and JavaScript
Whisper transcription in portable C++, no Python and no GPU required.
WHAT THIS PAGE COVERS
Calling the native library from C# and Java without a Python runtime.
ggml-base.en.bin
whisper.cpp
Model ids change. If a sample returns a 404 on the model, that is the first thing to check against the official docs.
TWO PACKAGES, and the second one is the one people forget. Whisper.net is managed code only — without a Whisper.net.Runtime package it compiles and then fails at load with a native-library error.
dotnet add package Whisper.net
dotnet add package Whisper.net.Runtime
# CPU is the default. There are CUDA, CoreML and Vulkan runtimes if
# the hardware is there, and they are drop-in replacements.
# Grab a model:
curl -L -o ggml-base.en.bin \
https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin
Three disposables, all holding native memory: the factory, the processor and the stream. ProcessAsync yields segments as they are decoded, so long audio starts producing output immediately.
using Whisper.net;
using var factory = WhisperFactory.FromPath("ggml-base.en.bin");
using var processor = factory.CreateBuilder()
.WithLanguage("auto")
.Build();
using var audio = File.OpenRead("meeting.wav");
await foreach (var segment in processor.ProcessAsync(audio))
{
Console.WriteLine($"{segment.Start:hh\\:mm\\:ss} {segment.Text}");
}
Whisper.net wants 16-bit 16 kHz mono WAV, same as everything else built on whisper.cpp. Renaming a file does not convert it, and a ggml model renamed to .gguf is a different format the runtime will refuse.
using System.Diagnostics;
static async Task<string> ToWhisperWavAsync(string input)
{
var output = Path.ChangeExtension(Path.GetTempFileName(), ".wav");
var ffmpeg = Process.Start(new ProcessStartInfo("ffmpeg")
{
ArgumentList =
{
"-y", "-i", input,
"-ar", "16000", // 16 kHz
"-ac", "1", // mono
"-c:a", "pcm_s16le", // 16-bit
output,
},
RedirectStandardError = true,
})!;
await ffmpeg.WaitForExitAsync();
if (ffmpeg.ExitCode != 0)
{
throw new InvalidOperationException(
await ffmpeg.StandardError.ReadToEndAsync());
}
return output;
}
WhisperFactory loads the model into native memory — seconds, and hundreds of megabytes. One per model for the life of the process; processors are cheap and can be per-request.
public sealed class Transcriber : IDisposable
{
private readonly WhisperFactory factory;
public Transcriber(string modelPath = "ggml-base.en.bin")
{
// Expensive. Registered as a singleton.
factory = WhisperFactory.FromPath(modelPath);
}
public async Task<string> TranscribeAsync(string wavPath)
{
// Cheap. Built per call, so language and prompt can vary.
using var processor = factory.CreateBuilder()
.WithLanguage("auto")
.Build();
using var audio = File.OpenRead(wavPath);
var text = new System.Text.StringBuilder();
await foreach (var segment in processor.ProcessAsync(audio))
{
text.Append(segment.Text);
}
return text.ToString().Trim();
}
public void Dispose() => factory.Dispose();
}
pywhispercpp bundles its own copy, so the build above is only needed if you want the CLI too. It is worth having: whisper-cli is how you check whether a problem is your code or your audio.
git clone https://github.com/ggml-org/whisper.cpp.git
cd whisper.cpp
cmake -B build
cmake --build build -j --config Release
# Models are downloaded separately. base.en is ~150MB and enough to
# evaluate; large-v3-turbo is the one to ship if the CPU can take it.
sh ./models/download-ggml-model.sh base.en
./build/bin/whisper-cli -m models/ggml-base.en.bin -f samples/jfk.wav
# The Python binding builds its own native library.
pip install pywhispercpp
The model name without a path downloads and caches it. Segments come back with start and end times in centiseconds — not milliseconds, which is a surprise exactly once.
from pywhispercpp.model import Model
# 'base.en' resolves to ggml-base.en.bin, downloading it once.
model = Model("base.en", print_realtime=False, print_progress=False)
segments = model.transcribe("meeting.wav")
for segment in segments:
# t0 and t1 are centiseconds.
print(f"[{segment.t0 / 100:7.2f}] {segment.text.strip()}")
16-BIT 16 kHz MONO WAV. Anything else transcribes to silence or gibberish with no error, which is the most common first-run failure with every whisper.cpp binding.
ffmpeg -i meeting.m4a -ar 16000 -ac 1 -c:a pcm_s16le meeting.wav
# Check before blaming the model:
ffprobe -hide_banner meeting.wav
# ... Audio: pcm_s16le, 16000 Hz, mono, s16
Loading is seconds; transcribing a short clip is milliseconds. A per-request Model() is the difference between a service that responds and one that times out.
from functools import lru_cache
from pywhispercpp.model import Model
@lru_cache(maxsize=1)
def get_model() -> Model:
# One instance for the life of the process. Not thread-safe, so
# guard it or keep one worker per model.
return Model("base.en", print_realtime=False, print_progress=False)
def transcribe(path: str) -> str:
segments = get_model().transcribe(path)
return " ".join(segment.text.strip() for segment in segments)
whisper-jni ships platform binaries in the jar and loads them at runtime. That is the whole reason to use it over a Python sidecar — one artifact, no interpreter on the box.
# Gradle
implementation("io.github.givimad:whisper-jni:1.7.1")
curl -L -o ggml-base.en.bin \
https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin
The context is a native handle. close() is not optional — a leaked context is memory the JVM heap will never account for and a profiler will not show you.
import io.github.givimad.whisperjni.WhisperJNI;
import io.github.givimad.whisperjni.WhisperContext;
import io.github.givimad.whisperjni.WhisperFullParams;
import java.nio.file.Path;
public class Transcribe {
public static void main(String[] args) throws Exception {
WhisperJNI.loadLibrary();
WhisperJNI whisper = new WhisperJNI();
try (WhisperContext ctx = whisper.init(Path.of("ggml-base.en.bin"))) {
float[] samples = readSamples(Path.of("meeting.wav"));
WhisperFullParams params = new WhisperFullParams();
int result = whisper.full(ctx, params, samples, samples.length);
if (result != 0) {
throw new IllegalStateException("whisper.full returned " + result);
}
int segments = whisper.fullNSegments(ctx);
for (int i = 0; i < segments; i++) {
System.out.println(whisper.fullGetSegmentText(ctx, i));
}
}
}
}
full() takes normalised floats in [-1, 1], not bytes. THE FILE MUST ALREADY BE 16-bit 16 kHz mono — javax.sound will read other formats happily and hand you samples the model cannot use.
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
static float[] readSamples(Path path) throws Exception {
try (AudioInputStream stream = AudioSystem.getAudioInputStream(path.toFile())) {
byte[] bytes = stream.readAllBytes();
ShortBuffer pcm = ByteBuffer.wrap(bytes)
.order(ByteOrder.LITTLE_ENDIAN)
.asShortBuffer();
float[] samples = new float[pcm.limit()];
for (int i = 0; i < samples.length; i++) {
// Divide by Short.MAX_VALUE, not 32768, or the loudest
// sample clips by one bit.
samples[i] = Math.max(-1f, Math.min(1f,
pcm.get(i) / (float) Short.MAX_VALUE));
}
return samples;
}
}
javax.sound cannot read mp3 or m4a, and will not resample. ffmpeg does both, and running it once up front removes the entire class of empty-transcript bug.
ffmpeg -i meeting.m4a -ar 16000 -ac 1 -c:a pcm_s16le meeting.wav
# Verify before blaming the model:
ffprobe -hide_banner meeting.wav
# ... Audio: pcm_s16le, 16000 Hz, mono, s16
THERE IS NO FIRST-PARTY NODE BINDING. Driving whisper-cli from child_process is the supported integration — it survives every version bump, and the CLI is also how you check whether a problem is your code or your audio.
git clone https://github.com/ggml-org/whisper.cpp.git
cd whisper.cpp
cmake -B build
cmake --build build -j --config Release
# Models are downloaded separately. base.en is ~150MB and enough to
# evaluate; large-v3-turbo is the one to ship if the CPU can take it.
sh ./models/download-ggml-model.sh base.en
./build/bin/whisper-cli -m models/ggml-base.en.bin -f samples/jfk.wav
-oj writes JSON beside the input rather than to stdout, which is the only reliable way to get structured output — the console output is meant for humans and its format is not stable.
import { execFile } from "node:child_process";
import { readFile } from "node:fs/promises";
import { promisify } from "node:util";
const run = promisify(execFile);
async function transcribe(wavPath) {
await run("./build/bin/whisper-cli", [
"-m", "models/ggml-base.en.bin",
"-f", wavPath,
"-oj", // write <wavPath>.json
"--no-prints", // keep progress chatter off stderr
]);
const parsed = JSON.parse(await readFile(`${wavPath}.json`, "utf8"));
return parsed.transcription
.map((segment) => segment.text.trim())
.join(" ");
}
console.log(await transcribe("meeting.wav"));
16-bit 16 kHz mono WAV or the transcript comes back empty, with exit code 0 and no explanation. Convert first, always.
async function toWhisperWav(input) {
const output = input.replace(/\.[^.]+$/, "") + "-16k.wav";
await run("ffmpeg", [
"-y", "-i", input,
"-ar", "16000", // 16 kHz
"-ac", "1", // mono
"-c:a", "pcm_s16le", // 16-bit
output,
]);
return output;
}
const wav = await toWhisperWav("meeting.m4a");
console.log(await transcribe(wav));
whisper-cli uses every core it can by default. On a server that is also serving requests, -t caps it, and a queue in front stops two transcriptions from fighting over the same cores.
import os from "node:os";
const THREADS = Math.max(1, Math.floor(os.cpus().length / 2));
async function transcribeCapped(wavPath) {
await run(
"./build/bin/whisper-cli",
[
"-m", "models/ggml-base.en.bin",
"-f", wavPath,
"-t", String(THREADS),
"-oj",
"--no-prints",
],
// A long recording on a small box genuinely takes minutes.
{ timeout: 15 * 60 * 1000, maxBuffer: 16 * 1024 * 1024 },
);
const parsed = JSON.parse(await readFile(`${wavPath}.json`, "utf8"));
return parsed.transcription.map((s) => s.text.trim()).join(" ");
}
QUESTIONS
Which languages are the whisper.cpp examples in?
C#, Python, Java and JavaScript. Every sample runs as pasted and reads its credentials from an environment variable — no key is ever written into the code.
Which package do I install for whisper.cpp?
The samples use whisper.cpp. The install step on each language tab gives the exact command.
Which whisper.cpp model do these examples use?
ggml-base.en.bin, checked against the official documentation on 26 September 2026. Model ids change often; if a call returns a model-not-found error, check the current list in the official docs.
Where is the official whisper.cpp documentation?
https://github.com/ggml-org/whisper.cpp — it is linked at the bottom of this page, and it is the authority whenever it disagrees with a sample here.
ALSO IN SPEECH & AUDIO
The authority
Official whisper.cpp documentation
Model names, pricing and rate limits change faster than any page here can. When this page and the official docs disagree, the official docs are right.
https://github.com/ggml-org/whisper.cpp