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

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 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 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 – Re-exports the public struct and registers the engine with the global ENGINE_REGISTRY
  • engine.rs – Holds the concrete Engine struct and implements the core methods
  • commands.rs – Tauri command wrappers (start_engine, stop_engine, transcribe_chunk) that forward to the engine implementation
  • config.rs – Optional configuration parsing for model paths, language, and device selection

Step 2: Implement the Core Engine Logic

In engine.rs, implement the lifecycle methods. Store model handles in an Option field and use logging for observability:

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, create a global static holder and command functions. This pattern matches the existing Whisper implementation in frontend/src-tauri/src/whisper_engine/commands.rs:

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 at line 311. Create an analogous static for your engine:

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 to include your new commands in the generate_handler! macro:

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 (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:

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.

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.

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
  • Implementation requires four files (mod.rs, engine.rs, commands.rs, 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 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 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 (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: 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →