ElBruno.Whisper
0.9.0
dotnet add package ElBruno.Whisper --version 0.9.0
NuGet\Install-Package ElBruno.Whisper -Version 0.9.0
<PackageReference Include="ElBruno.Whisper" Version="0.9.0" />
<PackageVersion Include="ElBruno.Whisper" Version="0.9.0" />
<PackageReference Include="ElBruno.Whisper" />
paket add ElBruno.Whisper --version 0.9.0
#r "nuget: ElBruno.Whisper, 0.9.0"
#:package ElBruno.Whisper@0.9.0
#addin nuget:?package=ElBruno.Whisper&version=0.9.0
#tool nuget:?package=ElBruno.Whisper&version=0.9.0
ElBruno.Whisper
Run local Whisper speech-to-text in .NET π€
Transcribe audio to text in .NET using OpenAI's Whisper model. Powered by ONNX Runtime with automatic model download from HuggingFace.
Packages
| Package | NuGet | Downloads | Description |
|---|---|---|---|
ElBruno.Whisper |
Core speech-to-text library with Whisper ONNX models | ||
ElBruno.Whisper.BlazorComponents |
Reusable Blazor components for transcription workflows |
Features
- π¦ Automatic model download β models are fetched from HuggingFace on first use
- π Multiple model sizes β tiny β base β small β medium β large (pick your speed/accuracy tradeoff)
- π Zero friction β works out of the box with sensible defaults (tiny.en)
- π Multilingual support β transcribe 99+ languages with multilingual models
- π DI-friendly β register with
AddWhisper()in ASP.NET Core - π§© Microsoft.Extensions.AI ready β resolve
ISpeechToTextClientfor standard speech-to-text integration - π§΅ Concurrent transcription β share one client with configurable queueing and pooled inference sessions
- π Incremental updates β get rolling provisional and committed text through
GetStreamingTextAsync() - πΌ Raw audio inputs β transcribe WAV streams, PCM16 byte streams, and Float32 memory without temporary files
- π Progress reporting β track model downloads with real-time callbacks
- π― English-optimized models β dedicated
.envariants for best accuracy on English audio - β±οΈ Timestamp-aware results β opt into segment and word timings for subtitle and caption workflows
Installation
dotnet add package ElBruno.Whisper
Quick Start
using ElBruno.Whisper;
// Create client (downloads tiny.en model on first run)
using var client = await WhisperClient.CreateAsync();
var result = await client.TranscribeAsync("audio.wav");
Console.WriteLine(result.Text);
First Run
The first time you create a WhisperClient, the model is downloaded from HuggingFace to your local cache directory (~75 MB - 3 GB depending on model size). This typically takes 10-60 seconds depending on your internet connection and chosen model.
Track download progress:
using var client = await WhisperClient.CreateAsync(
progress: new Progress<ElBruno.HuggingFace.DownloadProgress>(p =>
{
if (p.Stage == ElBruno.HuggingFace.DownloadStage.Downloading)
Console.WriteLine($"{p.CurrentFile}: {p.PercentComplete:F0}%");
else
Console.WriteLine($"{p.Stage}: {p.Message}");
})
);
Subsequent runs load instantly from cache (%LOCALAPPDATA%/ElBruno/Whisper/models).
Model Selection
Whisper offers various model sizes. English-optimized models (.en suffix) are smaller and faster for English audio:
using var client = await WhisperClient.CreateAsync(new WhisperOptions
{
Model = KnownWhisperModels.WhisperSmallEn
});
var result = await client.TranscribeAsync("english-audio.wav");
Console.WriteLine(result.Text);
Available Models
| Size | English | Multilingual | Parameters | Approx Size | Speed |
|---|---|---|---|---|---|
| tiny | tiny.en | tiny | 39M | 75 MB | β‘β‘β‘β‘β‘ |
| base | base.en | base | 74M | 140 MB | β‘β‘β‘β‘ |
| small | small.en | small | 244M | 460 MB | β‘β‘β‘ |
| medium | medium.en | medium | 769M | 1.5 GB | β‘β‘ |
| large | β | large | 1550M | 3.0 GB | β‘ |
Use English-optimized (.en) models for:
- English audio only (slightly smaller, faster, better accuracy on English)
Use Multilingual models for:
- Non-English audio
- Mixed-language content
- Language auto-detection
Progress Tracking
Monitor both file downloads and transcription progress:
var downloadProgress = new Progress<ElBruno.HuggingFace.DownloadProgress>(p =>
{
if (p.Stage == ElBruno.HuggingFace.DownloadStage.Downloading)
Console.Write($"\rβ¬οΈ {p.PercentComplete:F0}%");
else
Console.WriteLine($"\nβ {p.Message}");
});
using var client = await WhisperClient.CreateAsync(progress: downloadProgress);
var result = await client.TranscribeAsync("audio.wav");
Console.WriteLine($"β Transcribed: {result.Text}");
Dependency Injection
Register Whisper options in ASP.NET Core or other DI-enabled applications:
builder.Services.AddWhisper(options =>
{
options.Model = KnownWhisperModels.WhisperBaseEn;
options.Concurrency.MaximumConcurrentRequests = 2;
});
AddWhisper() registers WhisperOptions, WhisperSpeechToTextClient, and ISpeechToTextClient. Create and share a WhisperClient yourself when you want direct control over startup and disposal.
Microsoft.Extensions.AI
Use the adapter when you want the standard ISpeechToTextClient contract:
using ElBruno.Whisper;
using Microsoft.Extensions.AI;
builder.Services.AddWhisper(options =>
{
options.Model = KnownWhisperModels.WhisperTinyEn;
options.Language = "en";
});
var speechToText = builder.Services
.BuildServiceProvider()
.GetRequiredService<ISpeechToTextClient>();
await using var audioStream = File.OpenRead("audio.wav");
var response = await speechToText.GetTextAsync(
audioStream,
new SpeechToTextOptions
{
SpeechLanguage = "en",
AdditionalProperties = new()
{
["elbruno.whisper.enable_timestamps"] = true
}
});
Console.WriteLine(response.Text);
Console.WriteLine(response.AdditionalProperties?["elbruno.whisper.detected_language"]);
The adapter keeps caller-owned streams open, supports cancellation, exposes SpeechToTextClientMetadata, and returns these response metadata keys through AdditionalProperties:
elbruno.whisper.detected_languageelbruno.whisper.audio_duration_mselbruno.whisper.segmentselbruno.whisper.wordselbruno.whisper.model_idelbruno.whisper.execution_provider
Thread Safety and Concurrency
WhisperClient can now be shared across concurrent callers. By default it still processes one transcription at a time. Raise the concurrency limit to allow parallel work and reuse pooled ONNX sessions:
using var client = await WhisperClient.CreateAsync(new WhisperOptions
{
Model = KnownWhisperModels.WhisperTinyEn,
Concurrency = new WhisperConcurrencyOptions
{
MaximumConcurrentRequests = 2,
QueueTimeout = TimeSpan.FromSeconds(15),
EnableSessionPooling = true
}
});
var results = await Task.WhenAll(
client.TranscribeAsync("audio-1.wav"),
client.TranscribeAsync("audio-2.wav"));
If all inference slots are busy longer than QueueTimeout, TranscribeAsync throws TimeoutException. Cancelling the request also aborts queue waiting and the next safe decode checkpoint.
Incremental Transcription
WhisperClient.GetStreamingTextAsync() runs Whisper over rolling windows and emits ordered updates with both stable and provisional text:
using var client = await WhisperClient.CreateAsync();
await foreach (var update in client.GetStreamingTextAsync(
"audio.wav",
new WhisperStreamingOptions
{
WindowSize = TimeSpan.FromSeconds(8),
StepSize = TimeSpan.FromSeconds(1),
ContextOverlap = TimeSpan.FromSeconds(2),
UseLocalAgreement = true,
AgreementIterations = 2
}))
{
Console.WriteLine($"Committed: {update.CommittedText}");
Console.WriteLine($"Provisional: {update.ProvisionalText}");
if (update.IsFinal)
Console.WriteLine("Final update received.");
}
Each update exposes:
CommittedTextβ text that has stabilized across rolling windowsProvisionalTextβ the newest hypothesis that may still changeTextβ the combined transcript for the current updateIsFinalβ true exactly once, after the final flush
Limitations: Whisper is not a native streaming model. This API reads completed file or stream content, re-runs inference over overlapping windows, and uses local agreement to reduce duplicate committed text. Provisional text can still change between updates.
Realtime PCM and Memory Inputs
Use the explicit-audio overloads when your pipeline already has PCM in memory and you want to avoid temporary WAV files:
using var client = await WhisperClient.CreateAsync();
var pcm16Format = new WhisperAudioFormat(
sampleRate: 48000,
channels: 2,
sampleFormat: WhisperAudioSampleFormat.Pcm16);
await using var rawAudioStream = File.OpenRead("call.raw");
var streamResult = await client.TranscribeAsync(rawAudioStream, pcm16Format);
ReadOnlyMemory<byte> pcmBytes = await File.ReadAllBytesAsync("call.raw");
var byteResult = await client.TranscribeAsync(pcmBytes, pcm16Format);
ReadOnlyMemory<float> monoFloatSamples = GetNormalizedSamples();
var floatResult = await client.TranscribeAsync(monoFloatSamples, sampleRate: 16000);
Notes:
TranscribeAsync(Stream)auto-detects WAV headers and keeps the caller-owned stream open.- Raw PCM byte streams require
WhisperAudioFormatso the client can downmix and resample to Whisper's 16 kHz mono input. ReadOnlyMemory<float>overloads expect normalized PCM samples in the[-1, 1]range.
Blazor Components Package
Build speech-to-text interfaces quickly with ElBruno.Whisper.BlazorComponents:
dotnet add package ElBruno.Whisper.BlazorComponents
builder.Services.AddWhisper(options =>
{
options.Model = KnownWhisperModels.WhisperBaseEn;
});
builder.Services.AddWhisperBlazorComponents();
Available components:
WhisperModelSelectorFileTranscriptionPanelLiveTranscriptViewerTranscriptionHistoryList
Transcription Result
The TranscriptionResult includes:
var result = await client.TranscribeAsync("audio.wav");
Console.WriteLine(result.Text); // Transcribed text
Console.WriteLine(result.DetectedLanguage); // Detected language (for multilingual models)
Console.WriteLine(result.Duration); // Audio duration
Enable timestamps to access both segment-level and word-level timing metadata:
using var client = await WhisperClient.CreateAsync(new WhisperOptions
{
EnableTimestamps = true
});
var result = await client.TranscribeAsync("audio.wav");
foreach (var segment in result.Segments ?? [])
{
Console.WriteLine($"[{segment.Start:mm\\:ss\\.ff} - {segment.End:mm\\:ss\\.ff}] {segment.Text}");
foreach (var word in segment.Words)
{
Console.WriteLine($" {word.Start:mm\\:ss\\.ff} - {word.End:mm\\:ss\\.ff}: {word.Text}");
}
}
Word timings are derived from the timestamped transcript spans produced by Whisper. If a model returns text without explicit spans, the library falls back to a single full-duration segment and derives word timings within that range.
Troubleshooting
Model download fails?
- Check your internet connection
- For private HuggingFace models, set the
HF_TOKENenvironment variable
Out of memory?
- Use a smaller model (tiny or base instead of medium/large)
- Transcribe shorter audio files in chunks
For detailed troubleshooting, see docs.
Samples
| Sample | Description |
|---|---|
| HelloWhisper | Minimal console transcription |
| BlazorWhisper | Blazor app with audio recording and real-time transcription |
| Blazor Components demo page | Demonstrates the reusable Blazor components package |
What's New
- Blazor Components package β Added
ElBruno.Whisper.BlazorComponentswith model selector, file transcription panel, live transcript viewer, and transcription history list. - Real-time Blazor transcription sample β Added chunk-based live transcription UX in the
BlazorWhispersample. - Timestamp-aware transcription results β Added optional segment timestamps in
TranscriptionResult. - Model download progress reporting β Added structured progress callbacks during model download.
- OIDC trusted publishing β Added secure NuGet publishing with GitHub Actions and
NuGet/login.
Repository rule: keep last 5 important features
- The Whatβs New section must always list the latest 5 important repository/library features.
- Before every NuGet release, validate whether the release introduces a notable change and add/update an entry when needed.
Documentation
- Getting Started β installation, first steps, configuration
- API Reference β full API documentation
- Architecture β design decisions and internal structure
- Testing Guide β running tests, test organization, CI/CD pipeline
- Blazor Components β component API and integration guide
- Blazor Components Sample β walkthrough for the sample page
- Test Audio Files β audio resources for testing and transcription validation
- Image Prompts β prompts for generating blog and social media images
- Publishing β NuGet package publishing with OIDC
Building from Source
git clone https://github.com/elbruno/ElBruno.Whisper
cd ElBruno.Whisper
dotnet build ElBruno.Whisper.slnx
dotnet test ElBruno.Whisper.slnx --filter "Category!=Integration"
Testing
The repository includes comprehensive unit and integration tests:
Quick test run (unit tests, no model download):
dotnet test ElBruno.Whisper.slnx --filter "Category!=Integration"
Full test run (includes integration with real models):
dotnet test ElBruno.Whisper.slnx
Test audio files are provided in testdata/audio/ for validation and transcription testing. For details, see the Testing Guide.
π€ Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
π License
This project is licensed under the MIT License β see the LICENSE file for details.
Related Projects
- ElBruno.LocalLLMs β Run local LLMs in .NET
- ElBruno.HuggingFace β HuggingFace model utilities for .NET
π Acknowledgments
- ONNX Runtime β inference engine
- OpenAI Whisper β speech-to-text model
- Hugging Face β model hosting and community
- ONNX Community β ONNX model conversions
π About the Author
Hi! I'm ElBruno π§‘, a passionate developer and content creator exploring AI, .NET, and modern development practices.
Made with β€οΈ by ElBruno
If you like this project, consider following my work across platforms:
- π» Podcast: No Tienen Nombre β Spanish-language episodes on AI, development, and tech culture
- π» Blog: ElBruno.com β Deep dives on embeddings, RAG, .NET, and local AI
- πΊ YouTube: youtube.com/elbruno β Demos, tutorials, and live coding
- π LinkedIn: @elbruno β Professional updates and insights
- π Twitter: @elbruno β Quick tips, releases, and tech news
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- ElBruno.HuggingFace.Downloader (>= 0.6.0)
- Microsoft.Extensions.AI.Abstractions (>= 10.7.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.0)
- Microsoft.ML.OnnxRuntime (>= 1.22.0)
-
net8.0
- ElBruno.HuggingFace.Downloader (>= 0.6.0)
- Microsoft.Extensions.AI.Abstractions (>= 10.7.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.0)
- Microsoft.ML.OnnxRuntime (>= 1.22.0)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on ElBruno.Whisper:
| Package | Downloads |
|---|---|
|
ElBruno.MarkItDotNet.Whisper
Local audio transcription for ElBruno.MarkItDotNet using OpenAI Whisper via ONNX Runtime. Converts audio files to Markdown transcripts offline. |
|
|
ElBruno.Whisper.BlazorComponents
Reusable Blazor components for ElBruno.Whisper speech-to-text experiences, including model selection, live transcript viewing, and file transcription workflows. |
GitHub repositories
This package is not used by any popular GitHub repositories.