Recursive AI

Transcribe audio from a stream in C#

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

Terminal
dotnet add package RecursiveAI.OneSpeech

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

C#
using OneSpeech;
await using var input =
    File.OpenRead("sample.flac");
var transcript =
    await Speech.TranscribeAsync(
    input, language: "en");
Console.WriteLine(transcript.Text);

First run downloads Whisper large-v3-turbo weights (about 1.62 GB). Timing and speaker models download separately; the English sample also uses Qwen3-ForcedAligner (about 1.84 GB) and CAM++ English (about 29 MB of weights). 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 (the model’s real output, error included):

Text
stuffered 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;
 
await using var input = File.OpenRead("sample.flac");
using var cancellation = new CancellationTokenSource();
try
{
    var transcript = await Speech.TranscribeAsync(input, language: "en",
        cancellationToken: cancellation.Token);
    Console.WriteLine(transcript.Text);
    Console.WriteLine($"Caller stream still open: {input.CanRead}");
}
catch (UnsupportedAudioException error)
{
    Console.WriteLine($"Unsupported audio: {error.Message}");
}
catch (OperationCanceledException)
{
    Console.WriteLine("Transcription canceled.");
}

dotnet run prints:

Text
stuffered into you, his belly counseled him.
Caller stream still open: True

This API spools encoded input to disk before decoding; it does not emit live partial results.

The file is opened by the caller. TranscribeAsync leaves it open, and await using closes it when the scope ends. The full program supplies a cancellation token and distinguishes cancellation from unsupported audio.

This is the recognizer’s actual output, including its “stuffered” error. The default is Whisper large-v3-turbo. Whole-file transcription can also load timing and speaker models; prepare the files described in the model download guide before an offline run. Missing model files download on first use.

Unsupported or corrupt input throws UnsupportedAudioException; I/O and access errors remain separate, and cancellation raises OperationCanceledException.

Variations

Transcribe an in-memory stream. When your application already has encoded audio bytes, wrap them in MemoryStream. This example loads the bytes from sample.flac in the zip:

C#
using OneSpeech;
using var input = new MemoryStream(File.ReadAllBytes("sample.flac"));
Console.WriteLine((await Speech.TranscribeAsync(input, language: "en")).Text);
Text
stuffered into you, his belly counseled him.

Cancel before transcription. This uses the same sample and confirms that cancellation does not close the caller’s stream.

C#
using OneSpeech;
await using var input = File.OpenRead("sample.flac");
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
try
{
    await Speech.TranscribeAsync(input, language: "en",
        cancellationToken: cancellation.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine($"Canceled; caller stream open: {input.CanRead}");
}
Text
Canceled; caller stream open: True

Label speakers in live audio in C# · Display partial speech recognition results in C# · Limit live speech utterance duration in C#

The short answer

Use await Speech.TranscribeAsync(stream, ...) for encoded audio held in a .NET stream. The caller retains ownership. OneSpeech asynchronously copies encoded input to a temporary seekable file, then decodes and recognizes it. This returns one accumulated Transcript; it is not the live PCM event API.

How do I transcribe audio from a stream in C#?

Open the stream yourself, pass it to Speech.TranscribeAsync, and keep it alive until the awaited call returns. Use await using when your code owns that stream. If a framework supplied it, follow the framework’s ownership rules rather than disposing it merely because transcription finished.

The input is encoded media, such as the FLAC sample, not an array of raw floating-point samples disguised as bytes. The asynchronous spool permits nonseekable sources and formats needing random access. It uses temporary disk space proportional to the encoded input size while keeping decoding buffers bounded. The published entry point limits that spool to 512 MiB. Cancellation or failure removes the temporary file. The exact limit and ownership contract are in the pinned public API.

After input preparation, CPU inference is synchronous. Awaiting the API does not promise background inference or immediate partial text. Missing models are acquired on first use, so prepare the recognizer and its possible timing/speaker companions before requests if the environment must remain offline.

Can I transcribe a nonseekable audio stream in C#?

Yes: the async path spools caller-owned encoded input before opening the decoder. The decoder itself expects a seekable source, but the caller’s stream does not have to be seekable. This distinction matters for HTTP response bodies, uploads and other forward-only sources. A stream that never ends will not behave like a live microphone session: the spool must complete before decoding begins.

Supported encoded formats follow OneAudio’s registered codecs, including WAV, MP3, FLAC, Ogg Vorbis/Opus, AAC-LC in M4A/3GP/ADTS and AMR-NB/WB. The core audio adapter additionally supports G.711 A-law and μ-law in RIFF/WAVE; .au is not supported. A filename extension alone does not make corrupt bytes decodable. See audio input for the format and buffer contract.

Unsupported or corrupt encoded media raises UnsupportedAudioException. File access failures and cancellation are not disguised as format errors. In a request handler, distinguish invalid audio from operational failures so the caller can fix the right problem. Avoid logging full transcript text by default when diagnosing a bad input request.

Does transcribing an audio stream return live results?

Not with this overload. It returns a document, so the text and metadata grow with recognized output even though PCM decoding uses bounded buffers. It can perform whole-recording speaker clustering and a timing replay, unlike the live-session API.

For already-decoded live input, feed borrowed mono 16 kHz AudioBuffer blocks to Speech.Listen instead. That yields partial, final, speaker and endpoint events according to the selected model. Do not pass encoded FLAC or MP3 bytes as PCM. File-segment streaming is another separate operation: TranscribeStream yields final segments without accumulating the whole document.

All onespeech recipes