Recursive AI

Display partial speech results in C#

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

Replace draft speech text, then commit a final. sample.flac is in the zip.

Terminal
dotnet add package RecursiveAI.OneSpeech

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

C#
using OneSpeech;
using OneSpeech.Audio;
var view = new LiveText();
await foreach (var e in Speech.Listen(
    Pcm(), language: "en",
    model: "moonshine-ai/moonshine-streaming-tiny"))
    view.Apply(e);
 
static async IAsyncEnumerable<AudioBuffer> Pcm()
{
    using var input = AudioFileSource.Open(
        "sample.flac", CancellationToken.None);
    var buffer = new float[1600]; // 100 ms mono.
    int count;
    while ((count = input.Read(
        buffer, CancellationToken.None)) != 0)
    {
        yield return new AudioBuffer(
            buffer.AsMemory(0, count), 16000);
        await Task.Yield();
    }
}
 
sealed class LiveText
{
    readonly Dictionary<long, (long, string)>
        drafts = new();
    readonly Dictionary<long, string>
        finals = new();
    public void Apply(LiveEvent e)
    {
        long id = e.UtteranceId;
        if (e is PartialEvent p &&
            !finals.ContainsKey(id) &&
            (!drafts.TryGetValue(id, out var old) ||
             p.Revision > old.Item1))
        {
            drafts[id] =
                (p.Revision, p.Segment.Text);
            Console.WriteLine(
                $"Draft {id}/{p.Revision}: " +
                p.Segment.Text);
        }
        if (e is FinalEvent f &&
            finals.TryAdd(id, f.Segment.Text))
        {
            drafts.Remove(id);
            Console.WriteLine(finals[id]);
        }
        if (e is EndpointEvent) drafts.Remove(id);
    }
}

First run downloads Moonshine streaming tiny weights (about 176 MB) and CAM++ English speaker-model weights (about 29 MB). Change the model cache.

Speaker embeddings are biometric data. Obtain informed consent, limit retention and access, and check applicable law, including Illinois BIPA and GDPR Article 9, before enrollment, identification or verification. The library does not persist embeddings; applications remain responsible for audio, transcripts, logs and any copies they retain. This is not legal advice.

Unzip the example, then dotnet run.

Prints:

Text
Draft 0/1: Duff
Draft 0/2: Duff it
Draft 0/3: Stuff it into
Draft 0/4: Stuff it into you.
Draft 0/5: Stuff it into you.
Draft 0/6: Stuff it into you, his
Draft 0/7: Stuff it into you, his belly.
Draft 0/8: Stuff it into you, his belly came.
Draft 0/9: Duff it into you, his belly counts.
Draft 0/10: Stuff it into you, his belly counselled.
Draft 0/11: Stuff it into you, his belly counseled him.
Stuff it into you, his belly counseled him.

The full program

The zip contains the unmodified LibriSpeech test-clean utterance 1089-134686-0001: “stuff it into you his belly counselled him”. Credit: Vassil Panayotov, Guoguo Chen, Daniel Povey and Sanjeev Khudanpur (LibriSpeech), CC BY 4.0.

C#
using OneSpeech;
using OneSpeech.Audio;
 
var drafts = new Dictionary<long, (long Revision, string Text)>();
var committed = new HashSet<long>();
await foreach (var update in Speech.Listen(Pcm(), language: "en",
    model: "moonshine-ai/moonshine-streaming-tiny"))
{
    long id = update.UtteranceId;
    switch (update)
    {
        case PartialEvent partial when !committed.Contains(id):
            if (!drafts.TryGetValue(id, out var old) || partial.Revision > old.Revision)
            {
                drafts[id] = (partial.Revision, partial.Segment.Text);
                Console.WriteLine($"Draft {id}/{partial.Revision}: {drafts[id].Text}");
            }
            break;
        case FinalEvent final when committed.Add(id):
            drafts.Remove(id);
            Console.WriteLine($"Final {id}: {final.Segment.Text}");
            break;
        case EndpointEvent endpoint:
            drafts.Remove(id);
            Console.WriteLine($"Closed {id}: {endpoint.Reason}");
            break;
    }
}
 
static async IAsyncEnumerable<AudioBuffer> Pcm()
{
    using var input = AudioFileSource.Open("sample.flac", CancellationToken.None);
    var buffer = new float[1600]; // Mono sample: 100 ms.
    int count;
    while ((count = input.Read(buffer, CancellationToken.None)) != 0)
    {
        yield return new AudioBuffer(buffer.AsMemory(0, count), 16000);
        await Task.Yield();
    }
}

