pub mod decoder;
pub mod encoder;
mod llm;
#[cfg(test)]
pub mod mock;
mod multiplex;
mod speech_to_text;
mod system_prompt;
mod text_to_speech;
pub mod utils;
mod wav;
use anyhow::{Context, Result};
use std::sync::Arc;
pub use llm::{Llm, LlmConfig, ToolCall, ToolCallHandle, ToolDef, ToolResult};
pub use multiplex::{
start_session, Event, MsgIn, MsgOut, SessionConfig, SessionInputHandle, SessionOutputHandle,
DEFAULT_FLUSH_FOR_S,
};
pub use speech_to_text::SttClient;
pub use system_prompt::Lang;
pub use text_to_speech::TtsClient;
pub struct IoFormat {
pub input: crate::decoder::Format,
pub output: crate::encoder::Format,
}
pub const DEFAULT_GRADIUM_BASE_URL: &str = "https://api.gradium.ai/api";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Gender {
Masculine,
Feminine,
}
impl Gender {
pub fn as_str(&self) -> &'static str {
match self {
Gender::Masculine => "Masculine",
Gender::Feminine => "Feminine",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Country {
Us,
Gb,
Fr,
De,
Mx,
Es,
Br,
}
impl Country {
pub fn as_str(&self) -> &'static str {
match self {
Country::Us => "us",
Country::Gb => "gb",
Country::Fr => "fr",
Country::De => "de",
Country::Mx => "mx",
Country::Es => "es",
Country::Br => "br",
}
}
pub fn full_name(&self) -> &'static str {
match self {
Country::Us => "United States",
Country::Gb => "United Kingdom",
Country::Fr => "France",
Country::De => "Germany",
Country::Mx => "Mexico",
Country::Es => "Spain",
Country::Br => "Brazil",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct FlagshipVoice {
pub name: &'static str,
pub voice_id: &'static str,
pub language: Lang,
pub country: Country,
pub gender: Gender,
pub description: &'static str,
}
pub const FLAGSHIP_VOICES: &[FlagshipVoice] = &[
FlagshipVoice {
name: "Emma",
voice_id: "YTpq7expH9539ERJ",
language: Lang::En,
country: Country::Us,
gender: Gender::Feminine,
description: "A warm and welcoming voice with a friendly American accent, perfect for creating a comfortable conversational experience.",
},
FlagshipVoice {
name: "Kent",
voice_id: "LFZvm12tW_z0xfGo",
language: Lang::En,
country: Country::Us,
gender: Gender::Masculine,
description: "A confident and professional voice with a clear American accent, ideal for business and informative content.",
},
FlagshipVoice {
name: "Sydney",
voice_id: "jtEKaLYNn6iif5PR",
language: Lang::En,
country: Country::Gb,
gender: Gender::Feminine,
description: "A sophisticated and articulate voice with a refined British accent, suitable for elegant and professional applications.",
},
FlagshipVoice {
name: "John",
voice_id: "KWJiFWu2O9nMPYcR",
language: Lang::En,
country: Country::Gb,
gender: Gender::Masculine,
description: "A distinguished and authoritative voice with a classic British accent, perfect for narration and formal content.",
},
FlagshipVoice {
name: "Eva",
voice_id: "ubuXFxVQwVYnZQhy",
language: Lang::En,
country: Country::Gb,
gender: Gender::Feminine,
description: "A lively and engaging voice with a British accent, great for dynamic and energetic conversations.",
},
FlagshipVoice {
name: "Jack",
voice_id: "m86j6D7UZpGzHsNu",
language: Lang::En,
country: Country::Gb,
gender: Gender::Masculine,
description: "A relaxed and friendly voice with a British accent, ideal for casual and approachable interactions.",
},
FlagshipVoice {
name: "Elise",
voice_id: "b35yykvVppLXyw_l",
language: Lang::Fr,
country: Country::Fr,
gender: Gender::Feminine,
description: "A charming and melodic voice with an authentic French accent, perfect for romantic and artistic content.",
},
FlagshipVoice {
name: "Leo",
voice_id: "axlOaUiFyOZhy4nv",
language: Lang::Fr,
country: Country::Fr,
gender: Gender::Masculine,
description: "A smooth and charismatic voice with a French accent, ideal for sophisticated and cultured applications.",
},
FlagshipVoice {
name: "Mia",
voice_id: "-uP9MuGtBqAvEyxI",
language: Lang::De,
country: Country::De,
gender: Gender::Feminine,
description: "A clear and precise voice with a German accent, excellent for technical and professional content.",
},
FlagshipVoice {
name: "Maximilian",
voice_id: "0y1VZjPabOBU3rWy",
language: Lang::De,
country: Country::De,
gender: Gender::Masculine,
description: "A strong and reliable voice with a German accent, perfect for authoritative and instructional content.",
},
FlagshipVoice {
name: "Valentina",
voice_id: "B36pbz5_UoWn4BDl",
language: Lang::Es,
country: Country::Mx,
gender: Gender::Feminine,
description: "A vibrant and expressive voice with a Mexican Spanish accent, great for lively and engaging conversations.",
},
FlagshipVoice {
name: "Sergio",
voice_id: "xu7iJ_fn2ElcWp2s",
language: Lang::Es,
country: Country::Es,
gender: Gender::Masculine,
description: "A warm and passionate voice with a Castilian Spanish accent, ideal for expressive and emotional content.",
},
FlagshipVoice {
name: "Alice",
voice_id: "pYcGZz9VOo4n2ynh",
language: Lang::Pt,
country: Country::Br,
gender: Gender::Feminine,
description: "A bright and cheerful voice with a Brazilian Portuguese accent, perfect for friendly and upbeat interactions.",
},
FlagshipVoice {
name: "Davi",
voice_id: "M-FvVo9c-jGR4PgP",
language: Lang::Pt,
country: Country::Br,
gender: Gender::Masculine,
description: "A calm and reassuring voice with a Brazilian Portuguese accent, ideal for supportive and conversational content.",
},
];
pub fn flagship_voices() -> &'static [FlagshipVoice] {
FLAGSHIP_VOICES
}
static FLAGSHIP_VOICE_LOOKUP: std::sync::OnceLock<std::collections::HashMap<String, usize>> =
std::sync::OnceLock::new();
fn get_flagship_voice_lookup() -> &'static std::collections::HashMap<String, usize> {
FLAGSHIP_VOICE_LOOKUP.get_or_init(|| {
FLAGSHIP_VOICES
.iter()
.enumerate()
.map(|(i, v)| (v.name.to_lowercase(), i))
.collect()
})
}
pub fn flagship_voice(name: &str) -> Result<FlagshipVoice> {
let name_lower = name.to_lowercase();
get_flagship_voice_lookup()
.get(&name_lower)
.map(|&i| FLAGSHIP_VOICES[i])
.ok_or_else(|| anyhow::anyhow!("unknown flagship voice: {name}"))
}
pub struct GradbotClients {
tts_client: Arc<TtsClient>,
stt_client: Arc<SttClient>,
llm: Arc<Llm>,
}
#[allow(clippy::too_many_arguments)]
pub async fn run(
gradium_api_key: Option<&str>,
gradium_base_url: Option<&str>,
llm_base_url: Option<&str>,
llm_model_name: Option<&str>,
llm_api_key: Option<&str>,
max_completion_tokens: Option<u32>,
session_config: Option<SessionConfig>,
io_format: IoFormat,
) -> Result<(SessionInputHandle, SessionOutputHandle)> {
let clients = GradbotClients::new(
gradium_api_key,
gradium_base_url,
llm_base_url,
llm_model_name,
llm_api_key,
max_completion_tokens,
)
.await?;
clients.start_session(session_config, io_format).await
}
impl GradbotClients {
pub async fn new(
gradium_api_key: Option<&str>,
gradium_base_url: Option<&str>,
llm_base_url: Option<&str>,
llm_model_name: Option<&str>,
llm_api_key: Option<&str>,
max_completion_tokens: Option<u32>,
) -> Result<Self> {
let gradium_base_url = gradium_base_url
.map(|s| s.to_string())
.or_else(|| std::env::var("GRADIUM_BASE_URL").ok())
.unwrap_or_else(|| DEFAULT_GRADIUM_BASE_URL.to_string());
let llm_base_url = llm_base_url.map(|s| s.to_string());
let max_completion_tokens = max_completion_tokens.unwrap_or(4096);
tracing::info!(
gradium_base_url,
llm_base_url = llm_base_url.as_deref().unwrap_or("(default)"),
"creating clients"
);
let tts_client = Arc::new(TtsClient::new(gradium_api_key, &gradium_base_url).context(
format!("TTS: failed to create client (base_url={gradium_base_url})"),
)?);
let stt_client = Arc::new(SttClient::new(gradium_api_key, &gradium_base_url).context(
format!("STT: failed to create client (base_url={gradium_base_url})"),
)?);
let llm_base_url_display = llm_base_url
.as_deref()
.unwrap_or("https://api.openai.com/v1")
.to_string();
let llm = Arc::new(
Llm::new(
llm_base_url,
max_completion_tokens,
llm_model_name.map(|s| s.to_string()),
llm_api_key.map(|s| s.to_string()),
)
.await
.context(format!(
"LLM: failed to create client (base_url={llm_base_url_display})"
))?,
);
Ok(Self {
tts_client,
stt_client,
llm,
})
}
pub async fn start_session(
&self,
initial_config: Option<SessionConfig>,
io_format: IoFormat,
) -> Result<(SessionInputHandle, SessionOutputHandle)> {
start_session(
self.tts_client.clone(),
self.stt_client.clone(),
self.llm.clone(),
initial_config,
io_format,
)
.await
}
}