84 lines
2.4 KiB
Rust
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 }
|
|
}
|
|
}
|
|
}
|