Stream English text from a recording with Moonshine.
dotnet add package RecursiveAI.OneSpeechRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
sample.flac is included in the example zip.
using OneSpeech; await foreach (var segment in Speech.TranscribeStream( "sample.flac", model: "moonshine-ai/moonshine-streaming-tiny")) Console.WriteLine(segment.Text);
First use downloads about 0.18 GB of Moonshine streaming-tiny publisher files and caches them and locally converted graphs in ~/.cache/onespeech/models on Linux (change the cache location).
The snippet streams final segments; the full program below also prints live partials.
Stuff it into you, his belly counseled him.Unzip the example, then dotnet run.
Full program
The zip includes sample.flac, the unmodified mono 16 kHz LibriSpeech test-clean utterance 1089-134686-0001: “Stuff it into you, his belly counseled him.” Attribution: Vassil Panayotov, Guoguo Chen, Daniel Povey and Sanjeev Khudanpur (2015), CC BY 4.0. Recognition output can differ from that reference.
The full program feeds the recording through a live session, not a microphone. AudioBuffer contains mono float PCM at 16 kHz; the sample is already mono.
For microphone input, see Real-time speech recognition in C#; this example feeds a recording rather than capturing a device.
First use downloads about 0.18 GB of Moonshine streaming-tiny publisher files and caches them and locally converted graphs in ~/.cache/onespeech/models on Linux (change the cache location).
using OneSpeech; using OneSpeech.Audio; using OneSpeech.Catalog; using OneSpeech.Moonshine; using OneSpeech.Vad; var resolved = await ModelStore.Default.PullAsync( "moonshine-ai/moonshine-streaming-tiny"); using var recognizer = MoonshineRecognizer.Load(resolved); using var vad = new SileroVad(); using var live = recognizer.CreateLiveSession(vad, language: "en"); using var input = AudioFileSource.Open("sample.flac", CancellationToken.None); var buffer = new float[1600]; int count; while ((count = input.Read(buffer, CancellationToken.None)) != 0) Show(live.Push(new AudioBuffer(buffer.AsMemory(0, count), 16000))); Show(live.Complete()); static void Show(IEnumerable<LiveEvent> updates) { foreach (var update in updates) if (update is PartialEvent partial) Console.WriteLine("Partial: " + partial.Segment.Text); else if (update is FinalEvent final) Console.WriteLine("Final: " + final.Segment.Text); }
Output:
Partial: Duff Partial: Duff it Partial: Stuff it into Partial: Stuff it into you. Partial: Stuff it into you. Partial: Stuff it into you, his Partial: Stuff it into you, his belly. Partial: Stuff it into you, his belly came. Partial: Duff it into you, his belly counts. Partial: Stuff it into you, his belly counselled. Partial: Stuff it into you, his belly counseled him. Final: Stuff it into you, his belly counseled him.
Invalid or corrupt audio is rejected; I/O and model acquisition failures propagate as exceptions.
Variations
Print only committed text
Replace the body of Show with:
foreach (var update in updates) if (update is FinalEvent final) Console.WriteLine("Final: " + final.Segment.Text);
First use downloads about 0.18 GB of Moonshine streaming-tiny publisher files and caches them and locally converted graphs in ~/.cache/onespeech/models on Linux (change the cache location).
Final: Stuff it into you, his belly counseled him.Related recipes
The short answer
Select moonshine-ai/moonshine-streaming-tiny for Moonshine live partials. Feed finite mono 16 kHz PCM to a live session, consume replacing PartialEvent hypotheses, and retain FinalEvent text when an utterance closes. The file-streaming snippet uses the same live controller but returns final segments rather than every intermediate event.
How do I use Moonshine for live transcription in C#?
Load the streaming checkpoint explicitly, create a detector and a live session, and push the audio blocks in order. The full program reads the supplied mono FLAC in blocks of 1600 samples and wraps each block in AudioBuffer. This exercises the PCM input path using a recording; it does not capture a microphone or simulate the timing of a person speaking. For microphone input, your application must capture, downmix and resample before pushing blocks.
Keep the input memory valid until Push returns. A session consumes audio memory supplied by your application synchronously and returns immutable event snapshots. The sample reuses its buffer only after the call finishes. Do not push concurrently or interleave Push with another enumeration of that session. The recognizer and detector must outlive the session, which borrows them.
Replace partial text, append final text
A partial is a replacement for the previous hypothesis with the same utterance ID, not another phrase to append. Its revision increases within that utterance. The console example prints every update so you can inspect changes; a caption UI should replace its provisional line instead. A final commits an immutable segment. An endpoint then closes that utterance and resets the revision sequence for the next one.
Call Complete on normal end-of-input to flush pending speech. Disposing a session is not a flush, and cancellation or early iterator disposal does not manufacture a final. The full program’s explicit completion is why the last utterance is retained even if the file ends before a long silence. Invalid input and processing failures have different lifecycle consequences: a failed processing step faults the session, so create a new session rather than retrying an ambiguously consumed block.
Choose a streaming checkpoint
OneSpeech’s registered Moonshine family includes offline tiny/base and streaming tiny/small/medium. These are English models. Offline tiny/base use utterance mode with finals only; they do not gain live partials simply because you call a streaming API. Other languages, contextual biasing, translation and language detection are not supplied by this family.
Streaming state includes encoder caches local to the session. Completed encoder prefixes are retained while only a bounded suffix is re-encoded. A new endpoint resets those caches. The documented live controller has a shared 30-second limit for each continuous stretch of speech; that is separate from the low-level recognizer’s 60-second input limit. For longer recordings, use the bounded pipeline rather than increasing an input buffer without limit.
Do not treat segment times as word times
Moonshine checkpoints do not produce native word timestamps. Unknown word boundaries and confidence remain null, with TimingMethod.None. VAD bounds mark retained speech segments; they are not exact boundaries for every word. Use a separate alignment stage if your application needs word highlighting or word-level subtitles.
The Moonshine model contract covers its input and cache bounds. The live event contract explains audio-relative clocks, finalization and endpoint reasons. Neither an event’s arrival nor the block’s accepted-audio time is a wall-clock latency measurement.