car-voice 0.51.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,
    _resident: crate::local_admission::VoiceResidentLease,
}

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 model_path = car_whisper::ensure_model(&config.whisper_cpp_model)
            .map_err(|e| VoiceError::Stt(e.to_string()))?;
        // LOCAL_ADMISSION_BOUNDARY:voice-whisper-cpp
        let (inner, resident) = crate::local_admission::VoiceLocalAdmission::shared()
            .load_installed(
                &format!("voice/whisper-cpp/{}", config.whisper_cpp_model),
                &model_path,
                256,
                || {
                    car_whisper::WhisperStt::load_from_path(&model_path, &config.language)
                        .map_err(|e| VoiceError::Stt(e.to_string()))
                },
            )?;
        Ok(Self {
            inner,
            _resident: resident,
        })
    }
}

#[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 resident = self._resident.clone();
        let samples = samples.to_vec();
        tokio::task::spawn_blocking(move || {
            let _resident = resident;
            inner.transcribe(&samples, sample_rate)
        })
        .await
        .map_err(|e| VoiceError::Stt(format!("whisper join: {e}")))?
        .map_err(|e| VoiceError::Stt(e.to_string()))
    }
}