SubtitleEdit/subtitleedit · error · ArgumentException
Voice is not a Qwen3TtsVoice
Error message
Voice is not a Qwen3TtsVoice
What it means
ArgumentException thrown at the top of Qwen3TtsCrispAsr.Speak when voice.EngineVoice is not a Qwen3TtsVoice. The engine contract is that the Voice passed in was produced by this engine's GetVoices (which wraps each entry in a Qwen3TtsVoice); any other engine's voice object is a programming error, not a runtime hazard.
Source
Thrown at src/ui/Features/Video/TextToSpeech/Engines/Qwen3TtsCrispAsr.cs:737
public Task<string[]> GetModels() => Task.FromResult(new[] { ModelKeyVoiceDesign, ModelKeyCustomVoice, ModelKeyClone });
public Task<TtsLanguage[]> GetLanguages(Voice voice, string? model) => Task.FromResult(Qwen3TtsCrispAsrLanguages.All);
public Task<Voice[]> RefreshVoices(string language, CancellationToken cancellationToken) =>
GetVoices(language);
public async Task<TtsResult> Speak(
string text,
string outputFolder,
Voice voice,
TtsLanguage? language,
string? region,
string? model,
CancellationToken cancellationToken)
{
if (voice.EngineVoice is not Qwen3TtsVoice qwen3Voice)
{
throw new ArgumentException("Voice is not a Qwen3TtsVoice");
}
var modelKey = ResolveModelKey(model);
// Voice clone (Base) needs a reference WAV plus its transcript. The backend resolves the
// bare voice name against --voice-dir and auto-loads the matching <name>.txt as ref-text;
// without that transcript it returns "ref-text not set" (HTTP 500). Surface both gaps
// up-front with an actionable message instead of an opaque server error.
if (modelKey == ModelKeyClone)
{
if (string.IsNullOrEmpty(qwen3Voice.FilePath))
{
throw new InvalidOperationException(
"Qwen3 TTS (CrispASR) voice cloning requires a reference voice. "
+ "Import one via the voice settings (you'll be asked for its transcript), "
+ "then pick it in the voice combo.");
}
View on GitHub (pinned to 17a9f07487)
Solutions
- Ensure the Voice passed to Speak was obtained from this engine's own GetVoices/RefreshVoices.
- On engine switch, clear or re-resolve the selected voice before calling Speak.
- Add an IsVoiceInstalled/voice-type check in the UI layer so an incompatible voice can't be selected for this engine.
- In tests, construct voices with `new Voice(new Qwen3TtsVoice(...))`.
Example fix
// before await engine.Speak(text, folder, someOtherEngineVoice, ...); // after — resolve a voice from this engine first var voices = await engine.GetVoices(language); var voice = voices.Single(v => ((Qwen3TtsVoice)v.EngineVoice).Voice == desiredName); await engine.Speak(text, folder, voice, ...);
Defensive patterns
Strategy: type-guard
Validate before calling
if (voice.EngineVoice is not Qwen3TtsVoice)
throw new ArgumentException("Voice is not a Qwen3TtsVoice"); Type guard
static bool IsQwen3TtsVoice(Voice v) => v.EngineVoice is Qwen3TtsVoice;
Prevention
- Resolve voices from the active engine's GetVoices, never reuse across engines.
- Clear the selected voice on engine switch.
- Filter the voice combo by the current engine's voice subtype.
When it happens
Trigger: The voice combo was populated by a different engine (e.g. Qwen3TtsCpp or VibeVoiceCrispAsr) and that Voice was passed into this engine's Speak; a stale cached Voice survived an engine switch; a test constructed a Voice with the wrong EngineVoice subtype.
Common situations: Engine-switch race where the UI hands the previous engine's selected voice to the new engine; serialization/deserialization of a Voice that lost its subtype; unit tests using a shared voice fixture across engines.
Related errors
- Voice is not a VibeVoice
- Voice is not a AllTalkVoice
- Voice is not an AzureVoice
- Voice is not a ChatterboxVoice
- Voice is not a CosyVoice3Voice
AI-assisted analysis of SubtitleEdit/subtitleedit@17a9f07487 (2026-08-13).
Data as JSON: /api/errors/6df7fefb57ca2ed4.
Report an issue: GitHub.