1use crate::error::{CliError, Result};
4use std::collections::HashMap;
5use voirs_sdk::config::AppConfig;
6use voirs_sdk::types::VoiceConfig;
7use voirs_sdk::{QualityLevel, Result as VoirsResult, VoirsPipeline};
8
9pub struct VoiceSearch {
11 voices: Vec<VoiceConfig>,
13}
14
15impl VoiceSearch {
16 pub async fn new(config: &AppConfig) -> Result<Self> {
18 let pipeline = VoirsPipeline::builder().build().await?;
19 let voices = pipeline.list_voices().await?;
20
21 Ok(Self { voices })
22 }
23
24 pub fn search(&self, query: &str) -> Vec<VoiceSearchResult> {
26 let query_lower = query.to_lowercase();
27 let terms: Vec<&str> = query_lower.split_whitespace().collect();
28
29 let mut results = Vec::new();
30
31 for voice in &self.voices {
32 let score = self.calculate_relevance_score(voice, &terms);
33 if score > 0.0 {
34 results.push(VoiceSearchResult {
35 voice: voice.clone(),
36 relevance_score: score,
37 match_reasons: self.get_match_reasons(voice, &terms),
38 });
39 }
40 }
41
42 results.sort_by(|a, b| b.relevance_score.partial_cmp(&a.relevance_score).unwrap());
44
45 results
46 }
47
48 pub fn search_by_criteria(&self, criteria: &VoiceSearchCriteria) -> Vec<VoiceSearchResult> {
50 let mut results = Vec::new();
51
52 for voice in &self.voices {
53 let mut score = 1.0;
54 let mut reasons = Vec::new();
55 let mut matches = true;
56
57 if let Some(ref language) = criteria.language {
59 if voice.language.as_str().to_lowercase() != language.to_lowercase() {
60 matches = false;
61 } else {
62 reasons.push("Matching language".to_string());
63 }
64 }
65
66 if let Some(ref gender) = criteria.gender {
68 if let Some(voice_gender) = &voice.characteristics.gender {
69 if format!("{:?}", voice_gender).to_lowercase() != gender.to_lowercase() {
70 matches = false;
71 } else {
72 reasons.push("Matching gender".to_string());
73 score += 0.5;
74 }
75 } else if criteria.require_gender {
76 matches = false;
77 }
78 }
79
80 if let Some(ref age) = criteria.age {
82 if let Some(voice_age) = &voice.characteristics.age {
83 if format!("{:?}", voice_age).to_lowercase() != age.to_lowercase() {
84 matches = false;
85 } else {
86 reasons.push("Matching age".to_string());
87 score += 0.3;
88 }
89 } else if criteria.require_age {
90 matches = false;
91 }
92 }
93
94 if let Some(ref style) = criteria.style {
96 if format!("{:?}", voice.characteristics.style).to_lowercase()
97 != style.to_lowercase()
98 {
99 matches = false;
100 } else {
101 reasons.push("Matching style".to_string());
102 score += 0.4;
103 }
104 }
105
106 if let Some(ref quality) = criteria.min_quality {
108 let voice_quality_score = match voice.characteristics.quality {
109 QualityLevel::Low => 1,
110 QualityLevel::Medium => 2,
111 QualityLevel::High => 3,
112 QualityLevel::Ultra => 4,
113 };
114 let min_quality_score = match quality.as_str() {
115 "low" => 1,
116 "medium" => 2,
117 "high" => 3,
118 "ultra" => 4,
119 _ => 1,
120 };
121
122 if voice_quality_score < min_quality_score {
123 matches = false;
124 } else if voice_quality_score >= min_quality_score {
125 reasons.push("Meets quality requirements".to_string());
126 score += 0.2;
127 }
128 }
129
130 if criteria.emotion_support {
132 if voice.characteristics.emotion_support {
133 reasons.push("Supports emotions".to_string());
134 score += 0.3;
135 } else if criteria.require_emotion_support {
136 matches = false;
137 }
138 }
139
140 if matches {
141 results.push(VoiceSearchResult {
142 voice: voice.clone(),
143 relevance_score: score,
144 match_reasons: reasons,
145 });
146 }
147 }
148
149 results.sort_by(|a, b| b.relevance_score.partial_cmp(&a.relevance_score).unwrap());
151
152 results
153 }
154
155 pub fn recommend_for_text(&self, text: &str) -> Vec<VoiceSearchResult> {
157 let mut criteria = VoiceSearchCriteria::default();
158
159 let word_count = text.split_whitespace().count();
161 let has_questions = text.contains('?');
162 let has_exclamations = text.contains('!');
163 let is_formal = self.is_formal_text(text);
164
165 if has_exclamations || text.to_uppercase() == text {
167 criteria.style = Some("Energetic".to_string());
168 } else if has_questions || is_formal {
169 criteria.style = Some("Professional".to_string());
170 } else if word_count > 100 {
171 criteria.style = Some("Narrative".to_string());
172 }
173
174 if word_count > 50 {
176 criteria.min_quality = Some("high".to_string());
177 } else {
178 criteria.min_quality = Some("medium".to_string());
179 }
180
181 self.search_by_criteria(&criteria)
182 }
183
184 fn calculate_relevance_score(&self, voice: &VoiceConfig, terms: &[&str]) -> f32 {
186 let mut score = 0.0;
187
188 for term in terms {
189 if voice.id.to_lowercase().contains(term) {
191 score += 3.0;
192 }
193
194 if voice.name.to_lowercase().contains(term) {
196 score += 2.5;
197 }
198
199 if voice.language.as_str().to_lowercase().contains(term) {
201 score += 2.0;
202 }
203
204 if let Some(description) = voice.metadata.get("description") {
206 if description.to_lowercase().contains(term) {
207 score += 1.5;
208 }
209 }
210
211 if let Some(gender) = &voice.characteristics.gender {
213 if format!("{:?}", gender).to_lowercase().contains(term) {
214 score += 1.0;
215 }
216 }
217
218 if let Some(age) = &voice.characteristics.age {
219 if format!("{:?}", age).to_lowercase().contains(term) {
220 score += 1.0;
221 }
222 }
223
224 if format!("{:?}", voice.characteristics.style)
225 .to_lowercase()
226 .contains(term)
227 {
228 score += 1.0;
229 }
230
231 for (key, value) in &voice.metadata {
233 if key.to_lowercase().contains(term) || value.to_lowercase().contains(term) {
234 score += 0.5;
235 }
236 }
237 }
238
239 score
240 }
241
242 fn get_match_reasons(&self, voice: &VoiceConfig, terms: &[&str]) -> Vec<String> {
244 let mut reasons = Vec::new();
245
246 for term in terms {
247 if voice.id.to_lowercase().contains(term) {
248 reasons.push(format!("ID contains '{}'", term));
249 }
250 if voice.name.to_lowercase().contains(term) {
251 reasons.push(format!("Name contains '{}'", term));
252 }
253 if voice.language.as_str().to_lowercase().contains(term) {
254 reasons.push(format!("Language matches '{}'", term));
255 }
256 if let Some(description) = voice.metadata.get("description") {
257 if description.to_lowercase().contains(term) {
258 reasons.push(format!("Description contains '{}'", term));
259 }
260 }
261 }
262
263 if reasons.is_empty() {
264 reasons.push("General match".to_string());
265 }
266
267 reasons
268 }
269
270 fn is_formal_text(&self, text: &str) -> bool {
272 let formal_indicators = [
273 "please",
274 "thank you",
275 "regarding",
276 "furthermore",
277 "however",
278 "therefore",
279 "moreover",
280 "nevertheless",
281 "consequently",
282 "accordingly",
283 ];
284
285 let text_lower = text.to_lowercase();
286 formal_indicators
287 .iter()
288 .any(|&indicator| text_lower.contains(indicator))
289 }
290
291 pub fn get_statistics(&self) -> VoiceStatistics {
293 let mut stats = VoiceStatistics::default();
294
295 stats.total_voices = self.voices.len();
296
297 for voice in &self.voices {
299 let lang = voice.language.as_str();
300 *stats.languages.entry(lang.to_string()).or_insert(0) += 1;
301 }
302
303 for voice in &self.voices {
305 if let Some(gender) = &voice.characteristics.gender {
306 let gender_str = format!("{:?}", gender);
307 *stats.genders.entry(gender_str).or_insert(0) += 1;
308 } else {
309 *stats.genders.entry("Unknown".to_string()).or_insert(0) += 1;
310 }
311 }
312
313 for voice in &self.voices {
315 let quality_str = format!("{:?}", voice.characteristics.quality);
316 *stats.qualities.entry(quality_str).or_insert(0) += 1;
317 }
318
319 stats.emotion_support_count = self
321 .voices
322 .iter()
323 .filter(|v| v.characteristics.emotion_support)
324 .count();
325
326 stats
327 }
328}
329
330#[derive(Debug, Default)]
332pub struct VoiceSearchCriteria {
333 pub language: Option<String>,
334 pub gender: Option<String>,
335 pub age: Option<String>,
336 pub style: Option<String>,
337 pub min_quality: Option<String>,
338 pub emotion_support: bool,
339 pub require_gender: bool,
340 pub require_age: bool,
341 pub require_emotion_support: bool,
342}
343
344#[derive(Debug, Clone)]
346pub struct VoiceSearchResult {
347 pub voice: VoiceConfig,
348 pub relevance_score: f32,
349 pub match_reasons: Vec<String>,
350}
351
352#[derive(Debug, Default)]
354pub struct VoiceStatistics {
355 pub total_voices: usize,
356 pub languages: HashMap<String, usize>,
357 pub genders: HashMap<String, usize>,
358 pub qualities: HashMap<String, usize>,
359 pub emotion_support_count: usize,
360}
361
362#[derive(Debug)]
402pub struct VoiceSearchCommandConfig<'a> {
403 pub query: Option<&'a str>,
405 pub language: Option<&'a str>,
407 pub gender: Option<&'a str>,
409 pub age: Option<&'a str>,
411 pub style: Option<&'a str>,
413 pub min_quality: Option<&'a str>,
415 pub emotion_support: bool,
417 pub show_stats: bool,
419}
420
421pub async fn run_voice_search(
423 search_config: VoiceSearchCommandConfig<'_>,
424 config: &AppConfig,
425) -> Result<()> {
426 let search = VoiceSearch::new(config).await?;
427
428 if search_config.show_stats {
429 print_voice_statistics(&search.get_statistics());
430 return Ok(());
431 }
432
433 let results = if let Some(query) = search_config.query {
434 search.search(query)
435 } else {
436 let criteria = VoiceSearchCriteria {
437 language: search_config.language.map(|s| s.to_string()),
438 gender: search_config.gender.map(|s| s.to_string()),
439 age: search_config.age.map(|s| s.to_string()),
440 style: search_config.style.map(|s| s.to_string()),
441 min_quality: search_config.min_quality.map(|s| s.to_string()),
442 emotion_support: search_config.emotion_support,
443 require_gender: search_config.gender.is_some(),
444 require_age: search_config.age.is_some(),
445 require_emotion_support: search_config.emotion_support,
446 };
447 search.search_by_criteria(&criteria)
448 };
449
450 if results.is_empty() {
451 println!("No voices found matching your criteria.");
452 if let Some(query) = search_config.query {
453 println!("Try using broader search terms or check the available voices with 'voirs voices list'.");
454 }
455 return Ok(());
456 }
457
458 println!("Found {} voice(s):", results.len());
459 println!();
460
461 for (i, result) in results.iter().enumerate().take(10) {
462 print_voice_search_result(i + 1, result);
464 if i < results.len() - 1 {
465 println!("---");
466 }
467 }
468
469 if results.len() > 10 {
470 println!();
471 println!(
472 "... and {} more results. Use more specific search criteria to narrow down.",
473 results.len() - 10
474 );
475 }
476
477 Ok(())
478}
479
480fn print_voice_search_result(index: usize, result: &VoiceSearchResult) {
482 println!("{}. {} ({})", index, result.voice.name, result.voice.id);
483 println!(" Language: {}", result.voice.language.as_str());
484
485 if let Some(gender) = &result.voice.characteristics.gender {
486 println!(" Gender: {:?}", gender);
487 }
488
489 if let Some(age) = &result.voice.characteristics.age {
490 println!(" Age: {:?}", age);
491 }
492
493 println!(" Style: {:?}", result.voice.characteristics.style);
494 println!(" Quality: {:?}", result.voice.characteristics.quality);
495
496 if result.voice.characteristics.emotion_support {
497 println!(" ✓ Emotion support");
498 }
499
500 println!(" Relevance: {:.1}", result.relevance_score);
501 println!(" Matches: {}", result.match_reasons.join(", "));
502
503 if let Some(description) = result.voice.metadata.get("description") {
504 if description.len() <= 100 {
505 println!(" Description: {}", description);
506 } else {
507 println!(" Description: {}...", &description[..97]);
508 }
509 }
510}
511
512fn print_voice_statistics(stats: &VoiceStatistics) {
514 println!("Voice Database Statistics");
515 println!("========================");
516 println!("Total voices: {}", stats.total_voices);
517 println!();
518
519 println!("By Language:");
520 let mut lang_vec: Vec<_> = stats.languages.iter().collect();
521 lang_vec.sort_by_key(|(_, count)| **count);
522 lang_vec.reverse();
523 for (lang, count) in lang_vec {
524 println!(" {}: {}", lang, count);
525 }
526 println!();
527
528 println!("By Gender:");
529 let mut gender_vec: Vec<_> = stats.genders.iter().collect();
530 gender_vec.sort_by_key(|(_, count)| **count);
531 gender_vec.reverse();
532 for (gender, count) in gender_vec {
533 println!(" {}: {}", gender, count);
534 }
535 println!();
536
537 println!("By Quality:");
538 let mut quality_vec: Vec<_> = stats.qualities.iter().collect();
539 quality_vec.sort_by_key(|(_, count)| **count);
540 quality_vec.reverse();
541 for (quality, count) in quality_vec {
542 println!(" {}: {}", quality, count);
543 }
544 println!();
545
546 println!(
547 "Emotion Support: {} voices ({:.1}%)",
548 stats.emotion_support_count,
549 (stats.emotion_support_count as f32 / stats.total_voices as f32) * 100.0
550 );
551}