.NET bindings for transcribe.cpp: load GGUF speech-to-text models and transcribe audio (16 kHz mono float PCM) from C#.
Add the main wrapper package to your project:
dotnet add package TranscribeCppSharpTo include the native binaries for your platform, add the corresponding runtime package:
- Linux (x64):
TranscribeCppSharp.Native.linux-x64 - Linux (ARM64):
TranscribeCppSharp.Native.linux-arm64 - Windows (x64):
TranscribeCppSharp.Native.win-x64 - macOS (ARM64):
TranscribeCppSharp.Native.osx-arm64 - macOS (x64):
TranscribeCppSharp.Native.osx-x64
Note: For Linux Alpine (musl) or other platforms, please refer to the Building from source section. Like Using CUDA, a custom native build is picked up automatically when placed in the app output directory.
The wrapper resolves libtranscribe automatically in plain dotnet run scenarios (no <RuntimeIdentifier> needed): it searches the app output directory — including the runtimes/<rid>/native/ layout where .NET places runtime-package binaries — and the NuGet global packages folder. If the native library is still missing at runtime (e.g. you forgot the runtime package), the wrapper throws a DllNotFoundException that lists the exact package to add for your platform (e.g. dotnet add package TranscribeCppSharp.Native.linux-x64) and the paths it searched. It does not silently produce a misleading error.
Model.Load initializes the compute backends automatically on first use, but you can (and for custom setups, should) do it explicitly with Backends.InitDefault():
Backends.InitDefault(); // optional: automatic in Model.Load, but explicit is clearer
var modelPath = TestConfig.ModelPath; // your GGUF model file, e.g. "test-models/ggml-tiny.bin"
var audioPath = TestConfig.AudioPath; // your WAV audio file, e.g. "test-audio/jfk.wav"
using var model = Model.Load(modelPath, p => p.WithBackend(BackendRequest.BackendCpu));
using var session = model.CreateSession();
var pcm = PcmExtensions.ReadWavToPcm(audioPath);
var transcript = session.Run(pcm);Backends.InitDefault(); // optional: automatic in Model.Load, but explicit is clearer
var modelPath = TestConfig.ModelPath; // your GGUF model file, e.g. "test-models/ggml-tiny.bin"
var audioPath = TestConfig.AudioPath; // your WAV audio file, e.g. "test-audio/jfk.wav"
using var model = Model.Load(modelPath, p => p.WithBackend(BackendRequest.BackendCpu));
using var session = model.CreateSession();
var pcm1 = PcmExtensions.ReadWavToPcm(audioPath);
var pcm2 = PcmExtensions.ReadWavToPcm(audioPath);
var results = Batch.Run(session, new[] { pcm1, pcm2 });stream.Begin();
int chunkSize = 16000; // 1 second
for (int i = 0; i < pcm.Length; i += chunkSize)
{
int length = Math.Min(chunkSize, pcm.Length - i);
var chunk = pcm.AsSpan(i, length);
stream.Feed(chunk);
}
stream.Complete();
var text = stream.GetCurrentText();- Multi-Model: Loads GGUF models for the model families supported by transcribe.cpp (Whisper, Moonshine, Parakeet, Canary, GigaAM, and others — 16 families upstream).
- Hardware Acceleration: The bundled runtimes include CPU, Vulkan (Windows/Linux) and Metal (macOS) backends. See Using CUDA for NVIDIA GPUs.
- Modern .NET: Uses
LibraryImportfor interop andSafeHandlefor native resource lifetime. - Flexible APIs:
- High-Level Wrapper: Intuitive C# API for rapid development.
- Low-Level Interop: Direct access to the native C API when needed.
- Streaming & Batch: Support for incremental streaming transcription and batch processing.
- Cross-Platform: Pre-compiled native runtimes are packaged for Windows, Linux, and macOS (x64 and ARM64). Only linux-x64 is exercised by CI.
The NuGet packages do not bundle a CUDA runtime (the bundled binaries are CPU + Vulkan on Windows/Linux and Metal on macOS). The upstream releases do include CUDA archives, but shipping and supporting CUDA builds is out of scope for this packaging layer — so to use an NVIDIA GPU you provide your own CUDA build of transcribe.cpp and place it next to your app; the wrapper prefers native binaries in the app output directory over the packaged ones.
-
Download the upstream CUDA archive for your platform (this project is bound to transcribe.cpp v0.1.3):
- Linux x64:
transcribe-native-0.1.3-linux-x86_64-cuda.tar.gz - Windows x64:
transcribe-native-0.1.3-windows-x86_64-cuda.tar.gz
from the transcribe.cpp v0.1.3 release.
- Linux x64:
-
Extract it and copy
libtranscribe.so(Linux) ortranscribe.dll(Windows) — plus the siblinglibggml*.so/ggml*.dllfiles — into your app's output directory (where your.dll/.exeis produced). -
Request the CUDA backend at load time:
using var model = Model.Load("model.gguf", p => p .WithBackend(BackendRequest.BackendCuda) .WithGpuDevice(0));
You can verify CUDA is actually available in the current build with
BackendAvailable(BackendRequest.BackendCuda). If no CUDA build is installed, that returnsfalseand aBackendCudarequest will fail withErrBackend.
All transcription calls (Session.Run, Batch.Run, etc.) are blocking. This mirrors the native library, whose C API is fully synchronous (no async entry points); the wrapper does not add a "fake" async-over-sync layer on top.
- Desktop/CLI Apps: Run transcription on a background thread using
Task.Run()to keep the UI responsive. - Web APIs (ASP.NET Core): Use a pool of
Sessionobjects combined with aSemaphoreSlimto limit concurrent native calls and prevent thread pool starvation.
// Example: Pooling sessions in a service
private readonly SemaphoreSlim _semaphore = new(Environment.ProcessorCount);
public async Task<string> TranscribeAsync(float[] pcm)
{
await _semaphore.WaitAsync();
try {
return await Task.Run(() => _session.Run(pcm).FullText);
} finally {
_semaphore.Release();
}
}The project is divided into several layers, each with a distinct responsibility:
TranscribeCppSharp.Native.*(Runtimes): Platform-specific packages containing the pre-compiled nativelibtranscribebinaries.TranscribeCppSharp.Interop(Low-level): Auto-generated P/Invoke declarations usingLibraryImport.TranscribeCppSharp(High-level): Idiomatic C# abstraction layer providingIDisposableresources and typed exceptions.Generator(Tool): Ensures C# bindings stay in sync with the upstream native API by parsing Rust FFI definitions.
A DllImportResolver registered in the Interop layer finds libtranscribe in the app output directory (including the runtimes/<rid>/native/ layout), the NuGet global packages folder, or lets the runtime's default resolution (.deps.json runtime targets) handle it — without requiring LD_LIBRARY_PATH. Its libggml* dependencies are loaded from the same directory by the native loader.
The high-level wrapper throws TranscribeException when a native call fails. You can filter by StatusCode to handle specific errors.
Note: See the Status enum in the TranscribeCppSharp.Interop namespace for the full list of error codes.
Query what a loaded model supports:
Backends.InitDefault(); // optional: automatic in Model.Load, but explicit is clearer
var modelPath = TestConfig.ModelPath; // your GGUF model file, e.g. "test-models/ggml-tiny.bin"
using var model = Model.Load(modelPath, p => p.WithBackend(BackendRequest.BackendCpu));
var supportsPnc = model.Supports(Feature.FeaturePnc);
var caps = model.GetCapabilities();The native library and this wrapper are not thread-safe by default. The relevant rules:
- Concurrent compute is limited: at most one
Session.Run,Batch.Run, or active stream may be in flight across all sessions of the same model at a time. Sessions share the model's backend instances and some per-family state, so overlapping runs on the same model race (per the upstream library: corrupted decodes on CPU, command-buffer failures on Metal). This is a known limitation of the upstream native library in 0.x, documented in its public header (see "KNOWN 0.x LIMITATION — concurrent COMPUTE"), not something this wrapper imposes or can lift.- For parallel transcription, load one model per worker (each worker gets its own
Model, hence its own backend instances). - Serialized use of many sessions on one model (e.g. a session pool behind a mutex) is fully supported.
- For parallel transcription, load one model per worker (each worker gets its own
Model: believed thread-safe for creating sessions — you can create multipleSessionobjects from a singleModelinstance across different threads, as long as their runs do not overlap (see the concurrent-compute limit above). Not covered by concurrency tests yet.Session: Not thread-safe. A session maintains internal state (KV cache) for transcription. Do not run two operations on the same session concurrently; serialize them or use separate sessions.Batch: Not thread-safe. Calls into the provided session internally. Use separate sessions for concurrent batch processing.StreamSession: Not thread-safe. It is a view over aSessionand shares its state.
Dispose discipline: dispose explicitly (
using/Dispose()) — do not rely on the GC finalizer for cleanup. The native contract requires the model to outlive its sessions, and while aSessionkeeps its parentModelalive for the session's lifetime, the order in which finalizers run during GC-only collection is not guaranteed. Dispose theStreamSession/Sessionbefore theirModel(theusing var model; using var session;declaration order does this). This is consistent with the upstream requirement thattranscribe_model_freebe called only after all derived contexts are freed.
Memory and disk usage depend on the model file, quantization, and backend you use. These are not documented here; refer to the model documentation and transcribe.cpp for accurate numbers.
Two version numbers are in play, decoupled on purpose:
TranscribeCppSharp(this wrapper) follows Semantic Versioning (SemVer) for its own C# API. Breaking API changes bump the major/minor version of the wrapper.TranscribeCppSharp.InteropandTranscribeCppSharp.Native.*are versioned to match the upstreamtranscribe.cppversion they bind to (e.g.0.1.3= transcribe.cpp v0.1.3). They track the ABI, not the wrapper's API.
So TranscribeCppSharp 0.1.0 depends on TranscribeCppSharp.Interop 0.1.3; a later upstream release will ship as a new Interop/Native version without necessarily changing the wrapper's own version. The correspondence between a wrapper release and the upstream version it targets is recorded in CHANGELOG.md.
This project is a packaging and binding effort only — the underlying library is not my work:
- The native library (
transcribe.cpp) is developed and owned by the transcribe.cpp authors (MIT License). - The bundled native components (ggml, etc.) are owned by their respective authors; their MIT license texts are distributed alongside the binaries in the
TranscribeCppSharp.Native.*packages. - I did not author the native library and claim no credit for it. This repository only adds:
- A C# interop layer (auto-generated P/Invoke bindings via
LibraryImport). - A high-level C# wrapper (
IDisposableresources, typed exceptions). - Pre-built native binaries packaged for .NET consumption.
- A C# interop layer (auto-generated P/Invoke bindings via
The transcribe.cpp project is an independent upstream project; bug reports about the native library itself should go to its repository.
- .NET 10.0 or later.
- Native libraries (can be fetched using the provided script).
# Download native libraries for your current platform
dotnet run --project tools/FetchNative
# Run unit and integration tests
./scripts/run-integration-tests.sh
# Run the smoke test sample
dotnet run --project samples/SmokeTest -- model.gguf audio.wavThe Native.* packages redistribute exactly what upstream transcribe.cpp publishes in its releases — nothing more. This project is a packaging/binding layer, not a binary provider: it does not compile musl, CUDA, or other variant builds. If a variant you need is not in the upstream release, building it yourself is on you.
You need to build from source when:
- You are using Alpine Linux (which uses
muslinstead ofglibc, making the pre-built Linux binaries incompatible). Upstream transcribe.cpp does not ship musl builds, so there is noNative.*package to install for this case. - You need to support a non-standard architecture or custom OS.
- You want to enable specific hardware optimizations not included in the default build.
Steps:
- Clone transcribe.cpp.
- Build the native library using
cmake(ensureBUILD_SHARED_LIBS=ON). On Alpine, build inside the distro so the resulting library links againstmusl. - Copy the resulting
libtranscribe.so(or.dll/.dylib) — and the siblinglibggml*.sofiles it loads — into your application's output directory. As with Using CUDA, the wrapper prefers native binaries in the app output directory over the packaged ones, so noLD_LIBRARY_PATHis needed. The C# interop contract is unchanged: only the native binaries differ, not the P/Invoke signatures.
To report a security vulnerability, please use the GitHub Security Advisory feature.
This project is licensed under the MIT License (matching transcribe.cpp).
The MIT license covers this wrapper and the bundled native library, not the models you load with it. GGUF models come from different ecosystems with different licenses — some are permissive (MIT, Apache-2.0), some are non-commercial (e.g. CC-BY-NC-4.0 for some Parakeet/Canary variants). This project does not bundle or redistribute models, and it does not verify or curate their licenses.
Before using a model in a commercial product, check the license on the page you download it from (typically Hugging Face). The upstream transcribe.cpp docs describe each supported family and where its models come from; that is the source of truth, not this README.