diff --git a/Cargo.lock b/Cargo.lock index 98aaf30..0027b82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -912,6 +912,7 @@ dependencies = [ "serde_with", "serde_yaml 0.9.19", "tokio", + "urlencoding", ] [[package]] @@ -1445,6 +1446,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/rust-ai/Cargo.toml b/rust-ai/Cargo.toml index 83891db..2c039f1 100644 --- a/rust-ai/Cargo.toml +++ b/rust-ai/Cargo.toml @@ -32,4 +32,5 @@ log = "0.4.17" log4rs = "1.2.0" serde_with = "2.3.1" isolang = { version = "2.2.0", features = ["serde"] } -lazy_static = "1.4.0" \ No newline at end of file +lazy_static = "1.4.0" +urlencoding = "2.1.2" diff --git a/rust-ai/src/azure/apis/speech.rs b/rust-ai/src/azure/apis/speech.rs index 4d03f92..e0e6b8e 100644 --- a/rust-ai/src/azure/apis/speech.rs +++ b/rust-ai/src/azure/apis/speech.rs @@ -35,7 +35,8 @@ use reqwest::header::HeaderMap; use crate::azure::{ endpoint::{request_get_endpoint, request_post_endpoint_ssml, SpeechServiceEndpoint}, types::{ - common::{MicrosoftOutputFormat, ResponseExpectation, ResponseType, ServiceHealthResponse}, + common::{MicrosoftOutputFormat, ResponseExpectation, ResponseType}, + speech::ServiceHealthResponse, tts::Voice, SSML, }, @@ -90,7 +91,7 @@ impl Speech { /// /// Source: pub async fn voice_list() -> Result, Box> { - let text = request_get_endpoint(&SpeechServiceEndpoint::Get_List_of_Voices).await?; + let text = request_get_endpoint(&SpeechServiceEndpoint::Get_List_of_Voices, None).await?; match serde_json::from_str::>(&text) { Ok(voices) => Ok(voices), Err(e) => { @@ -100,13 +101,16 @@ impl Speech { } } - /// Health status provides insights about the overall health of the service + /// Health status provides insights about the overall health of the service /// and sub-components. - /// + /// /// V3.1 API supported only. pub async fn health_check() -> Result> { - let text = - request_get_endpoint(&SpeechServiceEndpoint::Get_Speech_to_Text_Health_Status_v3_1).await?; + let text = request_get_endpoint( + &SpeechServiceEndpoint::Get_Speech_to_Text_Health_Status_v3_1, + None, + ) + .await?; match serde_json::from_str::(&text) { Ok(status) => Ok(status), @@ -127,7 +131,7 @@ impl Speech { let mut headers = HeaderMap::new(); headers.insert("X-Microsoft-OutputFormat", self.output_format.into()); match request_post_endpoint_ssml( - &SpeechServiceEndpoint::Convert_Text_to_Speech_v1, + &SpeechServiceEndpoint::Post_Text_to_Speech_v1, self.ssml, ResponseExpectation::Bytes, headers, @@ -145,3 +149,208 @@ impl Speech { Ok(self.text_to_speech().await?) } } + +/// TODO: remove `allow(dead_code)` when `models()` implemented. +#[allow(dead_code)] +pub struct SpeechModel { + model_id: Option, + + skip: Option, + top: Option, + filter: Option, +} + +impl Default for SpeechModel { + fn default() -> Self { + Self { + model_id: None, + skip: None, + top: None, + filter: None, + } + } +} + +impl SpeechModel { + pub fn skip(self, skip: usize) -> Self { + Self { + skip: Some(skip), + ..self + } + } + pub fn top(self, top: usize) -> Self { + Self { + top: Some(top), + ..self + } + } + pub fn filter(self, filter: FilterOperator) -> Self { + Self { + filter: Some(filter), + ..self + } + } + + pub fn id(self, id: String) -> Self { + Self { + model_id: Some(id), + ..self + } + } + + /// [Custom Speech] + /// Gets the list of custom models for the authenticated subscription. + /// + /// TODO: implement this. + pub async fn models(self) -> Result<(), Box> { + todo!("Test with custom models"); + // let mut params = HashMap::::new(); + + // if let Some(skip) = self.skip { + // params.insert("skip".into(), skip.to_string()); + // } + // if let Some(top) = self.top { + // params.insert("top".into(), top.to_string()); + // } + // if let Some(filter) = self.filter { + // params.insert("filter".into(), filter.to_string()); + // } + + // let text = request_get_endpoint( + // &SpeechServiceEndpoint::Get_List_of_Models_v3_1, + // Some(params), + // ) + // .await?; + + // println!("{}", text); + + // match serde_json::from_str::(&text) { + // Ok(status) => Ok(status), + // Err(e) => { + // warn!(target: "azure", "Error parsing response: {:?}", e); + // Err("Unable to parse health status of speech cognitive services, check log for details".into()) + // } + // } + + // Ok(()) + } +} + +#[derive(Clone, Debug)] +pub enum FilterField { + DisplayName, + Description, + CreatedDateTime, + LastActionDateTime, + Status, + Locale, +} + +impl Into for FilterField { + fn into(self) -> String { + (match self { + Self::DisplayName => "displayName", + Self::Description => "description", + Self::CreatedDateTime => "createdDateTime", + Self::LastActionDateTime => "lastActionDateTime", + Self::Status => "status", + Self::Locale => "locale", + }) + .into() + } +} + +#[derive(Clone, Debug)] +pub enum FilterOperator { + Eq(FilterField, String), + Ne(FilterField, String), + Gt(FilterField, String), + Ge(FilterField, String), + Lt(FilterField, String), + Le(FilterField, String), + And(Box, Box), + Or(Box, Box), + Not(Box), +} +impl FilterOperator { + pub fn and(self, op: FilterOperator) -> Self { + Self::And(Box::new(self), Box::new(op)) + } + pub fn or(self, op: FilterOperator) -> Self { + Self::Or(Box::new(self), Box::new(op)) + } + pub fn not(self) -> Self { + Self::Not(Box::new(self)) + } + + fn str(self, not: bool) -> String { + match self { + Self::And(a, b) => { + if not { + format!("{} or {}", a.str(true), b.str(true)) + } else { + format!("{} and {}", a.str(false), b.str(false)) + } + } + + Self::Or(a, b) => { + if not { + format!("{} and {}", a.str(true), b.str(true)) + } else { + format!("{} or {}", a.str(false), b.str(false)) + } + } + + Self::Not(a) => format!("{}", a.str(!not)), + + Self::Eq(field, value) => format!( + "{} {} '{}'", + Into::::into(field), + if not { "ne" } else { "eq" }, + Into::::into(value) + ), + Self::Ne(field, value) => format!( + "{} {} '{}'", + Into::::into(field), + if not { "eq" } else { "ne" }, + Into::::into(value) + ), + Self::Gt(field, value) => format!( + "{} {} '{}'", + Into::::into(field), + if not { "le" } else { "gt" }, + Into::::into(value) + ), + Self::Ge(field, value) => format!( + "{} {} '{}'", + Into::::into(field), + if not { "lt" } else { "ge" }, + Into::::into(value) + ), + Self::Lt(field, value) => format!( + "{} {} '{}'", + Into::::into(field), + if not { "ge" } else { "lt" }, + Into::::into(value) + ), + Self::Le(field, value) => format!( + "{} {} '{}'", + Into::::into(field), + if not { "gt" } else { "le" }, + Into::::into(value) + ), + } + } +} + +impl Into for FilterOperator { + fn into(self) -> String { + self.str(false) + } +} + +impl ToString for FilterOperator { + fn to_string(&self) -> String { + Into::::into(self.clone()) + } +} diff --git a/rust-ai/src/azure/endpoint.rs b/rust-ai/src/azure/endpoint.rs index b12fc7e..8767607 100644 --- a/rust-ai/src/azure/endpoint.rs +++ b/rust-ai/src/azure/endpoint.rs @@ -1,6 +1,8 @@ use log::{debug, error}; use reqwest::{header::HeaderMap, Client}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use urlencoding::encode; use crate::utils::config::Config; @@ -13,8 +15,9 @@ use super::{ #[derive(Serialize, Deserialize, Debug, Clone)] pub enum SpeechServiceEndpoint { Get_List_of_Voices, - Convert_Text_to_Speech_v1, - Get_Speech_to_Text_Health_Status_v3_1 + Post_Text_to_Speech_v1, + Get_Speech_to_Text_Health_Status_v3_1, + Get_List_of_Models_v3_1, } impl SpeechServiceEndpoint { @@ -25,27 +28,43 @@ impl SpeechServiceEndpoint { region ), - Self::Convert_Text_to_Speech_v1 => format!( + Self::Post_Text_to_Speech_v1 => format!( "https://{}.tts.speech.microsoft.com/cognitiveservices/v1", region ), - Self::Get_Speech_to_Text_Health_Status_v3_1 => format!( "https://{}.cognitiveservices.azure.com/speechtotext/v3.1/healthstatus", region ), + + Self::Get_List_of_Models_v3_1 => format!( + "https://{}.cognitiveservices.azure.com/speechtotext/v3.1/models", + region + ), } } } pub async fn request_get_endpoint( endpoint: &SpeechServiceEndpoint, + params: Option>, ) -> Result> { let config = Config::load().unwrap(); let region = config.azure.speech.region; - let url = endpoint.build(®ion); + let mut url = endpoint.build(®ion); + + if let Some(params) = params { + let combined = params + .iter() + .map(|(k, v)| format!("{}={}", encode(k), encode(v))) + .collect::>() + .join("&"); + url.push_str(&format!("?{}", combined)); + } + + println!("URL={}", url); let client = Client::new(); let mut req = client.get(url); diff --git a/rust-ai/src/azure/mod.rs b/rust-ai/src/azure/mod.rs index 953bcb1..8f012eb 100644 --- a/rust-ai/src/azure/mod.rs +++ b/rust-ai/src/azure/mod.rs @@ -19,10 +19,10 @@ pub mod apis; /// Azure types definition pub mod types; -pub use apis::speech::Speech; -pub use types::MicrosoftOutputFormat; +pub use apis::speech::{FilterField, FilterOperator, Speech, SpeechModel}; +pub use types::ssml; pub use types::Gender; pub use types::Locale; +pub use types::MicrosoftOutputFormat; pub use types::VoiceName; pub use types::SSML; -pub use types::ssml; diff --git a/rust-ai/src/azure/types/common.rs b/rust-ai/src/azure/types/common.rs index b7ce52d..90434dc 100644 --- a/rust-ai/src/azure/types/common.rs +++ b/rust-ai/src/azure/types/common.rs @@ -1,5 +1,4 @@ use reqwest::header::HeaderValue; -use serde::{Deserialize, Serialize}; /// Available gender variants implemented for Azure. #[derive(Debug, Clone)] @@ -210,53 +209,3 @@ impl Into for MicrosoftOutputFormat { .into() } } - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ServiceHealthResponse { - /// Health status of the service. - pub status: HealthStatus, - - /// Additional messages about the current service health. - #[serde(skip_serializing_if = "Option::is_none")] - pub message: Option, - - /// Optional subcomponents of this service and their status. - pub components: Vec, -} - -/// Subcomponent health status. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ComponentHealth { - /// Health status of the component. - pub status: HealthStatus, - - /// Additional messages about the current service component health. - #[serde(skip_serializing_if = "Option::is_none")] - pub message: Option, - - /// The name of the component. - pub name: String, - - /// The type of this component. - #[serde(rename = "type")] - pub ty: String, -} - -/// Health status of the service. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub enum HealthStatus { - Unhealthy, - Healthy, - Degraded, -} - -impl Into for HealthStatus { - fn into(self) -> String { - (match self { - Self::Degraded => "Degraded", - Self::Healthy => "Healthy", - Self::Unhealthy => "Unhealthy", - }) - .into() - } -} diff --git a/rust-ai/src/azure/types/mod.rs b/rust-ai/src/azure/types/mod.rs index 8094d6a..e837cfa 100644 --- a/rust-ai/src/azure/types/mod.rs +++ b/rust-ai/src/azure/types/mod.rs @@ -2,6 +2,7 @@ pub mod common; pub mod locale; pub mod tts; pub mod ssml; +pub mod speech; pub use locale::Locale; pub use ssml::voice_name::VoiceName; diff --git a/rust-ai/src/azure/types/speech.rs b/rust-ai/src/azure/types/speech.rs new file mode 100644 index 0000000..dc91f48 --- /dev/null +++ b/rust-ai/src/azure/types/speech.rs @@ -0,0 +1,65 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ServiceHealthResponse { + /// Health status of the service. + pub status: HealthStatus, + + /// Additional messages about the current service health. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + + /// Optional subcomponents of this service and their status. + pub components: Vec, +} + +/// Subcomponent health status. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ComponentHealth { + /// Health status of the component. + pub status: HealthStatus, + + /// Additional messages about the current service component health. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + + /// The name of the component. + pub name: String, + + /// The type of this component. + #[serde(rename = "type")] + pub ty: String, +} + +/// Health status of the service. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum HealthStatus { + Unhealthy, + Healthy, + Degraded, +} + +impl Into for HealthStatus { + fn into(self) -> String { + (match self { + Self::Degraded => "Degraded", + Self::Healthy => "Healthy", + Self::Unhealthy => "Unhealthy", + }) + .into() + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ErrorResponse { + pub code: String, + pub message: Option, + #[serde(rename = "innerError")] + pub inner_error: InnerError, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct InnerError { + pub code: String, + pub message: Option, +}