1use crate::{Qwen3TtsError, SynthesizeRequest, VoiceClonePromptV2};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum QtsMode {
6 #[default]
7 Base,
8 CustomVoice,
9 VoiceDesign,
10}
11
12impl QtsMode {
13 pub fn as_str(self) -> &'static str {
14 match self {
15 Self::Base => "base",
16 Self::CustomVoice => "customvoice",
17 Self::VoiceDesign => "voicedesign",
18 }
19 }
20}
21
22#[allow(non_camel_case_types)]
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum PresetSpeaker {
26 Vivian,
27 Serena,
28 Uncle_Fu,
29 Dylan,
30 Eric,
31 Ryan,
32 Aiden,
33 Ono_Anna,
34 Sohee,
35}
36
37impl PresetSpeaker {
38 pub const ALL: [Self; 9] = [
39 Self::Vivian,
40 Self::Serena,
41 Self::Uncle_Fu,
42 Self::Dylan,
43 Self::Eric,
44 Self::Ryan,
45 Self::Aiden,
46 Self::Ono_Anna,
47 Self::Sohee,
48 ];
49
50 pub fn as_str(self) -> &'static str {
51 match self {
52 Self::Vivian => "vivian",
53 Self::Serena => "serena",
54 Self::Uncle_Fu => "uncle_fu",
55 Self::Dylan => "dylan",
56 Self::Eric => "eric",
57 Self::Ryan => "ryan",
58 Self::Aiden => "aiden",
59 Self::Ono_Anna => "ono_anna",
60 Self::Sohee => "sohee",
61 }
62 }
63}
64
65#[derive(Debug, Clone, Default)]
67pub struct ModeSynthesizeRequest<'a> {
68 pub synthesis: SynthesizeRequest,
69 pub mode: QtsMode,
70 pub instruct: Option<String>,
71 pub speaker: Option<PresetSpeaker>,
72 pub voice_clone_prompt: Option<&'a VoiceClonePromptV2>,
73 pub speaker_embedding: Option<&'a [f32]>,
75}
76
77impl ModeSynthesizeRequest<'_> {
78 pub fn validate(&self) -> Result<(), Qwen3TtsError> {
79 let error = |message: &str| Err(Qwen3TtsError::InvalidInput(message.into()));
80 match self.mode {
81 QtsMode::Base => {
82 if self.voice_clone_prompt.is_none() {
83 return error("Base needs a voice-clone prompt");
84 }
85 if self.speaker.is_some() || self.instruct.is_some() {
86 return error("Base does not accept preset speaker or instruct");
87 }
88 }
89 QtsMode::CustomVoice => {
90 if self.speaker.is_none() {
91 return error("CustomVoice needs a preset speaker");
92 }
93 if self.voice_clone_prompt.is_some() || self.speaker_embedding.is_some() {
94 return error(
95 "CustomVoice does not accept voice-clone prompt or speaker embedding",
96 );
97 }
98 }
99 QtsMode::VoiceDesign => {
100 if self
101 .instruct
102 .as_ref()
103 .is_none_or(|text| text.trim().is_empty())
104 {
105 return error("VoiceDesign needs nonempty instruct");
106 }
107 if self.speaker.is_some()
108 || self.voice_clone_prompt.is_some()
109 || self.speaker_embedding.is_some()
110 {
111 return error("VoiceDesign does not accept speaker, speaker embedding or voice-clone prompt");
112 }
113 }
114 }
115 if let Some(prompt) = self.voice_clone_prompt {
116 prompt.validate()?;
117 }
118 Ok(())
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::TensorF32;
126
127 fn clone_prompt() -> VoiceClonePromptV2 {
128 VoiceClonePromptV2 {
129 schema_version: 2,
130 x_vector_only_mode: true,
131 ref_spk_embedding: Some(TensorF32 {
132 shape: vec![4],
133 values: vec![0.0; 4],
134 }),
135 ..Default::default()
136 }
137 }
138
139 #[test]
140 fn valid_mode_combinations() {
141 let prompt = clone_prompt();
142 ModeSynthesizeRequest {
143 voice_clone_prompt: Some(&prompt),
144 speaker_embedding: Some(&[1.0; 4]),
145 ..Default::default()
146 }
147 .validate()
148 .unwrap();
149 ModeSynthesizeRequest {
150 mode: QtsMode::CustomVoice,
151 speaker: Some(PresetSpeaker::Vivian),
152 instruct: Some("hi".into()),
153 ..Default::default()
154 }
155 .validate()
156 .unwrap();
157 ModeSynthesizeRequest {
158 mode: QtsMode::VoiceDesign,
159 instruct: Some("hi".into()),
160 ..Default::default()
161 }
162 .validate()
163 .unwrap();
164 }
165
166 #[test]
167 fn missing_required_mode_inputs() {
168 for (mode, expected) in [
169 (QtsMode::Base, "Base needs a voice-clone prompt"),
170 (QtsMode::CustomVoice, "CustomVoice needs a preset speaker"),
171 (QtsMode::VoiceDesign, "VoiceDesign needs nonempty instruct"),
172 ] {
173 let error = ModeSynthesizeRequest {
174 mode,
175 ..Default::default()
176 }
177 .validate()
178 .unwrap_err();
179 assert!(error.to_string().contains(expected));
180 }
181 assert!(ModeSynthesizeRequest {
182 mode: QtsMode::VoiceDesign,
183 instruct: Some(" ".into()),
184 ..Default::default()
185 }
186 .validate()
187 .is_err());
188 }
189
190 #[test]
191 fn incompatible_conditioning_is_rejected() {
192 let prompt = clone_prompt();
193 for req in [
194 ModeSynthesizeRequest {
195 voice_clone_prompt: Some(&prompt),
196 speaker: Some(PresetSpeaker::Ryan),
197 ..Default::default()
198 },
199 ModeSynthesizeRequest {
200 voice_clone_prompt: Some(&prompt),
201 instruct: Some("hi".into()),
202 ..Default::default()
203 },
204 ModeSynthesizeRequest {
205 mode: QtsMode::CustomVoice,
206 speaker: Some(PresetSpeaker::Ryan),
207 voice_clone_prompt: Some(&prompt),
208 ..Default::default()
209 },
210 ModeSynthesizeRequest {
211 mode: QtsMode::CustomVoice,
212 speaker: Some(PresetSpeaker::Ryan),
213 speaker_embedding: Some(&[0.0; 4]),
214 ..Default::default()
215 },
216 ModeSynthesizeRequest {
217 mode: QtsMode::VoiceDesign,
218 instruct: Some("hi".into()),
219 speaker: Some(PresetSpeaker::Ryan),
220 ..Default::default()
221 },
222 ModeSynthesizeRequest {
223 mode: QtsMode::VoiceDesign,
224 instruct: Some("hi".into()),
225 speaker_embedding: Some(&[0.0; 4]),
226 ..Default::default()
227 },
228 ModeSynthesizeRequest {
229 mode: QtsMode::VoiceDesign,
230 instruct: Some("hi".into()),
231 voice_clone_prompt: Some(&prompt),
232 ..Default::default()
233 },
234 ] {
235 assert!(req.validate().is_err());
236 }
237 }
238}