Recursive AI

Real-time speech recognition in C#

OneSpeech is a free, open-source speech recognition library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies.

Recognize speech as PCM audio blocks arrive. OneSpeech accepts your audio stream; it does not capture a microphone. The example replays a short English recording through a live session.

Terminal
dotnet add package RecursiveAI.OneSpeech
dotnet add package RecursiveAI.OneSpeech.Models.MoonshineStreamingTiny
dotnet add package RecursiveAI.OneAudio

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

sample.flac is included in the zip; you can substitute your own 16 kHz mono recording. VAD (voice activity detection) finds the speech parts, Silero is the VAD model, and Moonshine is the English recognition model.

C#
using OneSpeech;
using OneSpeech.Moonshine;
using OneSpeech.Vad;
var clip = OneAudio.Audio.Load("sample.flac");
using var recognizer = await MoonshineRecognizer.LoadAsync();
using var vad = new SileroVad();
using var session = recognizer.CreateLiveSession(vad, "en");
var updates = session.Push(new AudioBuffer(clip.Samples, 16000));
foreach (var final in updates.Concat(session.Complete()).OfType<FinalEvent>())
    Console.WriteLine(final.Segment.Text);

Prints:

Text
Stuff it into you, his belly counseled him.

Unzip the example, then dotnet run.

Full program

The zip includes sample.flac: a short English reading, “Stuff it into you, his belly counseled him.” Source: LibriSpeech test-clean utterance 1089-134686-0001, Vassil Panayotov, Guoguo Chen, Daniel Povey and Sanjeev Khudanpur (2015), CC BY 4.0. The FLAC is unmodified, mono, 16 kHz.

The snippet at the top uses Push and Complete to submit the whole short clip; the full program uses Speech.Listen to consume successive PCM blocks and complete automatically at end-of-input. Both use the same live-session machinery and print only final updates.

For a microphone, follow Record microphone audio in C# with OneAudio’s DeviceStreamOptions set to SampleRate = 16000 and Channels = 1. Its captured float PCM can feed AudioBuffer: in an application-owned polling loop, read the available frames, wrap only those samples in a 16 kHz mono buffer, and push them into the session. Drain capture consistently and perform recognition outside the device callback; real capture needs an available device and operating-system permissions.

C#
using OneSpeech;
using OneSpeech.Audio;
 
await foreach (var update in Speech.Listen(Blocks(), language: "en"))
    if (update is FinalEvent final)
        Console.WriteLine(final.Segment.Text);
 
static async IAsyncEnumerable<AudioBuffer> Blocks()
{
    using var input = AudioFileSource.Open("sample.flac", CancellationToken.None);
    var buffer = new float[1600]; // The sample is mono; 100 ms blocks.
    int count;
    while ((count = input.Read(buffer, CancellationToken.None)) != 0)
    {
        yield return new AudioBuffer(buffer.AsMemory(0, count), 16000);
        await Task.Yield();
    }
}

Output:

Text
Stuff it into you, his belly counseled him.

Unsupported or corrupt input throws UnsupportedAudioException; file-system errors remain I/O errors.

Variations

Mark final text

Inside the FinalEvent branch, prefix committed text:

C#
Console.WriteLine($"Final: {final.Segment.Text}");

Output:

Text
Final: Stuff it into you, his belly counseled him.

The short answer

Create a live recognition session, push mono 16 kHz PCM blocks into it and consume its updates. Call Complete at normal end-of-input to flush trailing speech. The downloadable program uses Speech.Listen, which performs that completion automatically when its input sequence ends normally. It replays a real file so you can run it without a microphone API.

How do I do real-time speech recognition in C#?

Separate audio capture from recognition. Your application opens the microphone, converts its samples to mono 16 kHz PCM and supplies blocks. OneSpeech’s live session API accepts those blocks; it does not choose an audio device or request operating-system microphone permission.

The program’s Blocks iterator shows the input boundary: each yielded AudioBuffer contains the next portion of the recording. Replace that file reader with your capture source when integrating it into an app. The file iterator yields without wall-clock pacing, so this example is a replay of the live API, not a microphone demo or a latency measurement.

Recognition is synchronous CPU work inside the session. An async input sequence lets the library await audio arrival; it does not turn inference into a background task. Arrange your application’s audio and UI work accordingly. Sessions are single-use and not thread-safe, so do not interleave a push loop with another consumer of the same session.

Is real-time speech recognition free?

OneSpeech is free and open source under MIT or Apache-2.0. The selected Moonshine streaming tiny weights are a tier-1 model; the model table lists the model licenses separately from the library license. The example does not call a paid recognition endpoint or require an API key.

“Streaming” describes the input and update behavior, not a promise that your machine will always keep up. Test the model with the block size and hardware you will actually deploy. The model package makes weights available during restore; it does not remove local model conversion and loading work on first use.

What models support real-time speech to text?

The example selects Moonshine streaming tiny through the library’s live default. Streaming checkpoints keep session-local encoder caches and emit replacing partials as well as final segments. The Moonshine guide describes the supported checkpoint families. These checkpoints recognize English; choose a model with the language capabilities your application needs rather than passing an unsupported hint.

An offline-family recognizer can also create a VAD-endpointed live session, but that path emits finals only. Do not build an interface that assumes every recognizer produces partials.

How do I display streaming speech-to-text results?

The sample prints only FinalEvent text so its output is short and reproducible. A partial is a draft that replaces the earlier draft for the same utterance, not new text to append. A final commits an immutable segment. Keep committed text and the current draft separate in your UI.

All event times are relative to accepted audio, not wall time. A large push can make several updates available at the same audio time. Neither that timestamp nor a VAD endpoint invents a word boundary. Cancellation and early iterator disposal do not flush pending speech; normal completion does. Dispose the session before the borrowed recognizer and detector.

All onespeech recipes