diff --git a/internal/config/config.go b/internal/config/config.go index 5d7da97..5185cce 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -534,22 +534,10 @@ const ( DefaultTickInterval = 60 * time.Second DefaultRepeatInterval = 5 * time.Minute DefaultAutotuneInterval = 10 * time.Minute - DefaultRouterThreshold = 0.55 // DefaultIntakeJournal — entries kept in the unified intake journal // (Vikunja #283). A busy day is a few hundred intake writes, so this is // roughly "today and yesterday" at a few hundred KB of memory. DefaultIntakeJournal = 512 - DefaultQueryMinScore = 0.55 - // Read off the margin sweep in internal/memory/recalleval on the e5 - // embedder: 0.008 answers 68% of real questions (down from 72%) and cuts - // false recall from 5/5 to 1/5. Every larger delta costs real recall - // without removing that last one until 0.020, which drops recall to 44%. - DefaultQueryMinMargin = 0.008 - // DefaultClarifyMaxAttempts — see dialogue.DefaultMaxAttempts. - DefaultClarifyMaxAttempts = 3 - DefaultToolTimeout = 30 * time.Second - // DefaultLLMRouter — route with the resident model unless told otherwise. - DefaultLLMRouter = true DefaultFactEnrichmentInterval = 30 * time.Second @@ -669,32 +657,7 @@ func (c *Config) applyDefaults() { c.normaliseWorkstation() - if c.Voice != nil { - if c.Voice.RouterThreshold <= 0 { - c.Voice.RouterThreshold = DefaultRouterThreshold - } - if c.Voice.QueryMinScore <= 0 { - c.Voice.QueryMinScore = DefaultQueryMinScore - } - // Unset ⇒ default. Negative is how you turn the margin off on purpose, - // so it is clamped to 0 rather than replaced by the default. - switch { - case c.Voice.QueryMinMargin == 0: - c.Voice.QueryMinMargin = DefaultQueryMinMargin - case c.Voice.QueryMinMargin < 0: - c.Voice.QueryMinMargin = 0 - } - if c.Voice.ClarifyMaxAttempts <= 0 { - c.Voice.ClarifyMaxAttempts = DefaultClarifyMaxAttempts - } - if c.Voice.ToolTimeout <= 0 { - c.Voice.ToolTimeout = Duration(DefaultToolTimeout) - } - if c.Voice.LLMRouter == nil { - on := DefaultLLMRouter - c.Voice.LLMRouter = &on - } - } + c.normaliseVoice() // routines: default severity to care-class (1) — the safe floor: a // misconfigured routine can't blast an away channel at 3am. @@ -712,15 +675,6 @@ func (c *Config) applyDefaults() { } } -// UseLLMRouter reports whether to route with the resident model. Unset means -// on; only an explicit false in the config turns it off. -func (v *VoiceConfig) UseLLMRouter() bool { - if v == nil || v.LLMRouter == nil { - return DefaultLLMRouter - } - return *v.LLMRouter -} - func (c *Config) validate() error { if c.Phraser != nil { if c.Phraser.ModelPath == "" { @@ -743,16 +697,8 @@ func (c *Config) validate() error { return err } } - if c.Voice != nil && c.Voice.Enabled { - if c.Voice.Bind == "" { - return errors.New("voice.enabled set but voice.bind is empty — refusing to start a voice surface with no bind address") - } - if c.Voice.Embedder != nil { - partial := c.Voice.Embedder.ModelPath == "" || c.Voice.Embedder.TokenizerPath == "" || c.Voice.Embedder.LibPath == "" - if partial { - return errors.New("voice.embedder: all three of model_path, tokenizer_path, lib_path must be set, or remove embedder to use the floor stub") - } - } + if err := c.validateVoice(); err != nil { + return err } // routines: name + body required, cron must parse. A typo here should fail // at startup, not silently never fire. diff --git a/internal/config/voice.go b/internal/config/voice.go index 8942b96..0a9852f 100644 --- a/internal/config/voice.go +++ b/internal/config/voice.go @@ -1,5 +1,84 @@ package config +import ( + "errors" + "time" +) + +// Voice defaults, applied in normaliseVoice. +const ( + DefaultRouterThreshold = 0.55 + DefaultQueryMinScore = 0.55 + // Read off the margin sweep in internal/memory/recalleval on the e5 + // embedder: 0.008 answers 68% of real questions (down from 72%) and cuts + // false recall from 5/5 to 1/5. Every larger delta costs real recall + // without removing that last one until 0.020, which drops recall to 44%. + DefaultQueryMinMargin = 0.008 + // DefaultClarifyMaxAttempts — see dialogue.DefaultMaxAttempts. + DefaultClarifyMaxAttempts = 3 + DefaultToolTimeout = 30 * time.Second + // DefaultLLMRouter — route with the resident model unless told otherwise. + DefaultLLMRouter = true +) + +// UseLLMRouter reports whether to route with the resident model. Unset means +// on; only an explicit false in the config turns it off. +func (v *VoiceConfig) UseLLMRouter() bool { + if v == nil || v.LLMRouter == nil { + return DefaultLLMRouter + } + return *v.LLMRouter +} + +// normaliseVoice applies the block's defaults. An absent block stays nil: the +// surface is off and there is nothing to tune. +func (c *Config) normaliseVoice() { + if c.Voice == nil { + return + } + if c.Voice.RouterThreshold <= 0 { + c.Voice.RouterThreshold = DefaultRouterThreshold + } + if c.Voice.QueryMinScore <= 0 { + c.Voice.QueryMinScore = DefaultQueryMinScore + } + // Unset ⇒ default. Negative is how you turn the margin off on purpose, + // so it is clamped to 0 rather than replaced by the default. + switch { + case c.Voice.QueryMinMargin == 0: + c.Voice.QueryMinMargin = DefaultQueryMinMargin + case c.Voice.QueryMinMargin < 0: + c.Voice.QueryMinMargin = 0 + } + if c.Voice.ClarifyMaxAttempts <= 0 { + c.Voice.ClarifyMaxAttempts = DefaultClarifyMaxAttempts + } + if c.Voice.ToolTimeout <= 0 { + c.Voice.ToolTimeout = Duration(DefaultToolTimeout) + } + if c.Voice.LLMRouter == nil { + on := DefaultLLMRouter + c.Voice.LLMRouter = &on + } +} + +// validateVoice refuses a surface that would listen nowhere, and an embedder +// block with only some of its three paths filled in. +func (c *Config) validateVoice() error { + if c.Voice == nil || !c.Voice.Enabled { + return nil + } + if c.Voice.Bind == "" { + return errors.New("voice.enabled set but voice.bind is empty — refusing to start a voice surface with no bind address") + } + if e := c.Voice.Embedder; e != nil { + if e.ModelPath == "" || e.TokenizerPath == "" || e.LibPath == "" { + return errors.New("voice.embedder: all three of model_path, tokenizer_path, lib_path must be set, or remove embedder to use the floor stub") + } + } + return nil +} + // VoiceConfig — the client↔core TCP surface + the stt/tts worker-module // seams. //