pub mod elevenlabs;
pub mod openai;
pub mod edge;
use crate::error::{DrivenError, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
pub use elevenlabs::ElevenLabsClient;
pub use openai::OpenAiTtsClient;
pub use edge::EdgeTtsClient;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum TtsProviderType {
#[default]
ElevenLabs,
OpenAi,
Edge,
Local,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TtsVoice {
pub id: String,
pub name: String,
pub language: String,
pub gender: Option<VoiceGender>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VoiceGender {
Male,
Female,
Neutral,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum AutoMode {
Always,
Never,
#[default]
Smart,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TtsConfig {
#[serde(default)]
pub provider: TtsProviderType,
#[serde(default)]
pub auto_mode: AutoMode,
#[serde(default = "default_max_text_length")]
pub max_text_length: usize,
#[serde(default = "default_true")]
pub summarize_long: bool,
pub elevenlabs: Option<ElevenLabsConfig>,
pub openai: Option<OpenAiTtsConfig>,
pub edge: Option<EdgeTtsConfig>,
pub local: Option<LocalTtsConfig>,
}
fn default_max_text_length() -> usize {
1500
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElevenLabsConfig {
#[serde(default)]
pub api_key: String,
#[serde(default = "default_elevenlabs_voice")]
pub voice_id: String,
#[serde(default = "default_elevenlabs_model")]
pub model_id: String,
#[serde(default = "default_stability")]
pub stability: f32,
#[serde(default = "default_similarity")]
pub similarity_boost: f32,
#[serde(default)]
pub style: f32,
#[serde(default = "default_speed")]
pub speed: f32,
}
fn default_elevenlabs_voice() -> String {
"pMsXgVXv3BLzUgSXRplE".to_string() }
fn default_elevenlabs_model() -> String {
"eleven_multilingual_v2".to_string()
}
fn default_stability() -> f32 {
0.5
}
fn default_similarity() -> f32 {
0.75
}
fn default_speed() -> f32 {
1.0
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAiTtsConfig {
#[serde(default)]
pub api_key: String,
#[serde(default = "default_openai_voice")]
pub voice: String,
#[serde(default = "default_openai_model")]
pub model: String,
#[serde(default = "default_speed")]
pub speed: f32,
#[serde(default = "default_openai_format")]
pub response_format: String,
}
fn default_openai_voice() -> String {
"alloy".to_string()
}
fn default_openai_model() -> String {
"tts-1".to_string()
}
fn default_openai_format() -> String {
"mp3".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdgeTtsConfig {
#[serde(default = "default_edge_voice")]
pub voice: String,
#[serde(default)]
pub rate: i32,
#[serde(default)]
pub pitch: i32,
#[serde(default)]
pub volume: i32,
}
fn default_edge_voice() -> String {
"en-US-AriaNeural".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalTtsConfig {
pub engine_path: PathBuf,
pub model_path: PathBuf,
#[serde(default = "default_sample_rate")]
pub sample_rate: u32,
}
fn default_sample_rate() -> u32 {
22050
}
impl Default for TtsConfig {
fn default() -> Self {
Self {
provider: TtsProviderType::default(),
auto_mode: AutoMode::default(),
max_text_length: default_max_text_length(),
summarize_long: true,
elevenlabs: None,
openai: None,
edge: Some(EdgeTtsConfig {
voice: default_edge_voice(),
rate: 0,
pitch: 0,
volume: 0,
}),
local: None,
}
}
}
impl TtsConfig {
pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self> {
let content = std::fs::read_to_string(path.as_ref())
.map_err(|e| DrivenError::Io(e))?;
Self::parse_sr(&content).or_else(|_| Ok(Self::default()))
}
fn parse_sr(content: &str) -> Result<Self> {
Ok(Self::default())
}
pub fn resolve_env_vars(&mut self) {
if let Some(ref mut el) = self.elevenlabs {
if el.api_key.is_empty() || el.api_key.starts_with('$') {
el.api_key = std::env::var("ELEVENLABS_API_KEY").unwrap_or_default();
}
}
if let Some(ref mut oai) = self.openai {
if oai.api_key.is_empty() || oai.api_key.starts_with('$') {
oai.api_key = std::env::var("OPENAI_API_KEY").unwrap_or_default();
}
}
}
}
#[derive(Debug, Clone)]
pub struct SynthesizedAudio {
pub data: Vec<u8>,
pub format: AudioFormat,
pub duration_ms: Option<u64>,
pub sample_rate: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioFormat {
Mp3,
Wav,
Ogg,
Opus,
Aac,
Flac,
Pcm,
}
impl AudioFormat {
pub fn extension(&self) -> &'static str {
match self {
Self::Mp3 => "mp3",
Self::Wav => "wav",
Self::Ogg => "ogg",
Self::Opus => "opus",
Self::Aac => "aac",
Self::Flac => "flac",
Self::Pcm => "pcm",
}
}
pub fn mime_type(&self) -> &'static str {
match self {
Self::Mp3 => "audio/mpeg",
Self::Wav => "audio/wav",
Self::Ogg => "audio/ogg",
Self::Opus => "audio/opus",
Self::Aac => "audio/aac",
Self::Flac => "audio/flac",
Self::Pcm => "audio/pcm",
}
}
}
pub struct TtsProvider {
config: TtsConfig,
elevenlabs: Option<ElevenLabsClient>,
openai: Option<OpenAiTtsClient>,
edge: Option<EdgeTtsClient>,
active: TtsProviderType,
}
impl TtsProvider {
pub fn new(config: &TtsConfig) -> Result<Self> {
let mut config = config.clone();
config.resolve_env_vars();
let elevenlabs = config.elevenlabs.as_ref().map(|c| ElevenLabsClient::new(c));
let openai = config.openai.as_ref().map(|c| OpenAiTtsClient::new(c));
let edge = config.edge.as_ref().map(|c| EdgeTtsClient::new(c));
Ok(Self {
active: config.provider,
config,
elevenlabs,
openai,
edge,
})
}
pub fn elevenlabs(config: &ElevenLabsConfig) -> Result<Self> {
let mut tts_config = TtsConfig::default();
tts_config.provider = TtsProviderType::ElevenLabs;
tts_config.elevenlabs = Some(config.clone());
Self::new(&tts_config)
}
pub fn openai(config: &OpenAiTtsConfig) -> Result<Self> {
let mut tts_config = TtsConfig::default();
tts_config.provider = TtsProviderType::OpenAi;
tts_config.openai = Some(config.clone());
Self::new(&tts_config)
}
pub fn edge(config: &EdgeTtsConfig) -> Result<Self> {
let mut tts_config = TtsConfig::default();
tts_config.provider = TtsProviderType::Edge;
tts_config.edge = Some(config.clone());
Self::new(&tts_config)
}
pub async fn synthesize(&self, text: &str) -> Result<SynthesizedAudio> {
let text_to_speak = if self.config.summarize_long && text.len() > self.config.max_text_length {
self.summarize_text(text)?
} else {
text.to_string()
};
match self.active {
TtsProviderType::ElevenLabs => {
if let Some(ref client) = self.elevenlabs {
return client.synthesize(&text_to_speak).await;
}
}
TtsProviderType::OpenAi => {
if let Some(ref client) = self.openai {
return client.synthesize(&text_to_speak).await;
}
}
TtsProviderType::Edge => {
if let Some(ref client) = self.edge {
return client.synthesize(&text_to_speak).await;
}
}
TtsProviderType::Local => {
return Err(DrivenError::Config("Local TTS not yet implemented".into()));
}
}
if let Some(ref client) = self.openai {
if let Ok(audio) = client.synthesize(&text_to_speak).await {
return Ok(audio);
}
}
if let Some(ref client) = self.edge {
return client.synthesize(&text_to_speak).await;
}
Err(DrivenError::Config("No TTS provider available".into()))
}
pub async fn synthesize_with_voice(&self, text: &str, voice_id: &str) -> Result<SynthesizedAudio> {
match self.active {
TtsProviderType::ElevenLabs => {
if let Some(ref client) = self.elevenlabs {
return client.synthesize_with_voice(text, voice_id).await;
}
}
TtsProviderType::OpenAi => {
if let Some(ref client) = self.openai {
return client.synthesize_with_voice(text, voice_id).await;
}
}
TtsProviderType::Edge => {
if let Some(ref client) = self.edge {
return client.synthesize_with_voice(text, voice_id).await;
}
}
_ => {}
}
Err(DrivenError::Config("TTS provider not configured".into()))
}
pub async fn speak(&self, text: &str) -> Result<()> {
let audio = self.synthesize(text).await?;
self.play_audio(&audio).await
}
pub async fn speak_with_voice(&self, text: &str, voice_id: &str) -> Result<()> {
let audio = self.synthesize_with_voice(text, voice_id).await?;
self.play_audio(&audio).await
}
pub async fn list_voices(&self) -> Result<Vec<TtsVoice>> {
match self.active {
TtsProviderType::ElevenLabs => {
if let Some(ref client) = self.elevenlabs {
return client.list_voices().await;
}
}
TtsProviderType::OpenAi => {
return Ok(OpenAiTtsClient::available_voices());
}
TtsProviderType::Edge => {
if let Some(ref client) = self.edge {
return client.list_voices().await;
}
}
_ => {}
}
Ok(vec![])
}
pub fn should_auto_speak(&self, text: &str) -> bool {
match self.config.auto_mode {
AutoMode::Always => true,
AutoMode::Never => false,
AutoMode::Smart => {
text.len() < self.config.max_text_length && !text.contains("```")
}
}
}
fn summarize_text(&self, text: &str) -> Result<String> {
let sentences: Vec<&str> = text
.split(|c| c == '.' || c == '!' || c == '?')
.filter(|s| !s.trim().is_empty())
.take(5)
.collect();
Ok(sentences.join(". ") + ".")
}
async fn play_audio(&self, audio: &SynthesizedAudio) -> Result<()> {
tracing::info!(
"Playing audio: {} bytes, format: {:?}",
audio.data.len(),
audio.format
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = TtsConfig::default();
assert_eq!(config.provider, TtsProviderType::ElevenLabs);
assert_eq!(config.auto_mode, AutoMode::Smart);
assert_eq!(config.max_text_length, 1500);
}
#[test]
fn test_audio_format() {
assert_eq!(AudioFormat::Mp3.extension(), "mp3");
assert_eq!(AudioFormat::Wav.mime_type(), "audio/wav");
}
}