# How to Add a New Transcription Engine to Meetily: Integrating Parakeet or Custom STT

> Integrate a new transcription engine like Parakeet into Meetily by implementing four Rust backend lifecycle methods. Learn how to add custom STT to your application.

- Repository: [Zackriya Solutions/meetily](https://github.com/Zackriya-Solutions/meetily)
- Tags: how-to-guide
- Published: 2026-07-14

---

**Meetily uses a plug-in-style abstraction in its Tauri Rust backend that lets you integrate any speech-to-text engine by implementing four core lifecycle methods—`new()`, `load()`, `unload()`, and `transcribe()`—that process 48 kHz PCM audio, then registering the engine in the global command handler.**

Meetily is an open-source meeting transcription application built with Tauri and React. To add a new transcription engine like Parakeet to Meetily, you implement the same engine contract used by the built-in Whisper integration, allowing the React frontend to invoke your engine through standardized Tauri commands without modifying the audio pipeline or UI components.

## Understanding Meetily's Transcription Architecture

Meetily's backend separates audio capture from speech recognition through a strict engine contract defined in the Rust source code.

### The Engine Contract and Audio Pipeline

The audio pipeline in [`frontend/src-tauri/src/audio/pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs) always delivers **48 kHz interleaved PCM mono** audio via the `AudioPipelineManager`. Any transcription engine must accept this format in its `transcribe()` method. The engine lifecycle requires four operations:

1. **Initialization** – `new()` creates the engine instance
2. **Model Loading** – `load_models()` discovers and loads weights from the filesystem
3. **Processing** – `transcribe(audio: &[i16])` converts PCM samples to text
4. **Cleanup** – `unload()` frees memory and model handles

Engines must use `Arc<RwLock<Option<Engine>>>` for shared mutable state, enabling safe access from multiple async tasks like VAD (Voice Activity Detection) and transcription workers.

### Reference Implementation in the Whisper Engine

The existing Whisper implementation in [`frontend/src-tauri/src/whisper_engine/whisper_engine.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/whisper_engine/whisper_engine.rs) demonstrates the required pattern. It implements model-directory fallback logic, checking `models_dir` first before falling back to built-in paths, and logs progress with `log::info!` and `log::warn!` for frontend observability.

## Step-by-Step: Adding a New Transcription Engine

Follow this pattern to integrate Parakeet or any alternative STT library.

### Step 1: Create the Engine Module Structure

Create a new directory under `frontend/src-tauri/src/<engine_name>_engine/` (e.g., `parakeet_engine/`). Mirror the Whisper layout with four files:

- **[`mod.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/mod.rs)** – Re-exports the public struct and registers the engine with the global `ENGINE_REGISTRY`
- **[`engine.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/engine.rs)** – Holds the concrete `Engine` struct and implements the core methods
- **[`commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/commands.rs)** – Tauri command wrappers (`start_engine`, `stop_engine`, `transcribe_chunk`) that forward to the engine implementation
- **[`config.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/config.rs)** – Optional configuration parsing for model paths, language, and device selection

### Step 2: Implement the Core Engine Logic

In [`engine.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/engine.rs), implement the lifecycle methods. Store model handles in an `Option` field and use logging for observability:

```rust
use anyhow::Result;
use std::sync::{Arc, RwLock};

pub struct ParakeetEngine {
    model_handle: Option<parakeet::Model>,
}

impl ParakeetEngine {
    pub fn new() -> Result<Self> {
        Ok(Self { model_handle: None })
    }

    pub fn load_models(&mut self, models_dir: &std::path::Path) -> Result<()> {
        let model_path = models_dir.join("parakeet.pt");
        self.model_handle = Some(parakeet::Model::load(&model_path)?);
        log::info!("Parakeet model loaded from {:?}", model_path);
        Ok(())
    }

    pub fn unload(&mut self) {
        self.model_handle = None;
        log::info!("Parakeet engine unloaded");
    }

    pub fn transcribe(&self, audio: &[i16]) -> Result<String> {
        let model = self
            .model_handle
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("Parakeet model not loaded"))?;
        model.transcribe(audio).map_err(|e| e.into())
    }
}

```

### Step 3: Add Tauri Command Wrappers

In [`commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/commands.rs), create a global static holder and command functions. This pattern matches the existing Whisper implementation in [`frontend/src-tauri/src/whisper_engine/commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/whisper_engine/commands.rs):

```rust
use crate::parakeet_engine::engine::ParakeetEngine;
use std::sync::Mutex;
use tauri::State;

lazy_static::lazy_static! {
    pub static ref PARAKEET_ENGINE: Mutex<Option<Arc<ParakeetEngine>>> = Mutex::new(None);
}

#[tauri::command]
pub async fn start_parakeet_engine(models_dir: Option<String>) -> Result<(), String> {
    let dir = models_dir
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|| default_models_dir());

    let mut engine = ParakeetEngine::new().map_err(|e| e.to_string())?;
    engine.load_models(&dir).map_err(|e| e.to_string())?;
    *PARAKEET_ENGINE.lock().unwrap() = Some(Arc::new(engine));
    Ok(())
}

