use std::path::PathBuf;
use clap::Parser;
use crate::error::Result;
pub const DEFAULT_SYSTEM_PROMPT: &str = "You are a voice assistant. Reply in short, \
spoken-style sentences. Keep answers under three sentences unless asked for detail.";
#[derive(Parser, Debug, Clone)]
#[command(
name = "skadoosh",
version,
about = "Modular, low-latency local voice agent framework",
long_about = "Skadoosh runs a continuous-listening local voice agent: \
mic → Silero VAD → Whisper STT → streaming LLM → ONNX TTS → playback, \
with barge-in."
)]
pub struct Config {
#[arg(
long,
env = "SKADOOSH_LLM_URL",
default_value = "http://localhost:11434/v1"
)]
pub llm_url: String,
#[arg(long, env = "SKADOOSH_LLM_MODEL", default_value = "qwen2.5:0.5b")]
pub llm_model: String,
#[arg(long, env = "SKADOOSH_SYSTEM_PROMPT", default_value = DEFAULT_SYSTEM_PROMPT)]
pub system_prompt: String,
#[arg(long, env = "SKADOOSH_MAX_HISTORY_TURNS", default_value_t = 8)]
pub max_history_turns: usize,
#[arg(
long,
env = "SKADOOSH_WHISPER_MODEL",
default_value = "models/ggml-tiny.en.bin"
)]
pub whisper_model: PathBuf,
#[arg(
long,
env = "SKADOOSH_VAD_MODEL",
default_value = "models/silero_vad.onnx"
)]
pub vad_model: PathBuf,
#[arg(long, env = "SKADOOSH_TTS_MODEL")]
pub tts_model: Option<PathBuf>,
#[arg(long, env = "SKADOOSH_TTS_VOICES")]
pub tts_voices: Option<PathBuf>,
#[arg(long, env = "SKADOOSH_VAD_THRESHOLD", default_value_t = 0.5)]
pub vad_threshold: f32,
#[arg(long, env = "SKADOOSH_SILENCE_MS", default_value_t = 300)]
pub silence_ms: u32,
#[arg(long, env = "SKADOOSH_INPUT_DEVICE")]
pub input_device: Option<String>,
#[arg(long, env = "SKADOOSH_OUTPUT_DEVICE")]
pub output_device: Option<String>,
#[arg(long)]
pub list_devices: bool,
#[arg(long, env = "SKADOOSH_MOCK_TTS")]
pub mock_tts: bool,
#[arg(long, env = "SKADOOSH_SELFTEST", value_name = "WAV_PATH")]
pub selftest: Option<PathBuf>,
}
impl Config {
pub fn parse() -> Config {
<Config as Parser>::parse()
}
pub fn validate(&self) -> Result<()> {
if self.list_devices {
return Ok(());
}
if !(0.0..1.0).contains(&self.vad_threshold) {
return Err(anyhow::anyhow!(
"--vad-threshold must be in [0, 1), got {}",
self.vad_threshold
)
.into());
}
for (flag, path) in [
("--whisper-model", &self.whisper_model),
("--vad-model", &self.vad_model),
] {
if !path.exists() {
return Err(anyhow::anyhow!(
"{flag} not found: {} (run scripts/download_models.sh)",
path.display()
)
.into());
}
}
if let Some(wav) = &self.selftest {
if !wav.exists() {
return Err(anyhow::anyhow!("--selftest wav not found: {}", wav.display()).into());
}
}
if !self.mock_tts {
match (&self.tts_model, &self.tts_voices) {
(Some(model), Some(voices)) if model.exists() && voices.exists() => {}
(None, None) => {
tracing::warn!(
"no TTS model configured (--tts-model/--tts-voices); \
falling back to sine-wave MockTts"
);
}
(model, voices) => {
tracing::warn!(
tts_model = ?model,
tts_voices = ?voices,
"Kokoro TTS files absent or incomplete; falling back to \
sine-wave MockTts (run scripts/download_models.sh --with-kokoro)"
);
}
}
}
Ok(())
}
}