Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ dashmap = "6"
hostname = "0.4"
http = "1"
hyper = { features = ["http1", "http2", "server"], version = "1" }
hyper-util = { features = ["tokio", "server-auto", "http1", "http2", "service"], version = "0.1" }
hyper-util = { features = ["http1", "http2", "server-auto", "service", "tokio"], version = "0.1" }
tower = "0.5"
tower-http = "0.6"
url = "2"
Expand Down Expand Up @@ -185,7 +185,7 @@ moltis-tools = { path = "crates/tools" }
moltis-voice = { path = "crates/voice" }

[profile.release]
lto = "thin"
strip = true
panic = "abort"
codegen-units = 1
codegen-units = 1
lto = "thin"
panic = "abort"
strip = true
2 changes: 2 additions & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ default = [
"tailscale",
"tls",
"voice",
"voicebox",
"web-ui",
]
# Minimal build for memory-constrained devices (Raspberry Pi, etc.).
Expand All @@ -94,6 +95,7 @@ qmd = ["moltis-gateway/qmd"]
tailscale = ["moltis-gateway/tailscale"]
tls = ["moltis-gateway/tls"]
voice = ["moltis-gateway/voice"]
voicebox = ["moltis-gateway/voicebox"]
web-ui = ["moltis-gateway/web-ui"]

[lints]
Expand Down
30 changes: 29 additions & 1 deletion crates/config/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ pub struct VoiceConfig {
pub struct VoiceTtsConfig {
/// Enable TTS globally.
pub enabled: bool,
/// Default provider: "elevenlabs", "openai", "google", "piper", "coqui".
/// Default provider: "elevenlabs", "openai", "google", "piper", "coqui", "voicebox".
pub provider: String,
/// Provider IDs to list in the UI. Empty means list all.
pub providers: Vec<String>,
Expand All @@ -237,6 +237,8 @@ pub struct VoiceTtsConfig {
pub piper: VoicePiperTtsConfig,
/// Coqui TTS (local server) settings.
pub coqui: VoiceCoquiTtsConfig,
/// Voicebox TTS (local voice-cloning server) settings.
pub voicebox: VoiceVoiceboxTtsConfig,
}

impl Default for VoiceTtsConfig {
Expand All @@ -250,6 +252,7 @@ impl Default for VoiceTtsConfig {
google: VoiceGoogleTtsConfig::default(),
piper: VoicePiperTtsConfig::default(),
coqui: VoiceCoquiTtsConfig::default(),
voicebox: VoiceVoiceboxTtsConfig::default(),
}
}
}
Expand Down Expand Up @@ -353,6 +356,31 @@ impl Default for VoiceCoquiTtsConfig {
}
}

/// Voicebox TTS (local voice-cloning server) configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct VoiceVoiceboxTtsConfig {
/// Voicebox server endpoint (default: http://localhost:8000).
pub endpoint: String,
/// Voice profile ID (must be created in Voicebox UI first).
pub profile_id: Option<String>,
/// Model size (e.g., "1.7B").
pub model_size: Option<String>,
/// Language code (e.g., "en").
pub language: Option<String>,
}

impl Default for VoiceVoiceboxTtsConfig {
fn default() -> Self {
Self {
endpoint: "http://localhost:8000".into(),
profile_id: None,
model_size: None,
language: None,
}
}
}

