whisper : run abort_callback on the language auto-detect path

1 hour ago

If you call whisper_full with the language set to auto, it runs a detection pass before it transcribes anything, one encode and one decode. Until this change, that pass ignored the abort callback you passed in. This pull request passes the callback down, so an abort during detection now stops the call before transcription starts.

Ask about this presentation

Chapters

  1. 0:00abort_callback reaches language detection
  2. 0:21Where the abort was lost
  3. 0:44The wrappers pass nullptr
  4. 1:06A static helper carries the callback
  5. 1:27Encode and decode inside the helper
  6. 1:49whisper_full hands over its params
  7. 2:11The test
  8. 2:33What it does not cover
  9. 2:54Covered in CI
Show transcript

abort_callback reaches language detection

[00:00:00.000 --> 00:00:21.754]

abort_callback now stops whisper_full during language auto-detect

whisper : run abort_callback on the language auto-detect path

Callers that set language = "auto" and pass an abort callback.

whisper.cpp

If you call whisper_full with the language set to auto, it runs a detection pass before it transcribes anything, one encode and one decode. Until this change, that pass ignored the abort callback you passed in. This pull request passes the callback down, so an abort during detection now stops the call before transcription starts.

Where the abort was lost

[00:00:21.754 --> 00:00:46.200]

Language detection ran before the abort callback was wired in

Mel spectrogram

whisper_full_with_state

language = "auto"

→

Language detection

whisper_lang_auto_detect_with_state

abort_callback: not passed

→

Main loop

encode, then decode

abort_callback: passed

Fixes #3888. encoder_begin_callback got its own fix in #3936.

Here is the order inside whisper_full_with_state. It computes the mel spectrogram, and when the language is auto, it runs language detection first. The abort callback isn't passed there. It's only handed to the encoder and decoder in the main loop, so an abort asked for during detection wasn't noticed until the first encode of the main loop had finished. That's issue 3888.

The wrappers pass nullptr

[00:00:46.200 --> 00:01:08.338]

Detection called public wrappers that hard-code nullptr for the callback

