car-voice 0.47.0

Voice I/O capability for CAR — mic capture, VAD, listener/speaker traits
//! car-voice's `SttProvider` over the shared [`car_whisper`] crate.
//!
//! The whisper.cpp path — model resolve/download, context loading, resampling,
//! and transcription — moved to the leaf `car-whisper` crate so `car-inference`
//! can route its `car speech` catalog entry through the same code without a
//! circular dependency (car-voice depends on car-inference). This file is now a
//! thin adapter: it maps `VoiceConfig` → a loaded `car_whisper::WhisperStt` and
//! bridges the async `SttProvider` trait to the crate's synchronous transcribe
//! (on a blocking pool, as before).

use async_trait::async_trait;

use crate::stt::SttProvider;
use crate::{Result, VoiceConfig, VoiceError};

/// In-process Whisper STT (delegates to [`car_whisper::WhisperStt`]).
#[derive(Debug)]
pub struct WhisperCppSttProvider {
    inner: car_whisper::WhisperStt,
}

impl WhisperCppSttProvider {
    /// Build a provider by resolving the configured model file and loading it.
    /// If the file isn't present at the cache path yet, this downloads it
    /// synchronously — first-run cost is the model download (~600 MB for the
    /// default quantized turbo model).
    pub fn from_config(config: &VoiceConfig) -> Result<Self> {
        let inner = car_whisper::WhisperStt::load(&config.whisper_cpp_model, &config.language)
            .map_err(|e| VoiceError::Stt(e.to_string()))?;
        Ok(Self { inner })
    }
}

#[async_trait]
impl SttProvider for WhisperCppSttProvider {
    async fn transcribe(&self, samples: &[f32], sample_rate: u32) -> Result<String> {
        // whisper.cpp inference is synchronous and CPU/GPU-bound — move it off
        // the tokio runtime onto a blocking pool. `WhisperStt` is cheap to clone
        // (the context is behind an `Arc`).
        let inner = self.inner.clone();
        let samples = samples.to_vec();
        tokio::task::spawn_blocking(move || inner.transcribe(&samples, sample_rate))
            .await
            .map_err(|e| VoiceError::Stt(format!("whisper join: {e}")))?
            .map_err(|e| VoiceError::Stt(e.to_string()))
    }
}