use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum AudioFormat {
#[default]
Wav,
Mp3,
Opus,
}
impl AudioFormat {
pub fn media_type(self) -> &'static str {
match self {
AudioFormat::Wav => "audio/wav",
AudioFormat::Mp3 => "audio/mpeg",
AudioFormat::Opus => "audio/opus",
}
}
pub fn extension(self) -> &'static str {
match self {
AudioFormat::Wav => "wav",
AudioFormat::Mp3 => "mp3",
AudioFormat::Opus => "opus",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpeechRequest {
pub text: String,
pub voice: String,
pub language: Option<String>,
pub format: AudioFormat,
}
impl SpeechRequest {
pub fn new(text: impl Into<String>) -> Self {
SpeechRequest {
text: text.into(),
voice: String::new(),
language: None,
format: AudioFormat::default(),
}
}
pub fn voice(mut self, voice: impl Into<String>) -> Self {
self.voice = voice.into();
self
}
pub fn language(mut self, language: impl Into<String>) -> Self {
self.language = Some(language.into());
self
}
pub fn format(mut self, format: AudioFormat) -> Self {
self.format = format;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpeechAudio {
pub media_type: String,
pub bytes: Vec<u8>,
}
impl SpeechAudio {
pub fn new(media_type: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Self {
SpeechAudio {
media_type: media_type.into(),
bytes: bytes.into(),
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum SpeechError {
Transport(String),
Provider(String),
BadInput(String),
Unsupported(String),
RateLimited(String),
}
impl fmt::Display for SpeechError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SpeechError::Transport(s) => write!(f, "speech transport: {s}"),
SpeechError::Provider(s) => write!(f, "speech provider: {s}"),
SpeechError::BadInput(s) => write!(f, "speech bad input: {s}"),
SpeechError::Unsupported(s) => write!(f, "speech unsupported: {s}"),
SpeechError::RateLimited(s) => write!(f, "speech rate limited: {s}"),
}
}
}
impl std::error::Error for SpeechError {}
#[async_trait]
pub trait SpeechModel: Send + Sync + 'static {
async fn synthesize(&self, req: &SpeechRequest) -> Result<SpeechAudio, SpeechError>;
fn handle(&self) -> &str;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_media_types_and_extensions() {
assert_eq!(AudioFormat::Wav.media_type(), "audio/wav");
assert_eq!(AudioFormat::Mp3.media_type(), "audio/mpeg");
assert_eq!(AudioFormat::Opus.extension(), "opus");
}
#[test]
fn default_format_is_wav() {
assert_eq!(SpeechRequest::new("hi").format, AudioFormat::Wav);
}
#[test]
fn builder_sets_fields() {
let r = SpeechRequest::new("Little Bear rode down the hill.")
.voice("Cherry")
.language("English")
.format(AudioFormat::Mp3);
assert_eq!(r.voice, "Cherry");
assert_eq!(r.language.as_deref(), Some("English"));
assert_eq!(r.format, AudioFormat::Mp3);
}
}