Skip to main content

ferrin_google/speech_translation/
mod.rs

1//! Google Live speech translation adapted from Vercel AI SDK (Apache-2.0,
2//! Copyright 2023 Vercel, Inc.); see the root NOTICE.
3
4mod mapper;
5
6use ferrin_spec::JsonObject;
7use ferrin_spec::ModelId;
8use ferrin_spec::ProviderId;
9use ferrin_spec::Warning;
10use ferrin_spec::error::InvalidArgumentError;
11use ferrin_spec::error::ProviderError;
12use ferrin_spec::language_model::RequestMetadata;
13use ferrin_spec::speech_translation_model::SpeechTranslationModel;
14use ferrin_spec::speech_translation_model::SpeechTranslationStreamOptions;
15use ferrin_spec::speech_translation_model::SpeechTranslationStreamResult;
16use serde::Deserialize;
17use serde_json::json;
18
19use crate::config::GoogleConfig;
20use crate::config::SharedConfig;
21use crate::live_audio;
22use crate::options::parse_merged;
23
24/// Provider options for Live speech translation.
25#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct GoogleSpeechTranslationOptions {
28    /// Echo input already in the target language instead of producing silence.
29    pub echo_target_language: Option<bool>,
30}
31
32impl GoogleSpeechTranslationOptions {
33    fn merge(mut self, other: Self) -> Self {
34        if other.echo_target_language.is_some() {
35            self.echo_target_language = other.echo_target_language;
36        }
37        self
38    }
39}
40
41/// Streaming speech translation over the Gemini Live API.
42#[derive(Debug, Clone)]
43pub struct GoogleSpeechTranslationModel {
44    config: SharedConfig,
45    provider: ProviderId,
46    model_id: ModelId,
47}
48
49impl GoogleSpeechTranslationModel {
50    /// Creates a Live speech translation model.
51    #[must_use]
52    pub fn new(config: SharedConfig, model_id: impl Into<ModelId>) -> Self {
53        Self {
54            provider: config.provider_id("speech-translation"),
55            config,
56            model_id: model_id.into(),
57        }
58    }
59}
60
61impl SpeechTranslationModel for GoogleSpeechTranslationModel {
62    fn provider(&self) -> &ProviderId {
63        &self.provider
64    }
65    fn model_id(&self) -> &ModelId {
66        &self.model_id
67    }
68
69    #[tracing::instrument(skip_all, fields(model = %self.model_id))]
70    async fn do_stream(
71        &self,
72        options: SpeechTranslationStreamOptions,
73    ) -> Result<SpeechTranslationStreamResult, ProviderError> {
74        live_audio::validate_format(&options.input_audio_format)?;
75        if options.target_language.trim().is_empty() {
76            return Err(InvalidArgumentError::new(
77                "target_language",
78                "target language must not be empty",
79            )
80            .into());
81        }
82        let google = parse_merged::<GoogleSpeechTranslationOptions>(
83            &self.config,
84            &options.provider_options,
85            GoogleSpeechTranslationOptions::merge,
86        )?;
87        let mut translation = JsonObject::from_iter([(
88            "targetLanguageCode".to_owned(),
89            json!(options.target_language),
90        )]);
91        if let Some(echo) = google.echo_target_language {
92            translation.insert("echoTargetLanguage".to_owned(), json!(echo));
93        }
94        let setup = json!({
95            "model": GoogleConfig::model_path(self.model_id.as_str()),
96            "generationConfig": {"responseModalities": ["AUDIO"], "translationConfig": translation},
97            "inputAudioTranscription": {}, "outputAudioTranscription": {},
98        });
99        let mut warnings = Vec::new();
100        if options.source_language.is_some() {
101            warnings.push(Warning::unsupported_with_details(
102                "source_language",
103                "Google Live automatically detects the source language",
104            ));
105        }
106        if options.output_audio_format.is_some() {
107            warnings.push(Warning::unsupported_with_details(
108                "output_audio_format",
109                "Google Live returns signed 16-bit PCM at 24 kHz",
110            ));
111        }
112        let request = RequestMetadata::with_body(setup.clone());
113        let mapper = mapper::Translation::new(self.config.clone(), warnings);
114        let (stream, response) = live_audio::start(
115            &self.config,
116            self.model_id.clone(),
117            live_audio::Options {
118                setup,
119                audio: options.audio,
120                headers: options.headers,
121                cancellation: options.cancellation,
122                include_raw: options.include_raw_chunks,
123            },
124            mapper,
125        )
126        .await?;
127        Ok(SpeechTranslationStreamResult {
128            stream,
129            request,
130            response,
131        })
132    }
133}