1use axum::{
4 extract::{FromRequest, Query, Request, State},
5 http::{header, HeaderMap, StatusCode},
6 middleware::{self, Next},
7 response::{IntoResponse, Response},
8 routing::{get, post},
9 Json, Router,
10};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::net::{IpAddr, SocketAddr};
14use std::sync::{Arc, Mutex};
15use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
16use tokio::signal;
17use tower::ServiceBuilder;
18use tower_http::{cors::CorsLayer, trace::TraceLayer};
19use voirs_sdk::{
20 config::AppConfig,
21 error::Result,
22 types::{AudioFormat, QualityLevel, SynthesisConfig},
23 VoirsPipeline,
24};
25
26#[derive(Debug, Clone)]
28pub struct ApiKeyConfig {
29 pub key: String,
31 pub name: String,
33 pub rate_limit: u32,
35 pub enabled: bool,
37 pub created_at: SystemTime,
39}
40
41#[derive(Debug, Clone)]
43pub struct RateLimitBucket {
44 pub requests: u32,
46 pub window_start: Instant,
48 pub limit: u32,
50}
51
52#[derive(Debug)]
54pub struct AuthState {
55 pub api_keys: HashMap<String, ApiKeyConfig>,
57 pub rate_limits: HashMap<String, RateLimitBucket>,
59 pub usage_stats: HashMap<String, UsageStats>,
61 pub access_logs: Vec<AccessLogEntry>,
63}
64
65#[derive(Debug, Clone, Default, Serialize)]
67pub struct UsageStats {
68 pub total_requests: u64,
69 pub successful_requests: u64,
70 pub failed_requests: u64,
71 pub total_audio_seconds: f64,
72 pub bytes_transferred: u64,
73 pub last_used: Option<SystemTime>,
74}
75
76#[derive(Debug, Clone, Serialize)]
78pub struct AccessLogEntry {
79 pub timestamp: SystemTime,
80 pub ip_address: String,
81 pub api_key: Option<String>,
82 pub method: String,
83 pub path: String,
84 pub status_code: u16,
85 pub response_time_ms: u64,
86 pub bytes_transferred: u64,
87}
88
89#[derive(Debug, Clone)]
91pub struct LogRequestParams<'a> {
92 pub client_ip: &'a str,
93 pub api_key: Option<String>,
94 pub method: &'a str,
95 pub path: &'a str,
96 pub status_code: u16,
97 pub start_time: Instant,
98 pub bytes_transferred: u64,
99}
100
101#[derive(Clone)]
103pub struct AppState {
104 pipeline: Arc<VoirsPipeline>,
105 config: AppConfig,
106 auth: Arc<Mutex<AuthState>>,
107 start_time: Instant,
108 shutdown_signal: Arc<tokio::sync::RwLock<bool>>,
109}
110
111#[derive(Debug, Deserialize)]
113pub struct SynthesisRequest {
114 pub text: String,
116 pub voice: Option<String>,
118 pub rate: Option<f32>,
120 pub pitch: Option<f32>,
122 pub volume: Option<f32>,
124 pub quality: Option<String>,
126 pub format: Option<String>,
128 pub enhance: Option<bool>,
130}
131
132#[derive(Debug, Serialize)]
134pub struct SynthesisResponse {
135 pub success: bool,
137 pub error: Option<String>,
139 pub audio_data: Option<String>,
141 pub duration: Option<f32>,
143 pub format: String,
145 pub sample_rate: u32,
147 pub channels: u16,
149}
150
151#[derive(Debug, Serialize)]
153pub struct VoiceInfo {
154 pub id: String,
155 pub name: String,
156 pub language: String,
157 pub gender: Option<String>,
158 pub description: Option<String>,
159 pub is_installed: bool,
160}
161
162#[derive(Debug, Serialize)]
164pub struct VoicesResponse {
165 pub voices: Vec<VoiceInfo>,
166 pub total: usize,
167}
168
169#[derive(Debug, Serialize)]
171pub struct HealthResponse {
172 pub status: String,
173 pub version: String,
174 pub uptime_seconds: u64,
175 pub pipeline_ready: bool,
176}
177
178#[derive(Debug, Serialize)]
180pub struct DetailedHealthResponse {
181 pub status: String,
182 pub version: String,
183 pub uptime_seconds: u64,
184 pub timestamp: u64,
185 pub checks: Vec<HealthCheck>,
186 pub system: SystemHealth,
187}
188
189#[derive(Debug, Serialize)]
191pub struct HealthCheck {
192 pub name: String,
193 pub status: String,
194 pub message: Option<String>,
195 pub duration_ms: u64,
196 pub last_checked: u64,
197}
198
199#[derive(Debug, Serialize)]
201pub struct SystemHealth {
202 pub memory_usage_mb: u64,
203 pub memory_available_mb: u64,
204 pub cpu_usage_percent: f32,
205 pub disk_usage_percent: f32,
206 pub thread_count: u64,
207 pub file_descriptors: u64,
208}
209
210#[derive(Debug, Serialize)]
212pub struct ServerStats {
213 pub requests_total: u64,
214 pub requests_successful: u64,
215 pub requests_failed: u64,
216 pub average_synthesis_time_ms: f64,
217 pub total_audio_generated_seconds: f64,
218 pub uptime_seconds: u64,
219 pub active_api_keys: usize,
220 pub rate_limited_requests: u64,
221}
222
223#[derive(Debug, Serialize)]
225pub struct AuthInfoResponse {
226 pub api_key_name: String,
227 pub rate_limit: u32,
228 pub requests_remaining: u32,
229 pub requests_used: u32,
230 pub window_reset_seconds: u64,
231}
232
233#[derive(Debug, Serialize)]
235pub struct UsageStatsResponse {
236 pub api_key_name: String,
237 pub stats: UsageStats,
238}
239
240#[derive(Debug, Deserialize)]
242pub struct VoicesQuery {
243 pub language: Option<String>,
244 pub gender: Option<String>,
245}
246
247#[derive(Debug)]
249pub struct ApiError {
250 pub status: StatusCode,
251 pub message: String,
252}
253
254impl IntoResponse for ApiError {
255 fn into_response(self) -> Response {
256 let body = Json(serde_json::json!({
257 "error": self.message,
258 "status": self.status.as_u16()
259 }));
260 (self.status, body).into_response()
261 }
262}
263
264impl From<voirs_sdk::VoirsError> for ApiError {
265 fn from(err: voirs_sdk::VoirsError) -> Self {
266 ApiError {
267 status: StatusCode::INTERNAL_SERVER_ERROR,
268 message: err.to_string(),
269 }
270 }
271}
272
273pub async fn auth_middleware(
275 State(state): State<AppState>,
276 headers: HeaderMap,
277 request: Request,
278 next: Next,
279) -> std::result::Result<Response, ApiError> {
280 let start_time = Instant::now();
281 let method = request.method().to_string();
282 let path = request.uri().path().to_string();
283
284 let client_ip = extract_client_ip(&headers, &request);
286
287 if path == "/docs"
289 || path == "/"
290 || path == "/api/v1/health"
291 || path == "/api/v1/health/detailed"
292 || path == "/api/v1/health/ready"
293 || path == "/api/v1/health/live"
294 {
295 let response = next.run(request).await;
296 log_request(
297 &state,
298 LogRequestParams {
299 client_ip: &client_ip,
300 api_key: None,
301 method: &method,
302 path: &path,
303 status_code: 200,
304 start_time,
305 bytes_transferred: 0,
306 },
307 )
308 .await;
309 return Ok(response);
310 }
311
312 if *state.shutdown_signal.read().await {
314 return Err(ApiError {
315 status: StatusCode::SERVICE_UNAVAILABLE,
316 message: "Server is shutting down".to_string(),
317 });
318 }
319
320 let api_key = extract_api_key(&headers);
322
323 let validation_result = validate_and_rate_limit(&state, &client_ip, api_key.as_deref()).await;
325
326 match validation_result {
327 Ok(api_key_config) => {
328 let mut request_with_extensions = request;
330 if let Some(ref config) = api_key_config {
331 request_with_extensions
332 .extensions_mut()
333 .insert(config.clone());
334 }
335
336 let response = next.run(request_with_extensions).await;
337 let status_code = response.status().as_u16();
338
339 let bytes_transferred = calculate_response_size(&response);
341
342 update_usage_stats(
344 &state,
345 api_key_config.as_ref(),
346 status_code < 400,
347 bytes_transferred as f64,
348 )
349 .await;
350
351 log_request(
353 &state,
354 LogRequestParams {
355 client_ip: &client_ip,
356 api_key: api_key_config.as_ref().map(|k| k.key.clone()),
357 method: &method,
358 path: &path,
359 status_code,
360 start_time,
361 bytes_transferred,
362 },
363 )
364 .await;
365
366 Ok(response)
367 }
368 Err(error) => {
369 log_request(
370 &state,
371 LogRequestParams {
372 client_ip: &client_ip,
373 api_key,
374 method: &method,
375 path: &path,
376 status_code: error.status.as_u16(),
377 start_time,
378 bytes_transferred: 0,
379 },
380 )
381 .await;
382 Err(error)
383 }
384 }
385}
386
387pub fn extract_client_ip(headers: &HeaderMap, _request: &Request) -> String {
389 if let Some(forwarded) = headers.get("x-forwarded-for") {
391 if let Ok(forwarded_str) = forwarded.to_str() {
392 if let Some(first_ip) = forwarded_str.split(',').next() {
393 return first_ip.trim().to_string();
394 }
395 }
396 }
397
398 if let Some(real_ip) = headers.get("x-real-ip") {
399 if let Ok(ip_str) = real_ip.to_str() {
400 return ip_str.to_string();
401 }
402 }
403
404 "unknown".to_string()
406}
407
408pub fn extract_api_key(headers: &HeaderMap) -> Option<String> {
410 headers
411 .get("authorization")
412 .and_then(|header| header.to_str().ok())
413 .and_then(|auth_str| {
414 auth_str
415 .strip_prefix("Bearer ")
416 .or_else(|| auth_str.strip_prefix("ApiKey "))
417 .map(|s| s.to_string())
418 })
419 .or_else(|| {
420 headers
421 .get("x-api-key")
422 .and_then(|header| header.to_str().ok())
423 .map(|s| s.to_string())
424 })
425}
426
427async fn validate_and_rate_limit(
429 state: &AppState,
430 client_ip: &str,
431 api_key: Option<&str>,
432) -> std::result::Result<Option<ApiKeyConfig>, ApiError> {
433 let mut auth_state = state.auth.lock().unwrap();
434
435 let api_key_config = if let Some(key) = api_key {
437 match auth_state.api_keys.get(key) {
438 Some(config) if config.enabled => Some(config.clone()),
439 Some(_) => {
440 return Err(ApiError {
441 status: StatusCode::UNAUTHORIZED,
442 message: "API key is disabled".to_string(),
443 });
444 }
445 None => {
446 return Err(ApiError {
447 status: StatusCode::UNAUTHORIZED,
448 message: "Invalid API key".to_string(),
449 });
450 }
451 }
452 } else {
453 if auth_state.api_keys.is_empty() {
455 None
456 } else {
457 return Err(ApiError {
458 status: StatusCode::UNAUTHORIZED,
459 message: "API key required".to_string(),
460 });
461 }
462 };
463
464 let (rate_limit_key, rate_limit) = if let Some(ref config) = api_key_config {
466 (format!("api_key:{}", config.key), config.rate_limit)
467 } else {
468 (format!("ip:{}", client_ip), 60) };
470
471 let now = Instant::now();
473 let window_duration = Duration::from_secs(60); let bucket = auth_state
476 .rate_limits
477 .entry(rate_limit_key)
478 .or_insert_with(|| RateLimitBucket {
479 requests: 0,
480 window_start: now,
481 limit: rate_limit,
482 });
483
484 if now.duration_since(bucket.window_start) >= window_duration {
486 bucket.requests = 0;
487 bucket.window_start = now;
488 }
489
490 if bucket.requests >= bucket.limit {
492 return Err(ApiError {
493 status: StatusCode::TOO_MANY_REQUESTS,
494 message: format!(
495 "Rate limit exceeded. Limit: {} requests per minute",
496 bucket.limit
497 ),
498 });
499 }
500
501 bucket.requests += 1;
503
504 Ok(api_key_config)
505}
506
507fn calculate_response_size(response: &Response) -> u64 {
509 if let Some(content_length) = response.headers().get("content-length") {
511 if let Ok(length_str) = content_length.to_str() {
512 if let Ok(length) = length_str.parse::<u64>() {
513 return length;
514 }
515 }
516 }
517
518 let headers_size = response
521 .headers()
522 .iter()
523 .map(|(name, value)| name.as_str().len() + value.len() + 4) .sum::<usize>() as u64;
525
526 let status_line_size = response.status().as_str().len() as u64 + 20; headers_size + status_line_size
530}
531
532async fn update_usage_stats(
534 state: &AppState,
535 api_key_config: Option<&ApiKeyConfig>,
536 success: bool,
537 bytes_transferred: f64,
538) {
539 update_usage_stats_with_audio(state, api_key_config, success, bytes_transferred, None).await;
540}
541
542async fn update_usage_stats_with_audio(
544 state: &AppState,
545 api_key_config: Option<&ApiKeyConfig>,
546 success: bool,
547 bytes_transferred: f64,
548 audio_duration: Option<f64>,
549) {
550 if let Some(config) = api_key_config {
551 let mut auth_state = state.auth.lock().unwrap();
552 let stats = auth_state
553 .usage_stats
554 .entry(config.key.clone())
555 .or_default();
556
557 stats.total_requests += 1;
558 stats.bytes_transferred += bytes_transferred as u64;
559
560 if success {
561 stats.successful_requests += 1;
562 } else {
563 stats.failed_requests += 1;
564 }
565
566 if let Some(duration) = audio_duration {
568 stats.total_audio_seconds += duration;
569 }
570
571 stats.last_used = Some(SystemTime::now());
572 }
573}
574
575async fn log_request(state: &AppState, params: LogRequestParams<'_>) {
577 let mut auth_state = state.auth.lock().unwrap();
578
579 let log_entry = AccessLogEntry {
580 timestamp: SystemTime::now(),
581 ip_address: params.client_ip.to_string(),
582 api_key: params.api_key,
583 method: params.method.to_string(),
584 path: params.path.to_string(),
585 status_code: params.status_code,
586 response_time_ms: params.start_time.elapsed().as_millis() as u64,
587 bytes_transferred: params.bytes_transferred,
588 };
589
590 auth_state.access_logs.push(log_entry);
591
592 if auth_state.access_logs.len() > 10000 {
594 auth_state.access_logs.drain(0..1000);
595 }
596}
597
598pub async fn run_server(host: &str, port: u16, config: &AppConfig) -> Result<()> {
600 println!("Initializing VoiRS HTTP server...");
601
602 let pipeline = Arc::new(
604 VoirsPipeline::builder()
605 .with_quality(QualityLevel::High)
606 .with_gpu_acceleration(config.pipeline.use_gpu)
607 .build()
608 .await?,
609 );
610
611 let mut auth_state = AuthState {
613 api_keys: HashMap::new(),
614 rate_limits: HashMap::new(),
615 usage_stats: HashMap::new(),
616 access_logs: Vec::new(),
617 };
618
619 let default_api_key = ApiKeyConfig {
621 key: "voirs-dev-key-123".to_string(),
622 name: "Development Key".to_string(),
623 rate_limit: 100, enabled: true,
625 created_at: SystemTime::now(),
626 };
627 auth_state
628 .api_keys
629 .insert(default_api_key.key.clone(), default_api_key);
630
631 let state = AppState {
633 pipeline,
634 config: config.clone(),
635 auth: Arc::new(Mutex::new(auth_state)),
636 start_time: Instant::now(),
637 shutdown_signal: Arc::new(tokio::sync::RwLock::new(false)),
638 };
639
640 let app = create_router(state.clone());
642
643 let addr: SocketAddr = format!("{}:{}", host, port)
645 .parse()
646 .map_err(|e| voirs_sdk::VoirsError::config_error(format!("Invalid address: {}", e)))?;
647
648 println!("Starting VoiRS server on http://{}", addr);
649 println!("API endpoints:");
650 println!(" POST /api/v1/synthesize - Synthesize text to speech (requires auth)");
651 println!(" GET /api/v1/voices - List available voices (requires auth)");
652 println!(" GET /api/v1/health - Basic health check (public)");
653 println!(" GET /api/v1/health/detailed - Detailed health check (public)");
654 println!(" GET /api/v1/health/ready - Readiness probe (public)");
655 println!(" GET /api/v1/health/live - Liveness probe (public)");
656 println!(" GET /api/v1/stats - Server statistics (requires auth)");
657 println!(" GET /api/v1/auth/info - Authentication information (requires auth)");
658 println!(" GET /api/v1/auth/usage - Usage statistics (requires auth)");
659 println!(" POST /api/v1/shutdown - Graceful shutdown (requires auth)");
660 println!(" GET /docs - API documentation (public)");
661 println!();
662 println!("Authentication:");
663 println!(" Default API key: voirs-dev-key-123");
664 println!(" Headers: Authorization: Bearer <api-key> or X-API-Key: <api-key>");
665 println!(" Rate limit: 100 requests per minute per API key");
666 println!();
667 println!("Graceful shutdown:");
668 println!(" Send SIGTERM or SIGINT to gracefully shutdown");
669 println!(" Or use POST /api/v1/shutdown endpoint");
670 println!();
671
672 let listener = tokio::net::TcpListener::bind(&addr).await.map_err(|e| {
674 voirs_sdk::VoirsError::config_error(format!("Failed to bind to {}: {}", addr, e))
675 })?;
676
677 let shutdown_signal = shutdown_signal();
679
680 let shutdown_state = state.clone();
682
683 axum::serve(listener, app)
685 .with_graceful_shutdown(async move {
686 shutdown_signal.await;
687 println!("Starting graceful shutdown...");
688
689 *shutdown_state.shutdown_signal.write().await = true;
691
692 tokio::time::sleep(Duration::from_secs(5)).await;
694
695 println!("Graceful shutdown complete");
696 })
697 .await
698 .map_err(|e| voirs_sdk::VoirsError::config_error(format!("Server error: {}", e)))?;
699
700 Ok(())
701}
702
703async fn shutdown_signal() {
705 let ctrl_c = async {
706 signal::ctrl_c()
707 .await
708 .expect("failed to install Ctrl+C handler");
709 };
710
711 #[cfg(unix)]
712 let terminate = async {
713 signal::unix::signal(signal::unix::SignalKind::terminate())
714 .expect("failed to install SIGTERM handler")
715 .recv()
716 .await;
717 };
718
719 #[cfg(not(unix))]
720 let terminate = std::future::pending::<()>();
721
722 tokio::select! {
723 _ = ctrl_c => {
724 println!("Received Ctrl+C signal");
725 },
726 _ = terminate => {
727 println!("Received SIGTERM signal");
728 }
729 }
730}
731
732async fn shutdown_handler(State(state): State<AppState>) -> impl IntoResponse {
734 *state.shutdown_signal.write().await = true;
736
737 let response = Json(serde_json::json!({
739 "message": "Server shutdown initiated",
740 "status": "shutting_down"
741 }));
742
743 let shutdown_state = state.clone();
745 tokio::spawn(async move {
746 tokio::time::sleep(Duration::from_millis(100)).await;
748
749 std::process::exit(0);
753 });
754
755 response
756}
757
758fn create_router(state: AppState) -> Router {
760 let middleware_state = state.clone();
761
762 Router::new()
763 .route("/api/v1/synthesize", post(synthesize_handler))
765 .route("/api/v1/voices", get(voices_handler))
766 .route("/api/v1/health", get(health_handler))
767 .route("/api/v1/health/detailed", get(detailed_health_handler))
768 .route("/api/v1/health/ready", get(readiness_handler))
769 .route("/api/v1/health/live", get(liveness_handler))
770 .route("/api/v1/stats", get(stats_handler))
771 .route("/api/v1/auth/info", get(auth_info_handler))
772 .route("/api/v1/auth/usage", get(usage_stats_handler))
773 .route("/api/v1/shutdown", post(shutdown_handler))
774 .route("/docs", get(docs_handler))
776 .route("/", get(root_handler))
777 .with_state(state)
779 .layer(
780 ServiceBuilder::new()
781 .layer(middleware::from_fn_with_state(
782 middleware_state,
783 auth_middleware,
784 ))
785 .layer(TraceLayer::new_for_http())
786 .layer(CorsLayer::permissive()),
787 )
788}
789
790async fn root_handler() -> impl IntoResponse {
792 axum::response::Redirect::permanent("/docs")
793}
794
795async fn synthesize_handler(
797 State(state): State<AppState>,
798 request: axum::extract::Request,
799) -> std::result::Result<Json<SynthesisResponse>, ApiError> {
800 let api_key_config = request.extensions().get::<ApiKeyConfig>().cloned();
802
803 let axum::extract::Json(synthesis_request): axum::extract::Json<SynthesisRequest> =
805 axum::extract::Json::from_request(request, &state)
806 .await
807 .map_err(|_| ApiError {
808 status: StatusCode::BAD_REQUEST,
809 message: "Invalid JSON request body".to_string(),
810 })?;
811 if synthesis_request.text.trim().is_empty() {
813 return Err(ApiError {
814 status: StatusCode::BAD_REQUEST,
815 message: "Text cannot be empty".to_string(),
816 });
817 }
818
819 if synthesis_request.text.len() > 10000 {
820 return Err(ApiError {
821 status: StatusCode::BAD_REQUEST,
822 message: "Text too long (max 10000 characters)".to_string(),
823 });
824 }
825
826 let quality = match synthesis_request.quality.as_deref() {
828 Some("low") => QualityLevel::Low,
829 Some("medium") => QualityLevel::Medium,
830 Some("high") => QualityLevel::High,
831 Some("ultra") => QualityLevel::Ultra,
832 None => QualityLevel::High,
833 Some(other) => {
834 return Err(ApiError {
835 status: StatusCode::BAD_REQUEST,
836 message: format!("Invalid quality level: {}", other),
837 });
838 }
839 };
840
841 let format = match synthesis_request.format.as_deref() {
843 Some("wav") => AudioFormat::Wav,
844 Some("flac") => AudioFormat::Flac,
845 Some("mp3") => AudioFormat::Mp3,
846 Some("opus") => AudioFormat::Opus,
847 None => AudioFormat::Wav,
848 Some(other) => {
849 return Err(ApiError {
850 status: StatusCode::BAD_REQUEST,
851 message: format!("Unsupported audio format: {}", other),
852 });
853 }
854 };
855
856 if let Some(rate) = synthesis_request.rate {
858 if !(0.5..=2.0).contains(&rate) {
859 return Err(ApiError {
860 status: StatusCode::BAD_REQUEST,
861 message: "Speaking rate must be between 0.5 and 2.0".to_string(),
862 });
863 }
864 }
865
866 if let Some(pitch) = synthesis_request.pitch {
867 if !(-12.0..=12.0).contains(&pitch) {
868 return Err(ApiError {
869 status: StatusCode::BAD_REQUEST,
870 message: "Pitch shift must be between -12.0 and 12.0 semitones".to_string(),
871 });
872 }
873 }
874
875 if let Some(volume) = synthesis_request.volume {
876 if !(-20.0..=20.0).contains(&volume) {
877 return Err(ApiError {
878 status: StatusCode::BAD_REQUEST,
879 message: "Volume gain must be between -20.0 and 20.0 dB".to_string(),
880 });
881 }
882 }
883
884 let synth_config = SynthesisConfig {
886 speaking_rate: synthesis_request.rate.unwrap_or(1.0),
887 pitch_shift: synthesis_request.pitch.unwrap_or(0.0),
888 volume_gain: synthesis_request.volume.unwrap_or(0.0),
889 enable_enhancement: synthesis_request.enhance.unwrap_or(false),
890 quality,
891 ..Default::default()
892 };
893
894 if let Some(voice_id) = &synthesis_request.voice {
896 if let Err(e) = state.pipeline.set_voice(voice_id).await {
897 return Err(ApiError {
898 status: StatusCode::BAD_REQUEST,
899 message: format!("Invalid voice '{}': {}", voice_id, e),
900 });
901 }
902 }
903
904 match state
906 .pipeline
907 .synthesize_with_config(&synthesis_request.text, &synth_config)
908 .await
909 {
910 Ok(audio) => {
911 let audio_bytes = audio.to_format(format)?;
913 let audio_base64 = base64::encode(&audio_bytes);
914
915 let duration_seconds = audio.duration() as f64;
917
918 if let Some(ref config) = api_key_config {
920 update_usage_stats_with_audio(
921 &state,
922 Some(config),
923 true, 0.0, Some(duration_seconds),
926 )
927 .await;
928 }
929
930 Ok(Json(SynthesisResponse {
931 success: true,
932 error: None,
933 audio_data: Some(audio_base64),
934 duration: Some(audio.duration()),
935 format: format.to_string(),
936 sample_rate: audio.sample_rate(),
937 channels: audio.channels() as u16,
938 }))
939 }
940 Err(e) => Ok(Json(SynthesisResponse {
941 success: false,
942 error: Some(e.to_string()),
943 audio_data: None,
944 duration: None,
945 format: format.to_string(),
946 sample_rate: 0,
947 channels: 0,
948 })),
949 }
950}
951
952async fn voices_handler(
954 State(state): State<AppState>,
955 Query(query): Query<VoicesQuery>,
956) -> std::result::Result<Json<VoicesResponse>, ApiError> {
957 let voice_configs = state.pipeline.list_voices().await.map_err(|e| ApiError {
959 status: StatusCode::INTERNAL_SERVER_ERROR,
960 message: format!("Failed to list voices: {}", e),
961 })?;
962
963 let mut voices: Vec<VoiceInfo> = voice_configs.iter().map(voice_config_to_info).collect();
965
966 if let Some(language) = &query.language {
968 voices.retain(|v| v.language.to_lowercase().contains(&language.to_lowercase()));
969 }
970
971 if let Some(gender) = &query.gender {
972 voices.retain(|v| {
973 v.gender
974 .as_ref()
975 .is_some_and(|g| g.eq_ignore_ascii_case(gender))
976 });
977 }
978
979 let total = voices.len();
980
981 Ok(Json(VoicesResponse { voices, total }))
982}
983
984fn voice_config_to_info(config: &voirs_sdk::types::VoiceConfig) -> VoiceInfo {
986 let quality_str = match config.characteristics.quality {
988 voirs_sdk::types::QualityLevel::Low => "Standard quality",
989 voirs_sdk::types::QualityLevel::Medium => "Good quality",
990 voirs_sdk::types::QualityLevel::High => "High quality",
991 voirs_sdk::types::QualityLevel::Ultra => "Ultra-high quality",
992 };
993
994 let style_str = match config.characteristics.style {
995 voirs_sdk::types::SpeakingStyle::Neutral => "neutral",
996 voirs_sdk::types::SpeakingStyle::Conversational => "conversational",
997 voirs_sdk::types::SpeakingStyle::News => "news",
998 voirs_sdk::types::SpeakingStyle::Formal => "formal",
999 voirs_sdk::types::SpeakingStyle::Casual => "casual",
1000 voirs_sdk::types::SpeakingStyle::Energetic => "energetic",
1001 voirs_sdk::types::SpeakingStyle::Calm => "calm",
1002 voirs_sdk::types::SpeakingStyle::Dramatic => "dramatic",
1003 voirs_sdk::types::SpeakingStyle::Whisper => "whisper",
1004 };
1005
1006 let mut description_parts = vec![quality_str.to_string()];
1007 description_parts.push(format!("{} style", style_str));
1008
1009 if config.characteristics.emotion_support {
1010 description_parts.push("emotion support".to_string());
1011 }
1012
1013 let description = Some(description_parts.join(", "));
1014
1015 let is_installed = config
1017 .metadata
1018 .get("installed")
1019 .and_then(|v| v.parse::<bool>().ok())
1020 .unwrap_or(false);
1021
1022 VoiceInfo {
1023 id: config.id.clone(),
1024 name: config.name.clone(),
1025 language: config.language.as_str().to_string(),
1026 gender: config
1027 .characteristics
1028 .gender
1029 .as_ref()
1030 .map(|g| g.to_string().to_lowercase()),
1031 description,
1032 is_installed,
1033 }
1034}
1035
1036async fn health_handler(State(state): State<AppState>) -> Json<HealthResponse> {
1038 let uptime_seconds = state.start_time.elapsed().as_secs();
1040
1041 let pipeline_ready = (state.pipeline.list_voices().await).is_ok();
1043
1044 Json(HealthResponse {
1045 status: if pipeline_ready {
1046 "healthy"
1047 } else {
1048 "degraded"
1049 }
1050 .to_string(),
1051 version: env!("CARGO_PKG_VERSION").to_string(),
1052 uptime_seconds,
1053 pipeline_ready,
1054 })
1055}
1056
1057async fn detailed_health_handler(State(state): State<AppState>) -> Json<DetailedHealthResponse> {
1059 let uptime_seconds = state.start_time.elapsed().as_secs();
1060 let timestamp = SystemTime::now()
1061 .duration_since(UNIX_EPOCH)
1062 .unwrap()
1063 .as_secs();
1064
1065 let mut checks = Vec::new();
1066 let mut overall_status = "healthy";
1067
1068 let pipeline_start = Instant::now();
1070 let pipeline_check = match state.pipeline.list_voices().await {
1071 Ok(_) => HealthCheck {
1072 name: "pipeline".to_string(),
1073 status: "healthy".to_string(),
1074 message: Some("Pipeline is operational".to_string()),
1075 duration_ms: pipeline_start.elapsed().as_millis() as u64,
1076 last_checked: timestamp,
1077 },
1078 Err(e) => {
1079 overall_status = "degraded";
1080 HealthCheck {
1081 name: "pipeline".to_string(),
1082 status: "unhealthy".to_string(),
1083 message: Some(format!("Pipeline error: {}", e)),
1084 duration_ms: pipeline_start.elapsed().as_millis() as u64,
1085 last_checked: timestamp,
1086 }
1087 }
1088 };
1089 checks.push(pipeline_check);
1090
1091 let memory_start = Instant::now();
1093 let memory_check = check_memory_health();
1094 checks.push(HealthCheck {
1095 name: "memory".to_string(),
1096 status: memory_check.0,
1097 message: Some(memory_check.1),
1098 duration_ms: memory_start.elapsed().as_millis() as u64,
1099 last_checked: timestamp,
1100 });
1101
1102 let auth_start = Instant::now();
1104 let auth_check = check_auth_health(&state);
1105 checks.push(HealthCheck {
1106 name: "authentication".to_string(),
1107 status: auth_check.0,
1108 message: Some(auth_check.1),
1109 duration_ms: auth_start.elapsed().as_millis() as u64,
1110 last_checked: timestamp,
1111 });
1112
1113 let fs_start = Instant::now();
1115 let fs_check = check_filesystem_health();
1116 checks.push(HealthCheck {
1117 name: "filesystem".to_string(),
1118 status: fs_check.0,
1119 message: Some(fs_check.1),
1120 duration_ms: fs_start.elapsed().as_millis() as u64,
1121 last_checked: timestamp,
1122 });
1123
1124 if checks.iter().any(|c| c.status == "unhealthy") {
1126 overall_status = "unhealthy";
1127 } else if checks.iter().any(|c| c.status == "degraded") {
1128 overall_status = "degraded";
1129 }
1130
1131 let system_health = get_system_health();
1133
1134 Json(DetailedHealthResponse {
1135 status: overall_status.to_string(),
1136 version: env!("CARGO_PKG_VERSION").to_string(),
1137 uptime_seconds,
1138 timestamp,
1139 checks,
1140 system: system_health,
1141 })
1142}
1143
1144async fn readiness_handler(State(state): State<AppState>) -> impl IntoResponse {
1146 let pipeline_ready = (state.pipeline.list_voices().await).is_ok();
1148
1149 let auth_ready = {
1150 let auth_state = state.auth.lock().unwrap();
1151 true };
1153
1154 if pipeline_ready && auth_ready {
1155 (
1156 StatusCode::OK,
1157 Json(serde_json::json!({
1158 "status": "ready",
1159 "message": "Service is ready to serve traffic"
1160 })),
1161 )
1162 } else {
1163 (
1164 StatusCode::SERVICE_UNAVAILABLE,
1165 Json(serde_json::json!({
1166 "status": "not_ready",
1167 "message": "Service is not ready to serve traffic"
1168 })),
1169 )
1170 }
1171}
1172
1173async fn liveness_handler(State(state): State<AppState>) -> impl IntoResponse {
1175 let uptime = state.start_time.elapsed().as_secs();
1177
1178 if uptime > 86400 {
1181 (
1182 StatusCode::SERVICE_UNAVAILABLE,
1183 Json(serde_json::json!({
1184 "status": "unhealthy",
1185 "message": "Service has been running too long, needs restart"
1186 })),
1187 )
1188 } else {
1189 (
1190 StatusCode::OK,
1191 Json(serde_json::json!({
1192 "status": "alive",
1193 "message": "Service is alive and responsive"
1194 })),
1195 )
1196 }
1197}
1198
1199fn check_memory_health() -> (String, String) {
1201 match std::fs::read_to_string("/proc/meminfo") {
1204 Ok(meminfo) => {
1205 let lines: Vec<&str> = meminfo.lines().collect();
1206 let mut total_kb = 0;
1207 let mut available_kb = 0;
1208
1209 for line in lines {
1210 if line.starts_with("MemTotal:") {
1211 if let Some(value) = line.split_whitespace().nth(1) {
1212 total_kb = value.parse().unwrap_or(0);
1213 }
1214 } else if line.starts_with("MemAvailable:") {
1215 if let Some(value) = line.split_whitespace().nth(1) {
1216 available_kb = value.parse().unwrap_or(0);
1217 }
1218 }
1219 }
1220
1221 if total_kb > 0 {
1222 let usage_percent = ((total_kb - available_kb) as f64 / total_kb as f64) * 100.0;
1223 if usage_percent > 90.0 {
1224 (
1225 "unhealthy".to_string(),
1226 format!("High memory usage: {:.1}%", usage_percent),
1227 )
1228 } else if usage_percent > 80.0 {
1229 (
1230 "degraded".to_string(),
1231 format!("Moderate memory usage: {:.1}%", usage_percent),
1232 )
1233 } else {
1234 (
1235 "healthy".to_string(),
1236 format!("Memory usage: {:.1}%", usage_percent),
1237 )
1238 }
1239 } else {
1240 (
1241 "degraded".to_string(),
1242 "Could not determine memory usage".to_string(),
1243 )
1244 }
1245 }
1246 Err(_) => (
1247 "degraded".to_string(),
1248 "Memory information not available".to_string(),
1249 ),
1250 }
1251}
1252
1253fn check_auth_health(state: &AppState) -> (String, String) {
1255 let auth_state = state.auth.lock().unwrap();
1256
1257 let api_key_count = auth_state.api_keys.len();
1258 let active_buckets = auth_state.rate_limits.len();
1259 let log_entries = auth_state.access_logs.len();
1260
1261 if log_entries > 50000 {
1263 return (
1264 "degraded".to_string(),
1265 "Too many access log entries".to_string(),
1266 );
1267 }
1268
1269 if active_buckets > 10000 {
1271 return (
1272 "degraded".to_string(),
1273 "Too many active rate limit buckets".to_string(),
1274 );
1275 }
1276
1277 (
1278 "healthy".to_string(),
1279 format!(
1280 "Auth system operational: {} API keys, {} active buckets",
1281 api_key_count, active_buckets
1282 ),
1283 )
1284}
1285
1286fn check_filesystem_health() -> (String, String) {
1288 let temp_file = "/tmp/voirs_health_check";
1290 match std::fs::write(temp_file, "health check") {
1291 Ok(_) => {
1292 let _ = std::fs::remove_file(temp_file);
1294 ("healthy".to_string(), "Filesystem is writable".to_string())
1295 }
1296 Err(e) => ("unhealthy".to_string(), format!("Filesystem error: {}", e)),
1297 }
1298}
1299
1300fn get_system_health() -> SystemHealth {
1302 let (memory_usage_mb, memory_available_mb) = get_memory_info();
1306 let cpu_usage_percent = get_cpu_usage();
1307 let disk_usage_percent = get_disk_usage();
1308 let thread_count = get_thread_count();
1309 let file_descriptors = get_file_descriptor_count();
1310
1311 SystemHealth {
1312 memory_usage_mb,
1313 memory_available_mb,
1314 cpu_usage_percent,
1315 disk_usage_percent,
1316 thread_count,
1317 file_descriptors,
1318 }
1319}
1320
1321fn get_memory_info() -> (u64, u64) {
1323 match std::fs::read_to_string("/proc/meminfo") {
1324 Ok(meminfo) => {
1325 let lines: Vec<&str> = meminfo.lines().collect();
1326 let mut total_kb = 0;
1327 let mut available_kb = 0;
1328
1329 for line in lines {
1330 if line.starts_with("MemTotal:") {
1331 if let Some(value) = line.split_whitespace().nth(1) {
1332 total_kb = value.parse().unwrap_or(0);
1333 }
1334 } else if line.starts_with("MemAvailable:") {
1335 if let Some(value) = line.split_whitespace().nth(1) {
1336 available_kb = value.parse().unwrap_or(0);
1337 }
1338 }
1339 }
1340
1341 let usage_mb = (total_kb - available_kb) / 1024;
1342 let available_mb = available_kb / 1024;
1343
1344 (usage_mb, available_mb)
1345 }
1346 Err(_) => (0, 0),
1347 }
1348}
1349
1350fn get_cpu_usage() -> f32 {
1352 let cpu_count = num_cpus::get() as f32;
1355
1356 #[cfg(unix)]
1358 {
1359 unsafe {
1360 let mut usage = std::mem::MaybeUninit::<libc::rusage>::uninit();
1361 if libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) == 0 {
1362 let usage = usage.assume_init();
1363 let user_time =
1365 usage.ru_utime.tv_sec as f32 + usage.ru_utime.tv_usec as f32 / 1_000_000.0;
1366 let sys_time =
1367 usage.ru_stime.tv_sec as f32 + usage.ru_stime.tv_usec as f32 / 1_000_000.0;
1368 let total_time = user_time + sys_time;
1369
1370 return ((total_time / 100.0).min(1.0) * 50.0).min(100.0);
1373 }
1374 }
1375 }
1376
1377 25.0
1379}
1380
1381fn get_disk_usage() -> f32 {
1383 #[cfg(target_os = "linux")]
1385 {
1386 use std::ffi::CString;
1388 use std::mem::MaybeUninit;
1389
1390 let path = CString::new("/").unwrap();
1391 unsafe {
1392 let mut stat: libc::statvfs = MaybeUninit::zeroed().assume_init();
1393 if libc::statvfs(path.as_ptr(), &mut stat) == 0 {
1394 let total_blocks = stat.f_blocks;
1395 let free_blocks = stat.f_bfree;
1396 let used_blocks = total_blocks - free_blocks;
1397
1398 if total_blocks > 0 {
1399 return (used_blocks as f32 / total_blocks as f32) * 100.0;
1400 }
1401 }
1402 }
1403 }
1404
1405 #[cfg(target_os = "macos")]
1406 {
1407 use std::ffi::CString;
1409 use std::mem::MaybeUninit;
1410
1411 let path = CString::new("/").unwrap();
1412 unsafe {
1413 let mut stat: libc::statfs = MaybeUninit::zeroed().assume_init();
1414 if libc::statfs(path.as_ptr(), &mut stat) == 0 {
1415 let total_blocks = stat.f_blocks;
1416 let free_blocks = stat.f_bfree;
1417 let used_blocks = total_blocks - free_blocks;
1418
1419 if total_blocks > 0 {
1420 return (used_blocks as f64 / total_blocks as f64 * 100.0) as f32;
1421 }
1422 }
1423 }
1424 }
1425
1426 0.0
1428}
1429
1430fn get_thread_count() -> u64 {
1432 match std::fs::read_to_string("/proc/self/status") {
1433 Ok(status) => {
1434 for line in status.lines() {
1435 if line.starts_with("Threads:") {
1436 if let Some(value) = line.split_whitespace().nth(1) {
1437 return value.parse().unwrap_or(0);
1438 }
1439 }
1440 }
1441 0
1442 }
1443 Err(_) => 0,
1444 }
1445}
1446
1447fn get_file_descriptor_count() -> u64 {
1449 match std::fs::read_dir("/proc/self/fd") {
1450 Ok(entries) => entries.count() as u64,
1451 Err(_) => 0,
1452 }
1453}
1454
1455async fn stats_handler(State(state): State<AppState>) -> Json<ServerStats> {
1457 let auth_state = state.auth.lock().unwrap();
1458 let uptime = state.start_time.elapsed().as_secs();
1459
1460 let mut total_requests = 0u64;
1462 let mut successful_requests = 0u64;
1463 let mut failed_requests = 0u64;
1464 let mut total_audio_seconds = 0.0;
1465
1466 for stats in auth_state.usage_stats.values() {
1467 total_requests += stats.total_requests;
1468 successful_requests += stats.successful_requests;
1469 failed_requests += stats.failed_requests;
1470 total_audio_seconds += stats.total_audio_seconds;
1471 }
1472
1473 let rate_limited_requests = auth_state
1475 .access_logs
1476 .iter()
1477 .filter(|log| log.status_code == 429)
1478 .count() as u64;
1479
1480 let synthesis_logs: Vec<_> = auth_state
1482 .access_logs
1483 .iter()
1484 .filter(|log| log.path == "/api/v1/synthesize" && log.status_code == 200)
1485 .collect();
1486
1487 let average_synthesis_time_ms = if synthesis_logs.is_empty() {
1488 0.0
1489 } else {
1490 synthesis_logs
1491 .iter()
1492 .map(|log| log.response_time_ms as f64)
1493 .sum::<f64>()
1494 / synthesis_logs.len() as f64
1495 };
1496
1497 Json(ServerStats {
1498 requests_total: total_requests,
1499 requests_successful: successful_requests,
1500 requests_failed: failed_requests,
1501 average_synthesis_time_ms,
1502 total_audio_generated_seconds: total_audio_seconds,
1503 uptime_seconds: uptime,
1504 active_api_keys: auth_state.api_keys.len(),
1505 rate_limited_requests,
1506 })
1507}
1508
1509async fn auth_info_handler(
1511 State(state): State<AppState>,
1512 headers: HeaderMap,
1513) -> std::result::Result<Json<AuthInfoResponse>, ApiError> {
1514 let api_key = extract_api_key(&headers).ok_or_else(|| ApiError {
1515 status: StatusCode::UNAUTHORIZED,
1516 message: "API key required".to_string(),
1517 })?;
1518
1519 let auth_state = state.auth.lock().unwrap();
1520
1521 let api_key_config = auth_state.api_keys.get(&api_key).ok_or_else(|| ApiError {
1522 status: StatusCode::UNAUTHORIZED,
1523 message: "Invalid API key".to_string(),
1524 })?;
1525
1526 let rate_limit_key = format!("api_key:{}", api_key);
1528 let bucket = auth_state.rate_limits.get(&rate_limit_key);
1529
1530 let (requests_used, requests_remaining, window_reset_seconds) = if let Some(bucket) = bucket {
1531 let elapsed = bucket.window_start.elapsed().as_secs();
1532 let reset_seconds = 60u64.saturating_sub(elapsed);
1533
1534 (
1535 bucket.requests,
1536 bucket.limit.saturating_sub(bucket.requests),
1537 reset_seconds,
1538 )
1539 } else {
1540 (0, api_key_config.rate_limit, 60)
1541 };
1542
1543 Ok(Json(AuthInfoResponse {
1544 api_key_name: api_key_config.name.clone(),
1545 rate_limit: api_key_config.rate_limit,
1546 requests_remaining,
1547 requests_used,
1548 window_reset_seconds,
1549 }))
1550}
1551
1552async fn usage_stats_handler(
1554 State(state): State<AppState>,
1555 headers: HeaderMap,
1556) -> std::result::Result<Json<UsageStatsResponse>, ApiError> {
1557 let api_key = extract_api_key(&headers).ok_or_else(|| ApiError {
1558 status: StatusCode::UNAUTHORIZED,
1559 message: "API key required".to_string(),
1560 })?;
1561
1562 let auth_state = state.auth.lock().unwrap();
1563
1564 let api_key_config = auth_state.api_keys.get(&api_key).ok_or_else(|| ApiError {
1565 status: StatusCode::UNAUTHORIZED,
1566 message: "Invalid API key".to_string(),
1567 })?;
1568
1569 let stats = auth_state
1570 .usage_stats
1571 .get(&api_key)
1572 .cloned()
1573 .unwrap_or_default();
1574
1575 Ok(Json(UsageStatsResponse {
1576 api_key_name: api_key_config.name.clone(),
1577 stats,
1578 }))
1579}
1580
1581async fn docs_handler() -> impl IntoResponse {
1583 let docs = r#"
1584<!DOCTYPE html>
1585<html>
1586<head>
1587 <title>VoiRS API Documentation</title>
1588 <style>
1589 body { font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; }
1590 .endpoint { background: #f5f5f5; padding: 15px; margin: 10px 0; border-radius: 5px; }
1591 .method { color: white; padding: 3px 8px; border-radius: 3px; font-weight: bold; }
1592 .post { background: #49cc90; }
1593 .get { background: #61affe; }
1594 code { background: #f0f0f0; padding: 2px 4px; border-radius: 3px; }
1595 </style>
1596</head>
1597<body>
1598 <h1>VoiRS Text-to-Speech API</h1>
1599 <p>RESTful API for high-quality speech synthesis.</p>
1600
1601 <div class="endpoint">
1602 <h3><span class="method post">POST</span> /api/v1/synthesize</h3>
1603 <p>Synthesize text to speech audio.</p>
1604 <p><strong>Request Body:</strong></p>
1605 <pre><code>{
1606 "text": "Hello, world!",
1607 "voice": "en-us-female-1",
1608 "rate": 1.0,
1609 "pitch": 0.0,
1610 "volume": 0.0,
1611 "quality": "high",
1612 "format": "wav",
1613 "enhance": false
1614}</code></pre>
1615 <p><strong>Response:</strong></p>
1616 <pre><code>{
1617 "success": true,
1618 "audio_data": "base64-encoded-audio",
1619 "duration": 2.5,
1620 "format": "wav",
1621 "sample_rate": 22050,
1622 "channels": 1
1623}</code></pre>
1624 </div>
1625
1626 <div class="endpoint">
1627 <h3><span class="method get">GET</span> /api/v1/voices</h3>
1628 <p>List available voices.</p>
1629 <p><strong>Query Parameters:</strong></p>
1630 <ul>
1631 <li><code>language</code> - Filter by language (e.g., "en-US")</li>
1632 <li><code>gender</code> - Filter by gender ("male" or "female")</li>
1633 </ul>
1634 </div>
1635
1636 <div class="endpoint">
1637 <h3><span class="method get">GET</span> /api/v1/health</h3>
1638 <p>Health check endpoint.</p>
1639 </div>
1640
1641 <div class="endpoint">
1642 <h3><span class="method get">GET</span> /api/v1/stats</h3>
1643 <p>Server statistics and usage metrics.</p>
1644 </div>
1645
1646 <div class="endpoint">
1647 <h3><span class="method get">GET</span> /api/v1/auth/info</h3>
1648 <p>Get authentication information and rate limit status.</p>
1649 <p><strong>Response:</strong></p>
1650 <pre><code>{
1651 "api_key_name": "Development Key",
1652 "rate_limit": 100,
1653 "requests_remaining": 85,
1654 "requests_used": 15,
1655 "window_reset_seconds": 42
1656}</code></pre>
1657 </div>
1658
1659 <div class="endpoint">
1660 <h3><span class="method get">GET</span> /api/v1/auth/usage</h3>
1661 <p>Get detailed usage statistics for your API key.</p>
1662 <p><strong>Response:</strong></p>
1663 <pre><code>{
1664 "api_key_name": "Development Key",
1665 "stats": {
1666 "total_requests": 1542,
1667 "successful_requests": 1489,
1668 "failed_requests": 53,
1669 "total_audio_seconds": 3847.2,
1670 "bytes_transferred": 15732481,
1671 "last_used": "2024-01-15T10:30:00Z"
1672 }
1673}</code></pre>
1674 </div>
1675
1676 <h2>Authentication</h2>
1677 <p>Most API endpoints require authentication using an API key. Include your API key in requests using one of these methods:</p>
1678 <ul>
1679 <li><strong>Authorization Header:</strong> <code>Authorization: Bearer your-api-key</code></li>
1680 <li><strong>API Key Header:</strong> <code>X-API-Key: your-api-key</code></li>
1681 </ul>
1682 <p><strong>Development API Key:</strong> <code>voirs-dev-key-123</code></p>
1683 <p><strong>Rate Limiting:</strong> Each API key has a rate limit (default: 100 requests per minute). Rate limit status is returned in response headers and available via the <code>/api/v1/auth/info</code> endpoint.</p>
1684
1685 <h2>Audio Formats</h2>
1686 <ul>
1687 <li><strong>wav</strong> - Uncompressed WAV (default)</li>
1688 <li><strong>flac</strong> - Lossless FLAC compression</li>
1689 <li><strong>mp3</strong> - Lossy MP3 compression</li>
1690 <li><strong>opus</strong> - Modern Opus codec</li>
1691 </ul>
1692
1693 <h2>Quality Levels</h2>
1694 <ul>
1695 <li><strong>low</strong> - Fast synthesis, lower quality</li>
1696 <li><strong>medium</strong> - Balanced speed and quality</li>
1697 <li><strong>high</strong> - High quality (default)</li>
1698 <li><strong>ultra</strong> - Maximum quality, slower</li>
1699 </ul>
1700</body>
1701</html>
1702"#;
1703
1704 ([(header::CONTENT_TYPE, "text/html")], docs)
1705}
1706
1707mod base64 {
1709 pub fn encode(data: &[u8]) -> String {
1710 use std::convert::TryInto;
1711 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1712
1713 let mut result = String::new();
1714 let chunks = data.chunks_exact(3);
1715 let remainder = chunks.remainder();
1716
1717 for chunk in chunks {
1718 let b = u32::from_be_bytes([0, chunk[0], chunk[1], chunk[2]]);
1719 result.push(ALPHABET[((b >> 18) & 63) as usize] as char);
1720 result.push(ALPHABET[((b >> 12) & 63) as usize] as char);
1721 result.push(ALPHABET[((b >> 6) & 63) as usize] as char);
1722 result.push(ALPHABET[(b & 63) as usize] as char);
1723 }
1724
1725 match remainder.len() {
1726 1 => {
1727 let b = (remainder[0] as u32) << 16;
1728 result.push(ALPHABET[((b >> 18) & 63) as usize] as char);
1729 result.push(ALPHABET[((b >> 12) & 63) as usize] as char);
1730 result.push_str("==");
1731 }
1732 2 => {
1733 let b = ((remainder[0] as u32) << 16) | ((remainder[1] as u32) << 8);
1734 result.push(ALPHABET[((b >> 18) & 63) as usize] as char);
1735 result.push(ALPHABET[((b >> 12) & 63) as usize] as char);
1736 result.push(ALPHABET[((b >> 6) & 63) as usize] as char);
1737 result.push('=');
1738 }
1739 _ => {}
1740 }
1741
1742 result
1743 }
1744}
1745
1746#[cfg(test)]
1747mod tests {
1748 use super::*;
1749
1750 #[test]
1751 fn test_synthesis_request_validation() {
1752 let request = SynthesisRequest {
1753 text: "Hello, world!".to_string(),
1754 voice: Some("en-us-female-1".to_string()),
1755 rate: Some(1.0),
1756 pitch: Some(0.0),
1757 volume: Some(0.0),
1758 quality: Some("high".to_string()),
1759 format: Some("wav".to_string()),
1760 enhance: Some(false),
1761 };
1762
1763 assert_eq!(request.text, "Hello, world!");
1764 assert_eq!(request.voice, Some("en-us-female-1".to_string()));
1765 assert_eq!(request.rate, Some(1.0));
1766 }
1767
1768 #[test]
1769 fn test_voice_info_creation() {
1770 let voice = VoiceInfo {
1771 id: "test-voice".to_string(),
1772 name: "Test Voice".to_string(),
1773 language: "en-US".to_string(),
1774 gender: Some("female".to_string()),
1775 description: Some("A test voice".to_string()),
1776 is_installed: true,
1777 };
1778
1779 assert_eq!(voice.id, "test-voice");
1780 assert_eq!(voice.language, "en-US");
1781 assert!(voice.is_installed);
1782 }
1783
1784 #[test]
1785 fn test_base64_encoding() {
1786 let data = b"Hello, world!";
1787 let encoded = base64::encode(data);
1788 assert!(!encoded.is_empty());
1789 assert!(encoded.is_ascii());
1790 }
1791}