config: capture and speaker join senses.go (V-410)
Pure move, plus a note on SpeakerConfig.LibPath: it is the one field in the tree with no reader, no default and no validation, because cmd/mavend's newSpeakerEmbedder discards the whole block — there is no speaker model on this box. It stays declared so a block written from the plan document matches, and the comment now says so rather than leaving the next reader to grep for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+2
-101
@@ -18,7 +18,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kami/maven/internal/delivery/ntfysink"
|
"github.com/kami/maven/internal/delivery/ntfysink"
|
||||||
@@ -435,104 +434,6 @@ type VoiceConfig struct {
|
|||||||
ToolTimeout Duration `json:"tool_timeout,omitempty"`
|
ToolTimeout Duration `json:"tool_timeout,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CaptureConfig — the meeting recorder (internal/capture,
|
|
||||||
// docs/plans/08-hearing.md).
|
|
||||||
//
|
|
||||||
// Absent, or enabled=false, ⇒ the recorder is not wired and the capture methods
|
|
||||||
// return "unknown method", so no client can start a recording however it asks.
|
|
||||||
// A media block is required too: audio is never held only in memory.
|
|
||||||
//
|
|
||||||
// There is deliberately no "auto", no keyword trigger and no duration default
|
|
||||||
// long enough to be forgotten about. Recording other people is an explicit act
|
|
||||||
// with a start, a stop, and a cap.
|
|
||||||
type CaptureConfig struct {
|
|
||||||
// Enabled — may she record a meeting when asked. Default false.
|
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
|
||||||
|
|
||||||
// MaxMinutes — hard cap on one session; it stops itself there. 0 ⇒
|
|
||||||
// capture.DefaultMaxDuration (120 minutes).
|
|
||||||
MaxMinutes int `json:"max_minutes,omitempty"`
|
|
||||||
|
|
||||||
// STTWindow — audio handed to whisper per call. 0 ⇒
|
|
||||||
// capture.DefaultSTTWindow (5m). Larger windows transcribe slightly better
|
|
||||||
// and block the STT worker for longer.
|
|
||||||
STTWindow Duration `json:"stt_window,omitempty"`
|
|
||||||
|
|
||||||
// ChunkRunes — transcript runes per summarisation prompt. 0 ⇒
|
|
||||||
// capture.DefaultChunkRunes (3000), sized for the resident model's n_ctx of
|
|
||||||
// 4096. Raise this only if the resident model's context grows.
|
|
||||||
ChunkRunes int `json:"chunk_runes,omitempty"`
|
|
||||||
|
|
||||||
// MaxChunks — how many windows one meeting may be summarised in before the
|
|
||||||
// transcript is truncated and the summary says so. 0 ⇒
|
|
||||||
// capture.DefaultMaxChunks (40).
|
|
||||||
MaxChunks int `json:"max_chunks,omitempty"`
|
|
||||||
|
|
||||||
// SaveTranscript — write the full transcript as a note alongside the
|
|
||||||
// summary. Default false, and the cost is not disk: a note is embedded and
|
|
||||||
// becomes recall corpus, so every later question can surface verbatim words
|
|
||||||
// other people said in a room. That is the reason it takes a deliberate yes.
|
|
||||||
// The audio blob is pruned by media.retention either way; the notes are not.
|
|
||||||
//
|
|
||||||
// A meeting with no summary writes its transcript regardless. The choice
|
|
||||||
// here is transcript IN ADDITION to a summary, not whether the meeting is
|
|
||||||
// remembered at all.
|
|
||||||
SaveTranscript bool `json:"save_transcript,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Records reports whether the recorder should be wired. Safe on a nil receiver.
|
|
||||||
func (c *CaptureConfig) Records() bool {
|
|
||||||
return c != nil && c.Enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaxDuration is the configured session cap as a duration, or 0 for the
|
|
||||||
// package default. Safe on a nil receiver.
|
|
||||||
func (c *CaptureConfig) MaxDuration() time.Duration {
|
|
||||||
if c == nil || c.MaxMinutes <= 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return time.Duration(c.MaxMinutes) * time.Minute
|
|
||||||
}
|
|
||||||
|
|
||||||
// SpeakerConfig — voice identification (internal/speaker,
|
|
||||||
// docs/plans/10-speaker-recognition.md).
|
|
||||||
//
|
|
||||||
// Absent, or enabled=false, ⇒ no voiceprint is computed for any turn, the
|
|
||||||
// enrolment methods do not exist, and nobody can be enrolled. A voiceprint is
|
|
||||||
// biometric data about a person, so this one is off until someone typed a model
|
|
||||||
// path on purpose.
|
|
||||||
//
|
|
||||||
// It cannot currently be turned on: there is no speaker-embedding model on this
|
|
||||||
// box. See the plan document for what to download.
|
|
||||||
type SpeakerConfig struct {
|
|
||||||
// Enabled — may she work out who is speaking. Default false.
|
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
|
||||||
|
|
||||||
// ModelPath — an ECAPA-TDNN (or equivalent) speaker-embedding ONNX model.
|
|
||||||
// Required; without it the recognizer runs disabled and says so once.
|
|
||||||
ModelPath string `json:"model_path,omitempty"`
|
|
||||||
|
|
||||||
// LibPath — onnxruntime shared library, as for the text embedder. Empty ⇒
|
|
||||||
// the same default the embedder block uses.
|
|
||||||
LibPath string `json:"lib_path,omitempty"`
|
|
||||||
|
|
||||||
// Threshold — cosine similarity a match must beat. 0 ⇒
|
|
||||||
// speaker.DefaultThreshold (0.7). Lower it and she starts calling guests by
|
|
||||||
// his name, which is the expensive direction of this error.
|
|
||||||
Threshold float64 `json:"threshold,omitempty"`
|
|
||||||
|
|
||||||
// MinSeconds — least speech an identification will look at. 0 ⇒
|
|
||||||
// speaker.DefaultMinSeconds (2s).
|
|
||||||
MinSeconds float64 `json:"min_seconds,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recognizes reports whether voice identification should be wired. Safe on a
|
|
||||||
// nil receiver, and false without a model path — enabled with nothing to embed
|
|
||||||
// with is a misconfiguration, not a capability.
|
|
||||||
func (s *SpeakerConfig) Recognizes() bool {
|
|
||||||
return s != nil && s.Enabled && strings.TrimSpace(s.ModelPath) != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// WeatherConfig configures the weather provider for voice queries.
|
// WeatherConfig configures the weather provider for voice queries.
|
||||||
type WeatherConfig struct {
|
type WeatherConfig struct {
|
||||||
Provider string `json:"provider,omitempty"` // "open-meteo" or "" → stub
|
Provider string `json:"provider,omitempty"` // "open-meteo" or "" → stub
|
||||||
@@ -988,8 +889,8 @@ func (c *Config) validate() error {
|
|||||||
if err := c.validateVision(); err != nil {
|
if err := c.validateVision(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if c.Capture.Records() && c.Media.StoreDir() == "" {
|
if err := c.validateCapture(); err != nil {
|
||||||
return errors.New("capture.enabled set but there is no media block to keep the audio in")
|
return err
|
||||||
}
|
}
|
||||||
if len(c.MorningRoutines) > 0 {
|
if len(c.MorningRoutines) > 0 {
|
||||||
if err := morning.Validate(morningRoutinesFromConfig(c.MorningRoutines)); err != nil {
|
if err := morning.Validate(morningRoutinesFromConfig(c.MorningRoutines)); err != nil {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/kami/maven/internal/vision"
|
"github.com/kami/maven/internal/vision"
|
||||||
)
|
)
|
||||||
@@ -127,3 +128,113 @@ func (c *Config) validateVision() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaptureConfig — the meeting recorder (internal/capture,
|
||||||
|
// docs/plans/08-hearing.md).
|
||||||
|
//
|
||||||
|
// Absent, or enabled=false, ⇒ the recorder is not wired and the capture methods
|
||||||
|
// return "unknown method", so no client can start a recording however it asks.
|
||||||
|
// A media block is required too: audio is never held only in memory.
|
||||||
|
//
|
||||||
|
// There is deliberately no "auto", no keyword trigger and no duration default
|
||||||
|
// long enough to be forgotten about. Recording other people is an explicit act
|
||||||
|
// with a start, a stop, and a cap.
|
||||||
|
type CaptureConfig struct {
|
||||||
|
// Enabled — may she record a meeting when asked. Default false.
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
|
||||||
|
// MaxMinutes — hard cap on one session; it stops itself there. 0 ⇒
|
||||||
|
// capture.DefaultMaxDuration (120 minutes).
|
||||||
|
MaxMinutes int `json:"max_minutes,omitempty"`
|
||||||
|
|
||||||
|
// STTWindow — audio handed to whisper per call. 0 ⇒
|
||||||
|
// capture.DefaultSTTWindow (5m). Larger windows transcribe slightly better
|
||||||
|
// and block the STT worker for longer.
|
||||||
|
STTWindow Duration `json:"stt_window,omitempty"`
|
||||||
|
|
||||||
|
// ChunkRunes — transcript runes per summarisation prompt. 0 ⇒
|
||||||
|
// capture.DefaultChunkRunes (3000), sized for the resident model's n_ctx of
|
||||||
|
// 4096. Raise this only if the resident model's context grows.
|
||||||
|
ChunkRunes int `json:"chunk_runes,omitempty"`
|
||||||
|
|
||||||
|
// MaxChunks — how many windows one meeting may be summarised in before the
|
||||||
|
// transcript is truncated and the summary says so. 0 ⇒
|
||||||
|
// capture.DefaultMaxChunks (40).
|
||||||
|
MaxChunks int `json:"max_chunks,omitempty"`
|
||||||
|
|
||||||
|
// SaveTranscript — write the full transcript as a note alongside the
|
||||||
|
// summary. Default false, and the cost is not disk: a note is embedded and
|
||||||
|
// becomes recall corpus, so every later question can surface verbatim words
|
||||||
|
// other people said in a room. That is the reason it takes a deliberate yes.
|
||||||
|
// The audio blob is pruned by media.retention either way; the notes are not.
|
||||||
|
//
|
||||||
|
// A meeting with no summary writes its transcript regardless. The choice
|
||||||
|
// here is transcript IN ADDITION to a summary, not whether the meeting is
|
||||||
|
// remembered at all.
|
||||||
|
SaveTranscript bool `json:"save_transcript,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Records reports whether the recorder should be wired. Safe on a nil receiver.
|
||||||
|
func (c *CaptureConfig) Records() bool {
|
||||||
|
return c != nil && c.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxDuration is the configured session cap as a duration, or 0 for the
|
||||||
|
// package default. Safe on a nil receiver.
|
||||||
|
func (c *CaptureConfig) MaxDuration() time.Duration {
|
||||||
|
if c == nil || c.MaxMinutes <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return time.Duration(c.MaxMinutes) * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateCapture refuses a recorder with nowhere to keep the audio.
|
||||||
|
func (c *Config) validateCapture() error {
|
||||||
|
if c.Capture.Records() && c.Media.StoreDir() == "" {
|
||||||
|
return errors.New("capture.enabled set but there is no media block to keep the audio in")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpeakerConfig — voice identification (internal/speaker,
|
||||||
|
// docs/plans/10-speaker-recognition.md).
|
||||||
|
//
|
||||||
|
// Absent, or enabled=false, ⇒ no voiceprint is computed for any turn, the
|
||||||
|
// enrolment methods do not exist, and nobody can be enrolled. A voiceprint is
|
||||||
|
// biometric data about a person, so this one is off until someone typed a model
|
||||||
|
// path on purpose.
|
||||||
|
//
|
||||||
|
// It cannot currently be turned on: there is no speaker-embedding model on this
|
||||||
|
// box. See the plan document for what to download.
|
||||||
|
type SpeakerConfig struct {
|
||||||
|
// Enabled — may she work out who is speaking. Default false.
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
|
||||||
|
// ModelPath — an ECAPA-TDNN (or equivalent) speaker-embedding ONNX model.
|
||||||
|
// Required; without it the recognizer runs disabled and says so once.
|
||||||
|
ModelPath string `json:"model_path,omitempty"`
|
||||||
|
|
||||||
|
// LibPath — onnxruntime shared library, as for the text embedder. Empty ⇒
|
||||||
|
// the same default the embedder block uses.
|
||||||
|
//
|
||||||
|
// Nothing reads it yet: cmd/mavend's newSpeakerEmbedder discards the whole
|
||||||
|
// block, because there is no speaker model on this box to load. It stays
|
||||||
|
// declared so the block a reader writes matches the plan document.
|
||||||
|
LibPath string `json:"lib_path,omitempty"`
|
||||||
|
|
||||||
|
// Threshold — cosine similarity a match must beat. 0 ⇒
|
||||||
|
// speaker.DefaultThreshold (0.7). Lower it and she starts calling guests by
|
||||||
|
// his name, which is the expensive direction of this error.
|
||||||
|
Threshold float64 `json:"threshold,omitempty"`
|
||||||
|
|
||||||
|
// MinSeconds — least speech an identification will look at. 0 ⇒
|
||||||
|
// speaker.DefaultMinSeconds (2s).
|
||||||
|
MinSeconds float64 `json:"min_seconds,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recognizes reports whether voice identification should be wired. Safe on a
|
||||||
|
// nil receiver, and false without a model path — enabled with nothing to embed
|
||||||
|
// with is a misconfiguration, not a capability.
|
||||||
|
func (s *SpeakerConfig) Recognizes() bool {
|
||||||
|
return s != nil && s.Enabled && strings.TrimSpace(s.ModelPath) != ""
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user