- 1
//! The closed set of voice providers and their static capability catalogue. - 2
//! - 3
//! A provider name is parsed exactly once, here. Every surface — the socket, - 4
//! channel voice notes, `/voice/speak`, `/voice/providers`, doctor — resolves - 5
//! through [`VoiceProvider`], so they cannot disagree about which names exist - 6
//! or what an unset route means. Model and voice ids are never listed here: - 7
//! they are properties of the operator's key (invariant 9). - 8
- 9
use crate::SpeakFormat; - 10
use serde::Serialize; - 11
use std::fmt; - 12
use std::str::FromStr; - 13
- 14
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - 15
pub enum VoiceProvider { - 16
Gemini, - 17
OpenAi, - 18
Local, - 19
} - 20
- 21
impl VoiceProvider { - 22
pub const ALL: [Self; 3] = [Self::Gemini, Self::OpenAi, Self::Local]; - 23
- 24
pub fn as_str(self) -> &'static str { - 25
match self { - 26
Self::Gemini => "gemini", - 27
Self::OpenAi => "openai", - 28
Self::Local => "local", - 29
} - 30
} - 31
- 32
/// Credential variables consulted, in order, through the canonical - 33
/// secret chain. Empty for the offline provider. - 34
pub fn credential_vars(self) -> &'static [&'static str] { - 35
match self { - 36
Self::Gemini => &["GEMINI_API_KEY", "GOOGLE_API_KEY"], - 37
Self::OpenAi => &["OPENAI_API_KEY"], - 38
Self::Local => &[], - 39
} - 40
} - 41
- 42
/// Encodings `/voice/speak` can return for this provider. - 43
pub fn speak_formats(self) -> &'static [SpeakFormat] { - 44
match self { - 45
Self::Gemini => &[SpeakFormat::Wav], - 46
Self::OpenAi | Self::Local => &[ - 47
SpeakFormat::Wav, - 48
SpeakFormat::Pcm16, - 49
SpeakFormat::OggOpus, - 50
SpeakFormat::Mp3, - 51
], - 52
} - 53
} - 54
- 55
/// The effective route, or an actionable reason there is none. An unset - 56
/// provider is a configuration gap, never an implicit vendor choice. - 57
pub fn resolve(configured: Option<&str>) -> Result<Self, String> { - 58
match configured.map(str::trim).filter(|name| !name.is_empty()) { - 59
None => Err("Choose a voice provider in Voice settings".into()), - 60
Some(name) => name.parse(), - 61
} - 62
} - 63
} - 64
- 65
impl FromStr for VoiceProvider { - 66
type Err = String; - 67
- 68
fn from_str(value: &str) -> Result<Self, Self::Err> { - 69
Self::ALL - 70
.into_iter() - 71
.find(|provider| provider.as_str() == value) - 72
.ok_or_else(|| { - 73
format!( - 74
"unknown voice provider '{value}'; expected one of {}", - 75
Self::ALL.map(Self::as_str).join(", ") - 76
) - 77
}) - 78
} - 79
} - 80
- 81
impl fmt::Display for VoiceProvider { - 82
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - 83
f.write_str(self.as_str()) - 84
} - 85
} - 86
- 87
/// Static capability description served by `/voice/providers`. Contains no - 88
/// credential values and no model ids. - 89
#[derive(Debug, Clone, Serialize)] - 90
pub struct VoiceDescriptor { - 91
pub name: &'static str, - 92
pub credential_vars: &'static [&'static str], - 93
pub formats: &'static [SpeakFormat], - 94
} - 95
- 96
pub fn catalogue() -> Vec<VoiceDescriptor> { - 97
VoiceProvider::ALL - 98
.into_iter() - 99
.map(|provider| VoiceDescriptor { - 100
name: provider.as_str(), - 101
credential_vars: provider.credential_vars(), - 102
formats: provider.speak_formats(), - 103
}) - 104
.collect() - 105
} - 106
- 107
#[cfg(test)] - 108
#[allow(clippy::expect_used, clippy::unwrap_used)] - 109
mod tests { - 110
use super::*; - 111
- 112
#[test] - 113
fn names_round_trip_and_nothing_else_parses() { - 114
for provider in VoiceProvider::ALL { - 115
assert_eq!(provider.as_str().parse::<VoiceProvider>(), Ok(provider)); - 116
} - 117
for alias in ["google", "gemini-live", "google-live", "OpenAI", ""] { - 118
assert!(alias.parse::<VoiceProvider>().is_err(), "{alias}"); - 119
} - 120
} - 121
- 122
#[test] - 123
fn an_unset_route_is_a_gap_not_a_default() { - 124
assert!(VoiceProvider::resolve(None).is_err()); - 125
assert!(VoiceProvider::resolve(Some(" ")).is_err()); - 126
assert_eq!( - 127
VoiceProvider::resolve(Some(" openai ")), - 128
Ok(VoiceProvider::OpenAi) - 129
); - 130
} - 131
- 132
#[test] - 133
fn catalogue_names_every_provider_and_no_secret_values() { - 134
let names: Vec<_> = catalogue().iter().map(|d| d.name).collect(); - 135
assert_eq!(names, vec!["gemini", "openai", "local"]); - 136
let json = serde_json::to_string(&catalogue()).unwrap(); - 137
assert!(json.contains("GEMINI_API_KEY")); - 138
assert!(!json.contains("KEY=")); - 139
} - 140
} - 141
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.