studio_worker/engine/
parakeet.rs1use crate::catalog::CatalogModel;
8use crate::engine::onnx_provision::{self, OrtFlavour};
9use crate::host::{LoadedModel, StreamingModel};
10use crate::stt_stream::session::StreamingTranscriber;
11use anyhow::{anyhow, Context, Result};
12use parakeet_rs::{ExecutionConfig, Nemotron, NemotronHandle, ParakeetEOU, ParakeetEOUHandle};
13use std::path::{Path, PathBuf};
14use std::time::Instant;
15
16const TRACE_TARGET: &str = "studio_worker::engine::parakeet";
17
18pub const NEMOTRON_CHUNK: usize = 8960;
20pub const EOU_CHUNK: usize = 2560;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum StreamKind {
25 Nemotron,
26 Eou,
27}
28
29pub fn stream_kind<'a>(filenames: impl IntoIterator<Item = &'a str>) -> Option<StreamKind> {
32 let names: Vec<&str> = filenames.into_iter().collect();
33 if names.contains(&"tokenizer.model") {
34 Some(StreamKind::Nemotron)
35 } else if names.contains(&"tokenizer.json") {
36 Some(StreamKind::Eou)
37 } else {
38 None
39 }
40}
41
42pub fn model_dir(models_root: &Path, model_id: &str) -> PathBuf {
45 models_root.join("stt").join(model_id)
46}
47
48enum Handle {
49 Nemotron(NemotronHandle),
50 Eou(ParakeetEOUHandle),
51}
52
53pub struct LoadedStream {
55 handle: Handle,
56}
57
58impl LoadedModel for LoadedStream {
59 fn as_any(&self) -> &dyn std::any::Any {
60 self
61 }
62
63 fn as_stream(&self) -> Option<&dyn StreamingModel> {
64 Some(self)
65 }
66}
67
68impl StreamingModel for LoadedStream {
69 fn open(&self) -> Result<Box<dyn StreamingTranscriber + '_>> {
70 Ok(match &self.handle {
71 Handle::Nemotron(h) => Box::new(NemotronStream(Nemotron::from_shared(h))),
72 Handle::Eou(h) => Box::new(EouStream(ParakeetEOU::from_shared(h))),
73 })
74 }
75}
76
77struct NemotronStream(Nemotron);
78
79impl StreamingTranscriber for NemotronStream {
80 fn chunk_samples(&self) -> usize {
81 NEMOTRON_CHUNK
82 }
83 fn step(&mut self, chunk: &[f32]) -> Result<String> {
84 Ok(self.0.transcribe_chunk(chunk)?)
85 }
86 fn reset(&mut self) {
87 self.0.reset();
88 }
89}
90
91struct EouStream(ParakeetEOU);
92
93impl StreamingTranscriber for EouStream {
94 fn chunk_samples(&self) -> usize {
95 EOU_CHUNK
96 }
97 fn step(&mut self, chunk: &[f32]) -> Result<String> {
98 Ok(self.0.transcribe(chunk, false)?)
99 }
100 fn reset(&mut self) {}
103}
104
105fn exec_config(flavour: OrtFlavour) -> ExecutionConfig {
109 let config = ExecutionConfig::new();
110 if flavour.is_cuda() {
111 config.with_custom_configure(|builder| {
112 Ok(builder
113 .with_execution_providers([ort::ep::CUDA::default().build().error_on_failure()])?)
114 })
115 } else {
116 config
117 }
118}
119
120#[cfg_attr(coverage_nightly, coverage(off))]
122pub fn load_resident(models_root: &Path, model: &CatalogModel) -> Result<LoadedStream> {
123 let runtime = onnx_provision::ensure_runtime(models_root)?;
124 let dir = model_dir(models_root, &model.id);
125 for file in &model.source.files {
126 crate::engine::download::ensure_file(&dir, file)
127 .with_context(|| format!("downloading {} for {}", file.filename, model.id))?;
128 }
129 let kind =
130 stream_kind(model.source.files.iter().map(|f| f.filename.as_str())).ok_or_else(|| {
131 anyhow!(
132 "{}: a streaming speech model needs tokenizer.model (Nemotron) or tokenizer.json (EOU)",
133 model.id
134 )
135 })?;
136 if !runtime.flavour.is_cuda() {
137 tracing::warn!(
138 target: TRACE_TARGET,
139 op = "load",
140 model = %model.id,
141 flavour = runtime.flavour.name(),
142 "no CUDA runtime on this host; the speech model runs on the CPU"
143 );
144 }
145 let started = Instant::now();
146 let config = Some(exec_config(runtime.flavour));
147 let handle = match kind {
148 StreamKind::Nemotron => Handle::Nemotron(NemotronHandle::from_pretrained(&dir, config)?),
149 StreamKind::Eou => Handle::Eou(ParakeetEOUHandle::from_pretrained(&dir, config)?),
150 };
151 tracing::info!(
152 target: TRACE_TARGET,
153 op = "load",
154 model = %model.id,
155 kind = ?kind,
156 flavour = runtime.flavour.name(),
157 elapsed_ms = started.elapsed().as_millis() as u64,
158 "streaming speech model loaded"
159 );
160 Ok(LoadedStream { handle })
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn the_tokenizer_file_names_the_family() {
169 assert_eq!(
170 stream_kind(["encoder.onnx", "decoder_joint.onnx", "tokenizer.model"]),
171 Some(StreamKind::Nemotron)
172 );
173 assert_eq!(
174 stream_kind(["encoder.onnx", "decoder_joint.onnx", "tokenizer.json"]),
175 Some(StreamKind::Eou)
176 );
177 assert_eq!(stream_kind(["encoder.onnx"]), None);
178 }
179
180 #[test]
181 fn each_model_gets_its_own_directory() {
182 assert_eq!(
183 model_dir(Path::new("/m"), "nemotron-3.5"),
184 Path::new("/m/stt/nemotron-3.5")
185 );
186 }
187}