1use crate::{
4 core::VoiceConverter,
5 types::{
6 ConversionRequest, ConversionResult, ConversionTarget, ConversionType, VoiceCharacteristics,
7 },
8 Error, Result,
9};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::sync::Arc;
13use std::time::{Duration, SystemTime};
14use tokio::sync::RwLock;
15use tracing::{debug, info, warn};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct MultiTargetConversionRequest {
20 pub id: String,
22 pub source_audio: Vec<f32>,
24 pub source_sample_rate: u32,
26 pub conversion_type: ConversionType,
28 pub targets: Vec<NamedTarget>,
30 pub realtime: bool,
32 pub quality_level: f32,
34 pub parameters: HashMap<String, f32>,
36 pub timestamp: SystemTime,
38}
39
40impl MultiTargetConversionRequest {
41 pub fn new(
43 id: String,
44 source_audio: Vec<f32>,
45 source_sample_rate: u32,
46 conversion_type: ConversionType,
47 targets: Vec<NamedTarget>,
48 ) -> Self {
49 Self {
50 id,
51 source_audio,
52 source_sample_rate,
53 conversion_type,
54 targets,
55 realtime: false,
56 quality_level: 0.8,
57 parameters: HashMap::new(),
58 timestamp: SystemTime::now(),
59 }
60 }
61
62 pub fn with_realtime(mut self, realtime: bool) -> Self {
64 self.realtime = realtime;
65 self
66 }
67
68 pub fn with_quality_level(mut self, level: f32) -> Self {
70 self.quality_level = level.clamp(0.0, 1.0);
71 self
72 }
73
74 pub fn with_parameter(mut self, key: String, value: f32) -> Self {
76 self.parameters.insert(key, value);
77 self
78 }
79
80 pub fn add_target(mut self, target: NamedTarget) -> Self {
82 self.targets.push(target);
83 self
84 }
85
86 pub fn validate(&self) -> Result<()> {
88 if self.source_audio.is_empty() {
89 return Err(Error::validation(
90 "Source audio cannot be empty".to_string(),
91 ));
92 }
93
94 if self.source_sample_rate == 0 {
95 return Err(Error::validation(
96 "Source sample rate must be positive".to_string(),
97 ));
98 }
99
100 if self.targets.is_empty() {
101 return Err(Error::validation(
102 "At least one target must be specified".to_string(),
103 ));
104 }
105
106 if self.targets.len() > 10 {
107 return Err(Error::validation(
108 "Maximum 10 targets supported for multi-target conversion".to_string(),
109 ));
110 }
111
112 if self.realtime && !self.conversion_type.supports_realtime() {
113 return Err(Error::validation(format!(
114 "Conversion type {:?} does not support real-time processing",
115 self.conversion_type
116 )));
117 }
118
119 for (i, target) in self.targets.iter().enumerate() {
121 if target.name.is_empty() {
122 return Err(Error::validation(format!(
123 "Target {i} must have a non-empty name"
124 )));
125 }
126 }
127
128 let mut names = std::collections::HashSet::new();
130 for target in &self.targets {
131 if !names.insert(&target.name) {
132 return Err(Error::validation(format!(
133 "Duplicate target name: {}",
134 target.name
135 )));
136 }
137 }
138
139 Ok(())
140 }
141
142 pub fn source_duration(&self) -> f32 {
144 self.source_audio.len() as f32 / self.source_sample_rate as f32
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150pub struct NamedTarget {
151 pub name: String,
153 pub target: ConversionTarget,
155 pub priority: i32,
157 pub custom_params: HashMap<String, f32>,
159}
160
161impl NamedTarget {
162 pub fn new(name: String, target: ConversionTarget) -> Self {
164 Self {
165 name,
166 target,
167 priority: 0,
168 custom_params: HashMap::new(),
169 }
170 }
171
172 pub fn with_priority(mut self, priority: i32) -> Self {
174 self.priority = priority;
175 self
176 }
177
178 pub fn with_custom_param(mut self, key: String, value: f32) -> Self {
180 self.custom_params.insert(key, value);
181 self
182 }
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct MultiTargetConversionResult {
188 pub request_id: String,
190 pub target_results: HashMap<String, ConversionResult>,
192 pub total_processing_time: Duration,
194 pub success: bool,
196 pub error_message: Option<String>,
198 pub timestamp: SystemTime,
200 pub stats: MultiTargetProcessingStats,
202}
203
204impl MultiTargetConversionResult {
205 pub fn success(
207 request_id: String,
208 target_results: HashMap<String, ConversionResult>,
209 total_processing_time: Duration,
210 stats: MultiTargetProcessingStats,
211 ) -> Self {
212 Self {
213 request_id,
214 target_results,
215 total_processing_time,
216 success: true,
217 error_message: None,
218 timestamp: SystemTime::now(),
219 stats,
220 }
221 }
222
223 pub fn failure(request_id: String, error_message: String) -> Self {
225 Self {
226 request_id,
227 target_results: HashMap::new(),
228 total_processing_time: Duration::from_millis(0),
229 success: false,
230 error_message: Some(error_message),
231 timestamp: SystemTime::now(),
232 stats: MultiTargetProcessingStats::default(),
233 }
234 }
235
236 pub fn get_target_result(&self, target_name: &str) -> Option<&ConversionResult> {
238 self.target_results.get(target_name)
239 }
240
241 pub fn successful_results(&self) -> HashMap<String, &ConversionResult> {
243 self.target_results
244 .iter()
245 .filter(|(_, result)| result.success)
246 .map(|(name, result)| (name.clone(), result))
247 .collect()
248 }
249
250 pub fn failed_results(&self) -> HashMap<String, &ConversionResult> {
252 self.target_results
253 .iter()
254 .filter(|(_, result)| !result.success)
255 .map(|(name, result)| (name.clone(), result))
256 .collect()
257 }
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize, Default)]
262pub struct MultiTargetProcessingStats {
263 pub targets_processed: usize,
265 pub successful_conversions: usize,
267 pub failed_conversions: usize,
269 pub average_processing_time: Duration,
271 pub max_processing_time: Duration,
273 pub min_processing_time: Duration,
275 pub parallel_processing: bool,
277 pub peak_memory_usage: usize,
279}
280
281#[derive(Debug)]
283pub struct MultiTargetConverter {
284 converter: Arc<VoiceConverter>,
286 processing_mode: ProcessingMode,
288 max_concurrent_targets: usize,
290 stats: Arc<RwLock<MultiTargetProcessingStats>>,
292}
293
294#[derive(Debug, Clone, PartialEq)]
296pub enum ProcessingMode {
297 Sequential,
299 Parallel,
301 Adaptive,
303}
304
305impl MultiTargetConverter {
306 pub fn new(converter: VoiceConverter) -> Self {
308 Self {
309 converter: Arc::new(converter),
310 processing_mode: ProcessingMode::Adaptive,
311 max_concurrent_targets: 4,
312 stats: Arc::new(RwLock::new(MultiTargetProcessingStats::default())),
313 }
314 }
315
316 pub fn with_processing_mode(mut self, mode: ProcessingMode) -> Self {
318 self.processing_mode = mode;
319 self
320 }
321
322 pub fn with_max_concurrent_targets(mut self, max: usize) -> Self {
324 self.max_concurrent_targets = max.clamp(1, 16); self
326 }
327
328 pub async fn convert_multi_target(
330 &self,
331 request: MultiTargetConversionRequest,
332 ) -> Result<MultiTargetConversionResult> {
333 request.validate()?;
334
335 let start_time = std::time::Instant::now();
336 info!(
337 "Starting multi-target conversion for request: {} with {} targets",
338 request.id,
339 request.targets.len()
340 );
341
342 let processing_mode = self.determine_processing_mode(&request);
344 let use_parallel = matches!(processing_mode, ProcessingMode::Parallel);
345
346 let mut stats = MultiTargetProcessingStats {
348 targets_processed: request.targets.len(),
349 parallel_processing: use_parallel,
350 ..Default::default()
351 };
352
353 let mut sorted_targets = request.targets.clone();
355 sorted_targets.sort_by(|a, b| b.priority.cmp(&a.priority));
356
357 let target_results = if use_parallel {
359 self.process_targets_parallel(&request, &sorted_targets)
360 .await?
361 } else {
362 self.process_targets_sequential(&request, &sorted_targets)
363 .await?
364 };
365
366 let total_processing_time = start_time.elapsed();
367
368 let successful_count = target_results.values().filter(|r| r.success).count();
370 let failed_count = target_results.len() - successful_count;
371
372 let processing_times: Vec<Duration> =
373 target_results.values().map(|r| r.processing_time).collect();
374
375 stats.successful_conversions = successful_count;
376 stats.failed_conversions = failed_count;
377 stats.average_processing_time = if !processing_times.is_empty() {
378 let total_nanos = processing_times.iter().map(|d| d.as_nanos()).sum::<u128>();
379 let avg_nanos = total_nanos / processing_times.len() as u128;
380 Duration::from_nanos(avg_nanos.min(u64::MAX as u128) as u64)
381 } else {
382 Duration::from_millis(0)
383 };
384 stats.max_processing_time = processing_times.iter().max().copied().unwrap_or_default();
385 stats.min_processing_time = processing_times.iter().min().copied().unwrap_or_default();
386
387 stats.peak_memory_usage = self.estimate_memory_usage(&request, use_parallel);
389
390 {
392 let mut global_stats = self.stats.write().await;
393 *global_stats = stats.clone();
394 }
395
396 info!(
397 "Multi-target conversion completed for request: {} in {:?} - {}/{} targets successful",
398 request.id,
399 total_processing_time,
400 successful_count,
401 request.targets.len()
402 );
403
404 Ok(MultiTargetConversionResult::success(
405 request.id,
406 target_results,
407 total_processing_time,
408 stats,
409 ))
410 }
411
412 fn determine_processing_mode(&self, request: &MultiTargetConversionRequest) -> ProcessingMode {
414 match self.processing_mode {
415 ProcessingMode::Sequential => ProcessingMode::Sequential,
416 ProcessingMode::Parallel => ProcessingMode::Parallel,
417 ProcessingMode::Adaptive => {
418 let target_count = request.targets.len();
420 let audio_duration = request.source_duration();
421 let is_realtime = request.realtime;
422
423 if is_realtime || target_count <= 2 || audio_duration > 30.0 {
424 ProcessingMode::Sequential
426 } else {
427 ProcessingMode::Parallel
429 }
430 }
431 }
432 }
433
434 async fn process_targets_parallel(
436 &self,
437 request: &MultiTargetConversionRequest,
438 targets: &[NamedTarget],
439 ) -> Result<HashMap<String, ConversionResult>> {
440 debug!("Processing {} targets in parallel", targets.len());
441
442 let mut target_results = HashMap::new();
443 let semaphore = Arc::new(tokio::sync::Semaphore::new(self.max_concurrent_targets));
444
445 let mut handles = Vec::new();
447 for target in targets {
448 let converter = Arc::clone(&self.converter);
449 let target = target.clone();
450 let request = request.clone();
451 let semaphore = Arc::clone(&semaphore);
452
453 let handle = tokio::spawn(async move {
454 let _permit = semaphore.acquire().await.expect("operation should succeed");
455 let conversion_request = Self::create_single_conversion_request(&request, &target);
456 let result = converter.convert(conversion_request).await;
457 (target.name.clone(), result)
458 });
459
460 handles.push(handle);
461 }
462
463 for handle in handles {
465 match handle.await {
466 Ok((target_name, result)) => match result {
467 Ok(conversion_result) => {
468 target_results.insert(target_name, conversion_result);
469 }
470 Err(e) => {
471 warn!("Conversion failed for target {}: {}", target_name, e);
472 let failed_result = ConversionResult {
474 request_id: request.id.clone(),
475 converted_audio: vec![],
476 output_sample_rate: request.source_sample_rate,
477 quality_metrics: HashMap::new(),
478 artifacts: None,
479 objective_quality: None,
480 processing_time: Duration::from_millis(0),
481 conversion_type: request.conversion_type.clone(),
482 success: false,
483 error_message: Some(e.to_string()),
484 timestamp: SystemTime::now(),
485 };
486 target_results.insert(target_name, failed_result);
487 }
488 },
489 Err(e) => {
490 warn!("Task failed for target: {}", e);
491 }
492 }
493 }
494
495 Ok(target_results)
496 }
497
498 async fn process_targets_sequential(
500 &self,
501 request: &MultiTargetConversionRequest,
502 targets: &[NamedTarget],
503 ) -> Result<HashMap<String, ConversionResult>> {
504 debug!("Processing {} targets sequentially", targets.len());
505
506 let mut target_results = HashMap::new();
507
508 for target in targets {
509 let conversion_request = Self::create_single_conversion_request(request, target);
510
511 match self.converter.convert(conversion_request).await {
512 Ok(result) => {
513 target_results.insert(target.name.clone(), result);
514 }
515 Err(e) => {
516 warn!("Conversion failed for target {}: {}", target.name, e);
517 let failed_result = ConversionResult {
519 request_id: request.id.clone(),
520 converted_audio: vec![],
521 output_sample_rate: request.source_sample_rate,
522 quality_metrics: HashMap::new(),
523 artifacts: None,
524 objective_quality: None,
525 processing_time: Duration::from_millis(0),
526 conversion_type: request.conversion_type.clone(),
527 success: false,
528 error_message: Some(e.to_string()),
529 timestamp: SystemTime::now(),
530 };
531 target_results.insert(target.name.clone(), failed_result);
532 }
533 }
534 }
535
536 Ok(target_results)
537 }
538
539 fn create_single_conversion_request(
541 request: &MultiTargetConversionRequest,
542 named_target: &NamedTarget,
543 ) -> ConversionRequest {
544 let mut single_request = ConversionRequest::new(
545 format!("{}_{}", request.id, named_target.name),
546 request.source_audio.clone(),
547 request.source_sample_rate,
548 request.conversion_type.clone(),
549 named_target.target.clone(),
550 )
551 .with_realtime(request.realtime)
552 .with_quality_level(request.quality_level);
553
554 for (key, value) in &request.parameters {
556 single_request = single_request.with_parameter(key.clone(), *value);
557 }
558
559 for (key, value) in &named_target.custom_params {
561 single_request = single_request.with_parameter(key.clone(), *value);
562 }
563
564 single_request
565 }
566
567 fn estimate_memory_usage(
569 &self,
570 request: &MultiTargetConversionRequest,
571 parallel: bool,
572 ) -> usize {
573 let audio_size = request.source_audio.len() * std::mem::size_of::<f32>();
574 let base_memory = audio_size * 2; if parallel {
577 base_memory * request.targets.len() } else {
579 base_memory * 2 }
581 }
582
583 pub async fn get_stats(&self) -> MultiTargetProcessingStats {
585 self.stats.read().await.clone()
586 }
587
588 pub async fn reset_stats(&self) {
590 let mut stats = self.stats.write().await;
591 *stats = MultiTargetProcessingStats::default();
592 }
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598 use crate::{
599 config::ConversionConfig,
600 types::{AgeGroup, ConversionTarget, Gender, VoiceCharacteristics},
601 };
602
603 fn create_test_converter() -> MultiTargetConverter {
604 let config = ConversionConfig::default();
605 let converter = VoiceConverter::with_config(config).unwrap();
606 MultiTargetConverter::new(converter)
607 }
608
609 fn create_test_audio() -> Vec<f32> {
610 vec![0.1, -0.1, 0.2, -0.2, 0.15, -0.15, 0.05, -0.05]
611 }
612
613 #[tokio::test]
614 async fn test_multi_target_conversion_request_creation() {
615 let audio = create_test_audio();
616 let target1 = NamedTarget::new(
617 "target1".to_string(),
618 ConversionTarget::new(VoiceCharacteristics::for_gender(Gender::Male)),
619 );
620 let target2 = NamedTarget::new(
621 "target2".to_string(),
622 ConversionTarget::new(VoiceCharacteristics::for_age(AgeGroup::Senior)),
623 );
624
625 let request = MultiTargetConversionRequest::new(
626 "test_multi".to_string(),
627 audio,
628 22050,
629 ConversionType::GenderTransformation,
630 vec![target1, target2],
631 );
632
633 assert_eq!(request.targets.len(), 2);
634 assert_eq!(
635 request.conversion_type,
636 ConversionType::GenderTransformation
637 );
638 assert!(request.validate().is_ok());
639 }
640
641 #[tokio::test]
642 async fn test_multi_target_conversion_validation() {
643 let audio = create_test_audio();
644
645 let empty_request = MultiTargetConversionRequest::new(
647 "test_empty".to_string(),
648 audio.clone(),
649 22050,
650 ConversionType::PitchShift,
651 vec![],
652 );
653 assert!(empty_request.validate().is_err());
654
655 let mut many_targets = Vec::new();
657 for i in 0..12 {
658 many_targets.push(NamedTarget::new(
659 format!("target_{}", i),
660 ConversionTarget::new(VoiceCharacteristics::default()),
661 ));
662 }
663 let many_request = MultiTargetConversionRequest::new(
664 "test_many".to_string(),
665 audio.clone(),
666 22050,
667 ConversionType::PitchShift,
668 many_targets,
669 );
670 assert!(many_request.validate().is_err());
671
672 let target1 = NamedTarget::new(
674 "same_name".to_string(),
675 ConversionTarget::new(VoiceCharacteristics::default()),
676 );
677 let target2 = NamedTarget::new(
678 "same_name".to_string(),
679 ConversionTarget::new(VoiceCharacteristics::default()),
680 );
681 let duplicate_request = MultiTargetConversionRequest::new(
682 "test_duplicate".to_string(),
683 audio,
684 22050,
685 ConversionType::PitchShift,
686 vec![target1, target2],
687 );
688 assert!(duplicate_request.validate().is_err());
689 }
690
691 #[tokio::test]
692 async fn test_named_target_creation() {
693 let characteristics = VoiceCharacteristics::for_gender(Gender::Female);
694 let target = ConversionTarget::new(characteristics);
695
696 let named_target = NamedTarget::new("female_voice".to_string(), target)
697 .with_priority(5)
698 .with_custom_param("strength".to_string(), 0.8);
699
700 assert_eq!(named_target.name, "female_voice");
701 assert_eq!(named_target.priority, 5);
702 assert_eq!(named_target.custom_params.get("strength"), Some(&0.8));
703 }
704
705 #[tokio::test]
706 async fn test_multi_target_converter_sequential() {
707 let converter = create_test_converter().with_processing_mode(ProcessingMode::Sequential);
708
709 let audio = create_test_audio();
710 let target1 = NamedTarget::new(
711 "pitch_high".to_string(),
712 ConversionTarget::new(VoiceCharacteristics::for_gender(Gender::Female)),
713 );
714 let target2 = NamedTarget::new(
715 "pitch_low".to_string(),
716 ConversionTarget::new(VoiceCharacteristics::for_gender(Gender::Male)),
717 );
718
719 let request = MultiTargetConversionRequest::new(
720 "test_sequential".to_string(),
721 audio,
722 22050,
723 ConversionType::GenderTransformation,
724 vec![target1, target2],
725 );
726
727 let result = converter.convert_multi_target(request).await;
728 assert!(result.is_ok());
729
730 let result = result.unwrap();
731 assert!(result.success);
732 assert_eq!(result.target_results.len(), 2);
733 assert!(result.target_results.contains_key("pitch_high"));
734 assert!(result.target_results.contains_key("pitch_low"));
735 assert!(!result.stats.parallel_processing);
736 }
737
738 #[tokio::test]
739 async fn test_multi_target_converter_parallel() {
740 let converter = create_test_converter()
741 .with_processing_mode(ProcessingMode::Parallel)
742 .with_max_concurrent_targets(2);
743
744 let audio = create_test_audio();
745 let target1 = NamedTarget::new(
746 "speed_fast".to_string(),
747 ConversionTarget::new(VoiceCharacteristics::default()),
748 );
749 let target2 = NamedTarget::new(
750 "speed_slow".to_string(),
751 ConversionTarget::new(VoiceCharacteristics::default()),
752 );
753
754 let request = MultiTargetConversionRequest::new(
755 "test_parallel".to_string(),
756 audio,
757 22050,
758 ConversionType::SpeedTransformation,
759 vec![target1, target2],
760 );
761
762 let result = converter.convert_multi_target(request).await;
763 assert!(result.is_ok());
764
765 let result = result.unwrap();
766 assert!(result.success);
767 assert_eq!(result.target_results.len(), 2);
768 assert!(result.stats.parallel_processing);
769 }
770
771 #[tokio::test]
772 async fn test_target_priority_ordering() {
773 let converter = create_test_converter().with_processing_mode(ProcessingMode::Sequential);
774
775 let audio = create_test_audio();
776 let target1 = NamedTarget::new(
777 "low_priority".to_string(),
778 ConversionTarget::new(VoiceCharacteristics::default()),
779 )
780 .with_priority(1);
781
782 let target2 = NamedTarget::new(
783 "high_priority".to_string(),
784 ConversionTarget::new(VoiceCharacteristics::default()),
785 )
786 .with_priority(10);
787
788 let request = MultiTargetConversionRequest::new(
789 "test_priority".to_string(),
790 audio,
791 22050,
792 ConversionType::PitchShift,
793 vec![target1, target2], );
795
796 let result = converter.convert_multi_target(request).await;
797 assert!(result.is_ok());
798
799 let result = result.unwrap();
800 assert!(result.success);
801 assert_eq!(result.target_results.len(), 2);
802
803 assert!(result.target_results.get("low_priority").unwrap().success);
805 assert!(result.target_results.get("high_priority").unwrap().success);
806 }
807
808 #[tokio::test]
809 async fn test_adaptive_processing_mode() {
810 let converter = create_test_converter().with_processing_mode(ProcessingMode::Adaptive);
811
812 let audio = create_test_audio();
813
814 let target1 = NamedTarget::new(
816 "target1".to_string(),
817 ConversionTarget::new(VoiceCharacteristics::default()),
818 );
819 let request_few = MultiTargetConversionRequest::new(
820 "test_adaptive_few".to_string(),
821 audio.clone(),
822 22050,
823 ConversionType::PitchShift,
824 vec![target1],
825 );
826
827 let result = converter.convert_multi_target(request_few).await;
828 assert!(result.is_ok());
829 let result = result.unwrap();
830 assert!(!result.stats.parallel_processing); let targets: Vec<NamedTarget> = (0..4)
834 .map(|i| {
835 NamedTarget::new(
836 format!("target_{}", i),
837 ConversionTarget::new(VoiceCharacteristics::default()),
838 )
839 })
840 .collect();
841
842 let request_many = MultiTargetConversionRequest::new(
843 "test_adaptive_many".to_string(),
844 audio,
845 22050,
846 ConversionType::PitchShift,
847 targets,
848 );
849
850 let result = converter.convert_multi_target(request_many).await;
851 assert!(result.is_ok());
852 let result = result.unwrap();
853 assert!(result.stats.parallel_processing); }
855
856 #[tokio::test]
857 async fn test_conversion_result_filtering() {
858 let converter = create_test_converter();
859
860 let audio = create_test_audio();
861 let target1 = NamedTarget::new(
862 "valid_target".to_string(),
863 ConversionTarget::new(VoiceCharacteristics::default()),
864 );
865
866 let request = MultiTargetConversionRequest::new(
867 "test_filtering".to_string(),
868 audio,
869 22050,
870 ConversionType::PitchShift,
871 vec![target1],
872 );
873
874 let result = converter.convert_multi_target(request).await;
875 assert!(result.is_ok());
876
877 let result = result.unwrap();
878 assert!(result.success);
879
880 let successful = result.successful_results();
881 let failed = result.failed_results();
882
883 assert_eq!(successful.len(), 1);
884 assert_eq!(failed.len(), 0);
885 assert!(successful.contains_key("valid_target"));
886 }
887
888 #[tokio::test]
889 async fn test_converter_statistics() {
890 let converter = create_test_converter();
891
892 let initial_stats = converter.get_stats().await;
894 assert_eq!(initial_stats.targets_processed, 0);
895
896 let audio = create_test_audio();
897 let targets: Vec<NamedTarget> = (0..3)
898 .map(|i| {
899 NamedTarget::new(
900 format!("target_{}", i),
901 ConversionTarget::new(VoiceCharacteristics::default()),
902 )
903 })
904 .collect();
905
906 let request = MultiTargetConversionRequest::new(
907 "test_stats".to_string(),
908 audio,
909 22050,
910 ConversionType::PitchShift,
911 targets,
912 );
913
914 let _result = converter.convert_multi_target(request).await.unwrap();
915
916 let final_stats = converter.get_stats().await;
918 assert_eq!(final_stats.targets_processed, 3);
919 assert_eq!(final_stats.successful_conversions, 3);
920 assert_eq!(final_stats.failed_conversions, 0);
921 assert!(final_stats.average_processing_time > Duration::from_millis(0));
922 }
923}