/// Voice STT configuration for moltis.toml.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
Expand Down
10 changes: 10 additions & 0 deletions crates/config/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,15 @@ fn build_schema_map() -> KnownKeys {
("endpoint", Leaf),
])),
),
(
"voicebox",
Struct(HashMap::from([
("endpoint", Leaf),
("profile_id", Leaf),
("model_size", Leaf),
("language", Leaf),
])),
),
])),
),
(
Expand Down Expand Up @@ -969,6 +978,7 @@ fn check_semantic_warnings(config: &MoltisConfig, diagnostics: &mut Vec<Diagnost
"google-tts",
"piper",
"coqui",
"voicebox",
];
for (idx, provider) in config.voice.tts.providers.iter().enumerate() {
if !valid_voice_tts_providers.contains(&provider.as_str()) {
Expand Down
2 changes: 1 addition & 1 deletion crates/cron/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ chrono = { workspace = true }
chrono-tz = { workspace = true }
cron = { workspace = true }
moltis-agents = { workspace = true }
moltis-config = { workspace = true }
moltis-common = { workspace = true }
moltis-config = { workspace = true }
moltis-metrics = { optional = true, workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
Expand Down
7 changes: 4 additions & 3 deletions crates/gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ async-trait = { workspace = true }
axum = { workspace = true }
axum-extra = { features = ["cookie"], workspace = true }
axum-server = { optional = true, workspace = true }
hyper = { optional = true, workspace = true }
hyper-util = { optional = true, workspace = true }
tower = { optional = true, workspace = true }
base64 = { workspace = true }
bytes = { workspace = true }
chrono = { features = ["serde"], optional = true, workspace = true }
Expand All @@ -23,6 +20,8 @@ dashmap = { workspace = true }
futures = { workspace = true }
gix = { workspace = true }
hostname = { workspace = true }
hyper = { optional = true, workspace = true }
hyper-util = { optional = true, workspace = true }
include_dir = { optional = true, workspace = true }
moltis-agents = { workspace = true }
moltis-browser = { workspace = true }
Expand Down Expand Up @@ -70,6 +69,7 @@ tokio-stream = { workspace = true }
tokio-tungstenite = { workspace = true }
tokio-util = { workspace = true }
toml = { workspace = true }
tower = { optional = true, workspace = true }
tower-http = { features = ["catch-panic", "compression-gzip", "compression-zstd", "cors", "limit", "request-id", "sensitive-headers", "set-header", "trace"], workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
Expand Down Expand Up @@ -118,6 +118,7 @@ tls = [
"dep:tower",
]
voice = ["dep:moltis-voice"]
voicebox = ["moltis-voice/voicebox", "voice"]
web-ui = ["dep:askama", "dep:chrono", "dep:include_dir", "dep:portable-pty", "dep:which"]

[dev-dependencies]
Expand Down
36 changes: 32 additions & 4 deletions crates/gateway/src/voice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ use moltis_voice::{
WhisperCliStt, WhisperStt, strip_ssml_tags,
};

#[cfg(feature = "voicebox")]
use moltis_voice::{VoiceboxTts, VoiceboxTtsConfig};

#[cfg(feature = "voice")]
use crate::services::TtsService;

Expand Down Expand Up @@ -123,6 +126,13 @@ impl LiveTtsService {
speaker: None,
language: None,
},
#[cfg(feature = "voicebox")]
voicebox: VoiceboxTtsConfig {
endpoint: cfg.voice.tts.voicebox.endpoint.clone(),
profile_id: cfg.voice.tts.voicebox.profile_id.clone(),
model_size: cfg.voice.tts.voicebox.model_size.clone(),
language: cfg.voice.tts.voicebox.language.clone(),
},
}
}

Expand Down Expand Up @@ -163,13 +173,23 @@ impl LiveTtsService {
None
}
},
#[cfg(feature = "voicebox")]
TtsProviderId::Voicebox => {
let voicebox = VoiceboxTts::new(&config.voicebox);
if voicebox.is_configured() {
Some(Box::new(voicebox) as Box<dyn TtsProvider + Send + Sync>)
} else {
None
}
},
}
}

