ElBruno.S1Mini
0.1.2
dotnet add package ElBruno.S1Mini --version 0.1.2
NuGet\Install-Package ElBruno.S1Mini -Version 0.1.2
<PackageReference Include="ElBruno.S1Mini" Version="0.1.2" />
<PackageVersion Include="ElBruno.S1Mini" Version="0.1.2" />
<PackageReference Include="ElBruno.S1Mini" />
paket add ElBruno.S1Mini --version 0.1.2
#r "nuget: ElBruno.S1Mini, 0.1.2"
#:package ElBruno.S1Mini@0.1.2
#addin nuget:?package=ElBruno.S1Mini&version=0.1.2
#tool nuget:?package=ElBruno.S1Mini&version=0.1.2
ElBruno.S1Mini
Local ASR transcript normalizer for .NET π§Ή
Clean raw speech-to-text transcripts into well-formed written text in .NET. Powered by superwhisper/s1-mini (a 0.6B Qwen3 fine-tune) running locally via ONNX Runtime GenAI. Auto-downloads the INT4 ONNX model from HuggingFace.
β οΈ s1-mini is not a chat model. It performs exactly one task β normalizing an ASR transcript. Give it anything else and you get unpredictable output.
Packages
| Package | NuGet | Downloads | Description |
|---|---|---|---|
ElBruno.S1Mini |
Self-contained transcript normalizer with S1MiniClient (IChatClient) and TranscriptNormalizer. |
Features
- π§Ή Filler removal + punctuation + capitalization in one call
- π Self-correction resolution β
"nine am no sorry ten thirty"becomes10:30 - π’ Written form for spoken numbers, dates, times, currency, emails
- ποΈ Control-line settings β
Styling(formal / semi-formal / casual) andContext(general / email) with empirically verified behavior - π¦ Auto-download β INT4 ONNX model fetched from HuggingFace on first use
- π§΅
IChatClientcompatible βS1MiniClientplugs intoMicrosoft.Extensions.AI - π DI-friendly β
AddTranscriptNormalizer()for ASP.NET Core - πͺ Chunking helper β
NormalizeChunkedAsyncfor transcripts longer than the ~1,000-token recommended input - π Empty-in / empty-out β pure filler returns
string.Empty, as the model card documents - π¦Ί Temperature-0 safe β the ORT-GenAI native divide-by-zero trap is guarded at the runtime layer so callers can always use greedy decoding
Installation
dotnet add package ElBruno.S1Mini
Quick Start
using ElBruno.S1Mini.Normalization;
// Downloads elbruno/s1-mini-onnx (int4) on first run.
using var normalizer = await TranscriptNormalizer.CreateAsync();
var cleaned = await normalizer.NormalizeAsync(
"so um i need to like send the the report by uh friday no wait make that thursday");
Console.WriteLine(cleaned);
// So I need to send the report by Thursday.
What it actually changes
Three short console samples. Each is a complete program β the only difference is the input.
1 Β· Collapses stutters and repeated words
using ElBruno.S1Mini.Normalization;
using var normalizer = await TranscriptNormalizer.CreateAsync();
Console.WriteLine(await normalizer.NormalizeAsync(
"you don't have any any any any change at all?"));
You don't have any change at all.
2 Β· Resolves self-corrections to what the speaker landed on
using ElBruno.S1Mini.Normalization;
using var normalizer = await TranscriptNormalizer.CreateAsync();
Console.WriteLine(await normalizer.NormalizeAsync(
"i think we should uh go with with option b i mean option c"));
I think we should go with option C.
Note it keeps option C β the choice the speaker corrected to, not the one they abandoned.
3 Β· Writes spoken numbers, times, and dates in written form
using ElBruno.S1Mini.Normalization;
using var normalizer = await TranscriptNormalizer.CreateAsync();
Console.WriteLine(await normalizer.NormalizeAsync(
"lets meet at uh three thirty on on tuesday the the tenth"));
Let's meet at 3:30 on Tuesday the 10th.
These outputs are measured against the real INT4 model with default options, and are pinned by regression tests in
TranscriptNormalizerModelTestsso they cannot silently drift.It normalizes rather than rewrites: casual phrasing survives.
the the total was like twenty five dollars and uh fifty centsbecomesThe total was like $25.50.β the numerals are fixed, but the colloquial "like" stays.
Control-line options
using ElBruno.S1Mini.Normalization;
var cleaned = await normalizer.NormalizeAsync(
transcript,
new TranscriptNormalizerOptions
{
Styling = TranscriptStyling.Formal,
Context = TranscriptContext.Email,
});
Empirically verified against the real INT4 model:
Styling:SemiFormal(default),Formal,Casualβ all three produce distinct output.Structure:Prose(default),Listsβ caveat:Listsdid not reliably produce Markdown bullets in testing; output stayed prose. Kept as a model-card value.Context:General(default),Emailβ distinct.MessageandNotesare accepted but empirically behave identically toGeneral; kept for API completeness.
See docs/transcript-normalization.md for the full behavior table and before/after examples.
Dependency Injection
using ElBruno.S1Mini;
builder.Services.AddTranscriptNormalizer(options =>
{
options.CacheDirectory = @"C:\models";
});
TranscriptNormalizer is registered as its own service type β not as IChatClient. s1-mini is not a chat model, and exposing it as one would mislead consumers expecting chat semantics.
Compose with any IChatClient
TranscriptNormalizer is a control-line + prompt-builder layer over any IChatClient. Supply your own client (as long as it is really pointed at s1-mini):
using ElBruno.S1Mini;
using ElBruno.S1Mini.Normalization;
using var chatClient = await S1MiniClient.CreateAsync();
using var normalizer = new TranscriptNormalizer(chatClient);
S1MiniClient also implements IChatClient directly, so you can plug it into any Microsoft.Extensions.AI pipeline β with the caveat that it only handles the exact prompt shape s1-mini was fine-tuned on.
Chunking long transcripts
var cleaned = await normalizer.NormalizeChunkedAsync(longTranscript, maxCharsPerChunk: 3500);
Each chunk is normalized statelessly at sentence boundaries; for tighter control on transcripts with context spanning boundaries, chunk manually at a natural pause instead.
FP16 is currently broken on CPU
elbruno/s1-mini-onnx also has an fp16/ subfolder, but that variant fails at inference on CPU with onnxruntime-genai 0.15.1 (upstream ORT GQA repeat_kv Reshape shape-mismatch bug). Use INT4 (the default). This library will not switch to FP16 automatically.
Model license
superwhisper/s1-mini is Apache-2.0 with a naming clause. The converted ONNX artifacts (elbruno/s1-mini-onnx) are an explicitly unofficial, unaffiliated, non-endorsed derivative. ElBruno.S1Mini's C# code is MIT; the downloaded model weights remain under the upstream Apache-2.0 license. Vendor quality claim: 94.8% token accuracy on 7,519 held-out English cases β Superwhisper's measurement on their official GGUF Q4_K_M build, not on the INT4 ONNX weights this library downloads, whose accuracy has not been separately measured. English only, v1.
Superwhisper also publishes official GGUF builds for llama.cpp, Ollama, and LM Studio. This library targets ONNX Runtime GenAI, which is what .NET can consume directly.
Building from Source
git clone https://github.com/elbruno/ElBruno.S1Mini
cd ElBruno.S1Mini
dotnet build ElBruno.S1Mini.slnx
dotnet test ElBruno.S1Mini.slnx --framework net8.0
What's New
- π
v0.1.2β documentation release: the package page now shows before/after examples, corrects the upstream accuracy attribution, and refreshes the author links. No library code changes. - π¦Ί ORT-GenAI temperature-0 crash guard β the native
temperature=0divide-by-zero trap is guarded at the runtime layer; greedy decoding is safe for every call. - ποΈ
LiveMicTranscriptionsample β microphone β Silero VAD β Whisper β s1-mini, fully on-device, with--save-audio/--wavreplay for reproducible testing. - π§΅
S1MiniClientβ self-containedIChatClientimplementation with automatic HuggingFace download ofelbruno/s1-mini-onnx(int4). - π§ͺ Qwen3 non-thinking prompt format β ported verbatim from the model's own
chat_template.jinja, verified byte-for-byte against the real model.
Documentation
- Getting Started β installation, first steps, configuration
- Transcript Normalization β full API guide, control-line reference, empirical behavior table
Samples
| Sample | Description |
|---|---|
| HelloS1Mini | Console sample covering default normalization, Context.Email, Structure.Lists, and pure-filler input. |
| S1MiniWebSample | Blazor Server web UI: textarea β Normalize β cleaned output with styling/structure/context selectors. |
| LiveMicTranscription | Windows-only console sample: default microphone β Silero VAD speech detection β ElBruno.Whisper speech-to-text β s1-mini cleanup, live and fully local. A Spectre.Console UI provides arrow-key model/style pickers, per-model download progress bars, a live input meter, and side-by-side raw vs. cleaned transcript panels, then offers to delete every downloaded model on exit. Supports --save-audio and --wav <file\|folder> for reproducible testing. |
Reproducible testing with recordings
Live microphone testing is not repeatable β every attempt is a new performance. The sample can therefore record what it captures and replay it later through the identical pipeline:
# Capture: writes each detected utterance to ./recordings/*.wav
dotnet run --project src/samples/LiveMicTranscription -- --save-audio
# Replay: same VAD, same models, no microphone needed
dotnet run --project src/samples/LiveMicTranscription -- --wav recordings
--wav accepts a single file or a folder, and resamples/downmixes anything to 16 kHz mono,
so a recording made with any tool works. This isolates model and setting changes from
variation in the speaking itself.
Note on Whisper + s1-mini: Whisper does transcribe spoken fillers (
um,uh) when they are actually captured, and s1-mini removes them. The hard part is capturing them: fillers are low-energy sounds that sit on the noise floor, so a simple energy-threshold gate discards exactly the words this library exists to clean up. The sample therefore uses Silero VAD (a neural speech detector) and cuts each utterance as one contiguous slice from the first to the last detected speech segment, plus padding β which preserves the quiet onsets. Measured on the same synthesized phrase:
Segmentation Whisper output Energy threshold So, um, hello.(truncated at the first pause)Silero VAD + contiguous slice So, um, hello. I have a, uh, question here. And I want to, um, see what I am going to do here.s1-mini then returns
So, hello. I have a question here. And I want to see what I am going to do here.The sample defaults to Whisper Tiny, which preserves fillers best; larger models tidy the transcript as they decode, which can make the cleanup step look like a no-op.Utterance grouping matters too. A hesitation ("I think⦠ummm⦠we should") contains a pause often longer than a second. Ending the phrase there splits one sentence into fragments and strands the filler at a boundary, so the sample waits 1.5 s of silence before closing an utterance.
Known model limitation: s1-mini recognizes every common filler spelling (
um,umm,ummm,uh,em,emm,eh,ehh,erm,hmm,er,ah) in ordinary sentences, but it passes greeting phrases containing a personal name through verbatim β"Hello, um, hi Kara."keeps theum, while"Hello, um, hi."and"Hello, um, this is a test."are both cleaned. UsingContext.Emailstrips the filler in that case.
Testing
dotnet test ElBruno.S1Mini.slnx --framework net8.0
Tests use a fake IChatClient and a recording IGenerationSearchOptions seam β no model downloads, no network, no GPU required.
π License
MIT β see LICENSE. The downloaded s1-mini model weights are Apache-2.0 (upstream), not MIT.
π Acknowledgments
superwhisper/s1-miniβ the fine-tuned ASR normalizer this library wraps. All model design and training credit belongs to the Superwhisper team.Qwen/Qwen3-0.6Bβ the base model s1-mini is fine-tuned from.- ONNX Runtime GenAI β inference engine.
- Hugging Face β model hosting.
- Microsoft.Extensions.AI β the
IChatClientcontract this library implements.
Related Projects
- ElBruno.LocalLLMs β Run local LLMs in .NET
- ElBruno.Whisper β Local Whisper speech-to-text in .NET
- ElBruno.HuggingFace β HuggingFace model utilities for .NET
π 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 Tiene 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.OnnxRuntimeGenAI (>= 0.15.1)
-
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.OnnxRuntimeGenAI (>= 0.15.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.