// src/whisper.cpp, before this change int whisper_encode_with_state(ctx, state, offset, n_threads) { if (!whisper_encode_internal(*ctx, *state, offset, n_threads, nullptr, nullptr)) {
int whisper_decode_with_state(ctx, state, tokens, n_tokens, n_past, n_threads) { whisper_batch_prep_legacy(state->batch, tokens, n_tokens, n_past, 0); whisper_kv_cache_seq_rm(state->kv_self, 0, n_past, -1); if (!whisper_decode_internal(*ctx, *state, state->batch, n_threads, false, nullptr, nullptr)) {

The cause is in the two public wrappers detection was calling. whisper_encode_with_state takes no callback, and it hands nullptr to the internal encoder. whisper_decode_with_state does the same, after it prepares the batch and clears the KV cache. Adding a parameter to either one would change the public API, and the author set out to avoid that.

A static helper carries the callback

[00:01:08.338 --> 00:01:26.246]

A static helper takes the callback, and the public function wraps it

- int whisper_lang_auto_detect_with_state( + static int whisper_lang_auto_detect_internal( struct whisper_context * ctx, struct whisper_state * state, int offset_ms, int n_threads, float * lang_probs, + ggml_abort_callback abort_callback, void * abort_callback_data) {
+ int whisper_lang_auto_detect_with_state(ctx, state, offset_ms, n_threads, lang_probs) { + return whisper_lang_auto_detect_internal(ctx, state, offset_ms, n_threads, lang_probs, + nullptr, nullptr); + }

So the detection body moves into a static function, whisper_lang_auto_detect_internal, with two more arguments, the callback and its user data. The public whisper_lang_auto_detect_with_state becomes a one line wrapper that calls it with nullptr for both. Existing callers keep the same signature and the same behavior.

Encode and decode inside the helper

[00:01:26.246 --> 00:01:49.923]

Inside the helper, encode and decode call the internal functions directly

- if (whisper_encode_with_state(ctx, state, seek, n_threads) != 0) { + if (!whisper_encode_internal(*ctx, *state, seek, n_threads, + abort_callback, abort_callback_data)) {
- if (whisper_decode_with_state(ctx, state, prompt.data(), prompt.size(), - 0, n_threads) != 0) { + whisper_batch_prep_legacy(state->batch, prompt.data(), prompt.size(), 0, 0); + whisper_kv_cache_seq_rm(state->kv_self, 0, 0, -1); + if (!whisper_decode_internal(*ctx, *state, state->batch, n_threads, false, + abort_callback, abort_callback_data)) {

The encode now calls whisper_encode_internal with the callback. The test flips, because the wrapper returned zero on success and the internal function returns true. Decode takes more. The wrapper also prepared the batch and cleared the KV cache, so those two lines move into the helper, with n past fixed at zero, which is the value the old call passed.

whisper_full hands over its params

[00:01:49.923 --> 00:02:13.985]

whisper_full_with_state now hands its abort callback to detection

- const auto lang_id = whisper_lang_auto_detect_with_state( - ctx, state, 0, params.n_threads, probs.data()); + const auto lang_id = whisper_lang_auto_detect_internal( + ctx, state, 0, params.n_threads, probs.data(), + params.abort_callback, params.abort_callback_user_data); if (lang_id < 0) { WHISPER_LOG_ERROR("%s: failed to auto-detect language\n", __func__); return -3; }

An abort in detection: encode fails, the helper returns -6, whisper_full returns -3.

And this is the line that connects it. whisper_full_with_state now calls the helper with the abort callback and the user data from its params. The callback is checked as soon as the detection encode finishes. When it says stop, the encode returns false, the helper returns minus 6, and whisper_full returns minus 3 with failed to auto-detect language in the log.

The test

[00:02:13.985 --> 00:02:39.200]

A new test aborts during detection and checks the main loop never began

tests/test-whisper-lang-detect-abort.cpp

std::vector<float> pcmf32(2*WHISPER_SAMPLE_RATE, 0.0f); // 2 s of silence params.language = "auto"; params.encoder_begin_callback = encoder_begin_cb; // don't block anything, just count params.abort_callback = abort_cb; // abort immediately const int rc = whisper_full(ctx, params, pcmf32.data(), pcmf32.size()); assert(rc != 0); // the call must fail because we aborted // aborted during auto-detect: main-loop encoder never started assert(n_encoder_begin == 1);

The evidence is a new test. It feeds two seconds of silence with the language on auto, an abort callback that returns true right away, and an encoder begin callback that only counts. whisper_full has to fail, and the counter has to read one. That one call is the check that runs just before detection, so the encoder in the main loop never began.

What it does not cover

[00:02:39.200 --> 00:03:00.569]

Calling the detection functions directly still can't be aborted

Public detect functions

The wrapper still passes nullptr, nullptr.

whisper_lang_auto_detect(…) whisper_lang_auto_detect_with_state(…)

Two lines, two places

Batch prep and KV cache clear now also live in the helper.

whisper_decode_with_state(…) whisper_lang_auto_detect_internal(…)

The fix lives inside whisper_full. If your code calls whisper_lang_auto_detect or its with state version directly, those still pass nullptr, so detection there still runs to the end. And the batch prep and KV cache lines now exist in two places, so a later change to whisper_decode_with_state needs the same change in the helper.

Covered in CI

[00:03:00.569 --> 00:03:22.323]

The abort callback now runs on the language auto-detect path

# tests/CMakeLists.txt registers it with LABELS "tiny;gh" ctest -L gh --output-on-failure

Using whisper_full with language = "auto" and an abort callback? Nothing to change.

The test is registered with the tiny and gh labels and runs against the tiny test model, so the ctest run with the gh label covers it. If you already pass an abort callback to whisper_full with the language on auto, there's nothing to change. The abort callback now runs on the language auto-detect path.

What happened in AI today, in your inbox

One short email each evening. Read it and you’re caught up.

Built in New York with ❤️