Packages

Native Elixir bindings for whisper.cpp. Accepts 16 kHz mono f32 PCM and runs on CPU, CUDA, ROCm (hipBLAS), Metal, Vulkan, or CoreML.

Current section

Files

Jump to
whisper_cpp CHANGELOG.md
Raw

CHANGELOG.md

# Changelog

All notable changes to `whisper_cpp` will be documented in this file. The
format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.0] - 2026-09-23

Several changes are caller-visible. Items that need an action from the
caller say so.

### Changed
- The precompiled NIFs have a fixed CPU baseline (#44). Before, each
  artefact was tuned for the CPU of the release runner. x86_64 Linux needs
  AVX2, FMA, F16C, and BMI2. aarch64 Linux needs ARMv8.2-A with dotprod and
  fp16. Apple Silicon needs an M1 or newer. On an older CPU, build from
  source with `WHISPER_CPP_BUILD=1`.
- The `cuda` variants carry kernels for the ggml release GPU list (`sm_50`
  to `sm_90`), not only `sm_52` PTX. Modern GPUs get faster kernels. The
  tarballs are larger.
- Vendored whisper.cpp 1.8.6 -> 1.9.4 (#47), through the same vendor branch
  of this repo. whisper-rs stays at the patched 0.16.0 (issue #26).
- `%Transcription{language: ...}` is always the ISO code (#52). A full name
  such as `"german"` now reports `"de"` on an empty result too (no speech,
  abort, no segment, or a `transcribe_slice/4` window under 0.3 s). Compare
  against the code, not the name. On a `transcribe_slice/4` window under
  0.3 s, `nil` and `"auto"` now report `"en"` on an English-only model and
  `""` on a multilingual model (was `""` and `"auto"`).
- `WHISPER_CPP_VARIANT` with a variant that the target does not publish
  fails the compile (#49), for example `hipblas` on aarch64 Linux or any
  variant on macOS. Before, it installed the CPU or Metal artefact without a
  message. Unset the variable or pick a published variant. A source build
  ignores the variant, as before.
- A change to `WHISPER_CPP_FEATURES`, `WHISPER_CPP_VARIANT`, or
  `WHISPER_CPP_BUILD` recompiles the NIF wrapper without `--force` (#46).
  To switch the backend of a Hex dependency, run
  `mix deps.compile whisper_cpp`.
- A `coreml` source build rejects `device: :cpu` and `use_gpu: false` with
  `:invalid_request`, and `available_devices/0` does not list `:cpu` there
  (#50). That build uses the Core ML encoder whenever the model's
  `-encoder.mlmodelc` loads, and a caller cannot turn it off per model.
- `:word_timestamps` returns one word per token group for Chinese, Japanese,
  Thai, Lao, Burmese, and Cantonese, not one word per segment (#51).
  Trailing punctuation joins the word before it.
- `WhisperCpp.Pcm.slice/4` rounds the end time, not the duration (#53). A
  window that ends at the buffer end no longer fails with "past the end".
  The slice can be one sample shorter than `round(duration_s * sample_rate)`.
- `transcribe_slice/4` reports every window past the buffer end as
  "requested window extends past the end of the buffer", with `start_s`,
  `end_s`, and `buffer_duration_s` in the details.
- `load_model/2` reports an empty or blank path as "path must be a
  non-empty UTF-8 string" (#54). Before, the message was "path must be a
  non-empty string".
- Source builds need Rust 1.98 or later (#60). The crate declares
  `rust-version = "1.98"`. Precompiled installs do not change.
- CI uses Elixir 1.20.4 (fixes CVE-2026-75758), OTP 29.1.1, and Rust 1.98.1.
  `mix.exs` keeps `elixir: "~> 1.19"` as the minimum version.
- Release CI builds a manual dispatch from the tag commit and never replaces
  a published tarball (#43). A push to `main` releases when the tag for the
  `mix.exs` version is missing, so a dropped run no longer loses a release.
  CI jobs have timeouts (#56).
- The NIF release profile uses 16 codegen units (was 1) and keeps thin LTO.
- Cargo refresh of transitive crates. `cfg-if` 1.0.5 and `smallvec` 1.16.1
  are in the NIF. The rest are build only. Dev only: ex_doc 0.40.3 -> 0.40.4,
  ex_slop 0.4.4 -> 0.4.5.

### Fixed
- The aarch64 Linux artefacts of 0.2.0 to 0.4.1 contain SVE and i8mm code
  and can die with SIGILL on Graviton2, Ampere Altra, Raspberry Pi 5, or
  Jetson Orin (#44). The 0.5.0 artefacts do not.
- whisper.cpp 1.9.4 fixes a heap out-of-bounds read when `transcribe/3` gets
  1 to 200 samples (ggml-org/whisper.cpp#3956), a stack buffer overflow when
  `load_model/2` reads a malformed model file (ggml-org/whisper.cpp#3957),
  and a C++ exception during a model load that could escape into the NIF
  (ggml-org/whisper.cpp#3831).
- `transcribe/3` on a multilingual model returns an empty transcription
  (`language: ""`) for 1 to 40 samples when it detects the language (#66).
  Before, it returned `:inference_error`.
- Native error messages no longer start with the internal `kind=<reason>: `
  tag (#48). The 0.4.0 fix for this did not work.
- These inputs return `:invalid_request` and no longer raise (#53, #54):
  - a time too large to convert to samples in `Pcm.slice/4` and
    `transcribe_slice/4` (was `ArithmeticError`);
  - an integer `:no_speech_thold` or `:logprob_thold` outside the i64 range
    (was `ErlangError`);
  - a `:progress_pid` on another node (was `ArgumentError`). The pid must be
    local;
  - a model path that is not valid UTF-8 in `load_model/2` (was
    `ArgumentError`).
- `transcribe_slice/4` windows under 0.3 s check the same things as longer
  windows (#55). A NaN or infinite sample in the window returns
  `:invalid_request` with the native details, and a window that rounds to
  zero samples returns `:invalid_request`. Before, both returned an empty
  success.
- `task build:*` sets the backend variables on its command line, so an
  exported `WHISPER_CPP_FEATURES` no longer changes the backend of the task.

## [0.4.1] - 2026-08-28

### Changed
- Dependency refresh. `libc` 0.2.188 -> 0.2.189 is the only runtime crate in
  the NIF that moved; the rest are build-only (`cc` 1.3.0 -> 1.4.4,
  `clang-sys` 1.8.1 -> 1.9.1, `aho-corasick`, `either`, `find-msvc-tools`,
  `log`, `regex-automata`). No API change. Every direct dependency, Hex and
  cargo, was already at its latest release; whisper-rs stays at the vendored
  0.16.0 patch (issue #26).
- Dev only: ex_slop 0.4.3 -> 0.4.4.
- `decode_pcm_f32` reads samples through `slice::as_chunks` instead of
  `chunks_exact`, which drops the manual byte indexing and clears the
  `clippy::chunks_exact_to_as_chunks` lint on newer toolchains. Same
  behaviour; the multiple-of-4 guard still rejects a misaligned buffer
  before any sample is read.

## [0.4.0] - 2026-06-11

### Added
- Built-in voice activity detection: pass `:vad_model_path` (a silero GGML
  model from `huggingface.co/ggml-org/whisper-vad`) to strip silence before
  the encoder, with `:vad_threshold`, `:vad_min_speech_ms`,
  `:vad_min_silence_ms`, and `:vad_speech_pad_ms` tuning options. Audio
  with no detected speech returns an empty transcription. The NIF runs the
  VAD itself and remaps all timestamps back to the original timeline -
  whisper.cpp's own VAD hook is dead code on the state-based API whisper-rs
  uses.

### Changed
- Native whisper.cpp/GGML logging is filtered to warnings and errors;
  the dozens of info lines per model load no longer reach stderr.
  `WHISPER_CPP_NATIVE_LOG` accepts `none`, `error`, `warn` (default),
  `info`, and `debug`. VAD contexts stay per-call: loading the silero
  model costs about a millisecond, and a shared context would serialise
  detection across concurrent transcribes.
- Integer options are bounded to `u32` and the VAD millisecond knobs to
  two minutes, returning `:invalid_request` instead of raising or
  overflowing inside the detector. Option validation now also runs
  before the sub-0.3 s `transcribe_slice` short-circuit, and an abort
  raised during the VAD pass is honoured before the encoder starts.
- `:duration_ms` must be at least 1. `0` previously meant "whole audio"
  without VAD but "empty window" with it; the ambiguity is rejected as
  `:invalid_request`.
- Passing `:vad_threshold`, `:vad_min_speech_ms`, `:vad_min_silence_ms`,
  or `:vad_speech_pad_ms` without `:vad_model_path` returns
  `:invalid_request` instead of being silently ignored.
- Buffers above `i32::MAX` samples (about 37 hours) are rejected instead
  of silently truncating at the FFI boundary.

### Fixed
- Multi-segment transcriptions no longer contain doubled spaces in
  `Transcription.text` (whisper segments carry their own leading space;
  the join added another). Space-free scripts no longer gain spurious
  spaces.
- `:temperature` is validated to `0.0..1.0` (above 1.0 whisper.cpp's
  retry ladder is empty and the decoder state undefined), `:n_threads`
  to GGML's 512-thread abort threshold, and `:beam_size`/`:best_of` to
  whisper.cpp's 8-decoder limit - all returning `:invalid_request`
  instead of native crashes or opaque inference errors.
- `:best_of` defaults to 5, matching whisper.cpp, and now also applies
  to temperature-fallback passes in beam-search mode.
- Sub-0.3 s `transcribe_slice` windows validate options, buffer bounds,
  and alignment before returning the documented empty transcription, and
  a window of exactly 0.3 s transcribes instead of being dropped by
  float subtraction error. The empty result keeps the pinned language.
- `translate: true` on English-only models returns `:invalid_request`
  instead of being silently ignored; `use_gpu: false` wins over a
  conflicting `:device`; invalid UTF-8 string options and non-keyword
  option lists return `:invalid_request` instead of raising.
- Native error messages no longer leak the internal "kind=..." routing
  tag; results with no decoded segments echo the requested language
  instead of fabricating "en"; progress percentages are clamped to the
  documented 0..100.
- `Pcm.slice/4` rounds sample positions instead of truncating, so
  millisecond-precise windows keep their last sample.
- Builds with two GPU features fail at compile time instead of silently
  picking one; unknown `WHISPER_CPP_VARIANT` values fail the build
  instead of falling back to the CPU artefact.
- `:abort_handle` and `:progress_pid` callbacks no longer leak memory per
  call: the vendored whisper-rs (branch `vendor/whisper-rs-0.16.0-patched`)
  fixes the abort-trampoline type confusion and the callback closure leak
  at the source (upstream issues 277/271, fix PR 278), replacing the
  downstream pre-boxing and sentinel workarounds. The progress sender
  thread now exits via natural channel close. The same vendor patch stops
  `set_language`, `set_initial_prompt`, and the VAD path from leaking one
  `CString` per call.

## [0.3.1] - 2026-06-11

### Changed
- Vendored whisper.cpp 1.8.3 -> 1.8.6. whisper-rs has no release vendoring
  anything newer, so `whisper-rs-sys` is patched via `[patch.crates-io]` to
  this repo's `vendor/whisper-rs-sys-1.8.6` branch - the published
  whisper-rs-sys 0.15.0 with only its whisper.cpp submodule bumped. The
  patch applies to source builds and the precompiled NIF artefacts alike,
  and is dropped as soon as upstream re-vendors (see issue #18).
- CI: `sccache-action` v0.0.9 -> v0.0.10 (Node 24; GitHub retires the
  Node 20 runtime on 2026-06-16).

## [0.3.0] - 2026-06-11

### Changed
- rustler 0.37 → 0.38 (Rust crate and optional Hex package). Additive
  upstream release; no NIF API changes needed. The vendored whisper.cpp
  stays at 1.8.3 until whisper-rs ships a release vendoring something
  newer - upstream's latest (0.16.0, 2026-03-12) predates whisper.cpp
  1.8.4.
- `language: nil` now actually auto-detects on multilingual models, as the
  docs always claimed. Previously `nil` silently fell through to
  whisper.cpp's forced-`"en"` default, decoding non-English audio as
  English. English-only models resolve `nil`/`"auto"` to `"en"`.
- `:language` is validated against whisper.cpp's language table. Unknown
  codes - including BCP 47 tags such as `"de-CH"` - return
  `:invalid_request` instead of silently corrupting the decoder prompt
  with an invalid language token. Passing a non-English language to an
  English-only model is rejected the same way instead of being silently
  ignored.
- The `:beam_size` and `:best_of` docs state the real defaults: greedy
  decoding with `best_of: 1`. The docs previously claimed a beam-search
  default of 5 that no code path produced.

### Fixed
- `:abort_handle` cancellation works now. The abort callback is passed to
  whisper-rs as a boxed trait object so the trampoline polls the real flag;
  the bare closure was reinterpreted memory (out-of-bounds reads) and the
  flag was never consulted, so cancellation silently did nothing.
- `:progress_pid` no longer leaks one OS thread per call. The progress
  sender thread is shut down explicitly after inference; the previous
  design waited for a channel close that whisper-rs's leaked callback
  closure could never trigger.
- `:word_timestamps` no longer corrupts multibyte UTF-8. Token bytes are
  accumulated per word and converted once, so characters split across BPE
  tokens (umlauts and most non-Latin scripts) survive instead of turning
  into replacement characters.
- Dropping the last reference to a loaded model frees the whisper context
  on a detached thread instead of the garbage-collecting BEAM scheduler,
  which a multi-gigabyte free would stall.
- `{:pcm_f32, _}` buffers containing NaN or infinity samples are rejected
  with `:invalid_request` instead of being fed to inference.

## [0.2.0] - 2026-05-20

### Added
- `WhisperCpp.load_model/2`: GGML/GGUF model loading with `:cpu`, `:cuda`,
  `:hipblas`, `:vulkan`, `:metal`, `:coreml`, `:intel_sycl`, and `:auto`
  device selection.
- `WhisperCpp.transcribe/3`: full whisper.cpp transcription on
  `{:pcm_f32, binary}` buffers (little-endian f32 mono at 16 kHz) with
  segment, token, and optional per-word output.
- `WhisperCpp.transcribe_slice/4`: time-shifted per-slice transcription that
  reuses one decoded PCM buffer.
- `WhisperCpp.AbortHandle`: cooperative cancellation. Pass an `%AbortHandle{}`
  via `:abort_handle` and call `AbortHandle.abort/1` from another process to
  stop in-flight inference; the partial transcription produced before the
  abort is returned.
- `:progress_pid` transcribe option: receive `{:whisper_progress, pct}`
  messages as work advances; duplicate percentages are coalesced.
- `:word_timestamps` option for per-word timing.
- `WhisperCpp.available_devices/0`: backend introspection for the loaded NIF
  artefact.
- `WhisperCpp.Pcm`: PCM slicing helpers. Audio file decoding is intentionally
  out of scope; callers decode upstream (ffmpeg, Bumblebee, ...) and share one
  decoded PCM buffer across stages.
- Rustler NIF built on `whisper-rs`, with cargo features for `cuda`,
  `hipblas`, `vulkan`, `metal`, `coreml`, `intel-sycl`, `openblas`, and
  `openmp`. Inference does not serialise across processes sharing one loaded
  model.
- Precompiled NIF artefacts via `rustler_precompiled` for x86_64 / aarch64
  Linux (CPU, CUDA, hipBLAS variants) and aarch64 macOS (Metal).