Run Qwen3-TTS text-to-speech locally from C# using ONNX Runtime — no Python needed at inference time. Models are downloaded automatically on first run.
Pre-exported ONNX models are hosted on HuggingFace: elbruno/Qwen3-TTS-12Hz-0.6B-CustomVoice-ONNX (0.6B preset voices) | elbruno/Qwen3-TTS-12Hz-1.7B-CustomVoice-ONNX (1.7B preset voices + instruct) | elbruno/Qwen3-TTS-12Hz-0.6B-Base-ONNX (voice cloning)
- Russian language support across CLI, web app, file reader, and client defaults
- Voice cloning package (
ElBruno.QwenTTS.VoiceCloning) with ICL mode improvements - 1.7B model variant with natural-language instruct control
- GPU acceleration options (CUDA and DirectML)
- Blazor web UI for text-to-speech and voice cloning
- Local TTS Inference — Run Qwen3-TTS entirely on your machine using ONNX Runtime
- Multi-Model Support — Choose between 0.6B (lightweight) and 1.7B (advanced instruct control) variants
- Automatic Model Download — Models download from HuggingFace on first run (~5.5 GB for 0.6B, ~10 GB for 1.7B)
- Instruct Control — Natural-language style control with 1.7B model (e.g., "speak with excitement", "whisper softly")
- Multi-Speaker — 9 built-in voices: ryan, serena, vivian, aiden, eric, dylan, uncle_fu, ono_anna, sohee
- Voice Cloning — Clone any voice from a 3-second audio sample (docs)
- Web UI — Blazor app with TTS generation and voice cloning pages (docs)
- GPU Acceleration — Optional CUDA or DirectML support via SessionOptions injection (docs)
- Multi-Language — English, Spanish, Chinese, Japanese, Korean, Russian
- Shared Model Cache — Models stored once in
%LOCALAPPDATA%/ElBruno/QwenTTS, shared across all apps - Reusable Sessions — ONNX sessions stay loaded and are reused across requests instead of being recreated
- Bounded Concurrency + Cancellation — Configure max concurrent syntheses and cancel queued or in-flight requests at safe boundaries
- Latency Metrics — Capture queue, first-audio, and total synthesis latency from the high-level client
- Streaming Audio Updates — Emit ordered WAV chunks with format metadata and explicit progressive-vs-chunked capability flags
- In-Memory PCM/WAV APIs — Get normalized PCM samples or WAV bytes without writing a temp/output file
- Microsoft.Extensions.AI Integration — Resolve
ITextToSpeechClientfrom DI and return in-memory WAV audio through the MEAI contract - 24 kHz WAV Output — High-quality mono audio
dotnet add package ElBruno.QwenTTSusing ElBruno.QwenTTS.Pipeline;
// 0.6B model (default) — models download automatically (~5.5 GB)
using var pipeline = await TtsPipeline.CreateAsync("models");
await pipeline.SynthesizeAsync("Hello world!", "ryan", "hello.wav", "english");
// 1.7B model — supports instruct control (~10 GB)
using var pipeline17 = await TtsPipeline.CreateAsync("models", variant: QwenModelVariant.Qwen17B);
await pipeline17.SynthesizeAsync("Hello world!", "ryan", "hello.wav", "english",
instruct: "speak with warmth and excitement");
// Reuse a single pipeline and get per-request latency metrics
using var sharedPipeline = await TtsPipeline.CreateAsync("models", maxConcurrency: 2);
var metrics = await sharedPipeline.SynthesizeWithMetricsAsync(
"Two callers can queue behind the same shared pipeline.",
"serena",
"queued.wav");
Console.WriteLine($"First audio: {metrics.FirstAudioLatency.TotalSeconds:F2}s");
// Keep the audio in memory instead of writing a file first
var pcm = await sharedPipeline.SynthesizeToPcmAsync(
"Return normalized PCM samples in memory.",
"ryan",
"english");
var wavBytes = await sharedPipeline.SynthesizeWavAsync(
"Return WAV bytes in memory too.",
"ryan",
"english");
await File.WriteAllBytesAsync("memory.wav", wavBytes.ToArray());
Console.WriteLine($"{pcm.SampleCount} samples, duration {pcm.Duration.TotalSeconds:F2}s");
// Stream WAV chunks without building a second full-audio byte[]
await foreach (var update in sharedPipeline.GetStreamingAudioAsync(
"Stream this response in WAV chunks.",
"ryan",
"english"))
{
if (update.Kind == TextToSpeechUpdateKind.SessionOpen)
{
Console.WriteLine($"{update.MediaType} @ {update.SampleRate} Hz, progressive={update.IsProgressive}");
}
if (update.Kind == TextToSpeechUpdateKind.AudioChunk)
{
await outputStream.WriteAsync(update.AudioData!);
}
}# Default (0.6B model)
dotnet run --project src/ElBruno.QwenTTS -- --model-dir models --text "Hello, this is a test." --speaker ryan --language english --output hello.wav
# 1.7B model with instruct control
dotnet run --project src/ElBruno.QwenTTS -- --model-dir models --variant 1.7b --text "Hello, this is a test." --speaker ryan --instruct "speak with excitement" --output hello.wavModels are downloaded automatically if not present in the --model-dir directory.
using ElBruno.QwenTTS.Pipeline;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddQwenTextToSpeechClient(options =>
{
options.ModelVariant = QwenModelVariant.Qwen17B;
options.ExecutionProvider = ExecutionProvider.Cuda;
options.InstructText = "Speak with warmth";
});
using var provider = services.BuildServiceProvider();
var client = provider.GetRequiredService<ITextToSpeechClient>();
var response = await client.GetAudioAsync(
"Hello from Microsoft.Extensions.AI.",
new TextToSpeechOptions
{
VoiceId = "serena",
Language = "english",
AdditionalProperties = new()
{
[QwenTextToSpeechMetadataKeys.Instruct] = "speak with excitement"
}
});
var wav = (DataContent)response.Contents.Single();
await File.WriteAllBytesAsync("meai.wav", wav.Data.ToArray());The adapter returns audio/wav and populates response metadata with QwenTextToSpeechMetadataKeys. Voice-cloning input is not supported through the MEAI adapter; use ElBruno.QwenTTS.VoiceCloning for reference-audio synthesis.
Clone any voice from a 3-second audio sample using the ElBruno.QwenTTS.VoiceCloning package:
dotnet add package ElBruno.QwenTTS.VoiceCloningusing ElBruno.QwenTTS.VoiceCloning.Pipeline;
var cloner = await VoiceClonePipeline.CreateAsync();
await cloner.SynthesizeAsync("Hello world!", "reference_speaker.wav", "output.wav", "english");See docs/voice-cloning.md for full documentation.
Pass a sessionOptionsFactory to use CUDA or DirectML instead of CPU:
using ElBruno.QwenTTS.Pipeline;
// CUDA (NVIDIA) — requires Microsoft.ML.OnnxRuntime.Gpu NuGet package
var tts = await TtsPipeline.CreateAsync(
sessionOptionsFactory: OrtSessionHelper.CreateCudaOptions);
// DirectML (any GPU on Windows) — requires Microsoft.ML.OnnxRuntime.DirectML NuGet package
// Uses GPU for language model, CPU for vocoder (hybrid mode)
var tts = await TtsPipeline.CreateAsync(
sessionOptionsFactory: OrtSessionHelper.CreateDirectMlOptions,
vocoderSessionOptionsFactory: OrtSessionHelper.CreateCpuOptions);See docs/gpu-acceleration.md for full setup instructions.
dotnet run --project src/ElBruno.QwenTTS -- --model-dir models --text "Welcome to the future of speech synthesis." --speaker serena --output welcome.wav
dotnet run --project src/ElBruno.QwenTTS -- --model-dir models --text "Speaking with excitement and energy!" --speaker aiden --variant 1.7b --instruct "speak with excitement" --output excited.wav
dotnet run --project src/ElBruno.QwenTTS -- --model-dir models --text "A calm and gentle narration." --speaker ryan --variant 1.7b --instruct "speak slowly and calmly" --output calm.wavdotnet run --project src/ElBruno.QwenTTS -- --model-dir models --text "Hola, esta es una prueba de texto a voz." --speaker ryan --language spanish --output hola.wav
dotnet run --project src/ElBruno.QwenTTS -- --model-dir models --text "Bienvenidos al futuro de la sintesis de voz." --speaker serena --language spanish --output bienvenidos.wavdotnet run --project src/ElBruno.QwenTTS -- --model-dir models --text "Привет, это тест синтеза речи." --speaker ryan --language russian --output russian.wavdotnet run --project src/ElBruno.QwenTTS.FileReader -- --model-dir models --input samples/hello_demo.txt --speaker ryan --language english --output-dir output/hello
dotnet run --project src/ElBruno.QwenTTS.FileReader -- --model-dir models --input samples/demo_subtitles.srt --speaker serena --output-dir output/subtitlesdotnet run --project src/ElBruno.QwenTTS.WebOpen http://localhost:5153 — two pages:
- 🔊 TTS — type text or upload files, pick a voice, and generate speech
- 🎭 Voice Clone — record your voice or upload a WAV, then synthesize with your cloned voice
| Document | Description |
|---|---|
| Prerequisites | System requirements (.NET 8+/10, disk space) |
| Getting Started | Setup, auto-download, and first run |
| Core Library | ElBruno.QwenTTS API reference and usage examples |
| CLI Reference | All command options, speakers, and examples |
| File Reader | Batch audio generation from text and SRT files |
| Web App | Blazor web UI for speech generation |
| Blazor Components | Reusable Razor components package for TTS and voice cloning UIs |
| Blazor Demo Sample | How to run and validate the BlazorQwenTtsDemo sample app |
| Release Validation Checklist | Issue/PR validation and merge-to-release gates |
| What's New Policy | Rules for maintaining the README "What's New" section |
| Architecture | Pipeline design, model components, project structure |
| Exporting Models | Re-exporting ONNX models from PyTorch weights |
| Voice Cloning | Clone any voice from a 3-second reference audio |
| GPU Acceleration | CUDA, DirectML, and CPU configuration |
| Troubleshooting | Common issues and fixes |
| Detailed Architecture | Full tensor shapes, KV-cache, codebook structure |
| Changelog | Versioned summary of notable changes |
The python/ directory contains tools for exporting ONNX models from PyTorch weights and downloading models from HuggingFace. These are only needed if you want to re-export or customize models — they are not required for running the C# pipeline.
git clone https://github.com/elbruno/ElBruno.QwenTTS.git
cd ElBruno.QwenTTS
dotnet build
dotnet test- .NET 8.0 or .NET 10.0 SDK
- ONNX Runtime compatible platform (Windows, Linux, macOS)
- ~5.5 GB disk space for model files
Contributions are welcome! Here's how to get started:
- Fork the repository
- Create a branch for your feature or fix:
git checkout -b feature/my-feature - Make your changes and ensure the solution builds:
dotnet build - Run tests:
dotnet test - Submit a pull request with a clear description of the changes
Please open an issue first for major changes or new features to discuss the approach.
- ElBruno.PersonaPlex — NVIDIA PersonaPlex-7B full-duplex speech-to-speech for local C# inference via ONNX Runtime. Pre-exported ONNX models: elbruno/personaplex-7b-v1-onnx
Hi! I'm ElBruno 🧡, a passionate developer and content creator exploring AI, .NET, and modern development practices.
Made with ❤️ by ElBruno
If you like this project, consider following my work across platforms:
- 📻 Podcast: No Tienen Nombre — Spanish-language episodes on AI, development, and tech culture
- 💻 Blog: ElBruno.com — Deep dives on embeddings, RAG, .NET, and local AI
- 📺 YouTube: youtube.com/elbruno — Demos, tutorials, and live coding
- 🔗 LinkedIn: @elbruno — Professional updates and insights
- 𝕏 Twitter: @elbruno — Quick tips, releases, and tech news
This project is licensed under the MIT License — see the LICENSE file for details.