diff options
| author | Konstantin <const@const.me> | 2023-01-16 14:52:43 +0100 |
|---|---|---|
| committer | Konstantin <const@const.me> | 2023-01-16 14:52:43 +0100 |
| commit | 8c4603c73675958efc960fbd4bb599a2909d106a (patch) | |
| tree | 714dc6fc9a1672d5fd7f89676b97e10959662abc /WhisperNet/Internal | |
| parent | 990a8d0dbaefc996244097397259e92758b15cce (diff) | |
Source codes
Diffstat (limited to 'WhisperNet/Internal')
| -rw-r--r-- | WhisperNet/Internal/AssemblyInfo.cs | 8 | ||||
| -rw-r--r-- | WhisperNet/Internal/NativeLogger.cs | 138 | ||||
| -rw-r--r-- | WhisperNet/Internal/iContext.cs | 37 | ||||
| -rw-r--r-- | WhisperNet/Internal/iTranscribeResult.cs | 122 | ||||
| -rw-r--r-- | WhisperNet/Internal/sCaptureCallbacks.cs | 23 | ||||
| -rw-r--r-- | WhisperNet/Internal/sCaptureDevice.cs | 22 | ||||
| -rw-r--r-- | WhisperNet/Internal/sFullParams.cs | 40 | ||||
| -rw-r--r-- | WhisperNet/Internal/sLoadModelCallbacks.cs | 64 | ||||
| -rw-r--r-- | WhisperNet/Internal/sLoggerSetup.cs | 16 | ||||
| -rw-r--r-- | WhisperNet/Internal/sProgressSink.cs | 20 |
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 |
