Skip to main content

aimux_core/
speech_model.rs

1//! The `SpeechModel` trait — the provider-facing interface for text-to-speech.
2//!
3//! Aligned with Vercel AI SDK `SpeechModelV4`
4//! (`reference/ai/packages/provider/src/speech-model/v4/`).
5
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use ts_rs::TS;
9
10use crate::error::AiMuxError;
11use crate::shared::{SharedHeaders, SharedProviderMetadata, SharedProviderOptions, Warning};
12use crate::{AbortSignal, retry, timeout};
13
14/// Generated audio: a base64-encoded string or raw binary bytes.
15///
16/// The TS result type is `string | Uint8Array`. Providers should return data
17/// without unnecessary conversion.
18#[derive(Debug, Clone, Serialize, Deserialize, TS)]
19#[ts(export)]
20pub enum AudioData {
21    /// Audio as a base64-encoded string.
22    Base64(String),
23    /// Audio as raw binary bytes.
24    Binary(Vec<u8>),
25}
26
27/// Options passed to [`SpeechModel::do_generate`].
28///
29/// Aligned with V4 `SpeechModelV4CallOptions`.
30#[derive(Debug, Clone, Serialize, Deserialize, TS)]
31#[ts(export)]
32pub struct SpeechCallOptions {
33    /// Text to convert to speech.
34    pub text: String,
35
36    /// The voice to use (provider-specific voice ID, name, or other
37    /// identifier).
38    pub voice: Option<String>,
39
40    /// The desired output format for the audio, e.g. `"mp3"`, `"wav"`.
41    pub output_format: Option<String>,
42
43    /// Instructions for the speech generation, e.g.
44    /// `"Speak in a slow and steady tone"`.
45    pub instructions: Option<String>,
46
47    /// The speed of the speech generation.
48    pub speed: Option<f64>,
49
50    /// The language for speech generation (ISO 639-1 code, e.g. `"en"`, or
51    /// `"auto"` for automatic detection). Provider support varies.
52    pub language: Option<String>,
53
54    /// Additional provider-specific options, keyed by provider name.
55    pub provider_options: Option<SharedProviderOptions>,
56
57    /// Abort signal for cancelling the operation.
58    #[serde(skip)]
59    #[ts(skip)]
60    pub abort_signal: Option<AbortSignal>,
61
62    /// Per-call retry override. `None` uses the model default.
63    pub max_retries: Option<u32>,
64
65    /// Per-call operation timeout.
66    pub timeout: Option<crate::options::TimeoutConfiguration>,
67
68    /// Additional HTTP headers to send with the request.
69    pub headers: Option<SharedHeaders>,
70}
71
72impl SpeechCallOptions {
73    /// Create options with the given text and all other fields unset.
74    pub fn new(text: impl Into<String>) -> Self {
75        Self {
76            text: text.into(),
77            voice: None,
78            output_format: None,
79            instructions: None,
80            speed: None,
81            language: None,
82            provider_options: None,
83            abort_signal: None,
84            max_retries: None,
85            timeout: None,
86            headers: None,
87        }
88    }
89}
90
91/// The result of [`SpeechModel::do_generate`].
92///
93/// Aligned with V4 `SpeechModelV4Result`.
94#[derive(Debug, Clone, Serialize, Deserialize, TS)]
95#[ts(export)]
96pub struct SpeechResult {
97    /// Generated audio (base64 string or binary data).
98    pub audio: AudioData,
99
100    /// Warnings for the call, e.g. unsupported settings.
101    pub warnings: Vec<Warning>,
102
103    /// Optional request information for telemetry and debugging.
104    pub request: Option<SpeechRequest>,
105
106    /// Response information for telemetry and debugging.
107    pub response: SpeechResponse,
108
109    /// Additional provider-specific metadata, keyed by provider name.
110    pub provider_metadata: Option<SharedProviderMetadata>,
111}
112
113/// Optional request information for a speech call.
114#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
115#[ts(export)]
116pub struct SpeechRequest {
117    /// Response body (HTTP providers only).
118    pub body: Option<serde_json::Value>,
119}
120
121/// Response information for a speech call.
122#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
123#[ts(export)]
124pub struct SpeechResponse {
125    /// Timestamp for the start of the generated response (ISO 8601 string).
126    pub timestamp: Option<String>,
127    /// The ID of the model that was used to generate the response.
128    pub model_id: Option<String>,
129    /// Response headers.
130    pub headers: Option<SharedHeaders>,
131    /// Response body (opaque JSON).
132    pub body: Option<serde_json::Value>,
133}
134
135/// The unified speech (TTS) model trait (provider-facing).
136///
137/// Aligned with V4 `SpeechModelV4`.
138#[async_trait]
139pub trait SpeechModel: Send + Sync {
140    /// Specification version (always `"v4"`).
141    fn specification_version(&self) -> &'static str {
142        "v4"
143    }
144
145    /// Provider name, e.g. `"openai"`.
146    fn provider(&self) -> &str;
147
148    /// Provider-specific model ID, e.g. `"tts-1"`.
149    fn model_id(&self) -> &str;
150
151    fn retry_config(&self) -> crate::retry::RetryConfig {
152        crate::retry::RetryConfig::default()
153    }
154
155    /// Generate speech audio from text.
156    ///
157    /// Naming: the `do_` prefix prevents accidental direct usage by users.
158    async fn do_generate(&self, options: &SpeechCallOptions) -> Result<SpeechResult, AiMuxError>;
159}
160
161/// User-facing speech generation with Core-owned retry and timeout.
162///
163/// # Errors
164///
165/// Returns the provider failure, retry exhaustion, timeout, or caller abort.
166pub async fn generate_speech(
167    model: &dyn SpeechModel,
168    options: SpeechCallOptions,
169) -> Result<SpeechResult, AiMuxError> {
170    let timeout = timeout::OperationTimeout::new(options.timeout.unwrap_or_default())?;
171    let abort_signal = options.abort_signal.clone();
172    let retries = retry::prepare_retries(
173        options.max_retries,
174        model.retry_config(),
175        abort_signal.clone(),
176    );
177    timeout::run(
178        retries.retry(|| model.do_generate(&options)),
179        abort_signal.as_ref(),
180        timeout,
181    )
182    .await
183}