bb584b6ea0
This commit removes the `whisper_variant` and `whisper_hotwords_variant` fields from the `run_id` generation and the `print_meta_summary` function. These variants are no longer used as the project is shifting focus to LLM-based generation. The `run_full_case.rs` example has also been updated to reflect this change.
279 lines
7.5 KiB
Rust
279 lines
7.5 KiB
Rust
//! Runtime-tunable settings loaded from `settings.toml`.
|
|
//!
|
|
//! Bootstrap config (port, data paths, log paths, auth) lives in [`crate::config::Config`].
|
|
//! Edits to `settings.toml` take effect on the next server start — hot
|
|
//! reload is a follow-up plan.
|
|
|
|
use serde::Deserialize;
|
|
|
|
#[derive(Debug, Clone, Default, Deserialize)]
|
|
pub struct Settings {
|
|
#[serde(default)]
|
|
pub retention: RetentionSettings,
|
|
#[serde(default)]
|
|
pub whisper: WhisperSettings,
|
|
#[serde(default)]
|
|
pub canary: CanarySettings,
|
|
#[serde(default)]
|
|
pub ollama: OllamaSettings,
|
|
#[serde(default)]
|
|
pub llm: LlmSettings,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct RetentionSettings {
|
|
#[serde(default = "default_retention_audio_days")]
|
|
pub audio_days: u32,
|
|
#[serde(default = "default_retention_transcript_days")]
|
|
pub transcript_days: u32,
|
|
#[serde(default = "default_retention_document_days")]
|
|
pub document_days: u32,
|
|
}
|
|
|
|
impl Default for RetentionSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
audio_days: default_retention_audio_days(),
|
|
transcript_days: default_retention_transcript_days(),
|
|
document_days: default_retention_document_days(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn default_retention_audio_days() -> u32 {
|
|
30
|
|
}
|
|
fn default_retention_transcript_days() -> u32 {
|
|
30
|
|
}
|
|
fn default_retention_document_days() -> u32 {
|
|
0
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct WhisperSettings {
|
|
#[serde(default = "default_whisper_url")]
|
|
pub url: String,
|
|
#[serde(default = "default_whisper_timeout_seconds")]
|
|
pub timeout_seconds: u64,
|
|
}
|
|
|
|
impl Default for WhisperSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
url: default_whisper_url(),
|
|
timeout_seconds: default_whisper_timeout_seconds(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn default_whisper_url() -> String {
|
|
"http://localhost:10300".into()
|
|
}
|
|
fn default_whisper_timeout_seconds() -> u64 {
|
|
120
|
|
}
|
|
|
|
/// Endpoint for the optional NeMo Canary container service. Active only
|
|
/// when `ASR_BACKEND=canary` is set in `.env`. Both this block and
|
|
/// `[whisper]` are always loaded; the worker reads only the active one.
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct CanarySettings {
|
|
#[serde(default = "default_canary_url")]
|
|
pub url: String,
|
|
#[serde(default = "default_canary_timeout_seconds")]
|
|
pub timeout_seconds: u64,
|
|
}
|
|
|
|
impl Default for CanarySettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
url: default_canary_url(),
|
|
timeout_seconds: default_canary_timeout_seconds(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn default_canary_url() -> String {
|
|
"http://localhost:9002".into()
|
|
}
|
|
/// Higher than Whisper's 120s default: Canary's buffered pipeline for
|
|
/// audio >25s adds chunk-stitching cost (see docs/canary.md §4.1), so
|
|
/// long dictations need a wider safety margin.
|
|
fn default_canary_timeout_seconds() -> u64 {
|
|
180
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct OllamaSettings {
|
|
#[serde(default = "default_ollama_url")]
|
|
pub url: String,
|
|
#[serde(default = "default_ollama_model")]
|
|
pub model: String,
|
|
#[serde(default = "default_ollama_keep_alive")]
|
|
pub keep_alive: u32,
|
|
}
|
|
|
|
impl Default for OllamaSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
url: default_ollama_url(),
|
|
model: default_ollama_model(),
|
|
keep_alive: default_ollama_keep_alive(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn default_ollama_url() -> String {
|
|
"http://localhost:11434".into()
|
|
}
|
|
fn default_ollama_model() -> String {
|
|
"gemma3:4b".into()
|
|
}
|
|
fn default_ollama_keep_alive() -> u32 {
|
|
0
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct LlmSettings {
|
|
#[serde(default)]
|
|
pub url: String,
|
|
#[serde(default)]
|
|
pub api_key: String,
|
|
#[serde(default)]
|
|
pub model: String,
|
|
#[serde(default)]
|
|
pub temperature: f32,
|
|
#[serde(default = "default_llm_timeout_seconds")]
|
|
pub timeout_seconds: u64,
|
|
/// Empty values fall back to the code default in [`Settings::load_or_default`]
|
|
/// — symmetric to the previous env-var behaviour.
|
|
#[serde(default = "default_llm_system_prompt")]
|
|
pub system_prompt: String,
|
|
}
|
|
|
|
impl Default for LlmSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
url: String::new(),
|
|
api_key: String::new(),
|
|
model: String::new(),
|
|
temperature: 0.0,
|
|
timeout_seconds: default_llm_timeout_seconds(),
|
|
system_prompt: default_llm_system_prompt(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn default_llm_timeout_seconds() -> u64 {
|
|
180
|
|
}
|
|
fn default_llm_system_prompt() -> String {
|
|
crate::analyze::prompt::SYSTEM_PROMPT.to_string()
|
|
}
|
|
|
|
impl Settings {
|
|
/// Missing file → defaults + warn. Parse error → panic (startup-fail-fast).
|
|
/// An empty `llm.system_prompt` field falls back to the code default.
|
|
pub fn load_or_default(path: &str) -> Self {
|
|
match std::fs::read_to_string(path) {
|
|
Ok(content) => {
|
|
let mut parsed: Settings = toml::from_str(&content)
|
|
.unwrap_or_else(|e| panic!("Failed to parse {path}: {e}"));
|
|
if parsed.llm.system_prompt.is_empty() {
|
|
parsed.llm.system_prompt = default_llm_system_prompt();
|
|
}
|
|
tracing::info!(path, "settings loaded");
|
|
parsed
|
|
}
|
|
Err(_) => {
|
|
tracing::warn!(path, "settings file not found — using code defaults");
|
|
Self::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `api_key` is intentionally not required — Ollama's OpenAI-compatible
|
|
/// endpoint accepts unauthenticated requests.
|
|
pub fn llm_configured(&self) -> bool {
|
|
!self.llm.url.is_empty() && !self.llm.model.is_empty()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parses_full_toml() {
|
|
let s: Settings = toml::from_str(
|
|
r#"
|
|
[whisper]
|
|
url = "http://h:1"
|
|
timeout_seconds = 60
|
|
|
|
[canary]
|
|
url = "http://h:9"
|
|
timeout_seconds = 240
|
|
|
|
[ollama]
|
|
url = "http://h:2"
|
|
model = "x"
|
|
keep_alive = 5
|
|
|
|
[llm]
|
|
url = "http://h:3"
|
|
api_key = "k"
|
|
model = "m"
|
|
temperature = 0.5
|
|
timeout_seconds = 30
|
|
system_prompt = "p"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(s.whisper.url, "http://h:1");
|
|
assert_eq!(s.whisper.timeout_seconds, 60);
|
|
assert_eq!(s.canary.url, "http://h:9");
|
|
assert_eq!(s.canary.timeout_seconds, 240);
|
|
assert_eq!(s.ollama.keep_alive, 5);
|
|
assert_eq!(s.llm.api_key, "k");
|
|
assert_eq!(s.llm.system_prompt, "p");
|
|
}
|
|
|
|
#[test]
|
|
fn empty_toml_yields_defaults() {
|
|
let s: Settings = toml::from_str("").unwrap();
|
|
assert_eq!(s.whisper.url, "http://localhost:10300");
|
|
assert_eq!(s.whisper.timeout_seconds, 120);
|
|
assert_eq!(s.canary.url, "http://localhost:9002");
|
|
assert_eq!(s.canary.timeout_seconds, 180);
|
|
assert_eq!(s.ollama.url, "http://localhost:11434");
|
|
assert_eq!(s.ollama.model, "gemma3:4b");
|
|
assert_eq!(s.llm.timeout_seconds, 180);
|
|
}
|
|
|
|
#[test]
|
|
fn partial_toml_fills_in_defaults() {
|
|
let s: Settings = toml::from_str(
|
|
r#"
|
|
[whisper]
|
|
url = "http://only-whisper"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(s.whisper.url, "http://only-whisper");
|
|
assert_eq!(s.whisper.timeout_seconds, 120);
|
|
assert_eq!(s.ollama.url, "http://localhost:11434");
|
|
}
|
|
|
|
#[test]
|
|
fn llm_configured_requires_url_and_model() {
|
|
let mut s = Settings::default();
|
|
assert!(!s.llm_configured());
|
|
s.llm.url = "http://x".into();
|
|
assert!(!s.llm_configured());
|
|
s.llm.model = "m".into();
|
|
assert!(s.llm_configured());
|
|
}
|
|
}
|