Time supplied words against audio (up to 60 seconds). First use downloads a roughly 1.3 GB model.
dotnet add package RecursiveAI.OneSpeechRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
using OneSpeech; var transcript = Speech.Align("speech.flac", "Stuff it into you his belly counselled him", "en", "facebook/omniASR-CTC-300M"); foreach (var word in transcript.Words) Console.WriteLine($"{word.Text}: " + $"{word.Start?.TotalSeconds:0.00}–" + $"{word.End?.TotalSeconds:0.00}");
The zip includes speech.flac; the output shows word boundaries in seconds.
Unzip the example, then dotnet run. It prints:
Stuff: 0.46–0.72 it: 0.78–0.86 into: 0.92–1.12 you: 1.18–1.28 his: 1.60–1.72 belly: 1.82–2.06 counselled: 2.22–2.60 him: 2.64–2.74
Full program
speech.flac is the unmodified LibriSpeech test-clean recording 1089-134686-0001, renamed for this example. It contains:
Stuff it into you, his belly counselled him.
LibriSpeech, Vassil Panayotov, Guoguo Chen, Daniel Povey and Sanjeev Khudanpur (2015); CC BY 4.0. The zip includes attribution. The sample is short so the download stays small.
The program supplies the written words rather than recognizing them again. Unalignable words keep null times, displayed as empty fields here; audio longer than 60 seconds is refused. The example selects the CTC aligner explicitly.
using OneSpeech; var transcript = Speech.Align( "speech.flac", "Stuff it into you his belly counselled him", language: "en", model: "facebook/omniASR-CTC-300M"); foreach (var word in transcript.Words) Console.WriteLine($"{word.Text}: {word.Start?.TotalSeconds:0.00}–{word.End?.TotalSeconds:0.00}");
Output from dotnet run:
Stuff: 0.46–0.72 it: 0.78–0.86 into: 0.92–1.12 you: 1.18–1.28 his: 1.60–1.72 belly: 1.82–2.06 counselled: 2.22–2.60 him: 2.64–2.74
Variations
Read encoded audio streams instead of paths. This replaces the full program and prints the same output above. The stream stays open until the using declaration disposes it.
using OneSpeech; using var stream = File.OpenRead("speech.flac"); var transcript = await Speech.AlignAsync(stream, "Stuff it into you his belly counselled him", language: "en", model: "facebook/omniASR-CTC-300M"); foreach (var word in transcript.Words) Console.WriteLine($"{word.Text}: {word.Start?.TotalSeconds:0.00}–{word.End?.TotalSeconds:0.00}");
Related recipes
The short answer
Call Speech.Align(audioPath, text, ...), then read each returned word’s nullable Start and End. This attaches times to supplied text; it does not discover the text. The example explicitly selects facebook/omniASR-CTC-300M, so the model choice does not depend on which ordered fallback is available. See word timing and the CTC adapter for the timing contract.
What is forced alignment in speech recognition?
Recognition asks which words were spoken. Forced alignment starts with written words and tries to place them on the audio timeline. That distinction matters when you already have an edited transcript: running recognition again can introduce different wording instead of timing the words you supplied.
The CTC aligner finds a path through frame probabilities for a fixed target. Its adapter maps that path back to written words. In this adapter, casing and punctuation remain in the result, while the spoken target is normalized for matching. The printed boundaries describe that alignment, not a recording device’s exact sample-level truth.
A successful alignment is not proof that every supplied word was actually spoken. An inaccurate transcript can still lead to a plausible-looking path. Use text that belongs to the audio and review uncertain regions. If your task requires recognizing an unknown recording, transcribe it first; alignment alone cannot replace recognition.
The helper also accepts an existing Transcript. That overload retains its written words and non-timing fields rather than splitting the text again. This is useful after correcting a document without wanting to replace its other information. Re-aligning is still a new timing operation; previous boundaries do not establish where words fall in different audio.
How do I get word timestamps?
In the program above, enumerate transcript.Words and read each word’s Start and End. They are TimeSpan values, so TotalSeconds gives a convenient display unit. The two decimal places are output formatting, not a promise of that much accuracy. Keep nullable values in your application’s data model instead of converting a missing boundary to zero.
The standalone helper accepts at most 60 seconds. For a longer recording, split audio and its corresponding text into matching requests. Offset each returned boundary by the chunk’s start time in the original recording. Otherwise every chunk appears to start at zero and the assembled transcript has an incorrect clock. A timestamp from the middle of a clip must not be presented as a timestamp from the start of the original file.
The stream variation consumes encoded audio from its current position and leaves the stream open. It can spool the input to temporary storage; asynchronous I/O does not mean CPU model execution becomes asynchronous. Allow for both temporary disk usage and model memory.
Do you need timestamps in a transcript?
Use them when a reader must jump from a word to its audio, or when another stage needs time-bounded text. Plain searchable text may not need them. Do not infer word confidence from the presence of a boundary: confidence, timing method and nullable boundaries are separate fields.
For evidence, subtitles or an editor, preserve missing times and review the output against the recording. The library’s alignment is not sample-accurate ground truth. Its model files and first-use conversion also need deployment planning; a small audio input does not imply a small model download. OneSpeech is unaffiliated with other products or companies named OneSpeech.