{"record":{"id":"d10348054c617e66","repo":"SubtitleEdit/subtitleedit","slug":"voice-is-not-an-indexttsvoice","errorCode":null,"errorMessage":"Voice is not an IndexTtsVoice","messagePattern":"Voice is not an IndexTtsVoice","errorType":"exception","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/ui/Features/Video/TextToSpeech/Engines/IndexTtsCrispAsr.cs","lineNumber":352,"sourceCode":"    public Task<string[]> GetModels() => Task.FromResult(new[] { ModelKeyQ4K, ModelKeyQ8_0, ModelKeyF16 });\n\n    public Task<TtsLanguage[]> GetLanguages(Voice voice, string? model) => Task.FromResult(Array.Empty<TtsLanguage>());\n\n    public Task<Voice[]> RefreshVoices(string language, CancellationToken cancellationToken) =>\n        GetVoices(language);\n\n    public async Task<TtsResult> Speak(\n        string text,\n        string outputFolder,\n        Voice voice,\n        TtsLanguage? language,\n        string? region,\n        string? model,\n        CancellationToken cancellationToken)\n    {\n        if (voice.EngineVoice is not IndexTtsVoice indexVoice)\n        {\n            throw new ArgumentException(\"Voice is not an IndexTtsVoice\");\n        }\n\n        if (string.IsNullOrEmpty(indexVoice.FilePath))\n        {\n            throw new InvalidOperationException(\n                \"IndexTTS (CrispASR) requires a reference voice WAV. \"\n                + \"Import one via the voice settings, then pick it in the voice combo. \"\n                + \"Reference WAV should be 24 kHz mono (3-10 s of clean speech).\");\n        }\n\n        var modelKey = ResolveModelKey(model);\n        await EnsureServerRunningAsync(modelKey, indexVoice.FilePath, cancellationToken);\n\n        var outputFileName = Path.Combine(TtsOutputFolder.Resolve(outputFolder, GetSetFolder), Guid.NewGuid() + \".wav\");\n        var inputText = text;\n\n        // OpenAI-compatible /v1/audio/speech payload. CrispASR's indextts backend uses:\n        //   - `input`             — the text to synthesise","sourceCodeStart":334,"sourceCodeEnd":370,"githubUrl":"https://github.com/SubtitleEdit/subtitleedit/blob/17a9f0748781032255db3526b7215d2fb891e3af/src/ui/Features/Video/TextToSpeech/Engines/IndexTtsCrispAsr.cs#L334-L370","documentation":"Type guard at the top of IndexTtsCrispAsr.Speak: requires `voice.EngineVoice` to be an `IndexTtsVoice`. Throws ArgumentException before any server interaction if a different voice subtype is supplied, preventing a bad cast in the cloning pipeline.","triggerScenarios":"Passing a non-IndexTts Voice (e.g. MossTtsVoice, KokoroVoice) into the IndexTTS engine; a saved project whose selected voice's EngineId no longer maps to IndexTTS; cross-engine voice list merging.","commonSituations":"User switched from MOSS-TTS to IndexTTS without re-selecting a voice; an import assigned the wrong concrete voice type; engine registry mismatch after a refactor.","solutions":["Obtain the Voice from IndexTtsCrispAsr.GetVoices() so EngineVoice is an IndexTtsVoice.","Re-select the voice in the UI combo after switching to the IndexTTS engine.","If constructing in code, set `voice.EngineVoice = new IndexTtsVoice { FilePath = \"...\" }`."],"exampleFix":"// before\nvar voice = new Voice { EngineVoice = new MossTtsVoice() };\nawait indexEngine.Speak(text, out, voice, ...);\n\n// after\nvar voice = new Voice { EngineVoice = new IndexTtsVoice { FilePath = refWavPath } };\nawait indexEngine.Speak(text, out, voice, ...);","handlingStrategy":"type-guard","validationCode":"if (voice.EngineVoice is not IndexTtsVoice)\n{\n    throw new InvalidOperationException($\"Refusing IndexTTS speak with a {voice.EngineVoice?.GetType().Name} voice.\");\n}","typeGuard":"static bool IsIndexTtsVoice(Voice v) => v.EngineVoice is IndexTtsVoice;\n\nif (!IsIndexTtsVoice(voice)) { /* re-pick an IndexTTS voice */ return; }","tryCatchPattern":"try { await indexEngine.Speak(...); }\ncatch (ArgumentException ex) when (ex.Message.Contains(\"not an IndexTtsVoice\"))\n{\n    ClearVoiceSelection(); // engine/voice mismatch\n}","preventionTips":["Source Voice lists only from IndexTtsCrispAsr.GetVoices().","Re-select the voice after switching into the IndexTTS engine.","Persist the concrete voice subtype alongside EngineId.","Guard at the UI layer so a wrong-type voice never reaches Speak."],"tags":["indextts","crispasr","type-guard","voice","tts","validation"],"backgroundTag":null,"analyzedSha":"17a9f0748781032255db3526b7215d2fb891e3af","analyzedAt":"2026-08-13T18:11:43.374Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}