Skip to main content

voirs_singing/
adaptive_learning.rs

1//! # Adaptive Learning System
2//!
3//! This module implements a comprehensive adaptive learning system that enables continuous
4//! improvement of singing synthesis quality through user feedback and preference learning.
5//!
6//! ## Features
7//!
8//! - **User Feedback Learning**: Collect and learn from user ratings and corrections
9//! - **Preference Learning**: Adapt to individual user preferences over time
10//! - **Quality Metric Fine-tuning**: Automatically adjust quality metrics based on feedback
11//! - **Style Adaptation**: Learn and adapt to user-preferred singing styles
12//! - **Automatic Model Improvement**: Continuously improve synthesis models
13//! - **Personalized Voice Characteristics**: Develop personalized voice profiles
14//!
15//! ## Example
16//!
17//! ```rust,ignore
18//! use voirs_singing::adaptive_learning::*;
19//!
20//! // Create adaptive learning system
21//! let config = AdaptiveLearningConfig::default();
22//! let mut system = AdaptiveLearningSystem::new(config);
23//!
24//! // Collect user feedback
25//! let feedback = UserFeedback::new(
26//!     "user123",
27//!     sample_audio,
28//!     4.5,  // rating out of 5
29//!     Some("Great vibrato, but pitch could be more accurate"),
30//! );
31//! system.add_feedback(feedback).await?;
32//!
33//! // Get personalized recommendations
34//! let recommendations = system.get_recommendations("user123").await?;
35//! ```
36
37use crate::synthesis::SynthesisResult;
38use crate::types::{Expression, VoiceCharacteristics};
39use crate::{Error, Result};
40use chrono::{DateTime, Utc};
41use serde::{Deserialize, Serialize};
42use std::collections::HashMap;
43use std::sync::Arc;
44use tokio::sync::RwLock;
45use uuid::Uuid;
46
47/// Configuration for adaptive learning system
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct AdaptiveLearningConfig {
50    /// Learning rate for preference updates
51    pub learning_rate: f32,
52    /// Minimum samples before adaptation
53    pub min_samples: usize,
54    /// Weight decay for old samples
55    pub decay_factor: f32,
56    /// Enable automatic model fine-tuning
57    pub auto_finetune: bool,
58    /// Confidence threshold for adaptation
59    pub confidence_threshold: f32,
60}
61
62impl Default for AdaptiveLearningConfig {
63    fn default() -> Self {
64        Self {
65            learning_rate: 0.01,
66            min_samples: 10,
67            decay_factor: 0.95,
68            auto_finetune: true,
69            confidence_threshold: 0.7,
70        }
71    }
72}
73
74/// User feedback for synthesis results
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct UserFeedback {
77    /// Unique feedback ID
78    pub id: String,
79    /// User identifier
80    pub user_id: String,
81    /// Synthesis sample ID
82    pub sample_id: String,
83    /// Audio data for reference
84    #[serde(skip)]
85    pub audio_data: Vec<f32>,
86    /// Overall rating (0.0-5.0)
87    pub rating: f32,
88    /// Specific quality ratings
89    pub quality_ratings: QualityRatings,
90    /// User comments and corrections
91    pub comments: Option<String>,
92    /// Timestamp
93    pub timestamp: DateTime<Utc>,
94}
95
96impl UserFeedback {
97    /// Create new user feedback
98    pub fn new(
99        user_id: impl Into<String>,
100        audio_data: Vec<f32>,
101        rating: f32,
102        comments: Option<impl Into<String>>,
103    ) -> Self {
104        Self {
105            id: Uuid::new_v4().to_string(),
106            user_id: user_id.into(),
107            sample_id: Uuid::new_v4().to_string(),
108            audio_data,
109            rating: rating.clamp(0.0, 5.0),
110            quality_ratings: QualityRatings::default(),
111            comments: comments.map(|c| c.into()),
112            timestamp: Utc::now(),
113        }
114    }
115
116    /// Set quality ratings
117    pub fn with_quality_ratings(mut self, ratings: QualityRatings) -> Self {
118        self.quality_ratings = ratings;
119        self
120    }
121}
122
123/// Specific quality ratings for different aspects
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct QualityRatings {
126    /// Pitch accuracy (0.0-5.0)
127    pub pitch_accuracy: f32,
128    /// Timing precision (0.0-5.0)
129    pub timing_precision: f32,
130    /// Naturalness (0.0-5.0)
131    pub naturalness: f32,
132    /// Expression quality (0.0-5.0)
133    pub expression: f32,
134    /// Voice quality (0.0-5.0)
135    pub voice_quality: f32,
136}
137
138impl Default for QualityRatings {
139    fn default() -> Self {
140        Self {
141            pitch_accuracy: 3.0,
142            timing_precision: 3.0,
143            naturalness: 3.0,
144            expression: 3.0,
145            voice_quality: 3.0,
146        }
147    }
148}
149
150/// User preference profile
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct UserPreferences {
153    /// User identifier
154    pub user_id: String,
155    /// Preferred voice characteristics
156    pub voice_preferences: VoiceCharacteristics,
157    /// Preferred expression style
158    pub expression_preferences: HashMap<String, f32>,
159    /// Quality metric weights
160    pub quality_weights: QualityWeights,
161    /// Number of feedback samples
162    pub sample_count: usize,
163    /// Confidence in preferences (0.0-1.0)
164    pub confidence: f32,
165    /// Last update timestamp
166    pub last_updated: DateTime<Utc>,
167}
168
169/// Weights for different quality metrics
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct QualityWeights {
172    /// Weight for pitch accuracy (0.0-1.0)
173    pub pitch_weight: f32,
174    /// Weight for timing precision (0.0-1.0)
175    pub timing_weight: f32,
176    /// Weight for naturalness quality (0.0-1.0)
177    pub naturalness_weight: f32,
178    /// Weight for expression quality (0.0-1.0)
179    pub expression_weight: f32,
180    /// Weight for voice quality (0.0-1.0)
181    pub voice_quality_weight: f32,
182}
183
184impl Default for QualityWeights {
185    fn default() -> Self {
186        Self {
187            pitch_weight: 1.0,
188            timing_weight: 1.0,
189            naturalness_weight: 1.0,
190            expression_weight: 1.0,
191            voice_quality_weight: 1.0,
192        }
193    }
194}
195
196/// Style adaptation parameters learned from examples
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct StyleAdaptation {
199    /// Style identifier
200    pub style_id: String,
201    /// Vibrato parameters
202    pub vibrato_params: VibratoParams,
203    /// Dynamics preferences
204    pub dynamics_params: DynamicsParams,
205    /// Articulation preferences
206    pub articulation_params: ArticulationParams,
207    /// Number of examples used
208    pub example_count: usize,
209    /// Adaptation confidence (0.0-1.0)
210    pub confidence: f32,
211}
212
213/// Vibrato parameters for style adaptation
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct VibratoParams {
216    /// Vibrato rate in Hz
217    pub rate: f32,
218    /// Vibrato depth (modulation amount)
219    pub depth: f32,
220    /// Onset delay in seconds
221    pub onset_delay: f32,
222}
223
224/// Dynamics parameters for style adaptation
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct DynamicsParams {
227    /// Average volume level
228    pub average_level: f32,
229    /// Dynamic range (loudness variation)
230    pub dynamic_range: f32,
231    /// Rate of crescendo/decrescendo
232    pub crescendo_rate: f32,
233}
234
235/// Articulation parameters for style adaptation
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct ArticulationParams {
238    /// Amount of legato (smooth connection between notes)
239    pub legato_amount: f32,
240    /// Amount of staccato (sharp, detached notes)
241    pub staccato_amount: f32,
242    /// Strength of note accents
243    pub accent_strength: f32,
244}
245
246/// Personalized recommendations for synthesis
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct PersonalizedRecommendations {
249    /// Recommended voice characteristics
250    pub voice_characteristics: VoiceCharacteristics,
251    /// Recommended parameter adjustments
252    pub parameter_adjustments: HashMap<String, f32>,
253    /// Recommended techniques
254    pub techniques: Vec<String>,
255    /// Confidence in recommendations (0.0-1.0)
256    pub confidence: f32,
257}
258
259/// Model improvement metrics
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct ModelImprovement {
262    /// Improvement iteration
263    pub iteration: usize,
264    /// Quality improvement over baseline
265    pub quality_delta: f32,
266    /// User satisfaction improvement
267    pub satisfaction_delta: f32,
268    /// Number of training samples used
269    pub training_samples: usize,
270    /// Timestamp
271    pub timestamp: DateTime<Utc>,
272}
273
274/// Main adaptive learning system
275pub struct AdaptiveLearningSystem {
276    config: AdaptiveLearningConfig,
277    user_preferences: Arc<RwLock<HashMap<String, UserPreferences>>>,
278    style_adaptations: Arc<RwLock<HashMap<String, StyleAdaptation>>>,
279    feedback_history: Arc<RwLock<Vec<UserFeedback>>>,
280    improvement_history: Arc<RwLock<Vec<ModelImprovement>>>,
281}
282
283impl AdaptiveLearningSystem {
284    /// Create new adaptive learning system
285    pub fn new(config: AdaptiveLearningConfig) -> Self {
286        Self {
287            config,
288            user_preferences: Arc::new(RwLock::new(HashMap::new())),
289            style_adaptations: Arc::new(RwLock::new(HashMap::new())),
290            feedback_history: Arc::new(RwLock::new(Vec::new())),
291            improvement_history: Arc::new(RwLock::new(Vec::new())),
292        }
293    }
294
295    /// Add user feedback and update learning models
296    pub async fn add_feedback(&self, feedback: UserFeedback) -> Result<()> {
297        let user_id = feedback.user_id.clone();
298
299        // Store feedback
300        self.feedback_history.write().await.push(feedback.clone());
301
302        // Update user preferences
303        self.update_user_preferences(&user_id, &feedback).await?;
304
305        // Update style adaptations if applicable
306        self.update_style_adaptations(&feedback).await?;
307
308        // Trigger automatic fine-tuning if enabled
309        if self.config.auto_finetune {
310            self.check_and_finetune(&user_id).await?;
311        }
312
313        Ok(())
314    }
315
316    /// Get personalized recommendations for a user
317    pub async fn get_recommendations(&self, user_id: &str) -> Result<PersonalizedRecommendations> {
318        let prefs = self.user_preferences.read().await;
319
320        let user_pref = prefs.get(user_id).ok_or_else(|| {
321            Error::Processing(format!("No preferences found for user: {}", user_id))
322        })?;
323
324        // Check if we have enough confidence
325        if user_pref.confidence < self.config.confidence_threshold {
326            return Err(Error::Processing(
327                "Insufficient data for personalized recommendations".to_string(),
328            ));
329        }
330
331        // Build recommendations
332        let mut parameter_adjustments = HashMap::new();
333
334        // Add voice characteristic adjustments
335        parameter_adjustments.insert(
336            "vibrato_frequency".to_string(),
337            user_pref.voice_preferences.vibrato_frequency,
338        );
339        parameter_adjustments.insert(
340            "vibrato_depth".to_string(),
341            user_pref.voice_preferences.vibrato_depth,
342        );
343
344        // Add expression adjustments
345        for (key, value) in &user_pref.expression_preferences {
346            parameter_adjustments.insert(key.clone(), *value);
347        }
348
349        // Recommend techniques based on quality weights
350        let mut techniques = Vec::new();
351        if user_pref.quality_weights.expression_weight > 1.2 {
352            techniques.push("enhanced_expression".to_string());
353        }
354        if user_pref.quality_weights.naturalness_weight > 1.2 {
355            techniques.push("natural_breath_patterns".to_string());
356        }
357
358        Ok(PersonalizedRecommendations {
359            voice_characteristics: user_pref.voice_preferences.clone(),
360            parameter_adjustments,
361            techniques,
362            confidence: user_pref.confidence,
363        })
364    }
365
366    /// Get user preferences
367    pub async fn get_user_preferences(&self, user_id: &str) -> Option<UserPreferences> {
368        self.user_preferences.read().await.get(user_id).cloned()
369    }
370
371    /// Get style adaptation
372    pub async fn get_style_adaptation(&self, style_id: &str) -> Option<StyleAdaptation> {
373        self.style_adaptations.read().await.get(style_id).cloned()
374    }
375
376    /// Get model improvement history
377    pub async fn get_improvement_history(&self) -> Vec<ModelImprovement> {
378        self.improvement_history.read().await.clone()
379    }
380
381    /// Update user preferences based on feedback
382    async fn update_user_preferences(&self, user_id: &str, feedback: &UserFeedback) -> Result<()> {
383        let mut prefs = self.user_preferences.write().await;
384
385        let user_pref = prefs
386            .entry(user_id.to_string())
387            .or_insert_with(|| UserPreferences {
388                user_id: user_id.to_string(),
389                voice_preferences: VoiceCharacteristics::default(),
390                expression_preferences: HashMap::new(),
391                quality_weights: QualityWeights::default(),
392                sample_count: 0,
393                confidence: 0.0,
394                last_updated: Utc::now(),
395            });
396
397        // Update sample count
398        user_pref.sample_count += 1;
399
400        // Update quality weights based on ratings
401        let lr = self.config.learning_rate;
402        user_pref.quality_weights.pitch_weight +=
403            lr * (feedback.quality_ratings.pitch_accuracy - 3.0);
404        user_pref.quality_weights.timing_weight +=
405            lr * (feedback.quality_ratings.timing_precision - 3.0);
406        user_pref.quality_weights.naturalness_weight +=
407            lr * (feedback.quality_ratings.naturalness - 3.0);
408        user_pref.quality_weights.expression_weight +=
409            lr * (feedback.quality_ratings.expression - 3.0);
410        user_pref.quality_weights.voice_quality_weight +=
411            lr * (feedback.quality_ratings.voice_quality - 3.0);
412
413        // Normalize weights
414        let weight_sum = user_pref.quality_weights.pitch_weight
415            + user_pref.quality_weights.timing_weight
416            + user_pref.quality_weights.naturalness_weight
417            + user_pref.quality_weights.expression_weight
418            + user_pref.quality_weights.voice_quality_weight;
419
420        if weight_sum > 0.0 {
421            user_pref.quality_weights.pitch_weight /= weight_sum / 5.0;
422            user_pref.quality_weights.timing_weight /= weight_sum / 5.0;
423            user_pref.quality_weights.naturalness_weight /= weight_sum / 5.0;
424            user_pref.quality_weights.expression_weight /= weight_sum / 5.0;
425            user_pref.quality_weights.voice_quality_weight /= weight_sum / 5.0;
426        }
427
428        // Update confidence based on sample count
429        user_pref.confidence =
430            (user_pref.sample_count as f32 / self.config.min_samples as f32).min(1.0);
431
432        user_pref.last_updated = Utc::now();
433
434        Ok(())
435    }
436
437    /// Update style adaptations based on feedback
438    async fn update_style_adaptations(&self, feedback: &UserFeedback) -> Result<()> {
439        // Extract style features from feedback
440        let style_id = format!("user_{}_style", feedback.user_id);
441
442        let mut adaptations = self.style_adaptations.write().await;
443
444        let adaptation = adaptations
445            .entry(style_id.clone())
446            .or_insert_with(|| StyleAdaptation {
447                style_id,
448                vibrato_params: VibratoParams {
449                    rate: 5.0,
450                    depth: 0.5,
451                    onset_delay: 0.1,
452                },
453                dynamics_params: DynamicsParams {
454                    average_level: 0.7,
455                    dynamic_range: 0.4,
456                    crescendo_rate: 0.1,
457                },
458                articulation_params: ArticulationParams {
459                    legato_amount: 0.7,
460                    staccato_amount: 0.3,
461                    accent_strength: 0.5,
462                },
463                example_count: 0,
464                confidence: 0.0,
465            });
466
467        // Update adaptation parameters based on feedback rating
468        let lr = self.config.learning_rate;
469        if feedback.rating > 4.0 {
470            // Positive feedback - strengthen current parameters
471            adaptation.example_count += 1;
472        } else if feedback.rating < 3.0 {
473            // Negative feedback - adjust parameters
474            adaptation.vibrato_params.rate *= 1.0 - lr;
475            adaptation.vibrato_params.depth *= 1.0 - lr;
476        }
477
478        // Update confidence
479        adaptation.confidence =
480            (adaptation.example_count as f32 / self.config.min_samples as f32).min(1.0);
481
482        Ok(())
483    }
484
485    /// Check if fine-tuning is needed and trigger it
486    async fn check_and_finetune(&self, user_id: &str) -> Result<()> {
487        let prefs = self.user_preferences.read().await;
488
489        if let Some(user_pref) = prefs.get(user_id) {
490            if user_pref.sample_count >= self.config.min_samples
491                && user_pref.confidence >= self.config.confidence_threshold
492            {
493                // Simulate model fine-tuning
494                drop(prefs); // Release read lock
495                self.perform_finetuning(user_id).await?;
496            }
497        }
498
499        Ok(())
500    }
501
502    /// Perform model fine-tuning
503    async fn perform_finetuning(&self, user_id: &str) -> Result<()> {
504        // Simulate fine-tuning process
505        let feedback = self.feedback_history.read().await;
506        let user_feedback: Vec<_> = feedback.iter().filter(|f| f.user_id == user_id).collect();
507
508        if user_feedback.is_empty() {
509            return Ok(());
510        }
511
512        // Calculate improvement metrics
513        let avg_rating: f32 =
514            user_feedback.iter().map(|f| f.rating).sum::<f32>() / user_feedback.len() as f32;
515        let baseline_rating = 3.0;
516        let quality_delta = avg_rating - baseline_rating;
517
518        let improvement = ModelImprovement {
519            iteration: self.improvement_history.read().await.len(),
520            quality_delta,
521            satisfaction_delta: quality_delta * 0.2, // Simplified calculation
522            training_samples: user_feedback.len(),
523            timestamp: Utc::now(),
524        };
525
526        self.improvement_history.write().await.push(improvement);
527
528        Ok(())
529    }
530
531    /// Get learning statistics
532    pub async fn get_statistics(&self) -> LearningStatistics {
533        let prefs = self.user_preferences.read().await;
534        let feedback = self.feedback_history.read().await;
535        let improvements = self.improvement_history.read().await;
536
537        let avg_rating = if !feedback.is_empty() {
538            feedback.iter().map(|f| f.rating).sum::<f32>() / feedback.len() as f32
539        } else {
540            0.0
541        };
542
543        let total_improvement = improvements.iter().map(|i| i.quality_delta).sum::<f32>();
544
545        LearningStatistics {
546            total_users: prefs.len(),
547            total_feedback: feedback.len(),
548            average_rating: avg_rating,
549            total_improvements: improvements.len(),
550            cumulative_improvement: total_improvement,
551        }
552    }
553}
554
555/// Learning statistics
556#[derive(Debug, Clone, Serialize, Deserialize)]
557pub struct LearningStatistics {
558    /// Total number of users providing feedback
559    pub total_users: usize,
560    /// Total number of feedback samples collected
561    pub total_feedback: usize,
562    /// Average user rating (1.0-5.0)
563    pub average_rating: f32,
564    /// Total number of model improvements performed
565    pub total_improvements: usize,
566    /// Cumulative quality improvement
567    pub cumulative_improvement: f32,
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    #[tokio::test]
575    async fn test_feedback_collection() {
576        let config = AdaptiveLearningConfig::default();
577        let system = AdaptiveLearningSystem::new(config);
578
579        let feedback = UserFeedback::new(
580            "user1",
581            vec![0.0; 1000],
582            4.5,
583            Some("Great quality".to_string()),
584        );
585
586        system.add_feedback(feedback).await.unwrap();
587
588        let stats = system.get_statistics().await;
589        assert_eq!(stats.total_feedback, 1);
590        assert_eq!(stats.total_users, 1);
591    }
592
593    #[tokio::test]
594    async fn test_preference_learning() {
595        let config = AdaptiveLearningConfig {
596            min_samples: 2,
597            ..Default::default()
598        };
599        let system = AdaptiveLearningSystem::new(config);
600
601        // Add multiple feedback samples
602        for i in 0..3 {
603            let mut feedback = UserFeedback::new(
604                "user1",
605                vec![0.0; 1000],
606                4.0 + i as f32 * 0.2,
607                None::<String>,
608            );
609            feedback.quality_ratings.pitch_accuracy = 4.5;
610            feedback.quality_ratings.naturalness = 4.0;
611            system.add_feedback(feedback).await.unwrap();
612        }
613
614        let prefs = system.get_user_preferences("user1").await;
615        assert!(prefs.is_some());
616
617        let prefs = prefs.unwrap();
618        assert!(prefs.confidence > 0.5);
619        assert_eq!(prefs.sample_count, 3);
620    }
621
622    #[tokio::test]
623    async fn test_personalized_recommendations() {
624        let config = AdaptiveLearningConfig {
625            min_samples: 2,
626            confidence_threshold: 0.5,
627            ..Default::default()
628        };
629        let system = AdaptiveLearningSystem::new(config);
630
631        // Add sufficient feedback
632        for _ in 0..3 {
633            let feedback = UserFeedback::new("user1", vec![0.0; 1000], 4.5, None::<String>);
634            system.add_feedback(feedback).await.unwrap();
635        }
636
637        let recommendations = system.get_recommendations("user1").await;
638        assert!(recommendations.is_ok());
639
640        let recs = recommendations.unwrap();
641        assert!(recs.confidence >= 0.5);
642        assert!(!recs.parameter_adjustments.is_empty());
643    }
644
645    #[tokio::test]
646    async fn test_style_adaptation() {
647        let config = AdaptiveLearningConfig::default();
648        let system = AdaptiveLearningSystem::new(config);
649
650        let feedback = UserFeedback::new(
651            "user1",
652            vec![0.0; 1000],
653            4.8,
654            Some("Excellent style".to_string()),
655        );
656
657        system.add_feedback(feedback).await.unwrap();
658
659        let style_id = "user_user1_style";
660        let adaptation = system.get_style_adaptation(style_id).await;
661        assert!(adaptation.is_some());
662    }
663
664    #[tokio::test]
665    async fn test_quality_weight_updates() {
666        let config = AdaptiveLearningConfig {
667            learning_rate: 0.1,
668            ..Default::default()
669        };
670        let system = AdaptiveLearningSystem::new(config);
671
672        let mut feedback = UserFeedback::new("user1", vec![0.0; 1000], 4.0, None::<String>);
673        feedback.quality_ratings.pitch_accuracy = 5.0;
674        feedback.quality_ratings.timing_precision = 2.0;
675
676        system.add_feedback(feedback).await.unwrap();
677
678        let prefs = system.get_user_preferences("user1").await.unwrap();
679
680        // Pitch weight should increase, timing weight should decrease
681        assert!(prefs.quality_weights.pitch_weight > 1.0);
682        assert!(prefs.quality_weights.timing_weight < 1.0);
683    }
684
685    #[tokio::test]
686    async fn test_model_improvement_tracking() {
687        let config = AdaptiveLearningConfig {
688            min_samples: 2,
689            auto_finetune: true,
690            confidence_threshold: 0.5,
691            ..Default::default()
692        };
693        let system = AdaptiveLearningSystem::new(config);
694
695        // Add feedback to trigger fine-tuning
696        for _ in 0..3 {
697            let feedback = UserFeedback::new("user1", vec![0.0; 1000], 4.5, None::<String>);
698            system.add_feedback(feedback).await.unwrap();
699        }
700
701        let history = system.get_improvement_history().await;
702        assert!(!history.is_empty());
703    }
704
705    #[tokio::test]
706    async fn test_confidence_calculation() {
707        let config = AdaptiveLearningConfig {
708            min_samples: 10,
709            ..Default::default()
710        };
711        let system = AdaptiveLearningSystem::new(config);
712
713        // Add 5 feedback samples (50% of min_samples)
714        for _ in 0..5 {
715            let feedback = UserFeedback::new("user1", vec![0.0; 1000], 4.0, None::<String>);
716            system.add_feedback(feedback).await.unwrap();
717        }
718
719        let prefs = system.get_user_preferences("user1").await.unwrap();
720        assert!((prefs.confidence - 0.5).abs() < 0.01);
721    }
722
723    #[tokio::test]
724    async fn test_learning_statistics() {
725        let config = AdaptiveLearningConfig::default();
726        let system = AdaptiveLearningSystem::new(config);
727
728        // Add feedback from multiple users
729        for user_id in &["user1", "user2", "user3"] {
730            for _ in 0..2 {
731                let feedback = UserFeedback::new(*user_id, vec![0.0; 1000], 4.0, None::<String>);
732                system.add_feedback(feedback).await.unwrap();
733            }
734        }
735
736        let stats = system.get_statistics().await;
737        assert_eq!(stats.total_users, 3);
738        assert_eq!(stats.total_feedback, 6);
739        assert!((stats.average_rating - 4.0).abs() < 0.01);
740    }
741
742    #[tokio::test]
743    async fn test_insufficient_data_error() {
744        let config = AdaptiveLearningConfig {
745            min_samples: 10,
746            confidence_threshold: 0.8,
747            ..Default::default()
748        };
749        let system = AdaptiveLearningSystem::new(config);
750
751        // Add insufficient feedback
752        let feedback = UserFeedback::new("user1", vec![0.0; 1000], 4.0, None::<String>);
753        system.add_feedback(feedback).await.unwrap();
754
755        // Should fail due to insufficient data
756        let result = system.get_recommendations("user1").await;
757        assert!(result.is_err());
758    }
759}