mod audio;
use async_trait::async_trait;
use reqwest_middleware::ClientWithMiddleware;
use crate::audio::{
AudioResponse, SpeechToTextRequest, TextToSpeechRequest, TranscriptionResponse,
};
use crate::client::{
build_http_client, merge_provider_options, merge_provider_options_into_form, ErrorMapper,
};
use crate::error::Error;
use crate::provider::Provider;
const DEFAULT_BASE_URL: &str = "https://api.elevenlabs.io/v1";
pub struct ElevenLabsProvider {
api_key: String,
base_url: String,
client: ClientWithMiddleware,
}
impl ElevenLabsProvider {
pub fn new(api_key: impl Into<String>) -> Self {
Self::with_base_url(api_key, DEFAULT_BASE_URL)
}
pub fn with_base_url(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
base_url: base_url.into(),
client: build_http_client(),
}
}
pub fn with_client(mut self, client: ClientWithMiddleware) -> Self {
self.client = client;
self
}
}
#[async_trait]
impl Provider for ElevenLabsProvider {
fn name(&self) -> &str {
"elevenlabs"
}
async fn text_to_speech(&self, request: TextToSpeechRequest) -> Result<AudioResponse, Error> {
let path = audio::speech_endpoint_path(&request);
let wire_request = audio::build_speech_request(&request);
let body = merge_provider_options(&wire_request, &request.provider_options)?;
let http_response = self
.client
.post(format!("{}/{path}", self.base_url))
.header("xi-api-key", &self.api_key)
.json(&body)
.send()
.await?;
let status = http_response.status();
if !status.is_success() {
let headers = http_response.headers().clone();
let body_text = http_response.text().await?;
let mapper = ErrorMapper {
provider: self.name(),
};
return Err(mapper.map_error_response(status, &headers, &body_text));
}
let mime_type = http_response
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.map(|value| value.to_string());
let data = http_response.bytes().await?;
Ok(audio::parse_speech_response(data.to_vec(), mime_type))
}
async fn speech_to_text(
&self,
request: SpeechToTextRequest,
) -> Result<TranscriptionResponse, Error> {
let part = reqwest::multipart::Part::bytes(request.audio.data.clone())
.file_name(request.audio.filename.clone())
.mime_str(&request.audio.mime_type)?;
let form = reqwest::multipart::Form::new()
.text("model_id", request.model.clone())
.part("file", part);
let form = merge_provider_options_into_form(form, &request.provider_options);
let http_response = self
.client
.post(format!("{}/speech-to-text", self.base_url))
.header("xi-api-key", &self.api_key)
.multipart(form)
.send()
.await?;
let status = http_response.status();
let headers = http_response.headers().clone();
let body_text = http_response.text().await?;
if !status.is_success() {
let mapper = ErrorMapper {
provider: self.name(),
};
return Err(mapper.map_error_response(status, &headers, &body_text));
}
let wire_response: audio::TranscriptionApiResponse = serde_json::from_str(&body_text)
.map_err(|e| Error::Decode {
provider: self.name().to_string(),
message: e.to_string(),
})?;
Ok(audio::parse_transcription_response(wire_response))
}
}