#[tauri::command]
pub async fn transcribe_parakeet_chunk(audio: Vec<i16>) -> Result<String, String> {
    let engine = PARAKEET_ENGINE
        .lock()
        .unwrap()
        .as_ref()
        .ok_or("Parakeet engine not initialized")?
        .clone();
    engine.transcribe(&audio).map_err(|e| e.to_string())
}

```

### Step 4: Register Global State

Add your engine to the global registry. The existing Whisper engine uses a static `Mutex` in [`commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/commands.rs) at line 311. Create an analogous static for your engine:

```rust
pub static PARAKEET_ENGINE: Mutex<Option<Arc<ParakeetEngine>>> = Mutex::new(None);

```

### Step 5: Wire Up Tauri Commands

Update the Tauri entry point in [`frontend/src-tauri/src/lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/lib.rs) to include your new commands in the `generate_handler!` macro:

```rust
tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![
        start_recording,
        // existing Whisper commands...
        start_parakeet_engine,
        transcribe_parakeet_chunk,
    ])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");

```

The command selector in [`frontend/src-tauri/src/whisper_engine/commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/whisper_engine/commands.rs) (around line 311) branches on `config.provider`. Add a case for your engine name (e.g., `"parakeet"`) alongside the existing `"localWhisper"` branch to instantiate your engine when selected.

### Step 6: Update Frontend Settings

In the React frontend (typically in `frontend/src/components/Settings/`), add a new provider option. When users select your engine, invoke the startup command:

```typescript
await invoke('start_parakeet_engine', { models_dir: '' });

```

The frontend sends `{ provider: "parakeet", model: "default" }` to the backend, triggering your engine's initialization path through the command selector.

## Model Management and Configuration

Place engine-specific model files in `frontend/models/` for development or the platform-specific app-data folder for production. Your `load_models()` method should scan these directories using the same discovery logic as Whisper's `discover_models()` function in [`whisper_engine/commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/whisper_engine/commands.rs).

If your engine requires a different sample rate or format, handle resampling internally. The audio pipeline strictly provides 48 kHz PCM, as defined in `AudioPipelineManager` in [`frontend/src-tauri/src/audio/pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs).

## Testing Your Integration

Validate your implementation through three stages:

1. **Unit testing** – Call `transcribe()` directly with a 48 kHz PCM fixture to verify text output
2. **Integration testing** – Run `RUST_LOG=debug ./clean_run.sh`, select your engine in the UI, and verify transcripts appear in the log stream
3. **Cross-platform verification** – Test on macOS, Windows, and Linux to ensure the engine handles the 48 kHz input rate correctly across all targets

## Summary

- **Meetily's architecture** uses a plug-in abstraction where any engine implementing `new()`, `load()`, `unload()`, and `transcribe()` can process audio from the 48 kHz PCM pipeline defined in [`audio/pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/audio/pipeline.rs)
- **Implementation requires** four files ([`mod.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/mod.rs), [`engine.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/engine.rs), [`commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/commands.rs), [`config.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/config.rs)) in a new `src/<engine>_engine/` directory mirroring the Whisper structure in `frontend/src-tauri/src/whisper_engine/`
- **Global registration** uses `Mutex<Option<Arc<Engine>>>` for thread-safe access and requires updating [`lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/lib.rs) with `tauri::generate_handler!` to expose your commands
- **Frontend integration** involves adding a provider option that triggers your `start_*_engine` command via Tauri's `invoke()` API
- **Model discovery** follows the fallback pattern: check `models_dir` first, then built-in paths, logging each step with `log::info!`

## Frequently Asked Questions

### What audio format does Meetily provide to transcription engines?

Meetily's `AudioPipelineManager` in [`frontend/src-tauri/src/audio/pipeline.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/pipeline.rs) delivers **48 kHz interleaved PCM mono** audio buffers as `&[i16]`. Your engine must accept this sample rate. If your engine requires a different format, implement resampling inside the `transcribe()` method before processing.

### How do I switch between Whisper and my new engine at runtime?

Meetily uses a provider selector in [`frontend/src-tauri/src/whisper_engine/commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/whisper_engine/commands.rs) (around line 311) that checks the `config.provider` string. Add a branch for your engine (e.g., `"parakeet"`) that instantiates your engine instead of Whisper. The global static holders ensure only one engine is active at a time, preventing backend conflicts.

### Where should I store model files for my custom engine?

Store development models in `frontend/models/` and production models in the platform-specific application data directory. Follow the fallback logic in `discover_models()` from [`whisper_engine/commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/whisper_engine/commands.rs): first check the user-specified `models_dir`, then check the built-in path. This ensures consistent behavior across development and packaged builds.

### Can I use multiple transcription engines simultaneously?

The current architecture supports only one active engine at a time through the global mutex pattern (`Mutex<Option<Arc<Engine>>>`). While the audio pipeline could theoretically fan out to multiple engines, the command registry and global state holders in [`commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/commands.rs) are designed for singleton access. To support multiple simultaneous engines, you would need to refactor the global state to a map of engine instances and update the command selectors accordingly.