use crate::catalog::CatalogModel;
use crate::engine::onnx_provision::{self, OrtFlavour};
use crate::host::{LoadedModel, StreamingModel};
use crate::stt_stream::session::StreamingTranscriber;
use anyhow::{anyhow, Context, Result};
use parakeet_rs::{ExecutionConfig, Nemotron, NemotronHandle, ParakeetEOU, ParakeetEOUHandle};
use std::path::{Path, PathBuf};
use std::time::Instant;
const TRACE_TARGET: &str = "studio_worker::engine::parakeet";
pub const NEMOTRON_CHUNK: usize = 8960;
pub const EOU_CHUNK: usize = 2560;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamKind {
Nemotron,
Eou,
}
pub fn stream_kind<'a>(filenames: impl IntoIterator<Item = &'a str>) -> Option<StreamKind> {
let names: Vec<&str> = filenames.into_iter().collect();
if names.contains(&"tokenizer.model") {
Some(StreamKind::Nemotron)
} else if names.contains(&"tokenizer.json") {
Some(StreamKind::Eou)
} else {
None
}
}
pub fn model_dir(models_root: &Path, model_id: &str) -> PathBuf {
models_root.join("stt").join(model_id)
}
enum Handle {
Nemotron(NemotronHandle),
Eou(ParakeetEOUHandle),
}
pub struct LoadedStream {
handle: Handle,
}
impl LoadedModel for LoadedStream {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_stream(&self) -> Option<&dyn StreamingModel> {
Some(self)
}
}
impl StreamingModel for LoadedStream {
fn open(&self) -> Result<Box<dyn StreamingTranscriber + '_>> {
Ok(match &self.handle {
Handle::Nemotron(h) => Box::new(NemotronStream(Nemotron::from_shared(h))),
Handle::Eou(h) => Box::new(EouStream(ParakeetEOU::from_shared(h))),
})
}
}
struct NemotronStream(Nemotron);
impl StreamingTranscriber for NemotronStream {
fn chunk_samples(&self) -> usize {
NEMOTRON_CHUNK
}
fn step(&mut self, chunk: &[f32]) -> Result<String> {
Ok(self.0.transcribe_chunk(chunk)?)
}
fn reset(&mut self) {
self.0.reset();
}
}
struct EouStream(ParakeetEOU);
impl StreamingTranscriber for EouStream {
fn chunk_samples(&self) -> usize {
EOU_CHUNK
}
fn step(&mut self, chunk: &[f32]) -> Result<String> {
Ok(self.0.transcribe(chunk, false)?)
}
fn reset(&mut self) {}
}
fn exec_config(flavour: OrtFlavour) -> ExecutionConfig {
let config = ExecutionConfig::new();
if flavour.is_cuda() {
config.with_custom_configure(|builder| {
Ok(builder
.with_execution_providers([ort::ep::CUDA::default().build().error_on_failure()])?)
})
} else {
config
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
pub fn load_resident(models_root: &Path, model: &CatalogModel) -> Result<LoadedStream> {
let runtime = onnx_provision::ensure_runtime(models_root)?;
let dir = model_dir(models_root, &model.id);
for file in &model.source.files {
crate::engine::download::ensure_file(&dir, file)
.with_context(|| format!("downloading {} for {}", file.filename, model.id))?;
}
let kind =
stream_kind(model.source.files.iter().map(|f| f.filename.as_str())).ok_or_else(|| {
anyhow!(
"{}: a streaming speech model needs tokenizer.model (Nemotron) or tokenizer.json (EOU)",
model.id
)
})?;
if !runtime.flavour.is_cuda() {
tracing::warn!(
target: TRACE_TARGET,
op = "load",
model = %model.id,
flavour = runtime.flavour.name(),
"no CUDA runtime on this host; the speech model runs on the CPU"
);
}
let started = Instant::now();
let config = Some(exec_config(runtime.flavour));
let handle = match kind {
StreamKind::Nemotron => Handle::Nemotron(NemotronHandle::from_pretrained(&dir, config)?),
StreamKind::Eou => Handle::Eou(ParakeetEOUHandle::from_pretrained(&dir, config)?),
};
tracing::info!(
target: TRACE_TARGET,
op = "load",
model = %model.id,
kind = ?kind,
flavour = runtime.flavour.name(),
elapsed_ms = started.elapsed().as_millis() as u64,
"streaming speech model loaded"
);
Ok(LoadedStream { handle })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_tokenizer_file_names_the_family() {
assert_eq!(
stream_kind(["encoder.onnx", "decoder_joint.onnx", "tokenizer.model"]),
Some(StreamKind::Nemotron)
);
assert_eq!(
stream_kind(["encoder.onnx", "decoder_joint.onnx", "tokenizer.json"]),
Some(StreamKind::Eou)
);
assert_eq!(stream_kind(["encoder.onnx"]), None);
}
#[test]
fn each_model_gets_its_own_directory() {
assert_eq!(
model_dir(Path::new("/m"), "nemotron-3.5"),
Path::new("/m/stt/nemotron-3.5")
);
}
}