diff --git a/internal/config/config.go b/internal/config/config.go index 08b1bca..5d7da97 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -327,113 +327,6 @@ type QuietHoursConfig struct { End string `json:"end,omitempty"` // "HH:MM" local time, e.g. "08:00" } -// VoiceConfig — the client↔core TCP surface + the stt/tts worker-module -// seams. -// -// Enabled gates wiring; Bind is the TCP address (inside the wg tunnel in -// production; "127.0.0.1:9100" for the local smoke). Lang is the default -// language hint passed to both stt and tts (per-call overrides later). -// -// Stt and Tts are the worker-module seams. nil Stt ⇒ daemon wires the -// in-process stt.Stub (the "no models on disk" floor — the loop is -// exercisable end-to-end with a deterministic no-model transcriber). -// non-nil Stt with Socket ⇒ daemon wires stt.Remote dialing that unix -// socket (cmd/mavsttd serves the other end; production swaps in a -// faster-whisper handler in cmd/mavsttd, no daemon or stt-package -// change). Tts mirrors for tts.Remote + cmd/mavttsd. -// -// Embedder configures the router's sentence embedder. When all three -// paths are set, the daemon constructs an ONNX multilingual embedder -// (in-process); when nil, it falls back to the floor HashEmbedder stub -// (deterministic, no model files required — good for CI and smoke). -// -// The daemon refuses to start if Voice.Enabled but Bind is empty — the -// bind is the one operational config the surface can't default (127.0.0.1 -// is too relaxed for production, a wg-tunnel address is the user's); -// surfacing the gap explicitly beats an idle listener the user thinks is -// wired but isn't reachable. -type VoiceConfig struct { - Enabled bool `json:"enabled,omitempty"` - Bind string `json:"bind,omitempty"` - Lang string `json:"lang,omitempty"` - Stt *WorkerConfig `json:"stt,omitempty"` - Tts *TtsConfig `json:"tts,omitempty"` - Embedder *EmbedderConfig `json:"embedder,omitempty"` - - // RouterThreshold — the minimum confidence score for the intent classifier - // (stage 3 gate). Below this → clarify, don't guess. 0.0 means permissive - // (never clarify); 0.35 is a reasonable floor for the ONNX embedder. - // The HashEmbedder floor scores lexically and may need a lower value. - // Default 0.35 if unset. - RouterThreshold float64 `json:"router_threshold,omitempty"` - - // LLMRouter — route with the resident model instead of the embedding - // classifier. On by default since Vikunja #320. - // - // Measured on the held-out fixture (docs/evals/2026-07-31-routing.md): 63.2% of - // intents right against the classifier's 50.0%, and no route errors. It - // costs about 1s per turn instead of 30ms. - // - // It is safe to leave on. The model can refuse — it answers "unknown" when - // it cannot route, and the turn drops to the classifier and its clarify - // gate. Any LLM error does the same, so a turn never breaks on the model. - // Slot extraction runs on LLM decisions too, so acts get their Fn and - // reminders their Time. - // - // Set it false to go back to the classifier, e.g. on a box with no - // llama-server or when 1s a turn is too slow. - // - // It is a pointer so that "missing from the file" and "explicitly false" - // are different things: missing means on, false means off. Read it with - // UseLLMRouter(), not directly. - LLMRouter *bool `json:"llm_router,omitempty"` - - // QueryMinScore — the note-recall confidence gate. Top cosine below this - // ⇒ "I don't know" instead of a guess. Tuned for the ONNX embedder (0.55); - // the HashEmbedder floor scores lexically and may never clear it. 0.55 - // default if unset. - QueryMinScore float64 `json:"query_min_score,omitempty"` - - // QueryMinMargin — the second half of the recall gate: the top hit must - // beat the runner-up by more than this. The absolute score above cannot do - // the job on its own, because the e5 embedder puts every cosine in one - // narrow high band, so a made-up question scores as high as a real one. - // The margin asks whether one note is clearly the best instead. - // Negative ⇒ off. 0 ⇒ the default below. - QueryMinMargin float64 `json:"query_min_margin,omitempty"` - - // ClarifyMaxAttempts — how many clarifying questions she may ask about one - // request before she gives up and says she did not understand. Default 3. - ClarifyMaxAttempts int `json:"clarify_max_attempts,omitempty"` - - // Persona — optional prompt prefix that tunes maven's character. Prepended - // to every LLM system prompt (nudge phrasing, note queries, general - // knowledge). Empty string ⇒ current hardcoded persona (feminine-gendered - // Russian self-reference). Example: "Be formal and answer in English only." - Persona string `json:"persona,omitempty"` - - // OwnerName / City — optional facts about the owner, added to the shared - // context block (internal/persona). Empty is fine: the block still states - // who he is grammatically (a man, addressed as "ты") and the current time. - // Nothing about correct behaviour may depend on these being filled in. - OwnerName string `json:"owner_name,omitempty"` - City string `json:"city,omitempty"` - - // Weather — the weather provider config. nil ⇒ the daemon wires - // the stub provider (returns ErrNotConfigured — "погода не настроена"). - // Set provider to "open-meteo" to use the keyless Open-Meteo API. - Weather *WeatherConfig `json:"weather,omitempty"` - - // Tools — the enabled act allowlist. Each is a spoken verb → argv the - // executor runs (args from the utterance appended). Editing this set is the - // human-only "enable" act (per spec); maven can't add to it from a request. - // Empty ⇒ every act is refused (nothing enabled). - Tools []ToolConfig `json:"tools,omitempty"` - - // ToolTimeout bounds each tool invocation. Zero ⇒ executor default (30s). - ToolTimeout Duration `json:"tool_timeout,omitempty"` -} - // WeatherConfig configures the weather provider for voice queries. type WeatherConfig struct { Provider string `json:"provider,omitempty"` // "open-meteo" or "" → stub diff --git a/internal/config/voice.go b/internal/config/voice.go new file mode 100644 index 0000000..8942b96 --- /dev/null +++ b/internal/config/voice.go @@ -0,0 +1,111 @@ +package config + +// VoiceConfig — the client↔core TCP surface + the stt/tts worker-module +// seams. +// +// Enabled gates wiring; Bind is the TCP address (inside the wg tunnel in +// production; "127.0.0.1:9100" for the local smoke). Lang is the default +// language hint passed to both stt and tts (per-call overrides later). +// +// Stt and Tts are the worker-module seams. nil Stt ⇒ daemon wires the +// in-process stt.Stub (the "no models on disk" floor — the loop is +// exercisable end-to-end with a deterministic no-model transcriber). +// non-nil Stt with Socket ⇒ daemon wires stt.Remote dialing that unix +// socket (cmd/mavsttd serves the other end; production swaps in a +// faster-whisper handler in cmd/mavsttd, no daemon or stt-package +// change). Tts mirrors for tts.Remote + cmd/mavttsd. +// +// Embedder configures the router's sentence embedder. When all three +// paths are set, the daemon constructs an ONNX multilingual embedder +// (in-process); when nil, it falls back to the floor HashEmbedder stub +// (deterministic, no model files required — good for CI and smoke). +// +// The daemon refuses to start if Voice.Enabled but Bind is empty — the +// bind is the one operational config the surface can't default (127.0.0.1 +// is too relaxed for production, a wg-tunnel address is the user's); +// surfacing the gap explicitly beats an idle listener the user thinks is +// wired but isn't reachable. +type VoiceConfig struct { + Enabled bool `json:"enabled,omitempty"` + Bind string `json:"bind,omitempty"` + Lang string `json:"lang,omitempty"` + Stt *WorkerConfig `json:"stt,omitempty"` + Tts *TtsConfig `json:"tts,omitempty"` + Embedder *EmbedderConfig `json:"embedder,omitempty"` + + // RouterThreshold — the minimum confidence score for the intent classifier + // (stage 3 gate). Below this → clarify, don't guess. 0 ⇒ + // DefaultRouterThreshold (0.55), tuned for the ONNX embedder; the + // HashEmbedder floor scores lexically and may need a lower value. + // + // There is no way to ask for "permissive, never clarify" through this + // field: normaliseVoice replaces anything ≤ 0 with the default, so a + // written 0 is the default and a written negative is too. + RouterThreshold float64 `json:"router_threshold,omitempty"` + + // LLMRouter — route with the resident model instead of the embedding + // classifier. On by default since Vikunja #320. + // + // Measured on the held-out fixture (docs/evals/2026-07-31-routing.md): 63.2% of + // intents right against the classifier's 50.0%, and no route errors. It + // costs about 1s per turn instead of 30ms. + // + // It is safe to leave on. The model can refuse — it answers "unknown" when + // it cannot route, and the turn drops to the classifier and its clarify + // gate. Any LLM error does the same, so a turn never breaks on the model. + // Slot extraction runs on LLM decisions too, so acts get their Fn and + // reminders their Time. + // + // Set it false to go back to the classifier, e.g. on a box with no + // llama-server or when 1s a turn is too slow. + // + // It is a pointer so that "missing from the file" and "explicitly false" + // are different things: missing means on, false means off. Read it with + // UseLLMRouter(), not directly. + LLMRouter *bool `json:"llm_router,omitempty"` + + // QueryMinScore — the note-recall confidence gate. Top cosine below this + // ⇒ "I don't know" instead of a guess. Tuned for the ONNX embedder (0.55); + // the HashEmbedder floor scores lexically and may never clear it. 0.55 + // default if unset. + QueryMinScore float64 `json:"query_min_score,omitempty"` + + // QueryMinMargin — the second half of the recall gate: the top hit must + // beat the runner-up by more than this. The absolute score above cannot do + // the job on its own, because the e5 embedder puts every cosine in one + // narrow high band, so a made-up question scores as high as a real one. + // The margin asks whether one note is clearly the best instead. + // Negative ⇒ off. 0 ⇒ the default below. + QueryMinMargin float64 `json:"query_min_margin,omitempty"` + + // ClarifyMaxAttempts — how many clarifying questions she may ask about one + // request before she gives up and says she did not understand. Default 3. + ClarifyMaxAttempts int `json:"clarify_max_attempts,omitempty"` + + // Persona — optional prompt prefix that tunes maven's character. Prepended + // to every LLM system prompt (nudge phrasing, note queries, general + // knowledge). Empty string ⇒ current hardcoded persona (feminine-gendered + // Russian self-reference). Example: "Be formal and answer in English only." + Persona string `json:"persona,omitempty"` + + // OwnerName / City — optional facts about the owner, added to the shared + // context block (internal/persona). Empty is fine: the block still states + // who he is grammatically (a man, addressed as "ты") and the current time. + // Nothing about correct behaviour may depend on these being filled in. + OwnerName string `json:"owner_name,omitempty"` + City string `json:"city,omitempty"` + + // Weather — the weather provider config. nil ⇒ the daemon wires + // the stub provider (returns ErrNotConfigured — "погода не настроена"). + // Set provider to "open-meteo" to use the keyless Open-Meteo API. + Weather *WeatherConfig `json:"weather,omitempty"` + + // Tools — the enabled act allowlist. Each is a spoken verb → argv the + // executor runs (args from the utterance appended). Editing this set is the + // human-only "enable" act (per spec); maven can't add to it from a request. + // Empty ⇒ every act is refused (nothing enabled). + Tools []ToolConfig `json:"tools,omitempty"` + + // ToolTimeout bounds each tool invocation. Zero ⇒ executor default (30s). + ToolTimeout Duration `json:"tool_timeout,omitempty"` +}