/// List all providers with their configuration status.
fn list_providers() -> Vec<(TtsProviderId, bool)> {
let config = Self::load_config();
vec![
#[allow(unused_mut)]
let mut providers = vec![
(
TtsProviderId::ElevenLabs,
config.elevenlabs.api_key.is_some(),
Expand All @@ -178,7 +198,10 @@ impl LiveTtsService {
(TtsProviderId::Google, config.google.api_key.is_some()),
(TtsProviderId::Piper, config.piper.model_path.is_some()),
(TtsProviderId::Coqui, true), // Always available if server running
]
];
#[cfg(feature = "voicebox")]
providers.push((TtsProviderId::Voicebox, true)); // Always available if server running
providers
}
}

Expand Down Expand Up @@ -804,8 +827,13 @@ mod tests {
let providers = service.providers().await.unwrap();

let providers_arr = providers.as_array().unwrap();
// 5 providers: elevenlabs, openai, google, piper, coqui
assert_eq!(providers_arr.len(), 5);
// Base providers: elevenlabs, openai, google, piper, coqui
let expected_count = if cfg!(feature = "voicebox") {
6
} else {
5
};
assert_eq!(providers_arr.len(), expected_count);

let ids: Vec<_> = providers_arr
.iter()
Expand Down
4 changes: 2 additions & 2 deletions crates/tools/src/web_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -823,7 +823,7 @@ mod tests {
async fn test_brave_missing_api_key_returns_hint() {
let tool = brave_tool();
let result = tool
.execute(serde_json::json!({"query": "test"}))
.search_brave("test", 5, &serde_json::json!({}), None, "")
.await
.unwrap();
assert!(result["error"].as_str().unwrap().contains("not configured"));
Expand All @@ -834,7 +834,7 @@ mod tests {
async fn test_perplexity_missing_api_key_returns_hint() {
let tool = perplexity_tool();
let result = tool
.execute(serde_json::json!({"query": "test"}))
.search_perplexity("test", "", "https://api.perplexity.ai", "sonar-pro")
.await
.unwrap();
assert!(result["error"].as_str().unwrap().contains("not configured"));
Expand Down
3 changes: 3 additions & 0 deletions crates/voice/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ tracing = { workspace = true }
urlencoding = "2"
which = "7"

[features]
voicebox = []

[dev-dependencies]
tokio = { features = ["macros", "test-util"], workspace = true }
wiremock = "0.6"
Expand Down
49 changes: 49 additions & 0 deletions crates/voice/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ pub enum TtsProviderId {
Piper,
#[serde(rename = "coqui")]
Coqui,
#[cfg(feature = "voicebox")]
#[serde(rename = "voicebox")]
Voicebox,
}

impl fmt::Display for TtsProviderId {
Expand All @@ -32,6 +35,8 @@ impl fmt::Display for TtsProviderId {
Self::Google => write!(f, "google"),
Self::Piper => write!(f, "piper"),
Self::Coqui => write!(f, "coqui"),
#[cfg(feature = "voicebox")]
Self::Voicebox => write!(f, "voicebox"),
}
}
}
Expand All @@ -45,6 +50,8 @@ impl TtsProviderId {
"google" => Some(Self::Google),
"piper" => Some(Self::Piper),
"coqui" => Some(Self::Coqui),
#[cfg(feature = "voicebox")]
"voicebox" => Some(Self::Voicebox),
_ => None,
}
}
Expand All @@ -57,6 +64,8 @@ impl TtsProviderId {
Self::Google => "Google Cloud TTS",
Self::Piper => "Piper",
Self::Coqui => "Coqui TTS",
#[cfg(feature = "voicebox")]
Self::Voicebox => "Voicebox",
}
}

Expand All @@ -68,6 +77,8 @@ impl TtsProviderId {
Self::Google,
Self::Piper,
Self::Coqui,
#[cfg(feature = "voicebox")]
Self::Voicebox,
]
}
}
Expand Down Expand Up @@ -200,6 +211,10 @@ pub struct TtsConfig {

/// Coqui TTS (local) settings.
pub coqui: CoquiTtsConfig,

/// Voicebox TTS (local voice-cloning) settings.
#[cfg(feature = "voicebox")]
pub voicebox: VoiceboxTtsConfig,
}

impl Default for TtsConfig {
Expand All @@ -214,6 +229,8 @@ impl Default for TtsConfig {
google: GoogleTtsConfig::default(),
piper: PiperTtsConfig::default(),
coqui: CoquiTtsConfig::default(),
#[cfg(feature = "voicebox")]
voicebox: VoiceboxTtsConfig::default(),
}
}
}
Expand Down Expand Up @@ -356,6 +373,36 @@ impl Default for CoquiTtsConfig {
}
}

/// Voicebox TTS (local voice-cloning) configuration.
#[cfg(feature = "voicebox")]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct VoiceboxTtsConfig {
/// Voicebox server endpoint (default: http://localhost:8000).
pub endpoint: String,

/// Voice profile ID (must be created in Voicebox UI first).
pub profile_id: Option<String>,

/// Model size (e.g., "1.7B").
pub model_size: Option<String>,

/// Language code (e.g., "en").
pub language: Option<String>,
}

#[cfg(feature = "voicebox")]
impl Default for VoiceboxTtsConfig {
fn default() -> Self {
Self {
endpoint: "http://localhost:8000".into(),
profile_id: None,
model_size: None,
language: None,
}
}
}

/// ElevenLabs Scribe STT configuration.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
Expand Down Expand Up @@ -662,6 +709,8 @@ mod tests {
google: GoogleTtsConfig::default(),
piper: PiperTtsConfig::default(),
coqui: CoquiTtsConfig::default(),
#[cfg(feature = "voicebox")]
voicebox: VoiceboxTtsConfig::default(),
},
stt: SttConfig::default(),
};
Expand Down
3 changes: 3 additions & 0 deletions crates/voice/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ pub use {
strip_ssml_tags,
},
};

#[cfg(feature = "voicebox")]
pub use {config::VoiceboxTtsConfig, tts::VoiceboxTts};
4 changes: 4 additions & 0 deletions crates/voice/src/tts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ mod elevenlabs;
mod google;
mod openai;
mod piper;
#[cfg(feature = "voicebox")]
mod voicebox;

#[cfg(feature = "voicebox")]
pub use voicebox::VoiceboxTts;
pub use {
coqui::CoquiTts, elevenlabs::ElevenLabsTts, google::GoogleTts, openai::OpenAiTts,
piper::PiperTts,
Expand Down
Loading