pub mod recovery;
pub mod reporting;
pub mod types;
pub use types::{
AudioBufferErrorType, AudioBufferInfo, ContextResult, ErrorContext, ErrorSeverity,
ErrorWithContext, IoOperation, ModelType, QualityMetrics, Result, SynthesisStage, VoirsError,
};
pub use recovery::{
CircuitBreaker, CircuitBreakerConfig, CircuitBreakerError, CircuitState, ErrorRecoveryManager,
RecoveryContext, RecoveryStrategy,
};
pub use reporting::{
ConsoleErrorListener, DiagnosticReport, ErrorCategory, ErrorListener, ErrorReport,
ErrorReporter, ErrorReporterConfig, ErrorStatistics, FileErrorListener, PerformanceImpact,
RuntimeInfo, SystemContext, SystemHealth,
};
static GLOBAL_ERROR_REPORTER: std::sync::OnceLock<ErrorReporter> = std::sync::OnceLock::new();
pub fn init_global_error_reporter(config: ErrorReporterConfig) {
let _ = GLOBAL_ERROR_REPORTER.set(ErrorReporter::new(config));
}
pub fn get_global_error_reporter() -> Option<&'static ErrorReporter> {
GLOBAL_ERROR_REPORTER.get()
}
pub fn report_global_error(error: &VoirsError, context: Option<&str>) {
if let Some(reporter) = get_global_error_reporter() {
reporter.report_error(error, context);
}
}
#[macro_export]
macro_rules! voirs_error {
(synthesis_failed: $text:expr, $cause:expr) => {
$crate::error::VoirsError::synthesis_failed($text, $cause)
};
(device_error: $device:expr, $message:expr) => {
$crate::error::VoirsError::device_error($device, $message)
};
(config_error: $message:expr) => {
$crate::error::VoirsError::config_error($message)
};
(model_error: $message:expr) => {
$crate::error::VoirsError::model_error($message)
};
(audio_error: $message:expr) => {
$crate::error::VoirsError::audio_error($message)
};
(g2p_error: $message:expr) => {
$crate::error::VoirsError::g2p_error($message)
};
(timeout: $message:expr) => {
$crate::error::VoirsError::timeout($message)
};
(internal: $component:expr, $message:expr) => {
$crate::error::VoirsError::InternalError {
component: $component.to_string(),
message: $message.to_string(),
}
};
}
#[macro_export]
macro_rules! with_recovery {
($manager:expr, $component:expr, $operation:expr) => {
$manager
.execute_with_recovery($component, || Box::pin(async move { $operation }))
.await
};
}
#[macro_export]
macro_rules! report_error {
($error:expr) => {
$crate::error::report_global_error(&$error, None);
};
($error:expr, $context:expr) => {
$crate::error::report_global_error(&$error, Some($context));
};
}
pub trait ResultExt<T> {
fn report_on_error(self, context: Option<&str>) -> Self;
fn report_and_convert<E, F>(self, context: Option<&str>, f: F) -> std::result::Result<T, E>
where
F: FnOnce(VoirsError) -> E;
#[allow(clippy::result_large_err)]
fn with_error_context(
self,
component: impl Into<String>,
operation: impl Into<String>,
) -> ContextResult<T>;
}
impl<T> ResultExt<T> for Result<T> {
fn report_on_error(self, context: Option<&str>) -> Self {
if let Err(ref error) = self {
report_global_error(error, context);
}
self
}
fn report_and_convert<E, F>(self, context: Option<&str>, f: F) -> std::result::Result<T, E>
where
F: FnOnce(VoirsError) -> E,
{
match self {
Ok(value) => Ok(value),
Err(error) => {
report_global_error(&error, context);
Err(f(error))
}
}
}
fn with_error_context(
self,
component: impl Into<String>,
operation: impl Into<String>,
) -> ContextResult<T> {
match self {
Ok(value) => Ok(value),
Err(error) => Err(error.with_context(component, operation)),
}
}
}
pub trait VoirsErrorExt {
fn is_permanent(&self) -> bool;
fn is_temporary(&self) -> bool;
fn is_user_error(&self) -> bool;
fn is_resource_error(&self) -> bool;
fn recommended_retry_delay(&self) -> Option<std::time::Duration>;
}
impl VoirsErrorExt for VoirsError {
fn is_permanent(&self) -> bool {
matches!(
self,
VoirsError::UnsupportedDevice { .. }
| VoirsError::ModelNotFound { .. }
| VoirsError::FileCorrupted { .. }
| VoirsError::LanguageNotSupported { .. }
| VoirsError::NotImplemented { .. }
)
}
fn is_temporary(&self) -> bool {
!self.is_permanent() && self.is_recoverable()
}
fn is_user_error(&self) -> bool {
matches!(
self,
VoirsError::VoiceNotFound { .. }
| VoirsError::InvalidConfiguration { .. }
| VoirsError::ConfigError { .. }
)
}
fn is_resource_error(&self) -> bool {
matches!(
self,
VoirsError::OutOfMemory { .. }
| VoirsError::GpuOutOfMemory { .. }
| VoirsError::ResourceExhausted { .. }
)
}
fn recommended_retry_delay(&self) -> Option<std::time::Duration> {
use std::time::Duration;
if !self.is_recoverable() {
return None;
}
Some(match self {
VoirsError::NetworkError { .. } => Duration::from_secs(1),
VoirsError::TimeoutError { .. } => Duration::from_millis(500),
VoirsError::DeviceError { .. } => Duration::from_millis(100),
VoirsError::ModelError { .. } => Duration::from_secs(2),
VoirsError::OutOfMemory { .. } => Duration::from_secs(5),
_ => Duration::from_millis(200),
})
}
}
impl From<std::io::Error> for VoirsError {
fn from(err: std::io::Error) -> Self {
Self::IoError {
path: std::path::PathBuf::from("unknown"),
operation: IoOperation::Read,
source: err,
}
}
}
impl VoirsError {
pub fn invalid_configuration_legacy(field: String, value: String, reason: String) -> Self {
Self::InvalidConfiguration {
field,
value,
reason,
valid_values: None,
}
}
pub fn device_not_available_legacy(device: String) -> Self {
Self::DeviceNotAvailable {
device,
alternatives: Vec::new(),
}
}
pub fn voice_not_found_legacy(voice: String, available: Vec<String>) -> Self {
Self::VoiceNotFound {
voice,
available: available.clone(),
suggestions: available.into_iter().take(3).collect(),
}
}
}
impl From<serde_json::Error> for VoirsError {
fn from(err: serde_json::Error) -> Self {
Self::SerializationError {
format: "JSON".to_string(),
message: err.to_string(),
}
}
}
impl From<toml::de::Error> for VoirsError {
fn from(err: toml::de::Error) -> Self {
Self::SerializationError {
format: "TOML".to_string(),
message: err.to_string(),
}
}
}
impl From<toml::ser::Error> for VoirsError {
fn from(err: toml::ser::Error) -> Self {
Self::SerializationError {
format: "TOML".to_string(),
message: err.to_string(),
}
}
}
impl From<hf_hub::api::sync::ApiError> for VoirsError {
fn from(err: hf_hub::api::sync::ApiError) -> Self {
Self::NetworkError {
message: format!("HuggingFace Hub API error: {err}"),
retry_count: 0,
max_retries: 3,
source: Some(Box::new(err)),
}
}
}
impl From<voirs_acoustic::AcousticError> for VoirsError {
fn from(err: voirs_acoustic::AcousticError) -> Self {
Self::SynthesisFailed {
text: "unknown".to_string(),
text_length: 0,
stage: SynthesisStage::AcousticModeling,
cause: Box::new(err),
}
}
}
impl From<voirs_vocoder::VocoderError> for VoirsError {
fn from(err: voirs_vocoder::VocoderError) -> Self {
Self::SynthesisFailed {
text: "unknown".to_string(),
text_length: 0,
stage: SynthesisStage::Vocoding,
cause: Box::new(err),
}
}
}
impl From<voirs_g2p::G2pError> for VoirsError {
fn from(err: voirs_g2p::G2pError) -> Self {
Self::SynthesisFailed {
text: "unknown".to_string(),
text_length: 0,
stage: SynthesisStage::G2pConversion,
cause: Box::new(err),
}
}
}
impl VoirsError {
pub fn synthesis_failed(
text: impl Into<String>,
cause: impl std::error::Error + Send + Sync + 'static,
) -> Self {
let text = text.into();
Self::SynthesisFailed {
text_length: text.len(),
text,
stage: SynthesisStage::TextPreprocessing,
cause: Box::new(cause),
}
}
pub fn device_error(device: impl Into<String>, message: impl Into<String>) -> Self {
Self::DeviceError {
device: device.into(),
message: message.into(),
recovery_hint: None,
}
}
pub fn config_error(message: impl Into<String>) -> Self {
Self::ConfigError {
field: "unknown".to_string(),
message: message.into(),
}
}
pub fn model_error(message: impl Into<String>) -> Self {
Self::ModelError {
model_type: ModelType::Acoustic,
message: message.into(),
source: None,
}
}
pub fn audio_error(message: impl Into<String>) -> Self {
Self::AudioError {
message: message.into(),
buffer_info: None,
}
}
pub fn plugin_error(message: impl Into<String>) -> Self {
Self::ConfigError {
field: "plugin".to_string(),
message: message.into(),
}
}
pub fn g2p_error(message: impl Into<String>) -> Self {
Self::G2pError {
text: "unknown".to_string(),
message: message.into(),
language: None,
}
}
pub fn cache_error(message: impl Into<String>) -> Self {
Self::ConfigError {
field: "cache".to_string(),
message: message.into(),
}
}
pub fn timeout(_message: impl Into<String>) -> Self {
Self::TimeoutError {
operation: "unknown".to_string(),
duration: std::time::Duration::from_secs(30),
expected_duration: None,
}
}
pub fn voice_not_found(voice: impl Into<String>, available: Vec<String>) -> Self {
let voice_str = voice.into();
let suggestions = available
.iter()
.filter(|v| v.contains(&voice_str) || voice_str.contains(*v))
.take(3)
.cloned()
.collect();
Self::VoiceNotFound {
voice: voice_str,
available,
suggestions,
}
}
pub fn invalid_config(
field: impl Into<String>,
value: impl Into<String>,
reason: impl Into<String>,
) -> Self {
Self::InvalidConfiguration {
field: field.into(),
value: value.into(),
reason: reason.into(),
valid_values: None,
}
}
pub fn invalid_config_with_values(
field: impl Into<String>,
value: impl Into<String>,
reason: impl Into<String>,
valid_values: Vec<String>,
) -> Self {
Self::InvalidConfiguration {
field: field.into(),
value: value.into(),
reason: reason.into(),
valid_values: Some(valid_values),
}
}
pub fn internal(component: impl Into<String>, message: impl Into<String>) -> Self {
Self::InternalError {
component: component.into(),
message: message.into(),
}
}
pub fn serialization(format: impl Into<String>, message: impl Into<String>) -> Self {
Self::SerializationError {
format: format.into(),
message: message.into(),
}
}
pub fn io_error(
path: impl Into<std::path::PathBuf>,
operation: IoOperation,
source: std::io::Error,
) -> Self {
Self::IoError {
path: path.into(),
operation,
source,
}
}
pub fn model_error_typed(model_type: ModelType, message: impl Into<String>) -> Self {
Self::ModelError {
model_type,
message: message.into(),
source: None,
}
}
pub fn invalid_argument(message: impl Into<String>) -> Self {
Self::config_error(message)
}
pub fn cancelled(message: impl Into<String>) -> Self {
Self::config_error(format!("Operation cancelled: {}", message.into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_macros() {
let error = voirs_error!(config_error: "Test error");
assert!(matches!(error, VoirsError::ConfigError { .. }));
let error = voirs_error!(internal: "test_component", "Test message");
assert!(matches!(error, VoirsError::InternalError { .. }));
}
#[test]
fn test_error_extensions() {
let error = VoirsError::NetworkError {
message: "Connection failed".to_string(),
retry_count: 1,
max_retries: 3,
source: None,
};
assert!(error.is_temporary());
assert!(!error.is_permanent());
assert!(!error.is_user_error());
assert!(error.recommended_retry_delay().is_some());
}
#[test]
fn test_result_extensions() {
let result: Result<i32> = Err(VoirsError::InternalError {
component: "test".to_string(),
message: "test error".to_string(),
});
let context_result = result.with_error_context("test_component", "test_operation");
assert!(context_result.is_err());
if let Err(error_with_context) = context_result {
assert_eq!(error_with_context.context.component, "test_component");
assert_eq!(error_with_context.context.operation, "test_operation");
}
}
#[tokio::test]
async fn test_integration() {
init_global_error_reporter(ErrorReporterConfig::default());
let manager = ErrorRecoveryManager::default();
let result: Result<()> = manager
.execute_with_recovery(
"test",
|| -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>> {
Box::pin(async {
Err(VoirsError::InternalError {
component: "test".to_string(),
message: "test error".to_string(),
})
})
},
)
.await;
assert!(result.is_err());
if let Some(reporter) = get_global_error_reporter() {
let _stats = reporter.get_statistics();
}
}
}