summaryrefslogtreecommitdiffstats
path: root/WhisperNet/Internal
diff options
context:
space:
mode:
authorKonstantin <const@const.me>2023-01-16 14:52:43 +0100
committerKonstantin <const@const.me>2023-01-16 14:52:43 +0100
commit8c4603c73675958efc960fbd4bb599a2909d106a (patch)
tree714dc6fc9a1672d5fd7f89676b97e10959662abc /WhisperNet/Internal
parent990a8d0dbaefc996244097397259e92758b15cce (diff)
Source codes
Diffstat (limited to 'WhisperNet/Internal')
-rw-r--r--WhisperNet/Internal/AssemblyInfo.cs8
-rw-r--r--WhisperNet/Internal/NativeLogger.cs138
-rw-r--r--WhisperNet/Internal/iContext.cs37
-rw-r--r--WhisperNet/Internal/iTranscribeResult.cs122
-rw-r--r--WhisperNet/Internal/sCaptureCallbacks.cs23
-rw-r--r--WhisperNet/Internal/sCaptureDevice.cs22
-rw-r--r--WhisperNet/Internal/sFullParams.cs40
-rw-r--r--WhisperNet/Internal/sLoadModelCallbacks.cs64
-rw-r--r--WhisperNet/Internal/sLoggerSetup.cs16
-rw-r--r--WhisperNet/Internal/sProgressSink.cs20
10 files changed, 490 insertions, 0 deletions
diff --git a/WhisperNet/Internal/AssemblyInfo.cs b/WhisperNet/Internal/AssemblyInfo.cs
new file mode 100644
index 0000000..29ec638
--- /dev/null
+++ b/WhisperNet/Internal/AssemblyInfo.cs
@@ -0,0 +1,8 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+[assembly: AssemblyTitle( "WhisperNet" )]
+[assembly: AssemblyCopyright( "Copyright © const.me, 2022" )]
+[assembly: ComVisible( false )]
+[assembly: Guid( "ced6cdb7-e040-4398-bae8-3417e5fa35f1" )]
+[assembly: AssemblyVersion( "1.0.0.0" )]
+[assembly: AssemblyDescription( "DirectCompute port of whisper.cpp library, C# bindings" )] \ No newline at end of file
diff --git a/WhisperNet/Internal/NativeLogger.cs b/WhisperNet/Internal/NativeLogger.cs
new file mode 100644
index 0000000..b4b4eb2
--- /dev/null
+++ b/WhisperNet/Internal/NativeLogger.cs
@@ -0,0 +1,138 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.ExceptionServices;
+using System.Runtime.InteropServices;
+
+namespace Whisper.Internal
+{
+ /// <summary>Utility class to supply logging function pointer to the C++ library,<br/>
+ /// and provide custom calling conventions to ComLight runtime to convert error messages printed in C++ into .NET exception messages</summary>
+ public static class NativeLogger
+ {
+ internal static void startup() { }
+
+ static NativeLogger()
+ {
+ sink = logSink;
+ sLoggerSetup setup = default;
+ setup.sink = sink;
+ setup.level = eLogLevel.Warning;
+ Library.setupLogger( ref setup );
+ }
+
+ internal static void setup( eLogLevel lvl, eLoggerFlags flags, pfnLogMessage? pfn )
+ {
+ logMessage = pfn;
+
+ sLoggerSetup setup = default;
+ setup.sink = sink;
+ setup.level = lvl;
+ setup.flags = flags;
+ Library.setupLogger( ref setup );
+ }
+
+ // This field is here to protect the function pointer from being collected by the GC
+ static readonly pfnLoggerSink sink;
+
+ static void logSink( IntPtr context, eLogLevel lvl, string message )
+ {
+ if( lvl == eLogLevel.Error )
+ state.setText( message );
+ logMessage?.Invoke( lvl, message );
+ }
+
+ sealed class ThreadState
+ {
+ string? errorText = null;
+ ExceptionDispatchInfo? dispatchInfo = null;
+
+ public void setText( string text ) => errorText = text;
+ public void capture( Exception ex ) => dispatchInfo = ExceptionDispatchInfo.Capture( ex );
+
+ public void clear()
+ {
+ errorText = null;
+ dispatchInfo = null;
+ }
+
+ public void Deconstruct( out string? text, out ExceptionDispatchInfo? edi )
+ {
+ text = errorText;
+ edi = dispatchInfo;
+ errorText = null;
+ dispatchInfo = null;
+ }
+ }
+
+ [ThreadStatic]
+ static ThreadState state = new ThreadState();
+
+ internal static void captureException( Exception ex ) =>
+ state.capture( ex );
+
+ static pfnLogMessage? logMessage = null;
+
+ /// <summary>Called internally by ComLight runtime</summary>
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
+ public static void prologue()
+ {
+ // https://stackoverflow.com/a/2043505/126995
+ if( null != state )
+ state.clear();
+ else
+ createState();
+ }
+
+ [MethodImpl( MethodImplOptions.NoInlining )]
+ static void createState()
+ {
+ state = new ThreadState();
+ }
+
+ /// <summary>Epilogue implementation for unsuccessful status codes</summary>
+ [MethodImpl( MethodImplOptions.NoInlining )]
+ static void throwException( int hr )
+ {
+ // Move state from the thread local object into local variables, and clear that object
+ (string? text, ExceptionDispatchInfo? edi) = state;
+
+ if( null != edi && edi.SourceException.HResult == hr )
+ {
+ // The error comes from a callback, and we have original context of that exception.
+ // Re-throw the original exception.
+ // This uses the original error message, and even correctly deals with the stack trace.
+ edi.Throw();
+ }
+
+ if( null != text )
+ {
+ // C++ code has printed an error on the current thread, between prologue and epilogue.
+ // Use that text for the exception message.
+ Exception? ex = Marshal.GetExceptionForHR( hr );
+ throw new ApplicationException( text, ex );
+ }
+
+ // We don’t have any additional info about the exception.
+ // Throw an exception from just the HRESULT code.
+ Marshal.ThrowExceptionForHR( hr );
+ }
+
+ /// <summary>Called internally by ComLight runtime</summary>
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
+ public static void throwForHR( int hr )
+ {
+ if( hr >= 0 )
+ return; // SUCCEEDED
+ throwException( hr );
+ }
+
+ /// <summary>Called internally by ComLight runtime</summary>
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
+ public static bool throwAndReturnBool( int hr )
+ {
+ if( hr >= 0 )
+ return 0 == hr;
+ throwException( hr );
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/WhisperNet/Internal/iContext.cs b/WhisperNet/Internal/iContext.cs
new file mode 100644
index 0000000..6adf8c5
--- /dev/null
+++ b/WhisperNet/Internal/iContext.cs
@@ -0,0 +1,37 @@
+using ComLight;
+using System.Runtime.InteropServices;
+using Whisper.Internals;
+
+namespace Whisper.Internal
+{
+ /// <summary>Stateful context, contains methods to transcribe audio</summary>
+ [ComInterface( "b9956374-3b18-4943-90f2-2ab18a404537", eMarshalDirection.ToManaged ), CustomConventions( typeof( NativeLogger ) )]
+ public interface iContext: IDisposable
+ {
+ /// <summary>Run the entire model: PCM -> log mel spectrogram -> encoder -> decoder -> text</summary>
+ void runFull( [In] ref sFullParams @params, iAudioBuffer buffer );
+
+ /// <summary>Run the entire model, streaming audio from the provided reader object</summary>
+ void runStreamed( [In] ref sFullParams @params, [In] ref sProgressSink progressSink, iAudioReader reader );
+
+ /// <summary>Continuously process audio from microphone or a similar capture device</summary>
+ void runCapture( [In] ref sFullParams @params, [In] ref sCaptureCallbacks callbacks, iAudioCapture reader );
+
+ /// <summary>Get text results out of the context</summary>
+ [RetValIndex( 1 )]
+ iTranscribeResult getResults( eResultFlags flags );
+
+ /// <summary>Get the model which was used to create this context</summary>
+ [RetValIndex]
+ iModel getModel();
+
+ /// <summary>Full the default parameters of the model, for the specified sampling strategy</summary>
+ [RetValIndex( 1 )]
+ sFullParams fullDefaultParams( eSamplingStrategy strategy );
+
+ /// <summary>Print timing data</summary>
+ void timingsPrint();
+ /// <summary>Reset timing data</summary>
+ void timingsReset();
+ }
+} \ No newline at end of file
diff --git a/WhisperNet/Internal/iTranscribeResult.cs b/WhisperNet/Internal/iTranscribeResult.cs
new file mode 100644
index 0000000..cbf49dd
--- /dev/null
+++ b/WhisperNet/Internal/iTranscribeResult.cs
@@ -0,0 +1,122 @@
+#pragma warning disable CS0649 // Field is never assigned to
+using ComLight;
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+
+namespace Whisper.Internal
+{
+ /// <summary>Size of the buffers owned by the <see cref="iTranscribeResult" /> object</summary>
+ public readonly struct sTranscribeLength
+ {
+ /// <summary>Count of segments</summary>
+ public readonly int countSegments;
+ /// <summary>Total count of tokens, for all segments combined</summary>
+ public readonly int countTokens;
+ }
+
+ /// <summary>Output data from the model</summary>
+ [ComInterface( "2871a73f-5ce3-48f8-8779-6582ee11935e", eMarshalDirection.ToManaged ), CustomConventions( typeof( NativeLogger ) )]
+ public interface iTranscribeResult
+ {
+ /// <summary>Get size of the buffers</summary>
+ [RetValIndex, EditorBrowsable( EditorBrowsableState.Never )]
+ public sTranscribeLength getSize();
+
+ /// <summary>Pointer to segment data, a vector of <see cref="sSegment" /> structures</summary>
+ [EditorBrowsable( EditorBrowsableState.Never )]
+ public IntPtr getSegments();
+
+ /// <summary>Pointer to tokens data, a vector of <see cref="sToken" /> structures</summary>
+ [EditorBrowsable( EditorBrowsableState.Never )]
+ public IntPtr getTokens();
+ }
+}
+
+namespace Whisper
+{
+ /// <summary>Start and end times of a segment or token</summary>
+ /// <remarks>The times are relative to the start of the media</remarks>
+ public readonly struct sTimeInterval
+ {
+ /// <summary>Start time</summary>
+ public readonly TimeSpan begin;
+ /// <summary>End time</summary>
+ public readonly TimeSpan end;
+ }
+
+ /// <summary>Segment data</summary>
+ public readonly struct sSegment
+ {
+ internal readonly IntPtr m_text;
+ /// <summary>Segment text</summary>
+ public string? text => Marshal.PtrToStringUTF8( m_text );
+ /// <summary>Start and end times of the segment</summary>
+ public readonly sTimeInterval time;
+ /// <summary>Slice of the tokens</summary>
+ public readonly int firstToken, countTokens;
+ }
+
+ /// <summary>Token flags</summary>
+ [Flags]
+ public enum eTokenFlags: uint
+ {
+ /// <summary>The token is special</summary>
+ Special = 1,
+ }
+
+ /// <summary>Token data</summary>
+ public readonly struct sToken
+ {
+ internal readonly IntPtr m_text;
+ /// <summary>Token text</summary>
+ public string? text => Marshal.PtrToStringUTF8( m_text );
+ /// <summary>Start and end times of the token</summary>
+ public readonly sTimeInterval time;
+ /// <summary>Probability of the token</summary>
+ public readonly float probability;
+ /// <summary>Probability of the timestamp token</summary>
+ public readonly float probabilityTimestamp;
+ /// <summary>Sum of probabilities of all timestamp tokens</summary>
+ public readonly float ptsum;
+ /// <summary>Voice length of the token</summary>
+ public readonly float vlen;
+ /// <summary>Token id</summary>
+ public readonly int id;
+ /// <summary>Token flags</summary>
+ readonly eTokenFlags flags;
+ /// <summary>True if the token flags has the specified bit set</summary>
+ public bool hasFlag( eTokenFlags bit ) => flags.HasFlag( bit );
+ }
+
+ /// <summary>Output data from the model</summary>
+ public readonly ref struct TranscribeResult
+ {
+ /// <summary>Segments in the results</summary>
+ public readonly ReadOnlySpan<sSegment> segments;
+ /// <summary>Tokens in the results, for all segments</summary>
+ public readonly ReadOnlySpan<sToken> tokens;
+
+ internal TranscribeResult( Internal.iTranscribeResult i )
+ {
+ Internal.sTranscribeLength len = i.getSize();
+ unsafe
+ {
+ // This does not copy the buffers to managed memory.
+ // Instead, the C# spans directly reference the native memory stored in these std::vectors
+ if( len.countSegments > 0 )
+ segments = new ReadOnlySpan<sSegment>( (void*)i.getSegments(), len.countSegments );
+ else
+ segments = ReadOnlySpan<sSegment>.Empty;
+
+ if( len.countTokens > 0 )
+ tokens = new ReadOnlySpan<sToken>( (void*)i.getTokens(), len.countTokens );
+ else
+ tokens = ReadOnlySpan<sToken>.Empty;
+ }
+ }
+
+ /// <summary>Get tokens for the specified segment</summary>
+ public ReadOnlySpan<sToken> getTokens( in sSegment seg ) =>
+ tokens.Slice( seg.firstToken, seg.countTokens );
+ }
+} \ No newline at end of file
diff --git a/WhisperNet/Internal/sCaptureCallbacks.cs b/WhisperNet/Internal/sCaptureCallbacks.cs
new file mode 100644
index 0000000..483c2f2
--- /dev/null
+++ b/WhisperNet/Internal/sCaptureCallbacks.cs
@@ -0,0 +1,23 @@
+using System.Runtime.InteropServices;
+
+namespace Whisper.Internal
+{
+ /// <summary>Unmanaged code calls this to check for cancellation</summary>
+ [UnmanagedFunctionPointer( CallingConvention.StdCall )]
+ public delegate int pfnShouldCancel( IntPtr pv );
+
+ /// <summary>Unmanaged code calls this to notify about the status</summary>
+ [UnmanagedFunctionPointer( CallingConvention.StdCall )]
+ public delegate int pfnCaptureStatus( IntPtr pv, eCaptureStatus status );
+
+ /// <summary>Capture callbacks for unmanaged code</summary>
+ public struct sCaptureCallbacks
+ {
+ /// <summary>Cancellation function pointer</summary>
+ public pfnShouldCancel shouldCancel;
+ /// <summary>Capture status function pointer</summary>
+ public pfnCaptureStatus captureStatus;
+ /// <summary>COntext pointer, only needed for C++ compatibility</summary>
+ public IntPtr pv;
+ }
+} \ No newline at end of file
diff --git a/WhisperNet/Internal/sCaptureDevice.cs b/WhisperNet/Internal/sCaptureDevice.cs
new file mode 100644
index 0000000..e2d524d
--- /dev/null
+++ b/WhisperNet/Internal/sCaptureDevice.cs
@@ -0,0 +1,22 @@
+#pragma warning disable CS0649 // Field is never assigned to
+using System.Runtime.InteropServices;
+
+namespace Whisper.Internal
+{
+ /// <summary>Identifiers for an audio capture device</summary>
+ public struct sCaptureDevice
+ {
+ readonly IntPtr m_displayName;
+ /// <summary>The display name is suitable for showing to the user, but might not be unique.</summary>
+ public string? displayName => Marshal.PtrToStringUni( m_displayName );
+
+ readonly IntPtr m_endpoint;
+ /// <summary>Endpoint ID for an audio capture device.<br/>
+ /// It uniquely identifies the device on the system, but is not a readable string.</summary>
+ public string? endpoint => Marshal.PtrToStringUni( m_endpoint );
+ }
+
+ /// <summary>Function pointer to consume a list of audio capture device IDs</summary>
+ [UnmanagedFunctionPointer( CallingConvention.StdCall )]
+ public delegate int pfnFoundCaptureDevices( int len, [In, MarshalAs( UnmanagedType.LPArray, SizeParamIndex = 0 )] sCaptureDevice[]? arr, IntPtr pv );
+} \ No newline at end of file
diff --git a/WhisperNet/Internal/sFullParams.cs b/WhisperNet/Internal/sFullParams.cs
new file mode 100644
index 0000000..7347afe
--- /dev/null
+++ b/WhisperNet/Internal/sFullParams.cs
@@ -0,0 +1,40 @@
+#pragma warning disable CS0649 // Field is never assigned to
+
+// Missing XML comment for publicly visible type or member
+// TODO: remove this line and document them.
+#pragma warning disable CS1591
+
+using System.Runtime.InteropServices;
+
+namespace Whisper.Internals
+{
+ /// <summary>This callback is called on each new segment</summary>
+ [UnmanagedFunctionPointer( CallingConvention.Cdecl )]
+ delegate int pfnNewSegment( IntPtr ctx, int countNew, IntPtr userData );
+
+ /// <summary>The callback is called before every encoder run. If it returns S_FALSE, the processing is aborted.</summary>
+ [UnmanagedFunctionPointer( CallingConvention.Cdecl )]
+ delegate int pfnEncoderBegin( IntPtr ctx, IntPtr userData );
+
+ /// <summary>Transcribe parameters</summary>
+ public struct sFullParams
+ {
+ internal Parameters publicParams;
+ // The rest of these parameters are not exposed to the user-friendly public API of this DLL
+
+ internal IntPtr prompt_tokens;
+ internal int prompt_n_tokens;
+
+ /// <summary>This callback is called on each new segment</summary>
+ [MarshalAs( UnmanagedType.FunctionPtr )]
+ internal pfnNewSegment? newSegmentCallback;
+ /// <summary>Parameter for the above, not needed in C#</summary>
+ internal IntPtr newSegmentCallbackData;
+
+ /// <summary>The callback is called before every encoder run. If it returns false, the processing is aborted</summary>
+ [MarshalAs( UnmanagedType.FunctionPtr )]
+ internal pfnEncoderBegin? encoderBeginCallback;
+ /// <summary>Parameter for the above, not needed in C#</summary>
+ internal IntPtr encoderBeginCallbackData;
+ }
+} \ No newline at end of file
diff --git a/WhisperNet/Internal/sLoadModelCallbacks.cs b/WhisperNet/Internal/sLoadModelCallbacks.cs
new file mode 100644
index 0000000..07f5199
--- /dev/null
+++ b/WhisperNet/Internal/sLoadModelCallbacks.cs
@@ -0,0 +1,64 @@
+using System.Runtime.InteropServices;
+
+namespace Whisper.Internal
+{
+ /// <summary>Function pointer to report model loading progress</summary>
+ [UnmanagedFunctionPointer( CallingConvention.StdCall )]
+ delegate int pfnLoadProgress( double progress, IntPtr pv );
+
+ /// <summary>Function pointer to implement cooperative cancellation</summary>
+ [UnmanagedFunctionPointer( CallingConvention.StdCall )]
+ delegate int pfnCancel( IntPtr pv );
+
+ /// <summary>Callback functions for loading models</summary>
+ public struct sLoadModelCallbacks
+ {
+ /// <summary>Function pointer to report model loading progress</summary>
+ [MarshalAs( UnmanagedType.FunctionPtr )]
+ pfnLoadProgress? progress;
+
+ /// <summary>Function pointer to implement cooperative cancellation</summary>
+ [MarshalAs( UnmanagedType.FunctionPtr )]
+ pfnCancel? cancel;
+
+ // Not needed in C#, delegates can capture things
+ IntPtr pv;
+
+ /// <summary>Wrap idiomatic C# things into these low-level C callbacks</summary>
+ internal sLoadModelCallbacks( CancellationToken cancelToken, Action<double>? pfnProgress )
+ {
+ if( cancelToken != CancellationToken.None )
+ {
+ cancel = delegate ( IntPtr pv )
+ {
+ if( cancelToken.IsCancellationRequested )
+ return 1; // S_FALSE
+ return 0; // S_OK
+ };
+ }
+ else
+ cancel = null;
+
+ if( null != pfnProgress )
+ {
+ progress = delegate ( double val, IntPtr pv )
+ {
+ try
+ {
+ pfnProgress( val );
+ return 0; // S_OK
+ }
+ catch( Exception ex )
+ {
+ NativeLogger.captureException( ex );
+ return ex.HResult;
+ }
+ };
+ }
+ else
+ progress = null;
+
+ pv = IntPtr.Zero;
+ }
+ }
+} \ No newline at end of file
diff --git a/WhisperNet/Internal/sLoggerSetup.cs b/WhisperNet/Internal/sLoggerSetup.cs
new file mode 100644
index 0000000..ed1baa4
--- /dev/null
+++ b/WhisperNet/Internal/sLoggerSetup.cs
@@ -0,0 +1,16 @@
+using System.Runtime.InteropServices;
+
+namespace Whisper.Internal
+{
+ [UnmanagedFunctionPointer( CallingConvention.StdCall )]
+ delegate void pfnLoggerSink( IntPtr context, eLogLevel lvl, [MarshalAs( UnmanagedType.LPUTF8Str )] string message );
+
+ struct sLoggerSetup
+ {
+ [MarshalAs( UnmanagedType.FunctionPtr )]
+ public pfnLoggerSink sink;
+ IntPtr context;
+ public eLogLevel level;
+ public eLoggerFlags flags;
+ }
+} \ No newline at end of file
diff --git a/WhisperNet/Internal/sProgressSink.cs b/WhisperNet/Internal/sProgressSink.cs
new file mode 100644
index 0000000..9155677
--- /dev/null
+++ b/WhisperNet/Internal/sProgressSink.cs
@@ -0,0 +1,20 @@
+#pragma warning disable CS0649 // Field is never assigned to
+using System.Runtime.InteropServices;
+
+namespace Whisper.Internal
+{
+ /// <summary>A callback to get notified about the progress</summary>
+ [UnmanagedFunctionPointer( CallingConvention.StdCall )]
+ delegate int pfnReportProgress( double value, IntPtr context, IntPtr pv );
+
+ /// <summary>C structure with a progress reporting function pointer</summary>
+ public struct sProgressSink
+ {
+ /// <summary>A callback to get notified about the progress</summary>
+ [MarshalAs( UnmanagedType.FunctionPtr )]
+ internal pfnReportProgress? pfn;
+
+ /// <summary>Last parameter to the callback</summary>
+ internal IntPtr pv;
+ }
+} \ No newline at end of file