Dev.to AI 🤖 Ai 👁 0 📖 3 min read

Shipping the first .NET F5-TTS library — and the ONNX bug I had to fix first

F5-TTS is one of the better open text-to-speech and voice-cloning models. There's just one catch if you live in the .NET world: running it means Python and PyTorch. No native story, no NuGet package, nothing you can drop

F5-TTS is one of the better open text-to-speech and voice-cloning models. There's just one catch if you live in the .NET world: running it means Python and PyTorch. No native story, no NuGet package, nothing you can drop into a desktop app without shipping an interpreter alongside it.

I wanted German (and any-language) TTS inside a native Windows app — so I set out to run F5-TTS on ONNX Runtime, which .NET already has first-class bindings for. This is the story of the bug that stood in the way, and the library that came out of fixing it.

The garbled-German problem

DakeQQ/F5-TTS-ONNX is an excellent project that exports F5-TTS to three ONNX graphs (preprocess → transformer → decode). It works great — for the default (v1) base checkpoint.

Point it at a non-English community fine-tune, though, and the output is fluent-but-wrong speech: the right voice, the right language, complete nonsense words. A classic sign that the text conditioning is broken while the acoustic model is fine.

Root cause: v0 vs v1

Many non-English fine-tunes (like hvoss-techfak/F5-TTS-German) are built on the older F5TTS_Base (v0) architecture, which differs from v1 in two config flags:

param v1 (default) v0
pe_attn_head None (RoPE on all heads) 1 (RoPE on the first head only)
text_mask_padding True False

The exporter's model reimplementation was hard-wired for v1: it applied RoPE to every attention head and always zeroed out padded text positions. On a v0 checkpoint, both corrupt the conditioning — and you get word-salad.

The fix was small and I contributed it upstream in PR #74 (merged): honor pe_attn_head in the attention processor, and honor mask_padding in the text embedding.

Wrapping it in a .NET API

With correct ONNX models in hand, the wrapper is tiny — all the heavy signal processing (STFT, the diffusion transformer, the vocoder) lives inside the graphs. The library just marshals tensors and runs the NFE loop:

using Horus.F5Tts.Onnx;

using var model = F5TtsModel.Load(
    "F5_Preprocess.onnx", "F5_Transformer.onnx", "F5_Decode.onnx", "vocab.txt",
    configureSession: o => o.AppendExecutionProvider_DML(0)); // CPU / DirectML / CUDA

var (reference, _) = WavAudio.ReadPcm16("voice.wav"); // 24 kHz mono
var result = model.Synthesize(reference, "Transcript of the clip.", "Hello from .NET!");
File.WriteAllBytes("out.wav", result.ToWav());

Design choices worth calling out:

  • No forced runtime. The package only references the ONNX Runtime managed API; the consumer adds the CPU / DirectML / CUDA native package and picks the provider via a session hook.
  • Character-level tokenizer. For Latin-script languages you don't need jieba/pinyin at all — a plain char→vocab-index mapping is enough (verified end-to-end).
  • Dependency-free WAV helpers, so the surface stays tiny.

Verifying it actually works

Compiling isn't shipping. I ran the whole pipeline against real models and transcribed the output with faster-whisper large-v3: correct language, exact transcript, confidence 1.00. (The smoke test caught a real runtime bug too — the NFE loop ran one step too many and overran the transformer's time-step table. CI + a smoke test earn their keep.)

Shipping it

As far as I can tell, it's the first library that runs F5-TTS natively from .NET. If you're building something that needs local, offline, natural-sounding TTS on Windows/.NET, give it a spin — and open an issue if you hit anything.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.