From 7f47b0d37f4e46d1d6cd0f92f1bc4931ed48f4da Mon Sep 17 00:00:00 2001 From: Sandro Hanea <40202887+sandrohanea@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:29:05 +0200 Subject: [PATCH 1/2] Throw exception when native processing fails --- Whisper.net/WhisperProcessingException.cs | 16 +++ Whisper.net/WhisperProcessor.cs | 12 ++- .../ProcessingFailureTests.cs | 101 ++++++++++++++++++ 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 Whisper.net/WhisperProcessingException.cs create mode 100644 tests/Whisper.net.Tests/ProcessingFailureTests.cs diff --git a/Whisper.net/WhisperProcessingException.cs b/Whisper.net/WhisperProcessingException.cs new file mode 100644 index 000000000..570989bc3 --- /dev/null +++ b/Whisper.net/WhisperProcessingException.cs @@ -0,0 +1,16 @@ +// Licensed under the MIT license: https://opensource.org/licenses/MIT + +namespace Whisper.net; + +/// +/// Exception thrown when the underlying native whisper library fails during processing. +/// +/// Error message. +/// Native error code returned by whisper. +public class WhisperProcessingException(string message, int errorCode) : Exception(message) +{ + /// + /// Gets the native error code returned by whisper. + /// + public int ErrorCode { get; } = errorCode; +} diff --git a/Whisper.net/WhisperProcessor.cs b/Whisper.net/WhisperProcessor.cs index 0aca52351..b38a51ec4 100755 --- a/Whisper.net/WhisperProcessor.cs +++ b/Whisper.net/WhisperProcessor.cs @@ -175,7 +175,11 @@ public unsafe void Process(ReadOnlySpan samples) processingSemaphore.Wait(); segmentIndex = 0; - nativeWhisper.Whisper_Full_With_State(currentWhisperContext, state, whisperParams, (IntPtr)pData, samples.Length); + var result = nativeWhisper.Whisper_Full_With_State(currentWhisperContext, state, whisperParams, (IntPtr)pData, samples.Length); + if (result != 0) + { + throw new WhisperProcessingException($"Native whisper stopped processing with error code {result}.", result); + } } finally { @@ -343,7 +347,11 @@ private unsafe Task ProcessInternalAsync(ReadOnlyMemory samples, Cancella try { - nativeWhisper.Whisper_Full_With_State(currentWhisperContext, state, whisperParams, (IntPtr)pData, samples.Length); + var result = nativeWhisper.Whisper_Full_With_State(currentWhisperContext, state, whisperParams, (IntPtr)pData, samples.Length); + if (result != 0) + { + throw new WhisperProcessingException($"Native whisper stopped processing with error code {result}.", result); + } } finally { diff --git a/tests/Whisper.net.Tests/ProcessingFailureTests.cs b/tests/Whisper.net.Tests/ProcessingFailureTests.cs new file mode 100644 index 000000000..4f612f4d2 --- /dev/null +++ b/tests/Whisper.net.Tests/ProcessingFailureTests.cs @@ -0,0 +1,101 @@ +// Licensed under the MIT license: https://opensource.org/licenses/MIT + +using System.Runtime.InteropServices; +using Whisper.net.Internals.Native; +using Whisper.net.Native; +using Xunit; + +namespace Whisper.net.Tests; + +public class ProcessingFailureTests +{ + private sealed class FakeNativeWhisper : INativeWhisper + { + private readonly int _errorCode; + + public FakeNativeWhisper(int errorCode) + { + _errorCode = errorCode; + Whisper_Full_With_State = (context, state, p, samples, n) => _errorCode; + Whisper_Init_State = _ => new IntPtr(1); + Whisper_Free_State = _ => { }; + Whisper_Full_Default_Params_By_Ref = strategy => + { + var ptr = Marshal.AllocHGlobal(Marshal.SizeOf()); + var param = new WhisperFullParams { Strategy = strategy }; + Marshal.StructureToPtr(param, ptr, false); + return ptr; + }; + Whisper_Free_Params = ptr => Marshal.FreeHGlobal(ptr); + Whisper_Init_From_File_With_Params_No_State = (_, _) => IntPtr.Zero; + Whisper_Init_From_Buffer_With_Params_No_State = (_, _, _) => IntPtr.Zero; + Whisper_Free = _ => { }; + Whisper_Full_N_Segments_From_State = _ => 0; + Whisper_Full_Get_Segment_T0_From_State = (_, _) => 0; + Whisper_Full_Get_Segment_T1_From_State = (_, _) => 0; + Whisper_Full_Get_Segment_Text_From_State = (_, _) => IntPtr.Zero; + Whisper_Full_N_Tokens_From_State = (_, _) => 0; + Whisper_Full_Get_Token_P_From_State = (_, _, _) => 0; + Whisper_Lang_Max_Id = () => 0; + Whisper_Lang_Auto_Detect_With_State = (_, _, _, _, _) => 0; + Whisper_PCM_To_Mel_With_State = (_, _, _, _, _) => 0; + Whisper_Lang_Str = _ => IntPtr.Zero; + Whisper_Full_Lang_Id_From_State = _ => 0; + Whisper_Log_Set = (_, _) => { }; + Whisper_Ctx_Init_Openvino_Encoder_With_State = (_, _, _, _, _) => { }; + Whisper_Full_Get_Token_Data_From_State = (_, _, _) => default; + Whisper_Full_Get_Token_Text_From_State = (_, _, _, _) => IntPtr.Zero; + WhisperPrintSystemInfo = () => IntPtr.Zero; + Whisper_Full_Get_Segment_No_Speech_Prob_From_State = (_, _) => 0; + } + + public INativeWhisper.whisper_init_from_file_with_params_no_state Whisper_Init_From_File_With_Params_No_State { get; } + public INativeWhisper.whisper_init_from_buffer_with_params_no_state Whisper_Init_From_Buffer_With_Params_No_State { get; } + public INativeWhisper.whisper_free Whisper_Free { get; } + public INativeWhisper.whisper_free_params Whisper_Free_Params { get; } + public INativeWhisper.whisper_full_default_params_by_ref Whisper_Full_Default_Params_By_Ref { get; } + public INativeWhisper.whisper_full_with_state Whisper_Full_With_State { get; } + public INativeWhisper.whisper_full_n_segments_from_state Whisper_Full_N_Segments_From_State { get; } + public INativeWhisper.whisper_full_get_segment_t0_from_state Whisper_Full_Get_Segment_T0_From_State { get; } + public INativeWhisper.whisper_full_get_segment_t1_from_state Whisper_Full_Get_Segment_T1_From_State { get; } + public INativeWhisper.whisper_full_get_segment_text_from_state Whisper_Full_Get_Segment_Text_From_State { get; } + public INativeWhisper.whisper_full_n_tokens_from_state Whisper_Full_N_Tokens_From_State { get; } + public INativeWhisper.whisper_full_get_token_p_from_state Whisper_Full_Get_Token_P_From_State { get; } + public INativeWhisper.whisper_lang_max_id Whisper_Lang_Max_Id { get; } + public INativeWhisper.whisper_lang_auto_detect_with_state Whisper_Lang_Auto_Detect_With_State { get; } + public INativeWhisper.whisper_pcm_to_mel_with_state Whisper_PCM_To_Mel_With_State { get; } + public INativeWhisper.whisper_lang_str Whisper_Lang_Str { get; } + public INativeWhisper.whisper_init_state Whisper_Init_State { get; } + public INativeWhisper.whisper_free_state Whisper_Free_State { get; } + public INativeWhisper.whisper_full_lang_id_from_state Whisper_Full_Lang_Id_From_State { get; } + public INativeWhisper.whisper_log_set Whisper_Log_Set { get; } + public INativeWhisper.whisper_ctx_init_openvino_encoder_with_state Whisper_Ctx_Init_Openvino_Encoder_With_State { get; } + public INativeWhisper.whisper_full_get_token_data_from_state Whisper_Full_Get_Token_Data_From_State { get; } + public INativeWhisper.whisper_full_get_token_text_from_state Whisper_Full_Get_Token_Text_From_State { get; } + public INativeWhisper.whisper_print_system_info WhisperPrintSystemInfo { get; } + public INativeWhisper.whisper_full_get_segment_no_speech_prob_from_state Whisper_Full_Get_Segment_No_Speech_Prob_From_State { get; } + + public void Dispose() { } + } + + [Fact] + public void Process_WhenNativeFails_ShouldThrow() + { + var options = new WhisperProcessorOptions { ContextHandle = IntPtr.Zero }; + using var processor = new WhisperProcessor(options, new FakeNativeWhisper(5)); + Assert.Throws(() => processor.Process(new float[1])); + } + + [Fact] + public async Task ProcessAsync_WhenNativeFails_ShouldThrow() + { + var options = new WhisperProcessorOptions { ContextHandle = IntPtr.Zero }; + await using var processor = new WhisperProcessor(options, new FakeNativeWhisper(7)); + await Assert.ThrowsAsync(async () => + { + await foreach (var _ in processor.ProcessAsync(new float[1])) + { + } + }); + } +} From 72c7650dc6ee9e722290b9dc00fbab51522216e8 Mon Sep 17 00:00:00 2001 From: Sandro Hanea <40202887+sandrohanea@users.noreply.github.com> Date: Tue, 17 Jun 2025 19:33:19 +0200 Subject: [PATCH 2/2] Improve error reporting and async cancellation --- Whisper.net/WhisperProcessingException.cs | 33 ++++++++++++++++++++--- Whisper.net/WhisperProcessor.cs | 20 ++++++++------ 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/Whisper.net/WhisperProcessingException.cs b/Whisper.net/WhisperProcessingException.cs index 570989bc3..79fccd995 100644 --- a/Whisper.net/WhisperProcessingException.cs +++ b/Whisper.net/WhisperProcessingException.cs @@ -5,12 +5,37 @@ namespace Whisper.net; /// /// Exception thrown when the underlying native whisper library fails during processing. /// -/// Error message. -/// Native error code returned by whisper. -public class WhisperProcessingException(string message, int errorCode) : Exception(message) +public class WhisperProcessingException : Exception { /// /// Gets the native error code returned by whisper. /// - public int ErrorCode { get; } = errorCode; + public int ErrorCode { get; } + + /// + /// Creates a new instance of , using a descriptive message based on the error code. + /// + /// Native error code returned by whisper. + public WhisperProcessingException(int errorCode) + : base(GetErrorMessage(errorCode)) + { + ErrorCode = errorCode; + } + + private static string GetErrorMessage(int errorCode) + { + return errorCode switch + { + -1 => "Failed to compute voice activity detection.", + -2 => "Failed to compute log mel spectrogram.", + -3 => "Failed to auto-detect language.", + -4 => "Too many decoders requested.", + -5 => "Audio context is larger than the maximum allowed.", + -6 => "Failed to encode audio features.", + -7 => "Failed to initialize key-value cache for self-attention.", + -8 => "Failed to decode audio features.", + -9 => "Failed to decode during token processing.", + _ => $"Native whisper stopped processing with error code {errorCode}." + }; + } } diff --git a/Whisper.net/WhisperProcessor.cs b/Whisper.net/WhisperProcessor.cs index b38a51ec4..75bea645d 100755 --- a/Whisper.net/WhisperProcessor.cs +++ b/Whisper.net/WhisperProcessor.cs @@ -178,7 +178,7 @@ public unsafe void Process(ReadOnlySpan samples) var result = nativeWhisper.Whisper_Full_With_State(currentWhisperContext, state, whisperParams, (IntPtr)pData, samples.Length); if (result != 0) { - throw new WhisperProcessingException($"Native whisper stopped processing with error code {result}.", result); + throw new WhisperProcessingException(result); } } finally @@ -236,17 +236,17 @@ bool OnWhisperAbortHandler() options.OnSegmentEventHandlers.Add(OnSegmentHandler); options.WhisperAbortEventHandler = OnWhisperAbortHandler; - currentCancellationToken = cancellationToken; - var whisperTask = ProcessInternalAsync(samples, cancellationToken) - .ContinueWith(_ => resetEvent.Set(), cancellationToken, TaskContinuationOptions.None, TaskScheduler.Default); + currentCancellationToken = cancellationToken; + var processingTask = ProcessInternalAsync(samples, cancellationToken); + var whisperTask = processingTask.ContinueWith(_ => resetEvent.Set(), cancellationToken, TaskContinuationOptions.None, TaskScheduler.Default); - while (!whisperTask.IsCompleted || !buffer.IsEmpty) + while (!processingTask.IsCompleted || !buffer.IsEmpty) { cancellationToken.ThrowIfCancellationRequested(); if (buffer.IsEmpty) { - await Task.WhenAny(whisperTask, resetEvent.WaitAsync()) + await Task.WhenAny(processingTask, resetEvent.WaitAsync()) .ConfigureAwait(false); } @@ -256,7 +256,11 @@ await Task.WhenAny(whisperTask, resetEvent.WaitAsync()) } } - await whisperTask.ConfigureAwait(false); + await processingTask.ConfigureAwait(false); + if (cancellationToken.IsCancellationRequested) + { + throw new TaskCanceledException(); + } while (buffer.TryDequeue(out var segmentData)) { @@ -350,7 +354,7 @@ private unsafe Task ProcessInternalAsync(ReadOnlyMemory samples, Cancella var result = nativeWhisper.Whisper_Full_With_State(currentWhisperContext, state, whisperParams, (IntPtr)pData, samples.Length); if (result != 0) { - throw new WhisperProcessingException($"Native whisper stopped processing with error code {result}.", result); + throw new WhisperProcessingException(result); } } finally