Skip to main content

ferrin_core/
speech.rs

1//! Speech synthesis: [`generate_speech`].
2//!
3//! Design: `docs/01-architecture/11-other-modalities.md` ยง3.
4
5use 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
28/// Media type used when byte detection cannot recognize the audio.
29const DEFAULT_AUDIO_MEDIA_TYPE: &str = "audio/mp3";
30
31/// Generated audio.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct GeneratedAudio {
34    /// Audio bytes.
35    pub data: Bytes,
36    /// Media type detected from the bytes, or `audio/mp3`.
37    pub media_type: MediaType,
38    /// Container format derived from the media type (`mp3` for
39    /// `audio/mpeg`, otherwise the subtype).
40    pub format: String,
41}
42
43impl GeneratedAudio {
44    /// Builds the audio value, deriving `format` from the media type.
45    #[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    /// The audio bytes as base64.
61    #[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/// Result of [`generate_speech`].
69#[derive(Debug, Clone, PartialEq)]
70pub struct GenerateSpeechResult {
71    /// The generated audio.
72    pub audio: GeneratedAudio,
73    /// Adapter warnings.
74    pub warnings: Vec<Warning>,
75    /// Request metadata.
76    pub request: RequestMetadata,
77    /// Response metadata of the calls made.
78    pub responses: Vec<ResponseMetadata>,
79    /// Provider-specific metadata.
80    pub provider_metadata: Option<ProviderMetadata>,
81}
82
83/// Synthesizes speech from `text`.
84#[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/// Builder returned by [`generate_speech`]; `.await` runs the call.
102#[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    /// Voice to use.
116    #[must_use]
117    pub fn voice(mut self, voice: impl Into<String>) -> Self {
118        self.voice = Some(voice.into());
119        self
120    }
121
122    /// Output format (`mp3`, `wav`, ...).
123    #[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    /// Style instructions.
130    #[must_use]
131    pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
132        self.instructions = Some(instructions.into());
133        self
134    }
135
136    /// Speed multiplier.
137    #[must_use]
138    pub fn speed(mut self, speed: f64) -> Self {
139        self.speed = Some(speed);
140        self
141    }
142
143    /// Language code.
144    #[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}