whisrs 0.1.1

Linux-first voice-to-text dictation tool
Documentation
//! Transcription backends: trait definition and implementations.

pub mod dedup;
pub mod groq;
pub mod local_parakeet;
pub mod local_vosk;
pub mod local_whisper;
pub mod openai_realtime;
pub mod openai_rest;

use async_trait::async_trait;
use tokio::sync::mpsc;

use crate::audio::AudioChunk;

/// Configuration for a transcription request.
#[derive(Debug, Clone)]
pub struct TranscriptionConfig {
    /// Language code (ISO 639-1), e.g. "en", or "auto" for auto-detection.
    pub language: String,
    /// Model identifier (backend-specific).
    pub model: String,
}

/// Trait for transcription backends.
///
/// Each backend takes WAV-encoded audio bytes and returns the transcribed text.
/// Backends that support streaming override `transcribe_stream`.
#[async_trait]
pub trait TranscriptionBackend: Send + Sync {
    /// Transcribe a complete WAV-encoded audio buffer, returning the text.
    async fn transcribe(
        &self,
        audio: &[u8],
        config: &TranscriptionConfig,
    ) -> anyhow::Result<String>;

    /// Streaming transcription: receive audio chunks and send text incrementally.
    ///
    /// The default implementation collects all audio, encodes to WAV, and calls
    /// `transcribe()` — so non-streaming backends work without overriding this.
    async fn transcribe_stream(
        &self,
        mut audio_rx: mpsc::Receiver<AudioChunk>,
        text_tx: mpsc::Sender<String>,
        config: &TranscriptionConfig,
    ) -> anyhow::Result<()> {
        use crate::audio::capture::encode_wav;

        // Collect all audio chunks.
        let mut all_samples: Vec<i16> = Vec::new();
        while let Some(chunk) = audio_rx.recv().await {
            all_samples.extend_from_slice(&chunk);
        }

        if all_samples.is_empty() {
            return Ok(());
        }

        // Encode to WAV and use the non-streaming method.
        let wav_data = encode_wav(&all_samples)?;
        let text = self.transcribe(&wav_data, config).await?;

        if !text.is_empty() {
            text_tx.send(text).await.ok();
        }

        Ok(())
    }

    /// Whether this backend supports true/chunked streaming.
    ///
    /// When true, the daemon will use `transcribe_stream` during recording
    /// rather than waiting for recording to finish.
    fn supports_streaming(&self) -> bool {
        false
    }
}