1use std::sync::Arc;
15
16use voxora_traits::{AsrEngine, StreamingAsrEngine};
17
18use crate::backend::BackendDescriptor;
19use crate::family::EngineFamily;
20use crate::info::EngineInfo;
21
22pub trait EngineAdapter: Send + Sync {
31 fn family(&self) -> EngineFamily;
33
34 fn info(&self) -> EngineInfo;
36
37 fn backend(&self) -> BackendDescriptor;
39
40 fn as_asr_engine(&self) -> &dyn AsrEngine;
45
46 fn as_streaming_engine(&self) -> Option<&dyn StreamingAsrEngine> {
54 None
55 }
56}
57
58#[derive(Clone)]
64pub struct AnyEngine {
65 inner: Arc<dyn EngineAdapter>,
66}
67
68impl AnyEngine {
69 pub fn new<A: EngineAdapter + 'static>(adapter: A) -> Self {
71 Self {
72 inner: Arc::new(adapter),
73 }
74 }
75
76 pub fn family(&self) -> EngineFamily {
78 self.inner.family()
79 }
80
81 pub fn info(&self) -> EngineInfo {
83 self.inner.info()
84 }
85
86 pub fn backend(&self) -> BackendDescriptor {
88 self.inner.backend()
89 }
90
91 pub fn as_asr_engine(&self) -> &dyn AsrEngine {
93 self.inner.as_asr_engine()
94 }
95
96 pub fn as_streaming_engine(&self) -> Option<&dyn StreamingAsrEngine> {
100 self.inner.as_streaming_engine()
101 }
102
103 pub fn as_engine_adapter(&self) -> &dyn EngineAdapter {
105 &*self.inner
106 }
107}
108
109impl std::fmt::Debug for AnyEngine {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 f.debug_struct("AnyEngine")
112 .field("family", &self.inner.family())
113 .field("backend", &self.inner.backend())
114 .finish_non_exhaustive()
115 }
116}
117
118impl AsrEngine for AnyEngine {
119 fn capabilities(&self) -> voxora_traits::ModelCapabilities {
120 self.inner.as_asr_engine().capabilities()
121 }
122
123 fn transcribe(
124 &self,
125 samples: &[f32],
126 opts: &voxora_traits::TranscribeOptions,
127 ) -> Result<voxora_traits::TranscriptionResult, voxora_traits::AsrError> {
128 self.inner.as_asr_engine().transcribe(samples, opts)
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::testing::MockAdapter;
136 use voxora_traits::{AsrEngine, TranscribeOptions};
137
138 #[test]
139 fn any_engine_dispatches_to_inner() {
140 let adapter = MockAdapter::new(EngineFamily::Whisper);
141 let any = AnyEngine::new(adapter);
142 assert_eq!(any.family(), EngineFamily::Whisper);
143
144 let result = any
145 .transcribe(&[0.0_f32; 4], &TranscribeOptions::default())
146 .expect("transcribe");
147 assert!(result.text.contains("mock"));
148 }
149
150 #[test]
151 fn any_engine_capabilities_match_inner() {
152 let adapter = MockAdapter::new(EngineFamily::Qwen3Asr);
153 let any = AnyEngine::new(adapter);
154 let caps = any.capabilities();
155 assert!(caps.multilingual);
156 }
157
158 #[test]
159 fn any_engine_is_clone() {
160 let any = AnyEngine::new(MockAdapter::new(EngineFamily::Whisper));
161 let any2 = any.clone();
162 assert_eq!(any.family(), any2.family());
163 }
164
165 #[test]
166 fn as_engine_adapter_borrows_inner() {
167 let any = AnyEngine::new(MockAdapter::new(EngineFamily::Whisper));
168 let _adapter: &dyn EngineAdapter = any.as_engine_adapter();
169 }
170
171 #[test]
172 fn as_streaming_engine_defaults_to_none() {
173 let any = AnyEngine::new(MockAdapter::new(EngineFamily::Whisper));
174 assert!(
175 any.as_streaming_engine().is_none(),
176 "no engine in the workspace implements streaming yet"
177 );
178 }
179
180 #[test]
181 fn mock_adapter_does_not_advertise_streaming() {
182 let adapter = MockAdapter::new(EngineFamily::Qwen3Asr);
183 assert!(adapter.as_streaming_engine().is_none());
184 }
185}