Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions examples/transcribe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,20 +321,20 @@ async fn start_conversation(
.or_else(|| env::var("GOOGLE_TRANSCRIBE_REGION").ok());

let region = match region.as_deref() {
Some("global") => google_transcribe::transcribe::Region::Global,
Some("eu") => google_transcribe::transcribe::Region::Eu,
Some("us") => google_transcribe::transcribe::Region::Us,
Some("global") => google_transcribe::Region::Global,
Some("eu") => google_transcribe::Region::Eu,
Some("us") => google_transcribe::Region::Us,
Some(invalid) => bail!(
"Invalid GOOGLE_TRANSCRIBE_REGION '{}'. Must be one of: global, eu, us",
invalid
),
None => google_transcribe::transcribe::Region::default(),
None => google_transcribe::Region::default(),
};

// Check model/language/region feature support (including diarization):
// https://docs.cloud.google.com/speech-to-text/docs/speech-to-text-supported-languages

let params = google_transcribe::transcribe::Params {
let params = google_transcribe::Params {
model: provider_args.model.map(str::to_owned).unwrap_or_else(|| {
env::var("GOOGLE_TRANSCRIBE_MODEL").unwrap_or_else(|_| "latest_long".to_owned())
}),
Expand Down
156 changes: 16 additions & 140 deletions services/google-transcribe/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,157 +1,25 @@
//! Tonic usage inspiration from:
//! <https://github.com/bouzuya/googleapis-tonic/blob/master/examples/googleapis-tonic-google-firestore-v1-1/>

use std::error;
use std::{env, sync::Arc};

use anyhow::{Context, Result, anyhow};
use anyhow::Result;
use async_stream::{stream, try_stream};
use context_switch_core::{AudioFormat, audio};
use futures::Stream;
use google_cloud_auth::credentials::AccessTokenCredentials;
use google_cloud_auth::credentials::service_account;
use google_cloud_token::TokenSource;
use tokio::sync::mpsc::UnboundedReceiver;
use tracing::debug;

use googleapis_tonic_google_cloud_speech_v2::google::cloud::speech::v2::recognition_config::DecodingConfig;
use googleapis_tonic_google_cloud_speech_v2::google::cloud::speech::v2::speech_client::SpeechClient;
use googleapis_tonic_google_cloud_speech_v2::google::cloud::speech::v2::{
ExplicitDecodingConfig, RecognitionConfig, RecognitionFeatures, StreamingRecognitionConfig,
StreamingRecognitionFeatures, StreamingRecognizeRequest, StreamingRecognizeResponse,
SpeakerDiarizationConfig,
explicit_decoding_config,
};
use googleapis_tonic_google_cloud_speech_v2::google::cloud::speech::v2::explicit_decoding_config;
use googleapis_tonic_google_cloud_speech_v2::google::cloud::speech::v2::streaming_recognize_request::StreamingRequest;
use tokio::sync::mpsc::UnboundedReceiver;
use tonic::transport;
use tracing::debug;

use crate::transcribe::Region;

type Client =
googleapis_tonic_google_cloud_speech_v2::google::cloud::speech::v2::speech_client::SpeechClient<
tonic::service::interceptor::InterceptedService<tonic::transport::Channel, AuthInterceptor>,
>;

#[derive(Default)]
pub(crate) struct Config {
endpoint: &'static str,
location: &'static str,
}

impl From<Region> for Config {
fn from(value: Region) -> Self {
match value {
Region::Global => Self {
endpoint: "https://speech.googleapis.com",
location: "global",
},
Region::Eu => Self {
endpoint: "https://eu-speech.googleapis.com",
location: "eu",
},
Region::Us => Self {
endpoint: "https://us-speech.googleapis.com",
location: "us",
},
}
}
}

#[derive(Clone)]
pub(crate) struct Host {
channel: tonic::transport::Channel,
token_source: Arc<dyn TokenSource>,
project_id: String,
location: String,
}

#[derive(Debug)]
struct ServiceAccountTokenSource {
credentials: AccessTokenCredentials,
}

#[async_trait::async_trait]
impl TokenSource for ServiceAccountTokenSource {
async fn token(&self) -> std::result::Result<String, Box<dyn error::Error + Send + Sync>> {
let access_token = self.credentials.access_token().await?;
Ok(format!("Bearer {}", access_token.token))
}
}
use context_switch_core::AudioFormat;
use context_switch_core::audio;

impl Host {
pub(crate) async fn new(params: Config) -> Result<Self> {
let credentials_path = env::var("GOOGLE_APPLICATION_CREDENTIALS")
.context("GOOGLE_APPLICATION_CREDENTIALS is not set")?;
let credentials_json = tokio::fs::read_to_string(&credentials_path)
.await
.with_context(|| {
format!(
"Failed to read GOOGLE_APPLICATION_CREDENTIALS from path: {credentials_path}"
)
})?;
let credentials_value: serde_json::Value = serde_json::from_str(&credentials_json)
.with_context(|| {
format!(
"GOOGLE_APPLICATION_CREDENTIALS does not contain valid JSON: {credentials_path}"
)
})?;

let project_id = credentials_value
.get("project_id")
.and_then(serde_json::Value::as_str)
.context("project_id missing in GOOGLE_APPLICATION_CREDENTIALS JSON")?
.to_owned();

let credentials = service_account::Builder::new(credentials_value)
.build_access_token_credentials()
.context("Failed to build Google service-account credentials")?;

let token_source: Arc<dyn google_cloud_token::TokenSource> =
Arc::new(ServiceAccountTokenSource { credentials });

let channel = transport::Channel::from_static(params.endpoint)
.tls_config(transport::ClientTlsConfig::new().with_webpki_roots())?
.connect()
.await?;

Ok(Self {
channel,
token_source,
project_id,
location: params.location.to_owned(),
})
}

pub async fn client(&self) -> Result<TranscribeClient> {
let inner = self.channel.clone();
let token = self.token_source.token().await.map_err(|e| anyhow!(e))?;
let mut metadata_value = tonic::metadata::AsciiMetadataValue::try_from(token)?;
metadata_value.set_sensitive(true);
let interceptor = AuthInterceptor { metadata_value };
let client = SpeechClient::with_interceptor(inner, interceptor);
Ok(TranscribeClient {
client,
project_id: self.project_id.clone(),
location: self.location.clone(),
})
}
}

#[derive(Clone)]
struct AuthInterceptor {
metadata_value: tonic::metadata::AsciiMetadataValue,
}

impl tonic::service::Interceptor for AuthInterceptor {
fn call(
&mut self,
mut request: tonic::Request<()>,
) -> std::result::Result<tonic::Request<()>, tonic::Status> {
request
.metadata_mut()
.insert("authorization", self.metadata_value.clone());
Ok(request)
}
}
use crate::host::Client;

/// A google transcribe client. Capable of streaming audio data in and transcribe results out.
#[derive(Debug)]
Expand All @@ -162,6 +30,14 @@ pub struct TranscribeClient {
}

impl TranscribeClient {
pub fn new(client: Client, project_id: String, location: String) -> Self {
Self {
client,
project_id,
location,
}
}

pub async fn transcribe<'a>(
&mut self,
model: &str,
Expand Down
144 changes: 144 additions & 0 deletions services/google-transcribe/src/host.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use std::env;
use std::error;
use std::sync::Arc;

use anyhow::{Context, Result, anyhow};
use google_cloud_auth::credentials::AccessTokenCredentials;
use google_cloud_auth::credentials::service_account;
use google_cloud_token::TokenSource;
use googleapis_tonic_google_cloud_speech_v2::google::cloud::speech::v2::speech_client::SpeechClient;
use tonic::service::interceptor;
use tonic::transport;

use crate::Region;
use crate::client::TranscribeClient;

pub type Client =
SpeechClient<interceptor::InterceptedService<transport::Channel, AuthInterceptor>>;

#[derive(Default)]
pub struct Config {
endpoint: &'static str,
location: &'static str,
}

impl From<Region> for Config {
fn from(value: Region) -> Self {
match value {
Region::Global => Self {
endpoint: "https://speech.googleapis.com",
location: "global",
},
Region::Eu => Self {
endpoint: "https://eu-speech.googleapis.com",
location: "eu",
},
Region::Us => Self {
endpoint: "https://us-speech.googleapis.com",
location: "us",
},
}
}
}

#[derive(Clone)]
pub struct Host {
channel: transport::Channel,
token_source: Arc<dyn TokenSource>,
project_id: String,
location: String,
}

impl Host {
pub async fn new(config: Config) -> Result<Self> {
let credentials_path = env::var("GOOGLE_APPLICATION_CREDENTIALS")
.context("GOOGLE_APPLICATION_CREDENTIALS is not set")?;
let credentials_json = tokio::fs::read_to_string(&credentials_path)
.await
.with_context(|| {
format!(
"Failed to read GOOGLE_APPLICATION_CREDENTIALS from path: {credentials_path}"
)
})?;
let credentials_value: serde_json::Value = serde_json::from_str(&credentials_json)
.with_context(|| {
format!(
"GOOGLE_APPLICATION_CREDENTIALS does not contain valid JSON: {credentials_path}"
)
})?;

let project_id = credentials_value
.get("project_id")
.and_then(serde_json::Value::as_str)
.context("project_id missing in GOOGLE_APPLICATION_CREDENTIALS JSON")?
.to_owned();

let credentials = service_account::Builder::new(credentials_value)
.build_access_token_credentials()
.context("Failed to build Google service-account credentials")?;

let token_source: Arc<dyn TokenSource> =
Arc::new(ServiceAccountTokenSource { credentials });

let channel = transport::Channel::from_static(config.endpoint)
.tls_config(transport::ClientTlsConfig::new().with_webpki_roots())?
.connect()
.await?;

Ok(Self {
channel,
token_source,
project_id,
location: config.location.to_owned(),
})
}

pub async fn client(&self) -> Result<TranscribeClient> {
let token = self
.token_source
.token()
.await
.map_err(|error| anyhow!(error))?;
let mut metadata_value = tonic::metadata::AsciiMetadataValue::try_from(token)?;
metadata_value.set_sensitive(true);
let client = SpeechClient::with_interceptor(
self.channel.clone(),
AuthInterceptor { metadata_value },
);
Ok(TranscribeClient::new(
client,
self.project_id.clone(),
self.location.clone(),
))
}
}

#[derive(Debug)]
struct ServiceAccountTokenSource {
credentials: AccessTokenCredentials,
}

#[async_trait::async_trait]
impl TokenSource for ServiceAccountTokenSource {
async fn token(&self) -> std::result::Result<String, Box<dyn error::Error + Send + Sync>> {
let access_token = self.credentials.access_token().await?;
Ok(format!("Bearer {}", access_token.token))
}
}

#[derive(Clone)]
pub struct AuthInterceptor {
metadata_value: tonic::metadata::AsciiMetadataValue,
}

impl tonic::service::Interceptor for AuthInterceptor {
fn call(
&mut self,
mut request: tonic::Request<()>,
) -> std::result::Result<tonic::Request<()>, tonic::Status> {
request
.metadata_mut()
.insert("authorization", self.metadata_value.clone());
Ok(request)
}
}
33 changes: 32 additions & 1 deletion services/google-transcribe/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,37 @@
//! A Google Speech to Text V2 service.
use serde::Deserialize;

mod client;
mod host;
pub mod transcribe;
pub(crate) use client::Host;

pub use transcribe::GoogleTranscribe;

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Params {
/// Google Cloud Speech-to-Text `V2` recognition model (for example, `latest_long`).
pub model: String,
/// One or more comma-separated BCP 47 locale codes sent as `language_codes`.
pub language: String,
/// Enable speaker diarization. Google determines the number of speakers; support depends on
/// the selected model, language, and region.
#[serde(default)]
pub diarization: bool,
/// Google Cloud location and API endpoint. Only `global`, `eu`, and `us` are supported.
/// Defaults to `global`.
#[serde(default)]
pub region: Region,
}

#[derive(Debug, Clone, Copy, Default, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Region {
/// Google Cloud global endpoint.
#[default]
Global,
/// Google Cloud European endpoint.
Eu,
/// Google Cloud United States endpoint.
Us,
}
Loading
Loading