Skip to main content

voirs_conversion/
multi_target.rs

1//! Multi-target voice conversion functionality
2
3use 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/// Multi-target conversion request
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct MultiTargetConversionRequest {
20    /// Request ID
21    pub id: String,
22    /// Source audio data
23    pub source_audio: Vec<f32>,
24    /// Source sample rate
25    pub source_sample_rate: u32,
26    /// Conversion type to apply to all targets
27    pub conversion_type: ConversionType,
28    /// Multiple conversion targets
29    pub targets: Vec<NamedTarget>,
30    /// Real-time processing flag
31    pub realtime: bool,
32    /// Quality level (0.0 to 1.0)
33    pub quality_level: f32,
34    /// Processing parameters
35    pub parameters: HashMap<String, f32>,
36    /// Request timestamp
37    pub timestamp: SystemTime,
38}
39
40impl MultiTargetConversionRequest {
41    /// Create new multi-target conversion request
42    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    /// Enable real-time processing
63    pub fn with_realtime(mut self, realtime: bool) -> Self {
64        self.realtime = realtime;
65        self
66    }
67
68    /// Set quality level
69    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    /// Add parameter
75    pub fn with_parameter(mut self, key: String, value: f32) -> Self {
76        self.parameters.insert(key, value);
77        self
78    }
79
80    /// Add a target
81    pub fn add_target(mut self, target: NamedTarget) -> Self {
82        self.targets.push(target);
83        self
84    }
85
86    /// Validate the request
87    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        // Validate each target
120        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        // Check for duplicate target names
129        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    /// Get source duration in seconds
143    pub fn source_duration(&self) -> f32 {
144        self.source_audio.len() as f32 / self.source_sample_rate as f32
145    }
146}
147
148/// Named conversion target for multi-target processing
149#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150pub struct NamedTarget {
151    /// Target name/identifier
152    pub name: String,
153    /// Conversion target specification
154    pub target: ConversionTarget,
155    /// Priority for processing (higher = processed first)
156    pub priority: i32,
157    /// Custom parameters for this specific target
158    pub custom_params: HashMap<String, f32>,
159}
160
161impl NamedTarget {
162    /// Create new named target
163    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    /// Set priority
173    pub fn with_priority(mut self, priority: i32) -> Self {
174        self.priority = priority;
175        self
176    }
177
178    /// Add custom parameter
179    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/// Multi-target conversion result
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct MultiTargetConversionResult {
188    /// Request ID this result corresponds to
189    pub request_id: String,
190    /// Results for each target (keyed by target name)
191    pub target_results: HashMap<String, ConversionResult>,
192    /// Overall processing time
193    pub total_processing_time: Duration,
194    /// Success status
195    pub success: bool,
196    /// Error message if failed
197    pub error_message: Option<String>,
198    /// Result timestamp
199    pub timestamp: SystemTime,
200    /// Processing statistics
201    pub stats: MultiTargetProcessingStats,
202}
203
204impl MultiTargetConversionResult {
205    /// Create successful result
206    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    /// Create failed result
224    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    /// Get result for specific target
237    pub fn get_target_result(&self, target_name: &str) -> Option<&ConversionResult> {
238        self.target_results.get(target_name)
239    }
240
241    /// Get all successful target results
242    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    /// Get all failed target results
251    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/// Processing statistics for multi-target conversion
261#[derive(Debug, Clone, Serialize, Deserialize, Default)]
262pub struct MultiTargetProcessingStats {
263    /// Number of targets processed
264    pub targets_processed: usize,
265    /// Number of successful conversions
266    pub successful_conversions: usize,
267    /// Number of failed conversions
268    pub failed_conversions: usize,
269    /// Average processing time per target
270    pub average_processing_time: Duration,
271    /// Maximum processing time among targets
272    pub max_processing_time: Duration,
273    /// Minimum processing time among targets
274    pub min_processing_time: Duration,
275    /// Whether parallel processing was used
276    pub parallel_processing: bool,
277    /// Memory usage during processing (in bytes)
278    pub peak_memory_usage: usize,
279}
280
281/// Multi-target voice converter
282#[derive(Debug)]
283pub struct MultiTargetConverter {
284    /// Underlying voice converter
285    converter: Arc<VoiceConverter>,
286    /// Processing mode configuration
287    processing_mode: ProcessingMode,
288    /// Maximum concurrent targets
289    max_concurrent_targets: usize,
290    /// Processing statistics
291    stats: Arc<RwLock<MultiTargetProcessingStats>>,
292}
293
294/// Processing mode for multi-target conversion
295#[derive(Debug, Clone, PartialEq)]
296pub enum ProcessingMode {
297    /// Process targets sequentially (lower memory usage)
298    Sequential,
299    /// Process targets in parallel (faster but higher memory usage)
300    Parallel,
301    /// Automatically choose based on number of targets and system resources
302    Adaptive,
303}
304
305impl MultiTargetConverter {
306    /// Create new multi-target converter
307    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    /// Create with custom processing mode
317    pub fn with_processing_mode(mut self, mode: ProcessingMode) -> Self {
318        self.processing_mode = mode;
319        self
320    }
321
322    /// Set maximum concurrent targets for parallel processing
323    pub fn with_max_concurrent_targets(mut self, max: usize) -> Self {
324        self.max_concurrent_targets = max.clamp(1, 16); // Clamp between 1 and 16
325        self
326    }
327
328    /// Convert audio to multiple targets
329    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        // Determine processing mode
343        let processing_mode = self.determine_processing_mode(&request);
344        let use_parallel = matches!(processing_mode, ProcessingMode::Parallel);
345
346        // Initialize statistics
347        let mut stats = MultiTargetProcessingStats {
348            targets_processed: request.targets.len(),
349            parallel_processing: use_parallel,
350            ..Default::default()
351        };
352
353        // Sort targets by priority (higher priority first)
354        let mut sorted_targets = request.targets.clone();
355        sorted_targets.sort_by(|a, b| b.priority.cmp(&a.priority));
356
357        // Process targets based on mode
358        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        // Calculate statistics
369        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        // Estimate memory usage
388        stats.peak_memory_usage = self.estimate_memory_usage(&request, use_parallel);
389
390        // Update global statistics
391        {
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    /// Determine optimal processing mode
413    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                // Use heuristics to decide
419                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                    // Use sequential for real-time, few targets, or long audio
425                    ProcessingMode::Sequential
426                } else {
427                    // Use parallel for multiple targets with shorter audio
428                    ProcessingMode::Parallel
429                }
430            }
431        }
432    }
433
434    /// Process targets in parallel
435    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        // Create futures for each target
446        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        // Wait for all conversions to complete
464        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                        // Create a failed result
473                        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    /// Process targets sequentially
499    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                    // Create a failed result
518                    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    /// Create single conversion request from multi-target request and named target
540    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        // Add global parameters
555        for (key, value) in &request.parameters {
556            single_request = single_request.with_parameter(key.clone(), *value);
557        }
558
559        // Add target-specific parameters (override global ones)
560        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    /// Estimate memory usage for the conversion
568    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; // Input + output audio
575
576        if parallel {
577            base_memory * request.targets.len() // Each target needs its own memory
578        } else {
579            base_memory * 2 // Sequential processing needs less memory
580        }
581    }
582
583    /// Get processing statistics
584    pub async fn get_stats(&self) -> MultiTargetProcessingStats {
585        self.stats.read().await.clone()
586    }
587
588    /// Reset processing statistics
589    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        // Test empty targets
646        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        // Test too many targets
656        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        // Test duplicate target names
673        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], // Add low priority first
794        );
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        // Both targets should succeed regardless of order
804        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        // Test with few targets (should use sequential)
815        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); // Should use sequential
831
832        // Test with multiple targets (should use parallel)
833        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); // Should use parallel
854    }
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        // Get initial stats
893        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        // Check updated stats
917        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}