---
title: "Loading and composing roles"
url: https://recursiveai.org/onespeech/docs/loading/
description: "Every downloadable family has the same canonical loader pair:"
version: "1.0.0"
source: https://github.com/recursive-ai-oss/onespeech-csharp/blob/63002de486786163a0a7800a51811e76d511fae6/docs/loading.md
---

# Loading and composing roles

Every downloadable family has the same canonical loader pair:

- `Load(ResolvedModel model, options/threads, cancellationToken)` opens already-pulled local files without network access. Obtain the directory/provenance pair from `ModelStore.PullAsync`, not by guessing cache filenames.
- `LoadAsync(ModelStore? store = null, string? model = null, options/threads, CancellationToken cancellationToken = default)` pulls, verifies and locally converts missing files before loading. Defaults are the family's declared checkpoint, not a tier-2 default for `Speech`. Calling a named tier-2 family loader is explicit selection of its documented publisher checkpoint.

Whisper, Moonshine, Qwen3-ASR and Nemotron ASR use typed options; other loaders accept `threads`, default 1. All family options call this knob `Threads`, default 1; families permitting 0 document it as runtime-auto. String-directory overloads and graph-path constructors remain for manually converted/custom graphs and require the documented family graph format. They are not needed for catalog loads. Embedded Silero uses its constructor and never downloads.

## Rebuild the file pipeline

```csharp
using SpeechRecognizer recognizer = await WhisperRecognizer.LoadAsync(
    model: "openai/whisper-tiny", cancellationToken: cancellationToken);
using VoiceActivityDetector recognitionVad = new SileroVad(cancellationToken: cancellationToken);
using SpeakerEmbedder embedder = await CampPlusEmbedder.LoadAsync(cancellationToken: cancellationToken);
using VoiceActivityDetector speakerVad = new SileroVad(SileroVadOptions.Diarization, cancellationToken);
using Diarizer diarizer = new ClusteringDiarizer(embedder, speakerVad);
using Aligner qwen = await Qwen3ForcedAligner.LoadAsync(cancellationToken: cancellationToken);
using Aligner ctc = await OmniAsrAligner.LoadAsync(cancellationToken: cancellationToken);
var pipeline = new VadTranscriptionPipeline(recognizer, recognitionVad)
{
    Diarizer = diarizer,
    TimingAligners = [qwen, ctc]
};
Transcript result = pipeline.Transcribe("meeting.wav", "en", cancellationToken);
```

Namespaces: `OneSpeech`, `.Whisper`, `.Vad`, `.Embedders`, `.Diarization`, `.Qwen3`, `.Alignment`, `.Pipeline`. `CampPlusEmbedder.LoadAsync(model: "iic/speech_campplus_sv_zh-cn_16k-common")` derives the Chinese dimension itself. The example uses explicit English timing/embedding; automatic per-recording language selection and deferred model acquisition are provided by `Speech.Transcribe`. It eagerly loads both timing candidates, unlike the lazy happy path. It uses default clustering and resegmentation with the diarization-specific VAD profile; no overlap detection is implied.

Roles are disposable owners of **their own** graph resources. Pipelines, clustering diarizers, live sessions and recognizer decorators borrow their collaborators; disposing a wrapper/session never disposes a borrowed recognizer, detector, aligner or embedder. Dispose sessions before their models and models after all processing has stopped. `Dispose` is idempotent. Custom subclasses override `Dispose(bool)` and call the base. Progress uses `IProgress<TimeSpan>` with processed audio time, never wall time.

`SpeechRecognizer.CreateLiveSession(VoiceActivityDetector detector, language)` accepts any detector. Silero sessions own independent recurrent state; custom detectors receive one zero-padded 32 ms mono frame at a time. They should be stateless or dedicated to that session. A custom VAD returns intervals, not calibrated probabilities; any positive interval counts as active for endpointing.
