use crate::{error::Result, traits::VoiceManager, voice::DefaultVoiceManager, VoirsError};
use super::builder_impl::VoirsPipelineBuilder;
impl VoirsPipelineBuilder {
pub(super) async fn validate(&self) -> Result<()> {
if !self.validation_enabled {
return Ok(());
}
tracing::debug!("Validating pipeline configuration");
self.validate_device()?;
if let Some(voice_id) = &self.voice_id {
self.validate_voice(voice_id).await?;
}
self.validate_synthesis_config()?;
self.validate_cache_directory()?;
self.validate_component_compatibility()?;
self.validate_resource_requirements()?;
self.validate_advanced_features()?;
tracing::debug!("Pipeline configuration validation completed successfully");
Ok(())
}
fn validate_device(&self) -> Result<()> {
tracing::debug!("Validating device configuration: {}", self.config.device);
if self.config.use_gpu && self.config.device == "cpu" {
return Err(VoirsError::InvalidConfiguration {
field: "device".to_string(),
value: self.config.device.clone(),
reason: "GPU acceleration enabled but device is set to CPU".to_string(),
valid_values: None,
});
}
if self.config.device.starts_with("cuda") && !self.is_cuda_available() {
tracing::warn!("CUDA device specified but CUDA is not available, falling back to CPU");
}
if self.config.device.starts_with("mps") && !self.is_mps_available() {
tracing::warn!("MPS device specified but Metal Performance Shaders is not available");
}
if !self.is_valid_device_format(&self.config.device) {
return Err(VoirsError::UnsupportedDevice {
device: self.config.device.clone(),
});
}
Ok(())
}
async fn validate_voice(&self, voice_id: &str) -> Result<()> {
tracing::debug!("Validating voice configuration: {}", voice_id);
let cache_dir = self.config.effective_cache_dir();
let voice_manager = DefaultVoiceManager::new(&cache_dir);
if !voice_manager.is_voice_available(voice_id) {
if self.auto_download {
tracing::info!(
"Voice '{}' not available locally, will download during build",
voice_id
);
if !cfg!(test) {
if !self.is_voice_available_remotely(voice_id).await {
let available_voices: Vec<String> = voice_manager
.list_voices()
.await?
.into_iter()
.map(|v| v.id)
.collect();
return Err(VoirsError::VoiceNotFound {
voice: voice_id.to_string(),
available: available_voices.clone(),
suggestions: available_voices.into_iter().take(3).collect(),
});
}
} else {
tracing::debug!("Skipping remote voice availability check in test mode");
}
} else {
let available_voices: Vec<String> = voice_manager
.list_voices()
.await?
.into_iter()
.map(|v| v.id)
.collect();
return Err(VoirsError::VoiceNotFound {
voice: voice_id.to_string(),
available: available_voices.clone(),
suggestions: available_voices.into_iter().take(3).collect(),
});
}
}
self.validate_voice_compatibility(voice_id)?;
Ok(())
}
fn validate_synthesis_config(&self) -> Result<()> {
tracing::debug!("Validating synthesis configuration");
let config = &self.config.default_synthesis;
if !(0.5..=2.0).contains(&config.speaking_rate) {
return Err(VoirsError::InvalidConfiguration {
field: "speaking_rate".to_string(),
value: config.speaking_rate.to_string(),
reason: "Speaking rate must be between 0.5 and 2.0".to_string(),
valid_values: None,
});
}
if !(-12.0..=12.0).contains(&config.pitch_shift) {
return Err(VoirsError::InvalidConfiguration {
field: "pitch_shift".to_string(),
value: config.pitch_shift.to_string(),
reason: "Pitch shift must be between -12.0 and 12.0 semitones".to_string(),
valid_values: None,
});
}
if !(-20.0..=20.0).contains(&config.volume_gain) {
return Err(VoirsError::InvalidConfiguration {
field: "volume_gain".to_string(),
value: config.volume_gain.to_string(),
reason: "Volume gain must be between -20.0 and 20.0 dB".to_string(),
valid_values: None,
});
}
if ![8000, 16000, 22050, 44100, 48000].contains(&config.sample_rate) {
tracing::warn!("Unusual sample rate: {}Hz", config.sample_rate);
}
if let Some(chunk_size) = config.streaming_chunk_size {
if chunk_size == 0 || chunk_size > 1000 {
return Err(VoirsError::InvalidConfiguration {
field: "streaming_chunk_size".to_string(),
value: chunk_size.to_string(),
reason: "Streaming chunk size must be between 1 and 1000 words".to_string(),
valid_values: None,
});
}
}
Ok(())
}
fn validate_cache_directory(&self) -> Result<()> {
tracing::debug!("Validating cache directory configuration");
let cache_dir = self.config.effective_cache_dir();
if let Some(parent) = cache_dir.parent() {
if !parent.exists() {
return Err(VoirsError::InvalidConfiguration {
field: "cache_dir".to_string(),
value: cache_dir.display().to_string(),
reason: format!(
"Cache directory parent does not exist: {}",
parent.display()
),
valid_values: None,
});
}
}
if self.config.max_cache_size_mb == 0 {
return Err(VoirsError::InvalidConfiguration {
field: "max_cache_size_mb".to_string(),
value: self.config.max_cache_size_mb.to_string(),
reason: "Cache size must be greater than 0".to_string(),
valid_values: None,
});
}
if self.config.max_cache_size_mb > 10_240 {
tracing::warn!(
"Very large cache size configured: {}MB",
self.config.max_cache_size_mb
);
}
Ok(())
}
fn validate_component_compatibility(&self) -> Result<()> {
tracing::debug!("Validating component compatibility");
if self.custom_g2p.is_some() && self.custom_acoustic.is_some() {
self.validate_g2p_acoustic_compatibility()?;
}
if self.custom_acoustic.is_some() && self.custom_vocoder.is_some() {
self.validate_acoustic_vocoder_compatibility()?;
}
if self.config.use_gpu {
self.validate_gpu_configuration()?;
}
self.validate_quality_compatibility()?;
Ok(())
}
fn validate_g2p_acoustic_compatibility(&self) -> Result<()> {
tracing::debug!("Validating G2P and acoustic model compatibility");
let target_language = self.config.default_synthesis.language;
if let Some(g2p) = &self.custom_g2p {
let supported_languages = g2p.supported_languages();
if !supported_languages.contains(&target_language) {
return Err(VoirsError::ComponentSynchronizationFailed {
component: "G2P".to_string(),
reason: format!(
"G2P does not support target language {target_language:?}. Supported: {supported_languages:?}"
),
});
}
}
tracing::debug!("G2P and acoustic model appear compatible");
Ok(())
}
fn validate_acoustic_vocoder_compatibility(&self) -> Result<()> {
tracing::debug!("Validating acoustic model and vocoder compatibility");
if let (Some(acoustic), Some(vocoder)) = (&self.custom_acoustic, &self.custom_vocoder) {
let acoustic_meta = acoustic.metadata();
let vocoder_meta = vocoder.metadata();
if acoustic_meta.sample_rate != vocoder_meta.sample_rate {
return Err(VoirsError::ComponentSynchronizationFailed {
component: "Acoustic-Vocoder".to_string(),
reason: format!(
"Sample rate mismatch: Acoustic={}, Vocoder={}",
acoustic_meta.sample_rate, vocoder_meta.sample_rate
),
});
}
if acoustic_meta.mel_channels != vocoder_meta.mel_channels {
return Err(VoirsError::ComponentSynchronizationFailed {
component: "Acoustic-Vocoder".to_string(),
reason: format!(
"Mel channel mismatch: Acoustic={}, Vocoder={}",
acoustic_meta.mel_channels, vocoder_meta.mel_channels
),
});
}
tracing::debug!("Acoustic model and vocoder are compatible");
}
Ok(())
}
fn validate_gpu_configuration(&self) -> Result<()> {
tracing::debug!("Validating GPU configuration");
if let Some(threads) = self.config.num_threads {
if threads > 16 {
tracing::warn!(
"High thread count ({}) with GPU acceleration may not improve performance",
threads
);
}
}
let estimated_gpu_memory = self.estimate_gpu_memory_usage();
if estimated_gpu_memory > 8192 {
tracing::warn!(
"High GPU memory usage estimated: {}MB. Consider using lower quality settings.",
estimated_gpu_memory
);
}
if self.config.device.starts_with("cuda") && !self.is_cuda_available() {
return Err(VoirsError::DeviceNotAvailable {
device: self.config.device.clone(),
alternatives: vec!["cpu".to_string(), "auto".to_string()],
});
}
if self.config.device.starts_with("mps") && !self.is_mps_available() {
return Err(VoirsError::DeviceNotAvailable {
device: self.config.device.clone(),
alternatives: vec!["cpu".to_string(), "auto".to_string()],
});
}
Ok(())
}
fn validate_quality_compatibility(&self) -> Result<()> {
tracing::debug!("Validating quality level compatibility");
let quality = self.config.default_synthesis.quality;
if let Some(chunk_size) = self.config.default_synthesis.streaming_chunk_size {
match quality {
crate::types::QualityLevel::Ultra if chunk_size < 100 => {
tracing::warn!(
"Ultra quality with small chunk size ({}) may cause artifacts",
chunk_size
);
}
crate::types::QualityLevel::Low if chunk_size > 500 => {
tracing::warn!(
"Low quality with large chunk size ({}) may not improve latency",
chunk_size
);
}
_ => {}
}
}
if !self.config.use_gpu && quality == crate::types::QualityLevel::Ultra {
tracing::warn!("Ultra quality without GPU acceleration may be very slow");
}
Ok(())
}
fn estimate_gpu_memory_usage(&self) -> u32 {
let mut memory_mb = 512;
match self.config.default_synthesis.quality {
crate::types::QualityLevel::Ultra => memory_mb += 3072, crate::types::QualityLevel::High => memory_mb += 2048, crate::types::QualityLevel::Medium => memory_mb += 1024, crate::types::QualityLevel::Low => memory_mb += 512, }
if self.custom_acoustic.is_some() {
memory_mb += 1024; }
if self.custom_vocoder.is_some() {
memory_mb += 512; }
memory_mb
}
fn validate_resource_requirements(&self) -> Result<()> {
tracing::debug!("Validating resource requirements");
let estimated_memory_mb = self.estimate_memory_usage();
if estimated_memory_mb > 8192 {
tracing::warn!(
"High memory usage estimated: {}MB. Consider using lower quality settings.",
estimated_memory_mb
);
}
if let Some(threads) = self.config.num_threads {
let max_threads = std::thread::available_parallelism()
.map(|p| p.get())
.unwrap_or(1);
if threads > max_threads * 2 {
tracing::warn!(
"Thread count ({}) exceeds 2x available parallelism ({})",
threads,
max_threads
);
}
}
Ok(())
}
fn is_cuda_available(&self) -> bool {
if !self.validation_enabled || cfg!(test) {
tracing::debug!("Skipping CUDA availability check in test/no-validation mode");
return false;
}
if let Ok(output) = std::process::Command::new("nvidia-smi")
.arg("--query-gpu=count")
.arg("--format=csv,noheader,nounits")
.output()
{
if output.status.success() {
if let Ok(count_str) = String::from_utf8(output.stdout) {
if let Ok(gpu_count) = count_str.trim().parse::<u32>() {
tracing::debug!("Found {} CUDA GPU(s)", gpu_count);
return gpu_count > 0;
}
}
}
}
#[cfg(unix)]
{
let cuda_lib_paths = [
"/usr/local/cuda/lib64/libcudart.so",
"/usr/lib/x86_64-linux-gnu/libcudart.so",
"/opt/cuda/lib64/libcudart.so",
];
for path in &cuda_lib_paths {
if std::path::Path::new(path).exists() {
tracing::debug!("Found CUDA runtime library at {}", path);
return true;
}
}
}
#[cfg(windows)]
{
if let Ok(cuda_path) = std::env::var("CUDA_PATH") {
let cudart_path = std::path::Path::new(&cuda_path)
.join("bin")
.join("cudart64_*.dll");
if let Ok(entries) = glob::glob(&cudart_path.to_string_lossy()) {
if entries.count() > 0 {
tracing::debug!("Found CUDA runtime on Windows");
return true;
}
}
}
}
if std::env::var("CUDA_VISIBLE_DEVICES").is_ok() {
tracing::debug!("CUDA_VISIBLE_DEVICES environment variable found");
return true;
}
tracing::debug!("CUDA not detected");
false
}
fn is_mps_available(&self) -> bool {
if !self.validation_enabled || cfg!(test) {
tracing::debug!("Skipping MPS availability check in test/no-validation mode");
return false;
}
#[cfg(target_os = "macos")]
{
if cfg!(target_arch = "aarch64") {
tracing::debug!("MPS available on Apple Silicon");
return true;
}
if let Ok(output) = std::process::Command::new("sw_vers")
.args(["-productVersion"])
.output()
{
if let Ok(version_str) = String::from_utf8(output.stdout) {
let version = version_str.trim();
let parts: Vec<u32> =
version.split('.').filter_map(|s| s.parse().ok()).collect();
if parts.len() >= 2 {
let major = parts[0];
let minor = parts[1];
if major > 12 || (major == 12 && minor >= 3) {
tracing::debug!("MPS potentially available on macOS {}", version);
return true;
}
}
}
}
}
tracing::debug!("MPS not available");
false
}
fn is_valid_device_format(&self, device: &str) -> bool {
matches!(device, "cpu" | "cuda" | "mps" | "auto")
|| device.starts_with("cuda:")
|| device.starts_with("mps:")
}
fn validate_voice_compatibility(&self, voice_id: &str) -> Result<()> {
if voice_id.is_empty() {
return Err(VoirsError::invalid_configuration_legacy(
"voice_id".to_string(),
voice_id.to_string(),
"Voice ID cannot be empty".to_string(),
));
}
if !voice_id
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(VoirsError::invalid_configuration_legacy(
"voice_id".to_string(),
voice_id.to_string(),
"Must contain only alphanumeric characters, hyphens, and underscores".to_string(),
));
}
if voice_id.len() > 64 {
return Err(VoirsError::invalid_configuration_legacy(
"voice_id".to_string(),
voice_id.to_string(),
"Voice ID is too long (max 64 characters)".to_string(),
));
}
if let Some((lang_part, _voice_part)) = voice_id.split_once('_') {
if lang_part.len() < 2 || lang_part.len() > 3 {
tracing::warn!(
"Voice ID '{}' may have invalid language code format",
voice_id
);
}
}
let device = &self.config.device;
if device == "cuda" && !self.is_cuda_available() {
return Err(VoirsError::device_not_available_legacy(format!(
"CUDA required for voice '{voice_id}' but not available"
)));
}
if device == "mps" && !self.is_mps_available() {
return Err(VoirsError::device_not_available_legacy(format!(
"MPS required for voice '{voice_id}' but not available"
)));
}
tracing::debug!("Voice '{}' passed compatibility validation", voice_id);
Ok(())
}
fn estimate_memory_usage(&self) -> u32 {
let mut memory_mb = 512;
match self.config.default_synthesis.quality {
crate::types::QualityLevel::Low => memory_mb += 256,
crate::types::QualityLevel::Medium => memory_mb += 512,
crate::types::QualityLevel::High => memory_mb += 1024,
crate::types::QualityLevel::Ultra => memory_mb += 2048,
}
memory_mb += self.config.max_cache_size_mb;
if self.config.use_gpu {
memory_mb += 512;
}
memory_mb
}
fn validate_advanced_features(&self) -> Result<()> {
tracing::debug!("Validating advanced features configuration");
self.validate_emotion_control()?;
self.validate_voice_cloning()?;
self.validate_voice_conversion()?;
self.validate_singing_synthesis()?;
self.validate_spatial_audio()?;
Ok(())
}
fn validate_emotion_control(&self) -> Result<()> {
#[cfg(feature = "emotion")]
{
let config = &self.config.default_synthesis;
if config.enable_emotion {
tracing::debug!("Validating emotion control configuration");
if !(0.0..=1.0).contains(&config.emotion_intensity) {
return Err(VoirsError::emotion_intensity_out_of_range(
config.emotion_intensity,
));
}
if let Some(emotion_type) = &config.emotion_type {
let supported_emotions = vec![
"happy",
"sad",
"angry",
"calm",
"excited",
"neutral",
"surprised",
"disgusted",
"fearful",
"contempt",
"energetic",
"relaxed",
];
if !supported_emotions.contains(&emotion_type.as_str()) {
return Err(VoirsError::emotion_not_supported(
emotion_type.clone(),
supported_emotions
.into_iter()
.map(|s| s.to_string())
.collect(),
));
}
}
if let Some(preset) = &config.emotion_preset {
let supported_presets =
vec!["happy", "sad", "angry", "calm", "excited", "neutral"];
if !supported_presets.contains(&preset.as_str()) {
return Err(VoirsError::EmotionConfigurationInvalid {
reason: format!(
"Invalid emotion preset '{}'. Supported presets: {:?}",
preset, supported_presets
),
});
}
}
}
}
Ok(())
}
fn validate_voice_cloning(&self) -> Result<()> {
#[cfg(feature = "cloning")]
{
let config = &self.config.default_synthesis;
if config.enable_cloning {
tracing::debug!("Validating voice cloning configuration");
if !(0.0..=1.0).contains(&config.cloning_quality) {
return Err(VoirsError::voice_cloning_quality_too_low(
config.cloning_quality,
0.0,
));
}
if let Some(method) = &config.cloning_method {
let supported_methods = vec!["quick_clone", "deep_clone", "adaptive_clone"];
let method_str = format!("{:?}", method).to_lowercase();
if !supported_methods.iter().any(|&m| method_str.contains(m)) {
return Err(VoirsError::VoiceCloningMethodNotSupported {
method: method_str,
supported: supported_methods
.into_iter()
.map(|s| s.to_string())
.collect(),
});
}
}
if !self.config.use_gpu && config.cloning_quality > 0.8 {
tracing::warn!(
"High-quality voice cloning without GPU acceleration may be very slow"
);
}
}
}
Ok(())
}
fn validate_voice_conversion(&self) -> Result<()> {
#[cfg(feature = "conversion")]
{
let config = &self.config.default_synthesis;
if config.enable_conversion {
tracing::debug!("Validating voice conversion configuration");
if let Some(target) = &config.conversion_target {
match target {
crate::builder::features::ConversionTarget::Gender(gender) => {
tracing::debug!("Gender conversion to {:?} is supported", gender);
}
crate::builder::features::ConversionTarget::Age(age) => {
tracing::debug!("Age conversion to {:?} is supported", age);
}
crate::builder::features::ConversionTarget::Voice(voice_id) => {
if voice_id.is_empty() {
return Err(VoirsError::voice_conversion_target_invalid(
"Target voice ID cannot be empty".to_string(),
voice_id.clone(),
));
}
}
}
}
if config.realtime_conversion {
if config.quality == crate::types::QualityLevel::Ultra {
tracing::warn!(
"Ultra quality with real-time conversion may cause latency issues"
);
}
if !self.config.use_gpu {
tracing::warn!("Real-time voice conversion without GPU acceleration may not meet real-time constraints");
}
}
}
}
Ok(())
}
fn validate_singing_synthesis(&self) -> Result<()> {
#[cfg(feature = "singing")]
{
let config = &self.config.default_synthesis;
if config.enable_singing {
tracing::debug!("Validating singing synthesis configuration");
if let Some(voice_type) = &config.singing_voice_type {
let supported_types = vec![
"pop_vocalist",
"opera_singer",
"jazz_vocalist",
"rock_vocalist",
"choir",
];
let voice_type_str = format!("{:?}", voice_type).to_lowercase();
if !supported_types.iter().any(|&t| voice_type_str.contains(t)) {
return Err(VoirsError::SingingTechniqueNotSupported {
technique: voice_type_str,
supported: supported_types.into_iter().map(|s| s.to_string()).collect(),
});
}
}
if let Some(technique) = &config.singing_technique {
if !(0.0..=1.0).contains(&technique.breath_control) {
return Err(VoirsError::SingingError {
message: format!(
"Breath control {} is out of range (0.0-1.0)",
technique.breath_control
),
voice_type: config
.singing_voice_type
.as_ref()
.map(|v| format!("{:?}", v)),
});
}
if !(0.0..=1.0).contains(&technique.vibrato_depth) {
return Err(VoirsError::SingingError {
message: format!(
"Vibrato depth {} is out of range (0.0-1.0)",
technique.vibrato_depth
),
voice_type: config
.singing_voice_type
.as_ref()
.map(|v| format!("{:?}", v)),
});
}
if !(0.0..=1.0).contains(&technique.vocal_fry) {
return Err(VoirsError::SingingError {
message: format!(
"Vocal fry {} is out of range (0.0-1.0)",
technique.vocal_fry
),
voice_type: config
.singing_voice_type
.as_ref()
.map(|v| format!("{:?}", v)),
});
}
if !(0.0..=1.0).contains(&technique.head_voice_ratio) {
return Err(VoirsError::SingingError {
message: format!(
"Head voice ratio {} is out of range (0.0-1.0)",
technique.head_voice_ratio
),
voice_type: config
.singing_voice_type
.as_ref()
.map(|v| format!("{:?}", v)),
});
}
}
if let Some(tempo) = config.tempo {
if !(40.0..=300.0).contains(&tempo) {
return Err(VoirsError::tempo_out_of_range(tempo, 40.0, 300.0));
}
}
if let Some(_key) = &config.musical_key {
tracing::debug!("Musical key validation passed");
}
}
}
Ok(())
}
fn validate_spatial_audio(&self) -> Result<()> {
#[cfg(feature = "spatial")]
{
let config = &self.config.default_synthesis;
if config.enable_spatial {
tracing::debug!("Validating 3D spatial audio configuration");
if let Some(position) = &config.listener_position {
if position.x.abs() > 1000.0
|| position.y.abs() > 1000.0
|| position.z.abs() > 1000.0
{
return Err(VoirsError::position_3d_invalid(
position.x,
position.y,
position.z,
"Position coordinates exceed reasonable limits (±1000.0)".to_string(),
));
}
if !position.x.is_finite() || !position.y.is_finite() || !position.z.is_finite()
{
return Err(VoirsError::position_3d_invalid(
position.x,
position.y,
position.z,
"Position coordinates must be finite numbers".to_string(),
));
}
}
if let Some(crate::builder::features::RoomSize::Huge) = &config.room_size {
if !self.config.use_gpu {
tracing::warn!("Large room acoustics without GPU acceleration may be computationally expensive");
}
}
if !(0.0..=1.0).contains(&config.reverb_level) {
return Err(VoirsError::room_acoustics_configuration_invalid(format!(
"Reverb level {} is out of range (0.0-1.0)",
config.reverb_level
)));
}
if config.hrtf_enabled && config.sample_rate < 44100 {
tracing::warn!("HRTF processing works best with sample rates >= 44.1kHz");
}
if !self.config.use_gpu && config.hrtf_enabled {
tracing::warn!(
"HRTF processing without GPU acceleration may introduce latency"
);
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::{builder::builder_impl::VoirsPipelineBuilder, types::QualityLevel};
#[tokio::test]
async fn test_device_validation() {
let cpu_builder = VoirsPipelineBuilder::new()
.with_gpu_acceleration(false)
.with_device("cpu");
assert!(cpu_builder.validate_device().is_ok());
let invalid_builder = VoirsPipelineBuilder::new()
.with_gpu_acceleration(true)
.with_device("cpu");
assert!(invalid_builder.validate_device().is_err());
let gpu_builder = VoirsPipelineBuilder::new()
.with_gpu_acceleration(true)
.with_device("cuda");
assert!(gpu_builder.validate_device().is_ok());
let invalid_device = VoirsPipelineBuilder::new().with_device("invalid-device");
assert!(invalid_device.validate_device().is_err());
}
#[test]
fn test_synthesis_config_validation() {
let valid_builder = VoirsPipelineBuilder::new()
.with_speaking_rate(1.0)
.with_pitch_shift(0.0)
.with_volume_gain(0.0)
.with_sample_rate(22050);
assert!(valid_builder.validate_synthesis_config().is_ok());
let invalid_rate = VoirsPipelineBuilder::new().with_speaking_rate(3.0); assert!(invalid_rate.validate_synthesis_config().is_err());
let invalid_pitch = VoirsPipelineBuilder::new().with_pitch_shift(15.0); assert!(invalid_pitch.validate_synthesis_config().is_err());
let invalid_volume = VoirsPipelineBuilder::new().with_volume_gain(25.0); assert!(invalid_volume.validate_synthesis_config().is_err());
}
#[test]
fn test_cache_directory_validation() {
let valid_builder = VoirsPipelineBuilder::new().with_cache_size(1024);
assert!(valid_builder.validate_cache_directory().is_ok());
let mut invalid_builder = VoirsPipelineBuilder::new();
invalid_builder.config.max_cache_size_mb = 0;
assert!(invalid_builder.validate_cache_directory().is_err());
}
#[test]
fn test_memory_estimation() {
let low_memory = VoirsPipelineBuilder::new()
.with_quality(QualityLevel::Low)
.with_cache_size(256)
.with_gpu_acceleration(false);
let high_memory = VoirsPipelineBuilder::new()
.with_quality(QualityLevel::Ultra)
.with_cache_size(2048)
.with_gpu_acceleration(true);
assert!(low_memory.estimate_memory_usage() < high_memory.estimate_memory_usage());
}
#[test]
fn test_device_format_validation() {
let builder = VoirsPipelineBuilder::new();
assert!(builder.is_valid_device_format("cpu"));
assert!(builder.is_valid_device_format("cuda"));
assert!(builder.is_valid_device_format("mps"));
assert!(builder.is_valid_device_format("cuda:0"));
assert!(builder.is_valid_device_format("mps:0"));
assert!(!builder.is_valid_device_format("invalid"));
assert!(!builder.is_valid_device_format(""));
}
}