diff --git a/START.md b/START.md index be56c6f..8f47094 100644 --- a/START.md +++ b/START.md @@ -58,6 +58,8 @@ Config path: `~/.config/maven/mavend.json`. Full example with all options: } ``` +Omit the `embedder` block entirely to use the deterministic HashEmbedder floor (no ML, no ONNX runtime dependency). Useful for testing or low-resource setups. + ## mavsttd — STT worker (optional, remote whisper.cpp) Requires `LD_LIBRARY_PATH` to include deps/lib (for libwhisper.so, libggml-vulkan.so). diff --git a/cmd/mavend/voice.go b/cmd/mavend/voice.go index 43c478d..1a148f9 100644 --- a/cmd/mavend/voice.go +++ b/cmd/mavend/voice.go @@ -156,6 +156,7 @@ func wireVoice(cfg *config.Config, coreAPI ipc.CoreAPI, phr phraser.Phraser) (*v log.Printf("voice: onnx embedder loaded (%d dim)", onnx.Dim()) emb = onnx } else { + log.Printf("voice: embedder not configured, using HashEmbedder floor") emb = router.NewHashEmbedder(1024) } w.embedder = emb diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 6660c96..2cae5c0 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -82,6 +82,49 @@ func TestVoiceEnabledWithBindOK(t *testing.T) { } } +func TestEmbedderAllPathsSetOK(t *testing.T) { + p := writeConfig(t, `{ + "voice": { + "enabled": true, "bind": "127.0.0.1:9100", + "embedder": { + "model_path": "m.onnx", + "tokenizer_path": "t.json", + "lib_path": "l.so" + } + } + }`) + if _, err := Load(p); err != nil { + t.Fatalf("Load: %v", err) + } +} + +func TestEmbedderPartialConfigRejected(t *testing.T) { + tests := []struct { + name string + json string + }{ + {"missing model_path", `{"voice":{"enabled":true,"bind":"127.0.0.1:9100","embedder":{"tokenizer_path":"t.json","lib_path":"l.so"}}}`}, + {"missing tokenizer_path", `{"voice":{"enabled":true,"bind":"127.0.0.1:9100","embedder":{"model_path":"m.onnx","lib_path":"l.so"}}}`}, + {"missing lib_path", `{"voice":{"enabled":true,"bind":"127.0.0.1:9100","embedder":{"model_path":"m.onnx","tokenizer_path":"t.json"}}}`}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := writeConfig(t, tt.json) + if _, err := Load(p); err == nil { + t.Fatal("Load succeeded for partial embedder; want error") + } + }) + } +} + +func TestEmbedderNilOK(t *testing.T) { + // voice block without embedder → ok (HashEmbedder floor) + p := writeConfig(t, `{"voice":{"enabled":true,"bind":"127.0.0.1:9100"}}`) + if _, err := Load(p); err != nil { + t.Fatalf("Load: %v", err) + } +} + func TestVoicePresentDisabledOK(t *testing.T) { // voice block present but not enabled — acceptable (the listener stays // down; the routing table's ChannelVoice selections drop).