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}