Skip to main content

voirs_cli/cloud/
api.rs

1// Cloud API integration for VoiRS external service integration
2use anyhow::Result;
3use reqwest::{Client, Response};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::time::Duration;
7use voirs_sdk::types::SynthesisConfig;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct CloudApiConfig {
11    pub base_url: String,
12    pub api_key: Option<String>,
13    pub timeout_seconds: u64,
14    pub retry_attempts: u32,
15    pub rate_limit_requests_per_minute: u32,
16    pub enabled_services: Vec<CloudService>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20pub enum CloudService {
21    Translation,
22    ContentManagement,
23    Analytics,
24    QualityAssurance,
25    VoiceTraining,
26    AudioProcessing,
27    SpeechRecognition,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct TranslationRequest {
32    pub text: String,
33    pub source_language: String,
34    pub target_language: String,
35    pub preserve_ssml: bool,
36    pub quality_level: TranslationQuality,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub enum TranslationQuality {
41    Fast,
42    Balanced,
43    HighQuality,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TranslationResponse {
48    pub translated_text: String,
49    pub detected_language: Option<String>,
50    pub confidence_score: f32,
51    pub processing_time_ms: u32,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ContentAnalysisRequest {
56    pub content: String,
57    pub analysis_types: Vec<AnalysisType>,
58    pub language: Option<String>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub enum AnalysisType {
63    Sentiment,
64    Entities,
65    Keywords,
66    Readability,
67    Appropriateness,
68    Complexity,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct ContentAnalysisResponse {
73    pub sentiment: Option<SentimentAnalysis>,
74    pub entities: Vec<EntityExtraction>,
75    pub keywords: Vec<KeywordExtraction>,
76    pub readability_score: Option<f32>,
77    pub appropriateness_rating: Option<AppropriattenessRating>,
78    pub complexity_level: Option<ComplexityLevel>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct SentimentAnalysis {
83    pub sentiment: String, // positive, negative, neutral
84    pub confidence: f32,
85    pub emotional_tone: Vec<EmotionScore>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct EmotionScore {
90    pub emotion: String,
91    pub score: f32,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct EntityExtraction {
96    pub text: String,
97    pub entity_type: String,
98    pub confidence: f32,
99    pub start_offset: usize,
100    pub end_offset: usize,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct KeywordExtraction {
105    pub keyword: String,
106    pub relevance: f32,
107    pub frequency: u32,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub enum AppropriattenessRating {
112    Appropriate,
113    Questionable,
114    Inappropriate,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub enum ComplexityLevel {
119    Simple,
120    Moderate,
121    Complex,
122    Advanced,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct AnalyticsEvent {
127    pub event_type: String,
128    pub timestamp: u64,
129    pub user_id: Option<String>,
130    pub session_id: Option<String>,
131    pub properties: HashMap<String, serde_json::Value>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct QualityAssessmentRequest {
136    pub audio_data: Vec<u8>,
137    pub text: String,
138    pub synthesis_config: SynthesisConfig,
139    pub assessment_types: Vec<QualityMetric>,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub enum QualityMetric {
144    Naturalness,
145    Intelligibility,
146    Prosody,
147    Pronunciation,
148    OverallQuality,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct QualityAssessmentResponse {
153    pub overall_score: f32,
154    pub metric_scores: HashMap<String, f32>,
155    pub detailed_feedback: Vec<QualityFeedback>,
156    pub improvement_suggestions: Vec<String>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct QualityFeedback {
161    pub metric: String,
162    pub score: f32,
163    pub description: String,
164    pub timestamp_range: Option<(f32, f32)>, // Start and end times in seconds
165}
166
167pub struct CloudApiClient {
168    client: Client,
169    config: CloudApiConfig,
170    rate_limiter: RateLimiter,
171}
172
173struct RateLimiter {
174    requests_per_minute: u32,
175    request_times: Vec<std::time::Instant>,
176}
177
178impl CloudApiClient {
179    pub fn new(config: CloudApiConfig) -> Result<Self> {
180        let client = Client::builder()
181            .timeout(Duration::from_secs(config.timeout_seconds))
182            .build()?;
183
184        let rate_limiter = RateLimiter::new(config.rate_limit_requests_per_minute);
185
186        Ok(Self {
187            client,
188            config,
189            rate_limiter,
190        })
191    }
192
193    /// Translate text using cloud translation service
194    pub async fn translate_text(
195        &mut self,
196        request: TranslationRequest,
197    ) -> Result<TranslationResponse> {
198        self.rate_limiter.wait_if_needed().await;
199
200        if !self
201            .config
202            .enabled_services
203            .contains(&CloudService::Translation)
204        {
205            return Err(anyhow::anyhow!("Translation service not enabled"));
206        }
207
208        let url = format!("{}/v1/translate", self.config.base_url);
209        let response = self.make_request("POST", &url, Some(&request)).await?;
210
211        let translation_response: TranslationResponse = response.json().await?;
212        Ok(translation_response)
213    }
214
215    /// Analyze content using cloud AI services
216    pub async fn analyze_content(
217        &mut self,
218        request: ContentAnalysisRequest,
219    ) -> Result<ContentAnalysisResponse> {
220        self.rate_limiter.wait_if_needed().await;
221
222        if !self
223            .config
224            .enabled_services
225            .contains(&CloudService::ContentManagement)
226        {
227            return Err(anyhow::anyhow!("Content analysis service not enabled"));
228        }
229
230        let url = format!("{}/v1/analyze", self.config.base_url);
231        let response = self.make_request("POST", &url, Some(&request)).await?;
232
233        let analysis_response: ContentAnalysisResponse = response.json().await?;
234        Ok(analysis_response)
235    }
236
237    /// Send analytics events to cloud analytics service
238    pub async fn send_analytics_event(&mut self, event: AnalyticsEvent) -> Result<()> {
239        self.rate_limiter.wait_if_needed().await;
240
241        if !self
242            .config
243            .enabled_services
244            .contains(&CloudService::Analytics)
245        {
246            return Err(anyhow::anyhow!("Analytics service not enabled"));
247        }
248
249        let url = format!("{}/v1/analytics/events", self.config.base_url);
250        let _response = self.make_request("POST", &url, Some(&event)).await?;
251
252        Ok(())
253    }
254
255    /// Assess audio quality using cloud QA service
256    pub async fn assess_quality(
257        &mut self,
258        request: QualityAssessmentRequest,
259    ) -> Result<QualityAssessmentResponse> {
260        self.rate_limiter.wait_if_needed().await;
261
262        if !self
263            .config
264            .enabled_services
265            .contains(&CloudService::QualityAssurance)
266        {
267            return Err(anyhow::anyhow!("Quality assessment service not enabled"));
268        }
269
270        let url = format!("{}/v1/quality/assess", self.config.base_url);
271
272        // For quality assessment, we'll use JSON format instead of multipart for simplicity
273        let payload = serde_json::json!({
274            "audio_data_base64": base64::encode(&request.audio_data),
275            "text": request.text,
276            "config": request.synthesis_config,
277            "metrics": request.assessment_types
278        });
279
280        let response = self.client.post(&url).json(&payload).send().await?;
281
282        if !response.status().is_success() {
283            return Err(anyhow::anyhow!(
284                "Quality assessment request failed: {}",
285                response.status()
286            ));
287        }
288
289        let quality_response: QualityAssessmentResponse = response.json().await?;
290        Ok(quality_response)
291    }
292
293    /// Get service health status
294    pub async fn get_service_health(&mut self) -> Result<ServiceHealth> {
295        let url = format!("{}/v1/health", self.config.base_url);
296        let response = self.make_request("GET", &url, None::<&()>).await?;
297
298        let health: ServiceHealth = response.json().await?;
299        Ok(health)
300    }
301
302    async fn make_request<T: Serialize>(
303        &self,
304        method: &str,
305        url: &str,
306        body: Option<&T>,
307    ) -> Result<Response> {
308        let mut request_builder = match method {
309            "GET" => self.client.get(url),
310            "POST" => self.client.post(url),
311            "PUT" => self.client.put(url),
312            "DELETE" => self.client.delete(url),
313            _ => return Err(anyhow::anyhow!("Unsupported HTTP method: {}", method)),
314        };
315
316        // Add authentication if available
317        if let Some(api_key) = &self.config.api_key {
318            request_builder =
319                request_builder.header("Authorization", format!("Bearer {}", api_key));
320        }
321
322        // Add body if provided
323        if let Some(body) = body {
324            request_builder = request_builder.json(body);
325        }
326
327        // Execute request with retries
328        let mut last_error = None;
329        for attempt in 0..=self.config.retry_attempts {
330            match request_builder.try_clone() {
331                Some(request) => match request.send().await {
332                    Ok(response) => {
333                        if response.status().is_success() {
334                            return Ok(response);
335                        } else {
336                            last_error = Some(anyhow::anyhow!("HTTP error: {}", response.status()));
337                        }
338                    }
339                    Err(e) => {
340                        last_error = Some(anyhow::anyhow!("Request error: {}", e));
341                    }
342                },
343                None => {
344                    last_error = Some(anyhow::anyhow!("Failed to clone request"));
345                    break;
346                }
347            }
348
349            if attempt < self.config.retry_attempts {
350                tokio::time::sleep(Duration::from_millis(1000 * (2_u64.pow(attempt)))).await;
351                // Exponential backoff
352            }
353        }
354
355        Err(last_error.unwrap_or_else(|| anyhow::anyhow!("Request failed after all retries")))
356    }
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct ServiceHealth {
361    pub status: String,
362    pub services: HashMap<String, ServiceStatus>,
363    pub response_time_ms: u32,
364    pub version: String,
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct ServiceStatus {
369    pub healthy: bool,
370    pub response_time_ms: Option<u32>,
371    pub error_message: Option<String>,
372}
373
374impl RateLimiter {
375    fn new(requests_per_minute: u32) -> Self {
376        Self {
377            requests_per_minute,
378            request_times: Vec::new(),
379        }
380    }
381
382    async fn wait_if_needed(&mut self) {
383        let now = std::time::Instant::now();
384        let one_minute_ago = now - Duration::from_secs(60);
385
386        // Remove old request times
387        self.request_times.retain(|&time| time > one_minute_ago);
388
389        // Check if we need to wait
390        if self.request_times.len() >= self.requests_per_minute as usize {
391            if let Some(&oldest) = self.request_times.first() {
392                let wait_until = oldest + Duration::from_secs(60);
393                if now < wait_until {
394                    let wait_duration = wait_until - now;
395                    tokio::time::sleep(wait_duration).await;
396                }
397            }
398        }
399
400        // Record current request time
401        self.request_times.push(now);
402    }
403}
404
405impl Default for CloudApiConfig {
406    fn default() -> Self {
407        Self {
408            base_url: "https://api.voirs.cloud".to_string(),
409            api_key: None,
410            timeout_seconds: 30,
411            retry_attempts: 3,
412            rate_limit_requests_per_minute: 60,
413            enabled_services: vec![
414                CloudService::Translation,
415                CloudService::ContentManagement,
416                CloudService::Analytics,
417                CloudService::QualityAssurance,
418            ],
419        }
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    #[test]
428    fn test_cloud_api_config_default() {
429        let config = CloudApiConfig::default();
430        assert_eq!(config.timeout_seconds, 30);
431        assert_eq!(config.retry_attempts, 3);
432        assert!(config.enabled_services.len() > 0);
433    }
434
435    #[test]
436    fn test_translation_request_serialization() {
437        let request = TranslationRequest {
438            text: "Hello world".to_string(),
439            source_language: "en".to_string(),
440            target_language: "es".to_string(),
441            preserve_ssml: true,
442            quality_level: TranslationQuality::HighQuality,
443        };
444
445        let serialized = serde_json::to_string(&request);
446        assert!(serialized.is_ok());
447
448        let deserialized: Result<TranslationRequest, _> =
449            serde_json::from_str(&serialized.unwrap());
450        assert!(deserialized.is_ok());
451    }
452
453    #[test]
454    fn test_analytics_event_creation() {
455        let mut properties = HashMap::new();
456        properties.insert(
457            "voice_id".to_string(),
458            serde_json::Value::String("en-US-1".to_string()),
459        );
460        properties.insert(
461            "duration_ms".to_string(),
462            serde_json::Value::Number(serde_json::Number::from(5000)),
463        );
464
465        let event = AnalyticsEvent {
466            event_type: "synthesis_completed".to_string(),
467            timestamp: 1620000000,
468            user_id: Some("user123".to_string()),
469            session_id: Some("session456".to_string()),
470            properties,
471        };
472
473        assert_eq!(event.event_type, "synthesis_completed");
474        assert_eq!(event.properties.len(), 2);
475    }
476
477    #[tokio::test]
478    async fn test_rate_limiter() {
479        let mut limiter = RateLimiter::new(60); // 60 requests per minute
480
481        // This should not block
482        let start = std::time::Instant::now();
483        limiter.wait_if_needed().await;
484        let elapsed = start.elapsed();
485
486        // Should be nearly instantaneous for first request
487        assert!(elapsed < Duration::from_millis(100));
488    }
489
490    #[test]
491    fn test_quality_feedback_serialization() {
492        let feedback = QualityFeedback {
493            metric: "naturalness".to_string(),
494            score: 0.85,
495            description: "Speech sounds natural with minor robotic artifacts".to_string(),
496            timestamp_range: Some((1.5, 3.2)),
497        };
498
499        let serialized = serde_json::to_string(&feedback);
500        assert!(serialized.is_ok());
501
502        let deserialized: Result<QualityFeedback, _> = serde_json::from_str(&serialized.unwrap());
503        assert!(deserialized.is_ok());
504    }
505}