Skip to main content

voxora_engine/
family.rs

1//! Engine family — the canonical spelling used in config files and
2//! CLI flags.
3//!
4//! This is the sole `EngineFamily` enum since 0.3.0; the duplicate
5//! `voxora-bridge::ModelKind` and `voxora-cli::BackendKind` enums
6//! were deprecated in 0.2.0 and removed in 0.3.0. Adding a new variant
7//! here is a SemVer-minor bump because the enum is `#[non_exhaustive]`
8//! for downstream pattern matches, but the `from_config` parser MUST
9//! be extended in lockstep.
10
11use std::fmt;
12use std::str::FromStr;
13
14/// Which engine family an [`crate::adapter::EngineAdapter`] represents.
15///
16/// Variants are matched by [`EngineFamily::from_config`] for the
17/// canonical config spelling. The enum is `#[non_exhaustive]` so
18/// adding a new family (parakeet, voxtral, …) does not break
19/// downstream pattern matches — but consumers that exhaustively
20/// match today must add a wildcard arm.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22#[non_exhaustive]
23pub enum EngineFamily {
24    /// whisper.cpp family (`voxora-whisper`).
25    Whisper,
26    /// Qwen3-ASR family (`voxora-qwen3asr`).
27    Qwen3Asr,
28}
29
30impl EngineFamily {
31    /// Parse the canonical config spelling. Accepts both
32    /// `"qwen3-asr"` and the legacy `"qwen3asr"` /
33    /// `"qwen3_asr"` aliases for ergonomics.
34    pub fn from_config(value: &str) -> Option<Self> {
35        match value.to_ascii_lowercase().as_str() {
36            "whisper" => Some(Self::Whisper),
37            "qwen3-asr" | "qwen3asr" | "qwen3_asr" => Some(Self::Qwen3Asr),
38            _ => None,
39        }
40    }
41
42    /// Canonical config spelling (matches `voxora-cli --engine`).
43    pub fn as_config(self) -> &'static str {
44        match self {
45            Self::Whisper => "whisper",
46            Self::Qwen3Asr => "qwen3-asr",
47        }
48    }
49
50    /// Short crate-label that identifies the voxora-* crate.
51    pub fn crate_label(self) -> &'static str {
52        match self {
53            Self::Whisper => "voxora-whisper",
54            Self::Qwen3Asr => "voxora-qwen3asr",
55        }
56    }
57}
58
59impl fmt::Display for EngineFamily {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        f.write_str(self.as_config())
62    }
63}
64
65impl FromStr for EngineFamily {
66    type Err = InvalidEngineFamily;
67
68    fn from_str(s: &str) -> Result<Self, Self::Err> {
69        Self::from_config(s).ok_or_else(|| InvalidEngineFamily(s.to_string()))
70    }
71}
72
73/// Error returned by [`EngineFamily`]'s [`FromStr`] impl when the
74/// input does not match a known engine family. The original input is
75/// preserved so callers can render it in their own error messages.
76#[derive(Debug, thiserror::Error)]
77#[error("unknown engine_family {0:?}; expected one of `whisper` or `qwen3-asr`")]
78pub struct InvalidEngineFamily(pub String);
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn from_config_accepts_canonical_and_aliases() {
86        assert_eq!(
87            EngineFamily::from_config("whisper"),
88            Some(EngineFamily::Whisper)
89        );
90        assert_eq!(
91            EngineFamily::from_config("qwen3-asr"),
92            Some(EngineFamily::Qwen3Asr)
93        );
94        assert_eq!(
95            EngineFamily::from_config("qwen3asr"),
96            Some(EngineFamily::Qwen3Asr)
97        );
98        assert_eq!(
99            EngineFamily::from_config("qwen3_asr"),
100            Some(EngineFamily::Qwen3Asr)
101        );
102    }
103
104    #[test]
105    fn from_config_is_case_insensitive() {
106        assert_eq!(
107            EngineFamily::from_config("WHISPER"),
108            Some(EngineFamily::Whisper)
109        );
110        assert_eq!(
111            EngineFamily::from_config("Qwen3-ASR"),
112            Some(EngineFamily::Qwen3Asr)
113        );
114    }
115
116    #[test]
117    fn from_config_rejects_unknown() {
118        assert_eq!(EngineFamily::from_config("parakeet"), None);
119        assert_eq!(EngineFamily::from_config(""), None);
120    }
121
122    #[test]
123    fn as_config_round_trips() {
124        for f in [EngineFamily::Whisper, EngineFamily::Qwen3Asr] {
125            assert_eq!(EngineFamily::from_config(f.as_config()), Some(f));
126        }
127    }
128
129    #[test]
130    fn from_str_error_carries_input() {
131        let err: InvalidEngineFamily = "bogus".parse::<EngineFamily>().unwrap_err();
132        assert_eq!(err.0, "bogus");
133    }
134
135    #[test]
136    fn crate_labels_match_workspace_member_names() {
137        assert_eq!(EngineFamily::Whisper.crate_label(), "voxora-whisper");
138        assert_eq!(EngineFamily::Qwen3Asr.crate_label(), "voxora-qwen3asr");
139    }
140}