use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use serde::Serialize;
use serde_json::{Value, json};
use super::interactions_api::streaming::usage_of;
use super::interactions_api::{InteractionSpeech, InteractionSpeechConfig};
use crate::audio_generation::{AudioGenerationRequest, AudioGenerationResponse};
use crate::error::{EncodeError, ProviderError};
use crate::json_utils::Lenient;
use crate::operation::AudioGeneration;
use crate::providers::internal::wire::classify_marker_keyed_frame;
use crate::wire::{
Decoder, Descriptor, Encoded, Flow, Framing, Mode, Out, Wire, WireEvent, WireFrame,
};
pub const GEMINI_3_8_FLASH_TTS: &str = "gemini-3.8-flash-tts";
pub const GEMINI_3_8_FLASH_LITE_TTS: &str = "gemini-3.8-flash-lite-tts";
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Turn {
pub text: String,
pub speaker: Option<String>,
pub style: Option<String>,
}
impl Turn {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
..Self::default()
}
}
pub fn speaker(mut self, speaker: impl Into<String>) -> Self {
self.speaker = Some(speaker.into());
self
}
pub fn style(mut self, style: impl Into<String>) -> Self {
self.style = Some(style.into());
self
}
}
impl Serialize for Turn {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
#[derive(Serialize)]
struct Metadata<'a> {
#[serde(rename = "type")]
kind: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
speaker: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
style: Option<&'a str>,
}
#[derive(Serialize)]
struct Text<'a> {
#[serde(rename = "type")]
kind: &'static str,
text: &'a str,
#[serde(skip_serializing_if = "Vec::is_empty")]
annotations: Vec<Metadata<'a>>,
}
let annotations = match (&self.speaker, &self.style) {
(None, None) => Vec::new(),
(speaker, style) => vec![Metadata {
kind: "speech_metadata",
speaker: speaker.as_deref(),
style: style.as_deref(),
}],
};
Text {
kind: "text",
text: &self.text,
annotations,
}
.serialize(serializer)
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum AudioEncoding {
#[serde(rename = "audio/wav")]
Wav,
#[serde(rename = "audio/l16")]
L16,
#[serde(rename = "audio/mulaw")]
Mulaw,
#[serde(rename = "audio/alaw")]
Alaw,
#[serde(rename = "audio/mp3")]
Mp3,
#[serde(rename = "audio/ogg_opus")]
OggOpus,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
pub struct AudioFormat {
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<AudioEncoding>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sample_rate: Option<u32>,
}
impl AudioFormat {
pub fn new(encoding: AudioEncoding) -> Self {
Self {
mime_type: Some(encoding),
sample_rate: None,
}
}
pub fn pcm(sample_rate: u32) -> Self {
Self::new(AudioEncoding::L16).sample_rate(sample_rate)
}
pub fn sample_rate(mut self, hertz: u32) -> Self {
self.sample_rate = Some(hertz);
self
}
}
#[derive(Serialize)]
struct ResponseFormat {
#[serde(rename = "type")]
kind: &'static str,
#[serde(flatten)]
format: AudioFormat,
}
impl From<AudioFormat> for ResponseFormat {
fn from(format: AudioFormat) -> Self {
Self {
kind: "audio",
format,
}
}
}
fn input(turns: &[Turn]) -> Value {
json!([{ "type": "user_input", "content": turns }])
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Script {
speech: InteractionSpeechConfig,
turns: Vec<Turn>,
format: Option<AudioFormat>,
}
impl Script {
pub fn voice(voice: impl Into<String>) -> Self {
Self::with(InteractionSpeechConfig::voice(InteractionSpeech::voice(
voice,
)))
}
pub fn speakers<S: Into<String>, V: Into<String>>(
speakers: impl IntoIterator<Item = (S, V)>,
) -> Self {
Self::with(InteractionSpeechConfig::speakers(speakers))
}
pub fn with(speech: InteractionSpeechConfig) -> Self {
Self {
speech,
turns: Vec::new(),
format: None,
}
}
pub fn turn(mut self, turn: Turn) -> Self {
self.turns.push(turn);
self
}
pub fn format(mut self, format: AudioFormat) -> Self {
self.format = Some(format);
self
}
pub fn build(self) -> AudioGenerationRequest {
let text = self
.turns
.iter()
.map(|turn| turn.text.as_str())
.collect::<Vec<_>>()
.join("\n");
let voice = match &self.speech {
InteractionSpeechConfig::Voice([voice]) => voice.voice.clone().unwrap_or_default(),
InteractionSpeechConfig::Speakers { .. } => String::new(),
};
let mut params = json!({
"input": input(&self.turns),
"generation_config": { "speech_config": self.speech },
});
if let (Some(format), Some(fields)) = (self.format, params.as_object_mut()) {
fields.insert(
"response_format".to_owned(),
json!(ResponseFormat::from(format)),
);
}
crate::audio_generation::AudioGenerationRequestBuilder::new(text, voice)
.additional_params(params)
.build()
}
}
fn last_audio(reply: &Value) -> Option<&Value> {
reply
.arr("steps")
.iter()
.filter(|step| step.str("type") == Some("model_output"))
.flat_map(|step| step.arr("content"))
.rfind(|content| content.str("type") == Some("audio"))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SpeechFormat {
pub mime_type: Option<String>,
pub sample_rate: Option<u64>,
pub channels: Option<u64>,
}
pub fn format_of(response: &AudioGenerationResponse) -> Option<SpeechFormat> {
let audio = last_audio(&response.raw)?;
Some(SpeechFormat {
mime_type: audio.str("mime_type").map(str::to_owned),
sample_rate: audio.u64("sample_rate"),
channels: audio.u64("channels"),
})
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Speech {
pub provider: super::GeminiConfig,
pub model: String,
}
impl Speech {
pub fn new(provider: super::GeminiConfig, model: impl Into<String>) -> Self {
Self {
provider,
model: model.into(),
}
}
}
fn create_request_body(model: &str, request: AudioGenerationRequest) -> Value {
let mut body = json!({
"model": model,
"input": input(&[Turn::new(request.text)]),
"response_format": ResponseFormat::from(AudioFormat::default()),
});
if let (false, Some(fields)) = (request.voice.is_empty(), body.as_object_mut()) {
let speech = InteractionSpeechConfig::voice(InteractionSpeech::voice(request.voice));
fields.insert(
"generation_config".to_owned(),
json!({ "speech_config": speech }),
);
}
match (&mut body, request.additional_params) {
(Value::Object(body), Some(Value::Object(params))) => {
crate::completion::options::deep_merge(body, params);
}
(body, Some(params)) => *body = params,
(_, None) => {}
}
body
}
impl Wire for Speech {
type Op = AudioGeneration;
type Payload = Encoded;
type Frame = WireFrame;
type Event = Value;
type Decoder<'id> = SpeechDecoder;
type Reassembler = crate::wire::document::Unreassembled;
fn describe(&self) -> Descriptor<'_> {
Descriptor::new(super::PROVIDER_NAME).model(self.model.as_str())
}
fn encode(&self, request: AudioGenerationRequest, _mode: Mode) -> Result<Encoded, EncodeError> {
if request.speed != 1.0 {
return Err(EncodeError::request(
"Gemini speech takes no speed; ask for the pace in a turn's style",
));
}
let body = serde_json::to_vec(&create_request_body(&self.model, request))?;
let request = http::Request::post(self.provider.interactions_uri("/v1beta/interactions"))
.header(http::header::CONTENT_TYPE, "application/json")
.header(
super::GeminiConfig::INTERACTIONS_KEY_HEADER,
self.provider.api_key.expose(),
)
.body(crate::wire::Body::Bytes(body))?;
Ok(Encoded::new(request, Framing::Whole))
}
fn decoder<'id>(&self) -> Self::Decoder<'id> {
SpeechDecoder
}
}
#[derive(Default)]
pub struct SpeechDecoder;
impl<'id> Decoder<'id, AudioGeneration> for SpeechDecoder {
type Event = Value;
fn classify(&self, frame: WireFrame) -> WireEvent<Self::Event> {
classify_marker_keyed_frame(&frame.as_str(), &["steps", "status", "usage", "id"])
}
fn decode(
&mut self,
event: Self::Event,
out: Out<'id, AudioGeneration>,
) -> Result<Flow, ProviderError> {
let data = last_audio(&event)
.and_then(|audio| audio.str("data"))
.ok_or_else(|| {
ProviderError::Response(format!(
"the Gemini speech interaction ({}) carried no inline audio",
event.str("status").unwrap_or("no status")
))
})?;
let audio = BASE64_STANDARD.decode(data).map_err(|err| {
ProviderError::Response(format!("Gemini audio data was not valid base64: {err}"))
})?;
Ok(out.end(AudioGenerationResponse {
model: event.str("model").map(str::to_owned),
response_id: event.str("id").map(str::to_owned),
usage: event.get("usage").map(usage_of).unwrap_or_default(),
..AudioGenerationResponse::new(audio)
}))
}
}
impl super::GeminiConfig {
pub(crate) fn audio_generation(&self, model: impl Into<String>) -> Speech {
Speech::new(self.clone(), model)
}
}
#[cfg(test)]
mod tests;