2
0
Files
shell/src/voice/mod.rs

84 lines
2.4 KiB
Rust

pub mod tts;
pub mod stt;
/// Load .env file from cwd, setting vars that aren't already set.
fn load_dotenv() {
for dir in &[".", env!("CARGO_MANIFEST_DIR")] {
let path = std::path::Path::new(dir).join(".env");
if let Ok(content) = std::fs::read_to_string(&path) {
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') { continue; }
if let Some((key, val)) = line.split_once('=') {
let key = key.trim();
let val = val.trim();
if std::env::var(key).is_err() {
std::env::set_var(key, val);
}
}
}
break;
}
}
}
pub struct VoiceConfig {
pub tts_api_key: String,
pub tts_voice_id: String,
pub tts_model: String,
pub stt_language: String,
}
impl VoiceConfig {
pub fn load() -> Option<Self> {
// Load .env file if present (cwd or project root)
load_dotenv();
let tts_api_key = std::env::var("ELEVENLABS_API_KEY").ok()?;
Some(Self {
tts_api_key,
tts_voice_id: std::env::var("ELEVENLABS_VOICE_ID").unwrap_or_default(),
tts_model: std::env::var("ELEVENLABS_MODEL_ID").unwrap_or_else(|_| "eleven_multilingual_v2".into()),
stt_language: std::env::var("STT_LANGUAGE").unwrap_or_else(|_| "ja-JP".into()),
})
}
pub fn is_available(&self) -> bool {
!self.tts_api_key.is_empty()
}
}
pub struct VoiceSystem {
pub config: VoiceConfig,
}
impl VoiceSystem {
pub fn new() -> Option<Self> {
let config = VoiceConfig::load()?;
if !config.is_available() {
return None;
}
Some(Self { config })
}
pub fn speak(&self, text: &str) {
if text.trim().is_empty() { return; }
let audio = match tts::synthesize(&self.config, text) {
Ok(data) => data,
Err(e) => { eprintln!("tts error: {e}"); return; }
};
if let Err(e) = tts::play_audio(&audio) {
eprintln!("audio play error: {e}");
}
}
pub fn listen(&self) -> Option<String> {
match stt::recognize(&self.config) {
Ok(text) if !text.is_empty() => Some(text),
Ok(_) => None,
Err(e) => { eprintln!("stt error: {e}"); None }
}
}
}