Skip to main content

blazen_audio_codec/
error.rs

1//! Capability-specific error type for codec backends.
2//!
3//! [`CodecError`] sits one level above [`blazen_audio::AudioError`]: it
4//! converts cleanly to and from `AudioError` so the bridge layer in
5//! `blazen-llm` can forward codec failures through the shared
6//! audio surface, while keeping codec-specific variants
7//! ([`Self::NotYetImplemented`], [`Self::EngineNotAvailable`], ...)
8//! visible to in-process Rust callers.
9
10use std::io;
11
12use blazen_audio::AudioError;
13use thiserror::Error;
14
15/// Errors that can be returned by any [`crate::CodecBackend`].
16#[derive(Debug, Error)]
17pub enum CodecError {
18    /// The crate was compiled without the matching backend feature
19    /// (`encodec`, `dac`, `snac`, ...), so the inference stack is not
20    /// linked in. Every codec call short-circuits with this error.
21    #[error(
22        "blazen-audio-codec was built without the matching backend feature -- \
23         rebuild with `--features <backend>` (e.g. `encodec`) to enable on-device codec inference"
24    )]
25    EngineNotAvailable,
26
27    /// The backend is a known slot (e.g. DAC, SNAC) but its real
28    /// implementation has not landed yet. Carries a detailed message
29    /// pointing at the tracking wave.
30    #[error("{0}")]
31    NotYetImplemented(String),
32
33    /// Hugging Face Hub fetch failed while pulling model weights
34    /// (offline, 404, auth, network, ...).
35    #[error("hf-hub fetch failed for {repo}: {source}")]
36    HfHub {
37        /// Repo identifier (e.g. `facebook/encodec_24khz`).
38        repo: String,
39        /// Underlying error message from `hf-hub`.
40        #[source]
41        source: io::Error,
42    },
43
44    /// Local filesystem error (cache directory, weight file, ...).
45    #[error("io error: {0}")]
46    Io(#[from] io::Error),
47
48    /// Underlying candle inference / tensor error.
49    #[error("candle error: {0}")]
50    Candle(String),
51
52    /// The caller-supplied PCM or token vector does not satisfy the
53    /// codec's requirements (wrong sample rate, empty buffer,
54    /// non-aligned token count, ...).
55    #[error("invalid input: {0}")]
56    InvalidInput(String),
57
58    /// Catch-all for unexpected runtime conditions.
59    #[error("{0}")]
60    Other(String),
61}
62
63impl CodecError {
64    /// Convenience constructor for [`Self::NotYetImplemented`].
65    #[must_use]
66    pub fn not_yet_implemented(message: impl Into<String>) -> Self {
67        Self::NotYetImplemented(message.into())
68    }
69
70    /// Convenience constructor for [`Self::InvalidInput`].
71    #[must_use]
72    pub fn invalid_input(message: impl Into<String>) -> Self {
73        Self::InvalidInput(message.into())
74    }
75
76    /// Convenience constructor for [`Self::Other`].
77    #[must_use]
78    pub fn other(message: impl Into<String>) -> Self {
79        Self::Other(message.into())
80    }
81}
82
83#[cfg(feature = "candle-backbone")]
84impl From<candle_core::Error> for CodecError {
85    fn from(err: candle_core::Error) -> Self {
86        Self::Candle(err.to_string())
87    }
88}
89
90/// Lossy projection of [`CodecError`] into the capability-agnostic
91/// [`blazen_audio::AudioError`] surface used at the bridge boundary.
92impl From<CodecError> for AudioError {
93    fn from(err: CodecError) -> Self {
94        match err {
95            CodecError::EngineNotAvailable => Self::Unsupported(
96                "codec backend not built (rebuild with the matching feature flag)".to_string(),
97            ),
98            CodecError::NotYetImplemented(msg) => Self::Unsupported(msg),
99            CodecError::InvalidInput(msg) => Self::InvalidInput(msg),
100            CodecError::Io(io) => Self::Io(io),
101            CodecError::HfHub { repo, source } => {
102                Self::Backend(format!("hf-hub fetch failed for {repo}: {source}"))
103            }
104            CodecError::Candle(msg) => Self::Backend(format!("candle error: {msg}")),
105            CodecError::Other(msg) => Self::Backend(msg),
106        }
107    }
108}
109
110/// Convenience alias used throughout the crate.
111pub type Result<T> = std::result::Result<T, CodecError>;
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn constructors_round_trip_messages() {
119        let nyi = CodecError::not_yet_implemented("foo");
120        assert!(matches!(nyi, CodecError::NotYetImplemented(ref m) if m == "foo"));
121
122        let invalid = CodecError::invalid_input("bar");
123        assert!(matches!(invalid, CodecError::InvalidInput(ref m) if m == "bar"));
124
125        let other = CodecError::other("baz");
126        assert!(matches!(other, CodecError::Other(ref m) if m == "baz"));
127    }
128
129    #[test]
130    fn engine_not_available_projects_to_unsupported() {
131        let err: AudioError = CodecError::EngineNotAvailable.into();
132        assert!(matches!(err, AudioError::Unsupported(_)));
133    }
134
135    #[test]
136    fn invalid_input_projects_to_invalid_input() {
137        let err: AudioError = CodecError::invalid_input("empty").into();
138        match err {
139            AudioError::InvalidInput(msg) => assert_eq!(msg, "empty"),
140            other => panic!("expected InvalidInput, got {other:?}"),
141        }
142    }
143}