transcribe-rs

repository·main·Indexed 42 days ago

https://github.com/cjpais/transcribe-rs

A multi-engine speech-to-text library for Rust providing a unified interface for local engines (ONNX, whisper.cpp, Whisperfile) and remote engines (OpenAI). Version 0.3.11 supports models such as Parakeet, Canary, Cohere, Moonshine, SenseVoice, and GigaAM, with optional GPU acceleration via CUDA, Metal, Vulkan, ROCm, DirectML, and CoreML.

Tokens
17.7K
Snippets
43
Records
61
Agent score
54%

What's inside transcribe-rs

  1. Configure Hardware Acceleration

    main

    By default, engines use the CPU. You can enable GPU acceleration by enabling the appropriate feature flag at compile time and setting the accelerator preference at runtime before loading any models.

    ORT (ONNX Runtime) Engines

    Use set_ort_accelerator with OrtAccelerator values. Note that OrtAccelerator::Auto does not include DirectML due to specific session requirements; you must select OrtAccelerator::DirectMl explicitly if using DirectML.

    • OrtAccelerator::Cuda: NVIDIA CUDA
    • OrtAccelerator::Auto: Auto-detect best available GPU
    • OrtAccelerator::DirectMl: Microsoft DirectML (Windows)
    • OrtAccelerator::available(): Query which accelerators are compiled in.

    whisper.cpp Engines

    GPU backends (Metal, Vulkan, CUDA) are selected at compile time via feature flags. Use set_whisper_accelerator to control usage at runtime.

    • WhisperAccelerator::CpuOnly: Force CPU usage.
    use transcribe_rs::{set_ort_accelerator, OrtAccelerator};
    
    // Use CUDA for all ORT engines (SenseVoice, GigaAM, Parakeet, Moonshine)
    set_ort_accelerator(OrtAccelerator::Cuda);
    
    // Or auto-detect the best available GPU
    set_ort_accelerator(OrtAccelerator::Auto);
    use transcribe_rs::{set_whisper_accelerator, WhisperAccelerator};
    
    set_whisper_accelerator(WhisperAccelerator::CpuOnly); // force CPU
  2. Add tests for models

    main

    Use one of two patterns depending on model loading cost:

    Pattern A: Lightweight models

    For models that load quickly (e.g., most ONNX models), load a new instance per test. Always use common::require_paths() to skip tests gracefully if model files are missing.

    // tests/your_model.rs
    mod common;
    
    use std::path::PathBuf;
    use transcribe_rs::your_engine::your_model::YourModel;
    use transcribe_rs::SpeechModel;
    
    #[test]
    fn test_your_model_transcribe() {
        env_logger::init();
    
        let model_dir = PathBuf::from("models/your-model");
        let wav_path = PathBuf::from("samples/jfk.wav");
    
        if !common::require_paths(&[&model_dir, &wav_path]) {
            return;
        }
    
        let mut model = YourModel::load(&model_dir).expect("Failed to load model");
    
        let result = model
            .transcribe_file(&wav_path, None)
            .expect("Failed to transcribe");
    
        assert!(!result.text.is_empty(), "Transcription should not be empty");
    }

    Pattern B: Expensive models

    For slow-loading models (e.g., whisper.cpp), use a shared instance via once_cell::sync::Lazy and std::sync::Mutex to avoid reloading the model for every test.

    // tests/your_model.rs
    mod common;
    
    use once_cell::sync::Lazy;
    use std::path::PathBuf;
    use std::sync::Mutex;
    use transcribe_rs::your_engine::YourEngine;
    use transcribe_rs::SpeechModel;
    
    static ENGINE: Lazy<Mutex<Option<YourEngine>>> = Lazy::new(|| {
        let model = PathBuf::from("models/your-model.bin");
        if !common::require_paths(&[&model]) {
            return Mutex::new(None);
        }
    
        match YourEngine::load(&model) {
            Ok(engine) => Mutex::new(Some(engine)),
            Err(_) => Mutex::new(None),
        }
    });
    
    #[test]
    fn test_transcription() {
        let mut guard = match ENGINE.lock().unwrap().as_mut() {
            Some(e) => e,
            None => return,
        };
    
        let audio_path = PathBuf::from("samples/jfk.wav");
        let result = guard.transcribe_file(&audio_path, None).expect("Failed");
        assert!(!result.text.is_empty());
    }

    Registration

    Register tests in Cargo.toml with the required feature:

    [[test]]
    name = "your_model"
    required-features = ["your-engine"]
  3. Add a new engine family

    main

    A new engine family is required when integrating a new inference runtime (e.g., Candle, Burn, MLX, TensorRT). Follow these steps:

    1. Create the source directory

    Create src/your_engine/ with a mod.rs for engine-level types and re-exports, and subdirectories for individual models (e.g., src/your_engine/your_model/mod.rs). For simple engines, a single file src/your_engine.rs is sufficient.

    2. Configure Cargo.toml

    Add a feature flag for your engine and include it in the all feature. If your engine requires shared audio features (like mel spectrograms), include the audio-features feature.

    [features]
    your-engine = ["dep:your-runtime-crate"]
    
    # Update the "all" feature
    all = ["onnx", "whisper-cpp", "whisperfile", "openai", "your-engine"]
    
    [dependencies]
    your-runtime-crate = { version = "...", optional = true }

    Note: To include audio features: your-engine = ["audio-features", "dep:your-runtime-crate"]

    3. Register the module

    In src/lib.rs, use a conditional compilation guard:

    #[cfg(feature = "your-engine")]
    pub mod your_engine;

    4. Implement the SpeechModel trait

    Every local model must implement the SpeechModel trait. Follow these conventions:

    • CAPABILITIES: Define const CAPABILITIES: ModelCapabilities.
    • Constructor: Implement Model::load(...) as a single-step constructor.
    • Params: Create a {Model}Params struct with #[derive(Debug, Clone, Default)].
    • Methods: Implement transcribe_with(&mut self, samples, &Params) for engine-specific parameters, and the standard transcribe() for the generic interface.
    • Errors: Map runtime errors to TranscribeError variants like ModelNotFound, Inference, Audio, or Config.

    5. Error Conversions

    If your runtime has a custom error type, implement From in src/error.rs:

    #[cfg(feature = "your-engine")]
    impl From<your_runtime::Error> for TranscribeError {
        fn from(e: your_runtime::Error) -> Self {
            TranscribeError::Inference(e.to_string())
        }
    }

    6. Documentation

    Create src/your_engine/PORTING.md to document how to add models to this specific engine family.

  4. Run tests with feature flags

    main

    Tests are feature-gated. Note that most tests require models to be present locally; tests will skip gracefully if models are not found.

    cargo test --features onnx
    cargo test --features whisper-cpp
    cargo test --features whisperfile
    cargo test --all-features

    Whisperfile Configuration:

    • The binary is expected at models/whisperfile-0.9.3. You can override this using the WHISPERFILE_BIN environment variable.
    • The model is expected at models/ggml-small.bin. You can override this using the WHISPERFILE_MODEL environment variable.

    GigaAM Requirement: GigaAM tests require the file samples/russian.wav to be present.

    cargo test --all-features
  5. Add a new ONNX model to transcribe-rs

    main

    To add a new speech recognition model to the onnx engine family, follow these steps:

    1. Create the directory structure: Create a new directory under src/onnx/ (e.g., src/onnx/your_model/) and add a mod.rs file.
    2. Register the module: Add pub mod your_model; to src/onnx/mod.rs.
    3. Implement the model: Implement the SpeechModel trait and provide a load method. Use src/onnx/gigaam/mod.rs as a simple reference.
    4. Add a test: Create a test file in tests/your_model.rs and register it in Cargo.toml with required-features = ["onnx"].
    5. Add an example: Create an example file in examples/your_model.rs and register it in Cargo.toml with required-features = ["onnx"].
    6. Prepare model files: Place your .onnx files and vocabulary files in a directory (e.g., models/your-model/).
    mkdir src/onnx/your_model
    touch src/onnx/your_model/mod.rs
  6. Quick Start with Parakeet (ONNX)

    main

    To perform transcription using the Parakeet model, load the model with a specific quantization (e.g., Int8), read your audio samples using transcribe_rs::audio::read_wav_samples, and call transcribe_with using ParakeetParams.

    use transcribe_rs::onnx::parakeet::{ParakeetModel, ParakeetParams, TimestampGranularity};
    use transcribe_rs::onnx::Quantization;
    use std::path::PathBuf;
    
    let mut model = ParakeetModel::load(
        &PathBuf::from("models/parakeet-tdt-0.6b-v3-int8"),
        &Quantization::Int8,
    )?;
    
    let samples = transcribe_rs::audio::read_wav_samples(&PathBuf::from("audio.wav"))?;
    let result = model.transcribe_with(
        &samples,
        &ParakeetParams {
            timestamp_granularity: Some(TimestampGranularity::Segment),
            ..Default::default()
        },
    )?;
    println!("{}", result.text);
  7. Implement the YourModel skeleton for ONNX

    main

    When implementing a new model, follow this structure in src/onnx/your_model/mod.rs. The model must define its ModelCapabilities, a Params struct for model-specific settings, and implement the SpeechModel trait.

    Key requirements:

    • Constructor: Use pub fn load(model_dir: &Path, quantization: &Quantization) -> Result<Self, TranscribeError>.
    • Capabilities: Define a const CAPABILITIES: ModelCapabilities containing the model's name, engine ID, sample rate, and supported languages.
    • Inference: Implement the infer method to handle feature extraction, tensor preparation, the ONNX forward pass, and decoding.
    • Trait Implementation: Implement SpeechModel to allow generic usage via the transcribe method.
    use ort::inputs;
    use ort::session::Session;
    use ort::value::TensorRef;
    use std::path::Path;
    
    use crate::decode::{ctc_greedy_decode, sentencepiece_to_text};
    use crate::decode::tokens::load_vocab;
    use crate::features::{compute_mel, MelConfig, WindowType};
    use crate::TranscribeError;
    use super::session;
    use super::Quantization;
    use crate::{ModelCapabilities, SpeechModel, TranscriptionResult};
    
    const CAPABILITIES: ModelCapabilities = ModelCapabilities {
        name: "Your Model",           // Human-readable name
        engine_id: "your_model",      // Machine identifier, matches directory name
        sample_rate: 16000,           // Expected input sample rate
        languages: &["en"],
        supports_timestamps: false,
        supports_translation: false,
        supports_streaming: false,
    };
    
    #[derive(Debug, Clone, Default)]
    pub struct YourModelParams {
        pub language: Option<String>,
    }
    
    pub struct YourModel {
        session: Session,
    }
    
    impl YourModel {
        pub fn load(model_dir: &Path, quantization: &Quantization) -> Result<Self, TranscribeError> {
            let model_path = session::resolve_model_path(model_dir, "model", quantization);
    
            if !model_path.exists() {
                return Err(TranscribeError::ModelNotFound(model_path));
            }
    
            let session = session::create_session(&model_path)?;
    
            Ok(Self { session })
        }
    
        pub fn transcribe_with(
            &mut self, 
            samples: &[f32], 
            params: &YourModelParams
        ) -> Result<TranscriptionResult, TranscribeError> {
            self.infer(samples)
        }
    
        fn infer(&mut self, samples: &[f32]) -> Result<TranscriptionResult, TranscribeError> {
            // 1. Feature extraction
            // 2. Prepare input tensors
            // 3. Run ONNX forward pass: self.session.run(inputs)?
            // 4. Decode output
            // 5. Return TranscriptionResult
            todo!()
        }
    }
    
    impl SpeechModel for YourModel {
        fn capabilities(&self) -> ModelCapabilities {
            CAPABILITIES
        }
    
        fn transcribe(
            &mut self, 
            samples: &[f32], 
            _options: &TranscribeOptions
        ) -> Result<TranscriptionResult, TranscribeError> {
            self.infer(samples)
        }
    }
  8. Download transcription models

    main

    Models must be downloaded and placed in your local directory before use. The required directory structure depends on the engine.

    EngineDownload Source
    Parakeet (int8)blob.handy.computer / HuggingFace
    Canary 180M FlashHuggingFace
    Canary 1B FlashHuggingFace
    Canary 1B v2HuggingFace
    Cohere (int4)HuggingFace
    Cohere (int8)HuggingFace
    SenseVoice (int8)blob.handy.computer / sherpa-onnx
    Moonshineblob.handy.computer (base), blob.handy.computer (tiny streaming en), blob.handy.computer (small streaming en), blob.handy.computer (medium streaming en)
    GigaAMHuggingFace
    Whisper (GGML)HuggingFace
    Whisperfile binaryGitHub
  9. Add a new model to an existing engine

    main

    If you are adding a model that uses an existing inference runtime (like ONNX), follow these steps:

    1. Create the model module at src/onnx/your_model/mod.rs (or the relevant engine directory).
    2. Register the new module in the engine's entry point (e.g., src/onnx/mod.rs).
    3. Implement the SpeechModel trait for your model.
    4. Add a corresponding test file in tests/, an example in examples/, and register them in Cargo.toml.

    For detailed ONNX-specific instructions, refer to src/onnx/PORTING.md.

  10. Install transcribe-rs via Cargo

    main

    Add transcribe-rs to your Cargo.toml. Note that no features are enabled by default; you must explicitly enable the engines you intend to use via feature flags.

    Engine Features

    FeatureEngines
    onnxParakeet, Canary, Cohere, Moonshine, SenseVoice, GigaAM (via ONNX Runtime)
    whisper-cppWhisper (local, GGML via whisper.cpp with Metal/Vulkan/CUDA)
    whisperfileWhisperfile (local server wrapper)
    openaiOpenAI API (remote, async)
    allEverything above

    GPU Accelerator Features for whisper.cpp

    FeatureBackend
    whisper-metalApple Metal (macOS)
    whisper-vulkanVulkan (Windows/Linux)
    whisper-cudaNVIDIA CUDA (requires CUDA Toolkit 12.0+)

    GPU Accelerator Features for ORT (ONNX Runtime) engines

    FeatureBackend
    ort-cudaNVIDIA CUDA
    ort-rocmAMD ROCm
    ort-directmlMicrosoft DirectML (Windows)
    ort-coremlApple CoreML (macOS/iOS)
    ort-webgpuWebGPU via Dawn
    [dependencies]
    transcribe-rs = { version = "0.3", features = ["onnx"] }
  11. Add examples for models

    main

    Every model should include an example demonstrating loading and transcription. Use transcribe_with() to show how to pass model-specific parameters.

    Conventions:

    • Show load and transcription timing.
    • Calculate and print the real-time speedup factor.
    • Print segments if the model supports timestamps.
    • Register the example in Cargo.toml with required-features.
    // examples/your_model.rs
    use std::path::PathBuf;
    use std::time::Instant;
    use transcribe_rs::your_engine::your_model::{YourModel, YourModelParams};
    use transcribe_rs::SpeechModel;
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        env_logger::init();
        let model_path = PathBuf::from("models/your-model");
        let wav_path = PathBuf::from("samples/jfk.wav");
    
        // Load
        let mut model = YourModel::load(&model_path)?;
    
        // Transcribe
        let samples = transcribe_rs::audio::read_wav_samples(&wav_path)?;
        let result = model.transcribe_with(
            &samples,
            &YourModelParams {
                language: Some("en".to_string()),
                ..Default::default()
            },
        )?;
    
        println!("Transcription result: {}", result.text);
        Ok(())
    }

    Registration in Cargo.toml:

    [[example]]
    name = "your_model"
    required-features = ["your-engine"]