1use std::future::IntoFuture;
6
7use bytes::Bytes;
8use ferrin_provider_util::media_type::detect_media_type_for;
9use ferrin_spec::BoxFuture;
10use ferrin_spec::MediaType;
11use ferrin_spec::ProviderMetadata;
12use ferrin_spec::RequestMetadata;
13use ferrin_spec::ResponseMetadata;
14use ferrin_spec::SpeechModelRef;
15use ferrin_spec::Warning;
16use ferrin_spec::speech_model::SpeechOptions;
17use tracing::Instrument;
18
19use crate::error::Error;
20use crate::modality::ModalityOptions;
21use crate::modality::impl_modality_builder;
22use crate::registry::ProviderRegistry;
23use crate::registry::default::resolve_model;
24use crate::retry::retry;
25use crate::telemetry::ModelIdentity;
26use crate::telemetry::spans;
27
28const DEFAULT_AUDIO_MEDIA_TYPE: &str = "audio/mp3";
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct GeneratedAudio {
34 pub data: Bytes,
36 pub media_type: MediaType,
38 pub format: String,
41}
42
43impl GeneratedAudio {
44 #[must_use]
46 pub fn new(data: Bytes, media_type: MediaType) -> Self {
47 let format = match media_type.subtype() {
48 Some(subtype) if media_type.as_str() != "audio/mpeg" && !subtype.is_empty() => {
49 subtype.to_owned()
50 }
51 _ => "mp3".to_owned(),
52 };
53 Self {
54 data,
55 media_type,
56 format,
57 }
58 }
59
60 #[must_use]
62 pub fn base64(&self) -> String {
63 use base64::Engine as _;
64 base64::engine::general_purpose::STANDARD.encode(&self.data)
65 }
66}
67
68#[derive(Debug, Clone, PartialEq)]
70pub struct GenerateSpeechResult {
71 pub audio: GeneratedAudio,
73 pub warnings: Vec<Warning>,
75 pub request: RequestMetadata,
77 pub responses: Vec<ResponseMetadata>,
79 pub provider_metadata: Option<ProviderMetadata>,
81}
82
83#[must_use]
85pub fn generate_speech(
86 model: impl Into<SpeechModelRef>,
87 text: impl Into<String>,
88) -> GenerateSpeech {
89 GenerateSpeech {
90 model: model.into(),
91 text: text.into(),
92 voice: None,
93 output_format: None,
94 instructions: None,
95 speed: None,
96 language: None,
97 base: ModalityOptions::default(),
98 }
99}
100
101#[derive(Debug)]
103pub struct GenerateSpeech {
104 model: SpeechModelRef,
105 text: String,
106 voice: Option<String>,
107 output_format: Option<String>,
108 instructions: Option<String>,
109 speed: Option<f64>,
110 language: Option<String>,
111 base: ModalityOptions,
112}
113
114impl GenerateSpeech {
115 #[must_use]
117 pub fn voice(mut self, voice: impl Into<String>) -> Self {
118 self.voice = Some(voice.into());
119 self
120 }
121
122 #[must_use]
124 pub fn output_format(mut self, output_format: impl Into<String>) -> Self {
125 self.output_format = Some(output_format.into());
126 self
127 }
128
129 #[must_use]
131 pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
132 self.instructions = Some(instructions.into());
133 self
134 }
135
136 #[must_use]
138 pub fn speed(mut self, speed: f64) -> Self {
139 self.speed = Some(speed);
140 self
141 }
142
143 #[must_use]
145 pub fn language(mut self, language: impl Into<String>) -> Self {
146 self.language = Some(language.into());
147 self
148 }
149}
150
151impl_modality_builder!(GenerateSpeech);
152
153impl IntoFuture for GenerateSpeech {
154 type Output = Result<GenerateSpeechResult, Error>;
155 type IntoFuture = BoxFuture<'static, Self::Output>;
156
157 fn into_future(self) -> Self::IntoFuture {
158 Box::pin(run(self))
159 }
160}
161
162async fn run(builder: GenerateSpeech) -> Result<GenerateSpeechResult, Error> {
163 let model = resolve_model(&builder.model, ProviderRegistry::speech_model)?;
164 let identity = ModelIdentity::new(model.provider().clone(), model.model_id().clone());
165 let span = spans::modality_span("speech", &identity);
166 let base = builder.base.clone();
167 base.run(|base, token| {
168 async move {
169 let headers = base.request_headers();
170 let result = retry(&base.retry_policy, &token, |_| {
171 let options = SpeechOptions {
172 text: builder.text.clone(),
173 voice: builder.voice.clone(),
174 output_format: builder.output_format.clone(),
175 instructions: builder.instructions.clone(),
176 speed: builder.speed,
177 language: builder.language.clone(),
178 provider_options: base.provider_options.clone(),
179 headers: headers.clone(),
180 cancellation: token.child_token(),
181 };
182 let model = &model;
183 async move { model.do_generate(options).await.map_err(Error::from) }
184 })
185 .await?;
186 if result.audio.is_empty() {
187 return Err(Error::NoSpeechGenerated {
188 responses: vec![result.response],
189 });
190 }
191 spans::log_warnings(&result.warnings, &identity);
192 let media_type = detect_media_type_for(&result.audio, "audio")
193 .unwrap_or_else(|| MediaType::new(DEFAULT_AUDIO_MEDIA_TYPE));
194 Ok(GenerateSpeechResult {
195 audio: GeneratedAudio::new(result.audio, media_type),
196 warnings: result.warnings,
197 request: result.request,
198 responses: vec![result.response],
199 provider_metadata: result.provider_metadata,
200 })
201 }
202 .instrument(span)
203 })
204 .await
205}