ferrin_google/speech_translation/
mod.rs1mod 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#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct GoogleSpeechTranslationOptions {
28 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#[derive(Debug, Clone)]
43pub struct GoogleSpeechTranslationModel {
44 config: SharedConfig,
45 provider: ProviderId,
46 model_id: ModelId,
47}
48
49impl GoogleSpeechTranslationModel {
50 #[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}