dotnet run prints:

Text
Draft 0/1: Duff
Draft 0/2: Duff it
Draft 0/3: Stuff it into
Draft 0/4: Stuff it into you.
Draft 0/5: Stuff it into you.
Draft 0/6: Stuff it into you, his
Draft 0/7: Stuff it into you, his belly.
Draft 0/8: Stuff it into you, his belly came.
Draft 0/9: Duff it into you, his belly counts.
Draft 0/10: Stuff it into you, his belly counselled.
Draft 0/11: Stuff it into you, his belly counseled him.
Final 0: Stuff it into you, his belly counseled him.
Closed 0: Silence

The full program logs each draft replacement, commits a final once, then clears the draft on the endpoint. The dictionaries represent UI state; dispatch changes to your UI thread when integrating this loop into an app.

Pcm reads the bundled mono file in 100 ms blocks. It is a replay source, not microphone capture. CAM++ English compares voices so Speech.Listen can label speakers; it loads even though this example displays only text. The model download guide lists the required files.

If processing faults a session, create a new session; do not retry an ambiguously consumed block.

Variations

Use finals-only recognition. Change the full program’s model ID to moonshine-ai/moonshine-tiny. On the same recording it prints no drafts:

Text
Final 0: Duff it into you, his belly counseled him.
Closed 0: Silence

Offline-family utterance mode does not produce partials.

Label speakers in live audio in C# · Limit live speech utterance duration in C# · Transcribe audio from a stream in C#

The short answer

Treat each partial result as a replacement, not an append. Key the draft by UtteranceId, accept only newer Revision values, remove that draft when the final text arrives, which the library will not change, and commit that final once. An endpoint closes the utterance; it is not another copy of its text. Select a streaming family that actually emits partials.

How do I display partial speech recognition results in C#?

Keep two kinds of state: provisional text for the active utterance and committed text for completed utterances. A partial is the current hypothesis for that utterance. The next partial can change earlier words, punctuation or the entire phrase. Appending every hypothesis produces duplicated text and leaves mistakes visible after the model has replaced them.

The example’s drafts dictionary stores both the revision and its text. Its committed set prevents a late draft from reopening a completed utterance and makes final handling idempotent. The console logs every accepted replacement so you can see the sequence. A screen should instead replace the visible draft region with the newest value and move the final to the committed region once.

OneSpeech assigns revisions starting at one within each utterance. An endpoint advances the utterance ID and resets that revision sequence. Compare revisions only within the same utterance. Keep the ID in your view model even if your screen shows only the current draft. The live-session contract describes the ordering and clock.

How do I use a C# partial speech recognition example in a UI?

The downloaded program contains the PCM source and result-state logic. Replace Pcm with your application’s async source of mono 16 kHz blocks that your application owns. OneSpeech does not open a microphone or perform capture resampling. Keep the samples alive until the consumer advances; the example reuses its decoding buffer only after that point.

Marshal dictionary and control changes onto your UI’s dispatcher. The recognition loop itself is not a UI-thread abstraction. Build the displayed transcript from committed utterances plus the current drafts, preserving utterance order rather than concatenating event arrival strings. Drop a draft at endpoint even when no final was emitted, so stale provisional text does not become permanent text by accident.

The library does not change text after emitting a final. Later speaker activity or a new utterance is not permission to edit it. If your product permits manual text correction, store that as an application edit separately from the recognizer’s result. Do not infer word positions from event availability times: AudioTime measures how much audio had been accepted when the event became available.

Why are there no partial speech recognition results?

Speech.Listen defaults to moonshine-ai/moonshine-streaming-tiny, which emits replacing partials. The base implementation of SpeechRecognizer.CreateLiveSession instead waits for a speech boundary found by a voice activity detector (VAD) and emits only final text; streaming recognizers override it. Selecting an offline checkpoint therefore produces no drafts, as the variation shows.

moonshine-ai/moonshine-streaming-tiny, selected in the full example, has replacing partials. A null language hint for this English family is a fixed English task, not language identification. Use a compatible model for your language requirements rather than treating missing partials as a UI bug.

Normal completion flushes real input. Cancellation or early iterator disposal does not flush a final. Keep cancellation distinct from completion in your UI: discard a canceled draft rather than presenting it as committed recognition. On a processing failure the session is faulted; begin a fresh session instead of replaying a block whose consumption is uncertain.

All onespeech recipes