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 {}