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