Skip to main content

pipecrab_stt/
transcriber.rs

1//! The [`Transcriber`] trait and its [`SttError`].
2
3use async_trait::async_trait;
4use pipecrab_core::AudioFormat;
5use pipecrab_runtime::MaybeSendSync;
6use std::sync::Arc;
7
8/// The swappable speech-to-text capability: `f32` samples in, a transcript out.
9///
10/// This is the durable interface. A native engine (`ort`) and a browser engine
11/// (Transformers.js in a Web Worker) both implement this one trait, so
12/// [`SttStage`](crate::SttStage) — and the pipeline above it — never names a
13/// concrete model. The offload decision lives in the *impl* (native offloads to
14/// a worker thread; wasm awaits a Web Worker), so the stage stays engine-neutral
15/// and just `.await`s [`transcribe`](Transcriber::transcribe).
16///
17/// `?Send` matches pipecrab's single-threaded execution model, so one
18/// implementation runs unchanged on a current-thread executor and on `wasm32`,
19/// where `Send` bounds cannot be satisfied.
20#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
21#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
22pub trait Transcriber: MaybeSendSync {
23    /// The one format this engine accepts. The stage caches it and enforces it
24    /// *before* feeding — see the crate-level [format authority](crate) note.
25    /// Sync and infallible: it is known at construction, so it is callable from
26    /// a stage's `decide_*` under the control-call carve-out.
27    fn input_format(&self) -> AudioFormat;
28
29    /// Transcribe `samples` (interleaved `f32` PCM) to text. Shared ownership
30    /// lets a worker-backed implementation retain or enqueue the buffer without
31    /// copying its samples.
32    ///
33    /// Samples are interpreted as [`input_format()`](Self::input_format): an
34    /// `Arc<[f32]>` carries no sample rate, so no runtime detection is possible.
35    /// Feeding a mismatch is a wiring bug the stage rejects fatally before a
36    /// sample reaches here, so this method never has to.
37    ///
38    /// Takes `&self`: like every
39    /// [`Stage::perform`](pipecrab_runtime::Stage::perform), transcription must
40    /// not mutate observable state, so the run loop can drop an in-flight call on
41    /// a barge-in interrupt without tearing anything.
42    async fn transcribe(&self, samples: Arc<[f32]>) -> Result<String, SttError>;
43}
44
45/// Why a [`Transcriber::transcribe`] call failed.
46///
47/// Mirrors the message-plus-kind shape of the pipeline's other error types so
48/// the conversion at the stage boundary
49/// (`impl From<SttError> for StageError`) is direct. There is deliberately no
50/// format-mismatch variant: samples are interpreted as
51/// [`input_format()`](Transcriber::input_format) and the stage enforces that
52/// format fatally, so an engine never sees nonconforming audio to reject.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum SttError {
55    /// The transcription engine itself failed — an inference error, a worker
56    /// that crashed, a model that never loaded. Carries a human-readable
57    /// description.
58    Engine(String),
59}
60
61impl std::fmt::Display for SttError {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        match self {
64            SttError::Engine(msg) => write!(f, "stt engine error: {msg}"),
65        }
66    }
67}
68
69impl std::error::Error for SttError {}