feat(silero-vad): allow threshold/silence/pad via model options (#12430)

* feat(silero-vad): allow threshold/silence/pad via model options

Signed-off-by: anton ziderer <Antonziderer@mail.ru>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(silero-vad): ignore NaN thresholds

NaN passes the detector validation but prevents speech comparisons from succeeding.
Ignore it like malformed input and document the option validation.
Convert the option tests to Ginkgo and cover invalid overrides.

Assisted-by: Codex:gpt-6
Signed-off-by: anton ziderer <Antonziderer@mail.ru>

---------

Signed-off-by: anton ziderer <Antonziderer@mail.ru>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-02 23:59:49 +02:00
1 parent f52cc1b96a
commit 372b1f8983
4 files changed
+145 -10

No files matched your search

+47 -9
View File
@@ -4,32 +4,70 @@ package main
// It is meant to be used by the main executable that is the server for the specific backend type (falcon, gpt3, etc)
import (
"fmt"
"math"
"strconv"
"strings"
"github.com/mudler/LocalAI/pkg/grpc/base"
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
"github.com/streamer45/silero-vad-go/speech"
)
const (
defaultThreshold = 0.5
defaultMinSilenceDurationMs = 100
defaultSpeechPadMs = 30
)
type VAD struct {
base.SingleThread
detector *speech.Detector
}
func (vad *VAD) Load(opts *pb.ModelOptions) error {
v, err := speech.NewDetector(speech.DetectorConfig{
ModelPath: opts.ModelFile,
SampleRate: 16000,
//WindowSize: 1024,
Threshold: 0.5,
MinSilenceDurationMs: 100,
SpeechPadMs: 30,
})
cfg := detectorConfigFromOptions(opts)
v, err := speech.NewDetector(cfg)
if err != nil {
return fmt.Errorf("create silero detector: %w", err)
}
vad.detector = v
return err
return nil
}
func detectorConfigFromOptions(opts *pb.ModelOptions) speech.DetectorConfig {
cfg := speech.DetectorConfig{
ModelPath: opts.ModelFile,
SampleRate: 16000,
Threshold: defaultThreshold,
MinSilenceDurationMs: defaultMinSilenceDurationMs,
SpeechPadMs: defaultSpeechPadMs,
}
for _, opt := range opts.Options {
key, value, ok := strings.Cut(opt, ":")
if !ok || value == "" {
continue
}
switch strings.ToLower(strings.TrimSpace(key)) {
case "threshold":
if v, err := strconv.ParseFloat(strings.TrimSpace(value), 32); err == nil && !math.IsNaN(v) {
cfg.Threshold = float32(v)
}
case "min_silence_duration_ms":
if v, err := strconv.Atoi(strings.TrimSpace(value)); err == nil && v >= 0 {
cfg.MinSilenceDurationMs = v
}
case "speech_pad_ms":
if v, err := strconv.Atoi(strings.TrimSpace(value)); err == nil && v >= 0 {
cfg.SpeechPadMs = v
}
}
}
return cfg
}
func (vad *VAD) VAD(req *pb.VADRequest) (pb.VADResponse, error) {
+67
View File
@@ -0,0 +1,67 @@
package main
import (
"testing"
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestVADOptions(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Silero VAD Options")
}
var _ = Describe("Detector configuration", func() {
It("preserves the model path and defaults", func() {
cfg := detectorConfigFromOptions(&pb.ModelOptions{ModelFile: "silero_vad.onnx"})
Expect(cfg.ModelPath).To(Equal("silero_vad.onnx"))
Expect(cfg.SampleRate).To(Equal(16000))
Expect(cfg.Threshold).To(Equal(float32(0.5)))
Expect(cfg.MinSilenceDurationMs).To(Equal(100))
Expect(cfg.SpeechPadMs).To(Equal(30))
})
It("applies model options", func() {
cfg := detectorConfigFromOptions(&pb.ModelOptions{
ModelFile: "silero_vad.onnx",
Options: []string{
"threshold:0.55",
"min_silence_duration_ms:50",
"speech_pad_ms:450",
"ignored",
"bad_threshold:abc",
},
})
Expect(cfg.Threshold).To(Equal(float32(0.55)))
Expect(cfg.MinSilenceDurationMs).To(Equal(50))
Expect(cfg.SpeechPadMs).To(Equal(450))
})
It("ignores NaN and malformed values without replacing valid options", func() {
cfg := detectorConfigFromOptions(&pb.ModelOptions{
ModelFile: "silero_vad.onnx",
Options: []string{
"threshold:0.55",
"threshold:NaN",
"threshold:abc",
"min_silence_duration_ms:-1",
"speech_pad_ms:abc",
},
})
Expect(cfg.Threshold).To(Equal(float32(0.55)))
Expect(cfg.MinSilenceDurationMs).To(Equal(100))
Expect(cfg.SpeechPadMs).To(Equal(30))
Expect(cfg.IsValid()).To(Succeed())
})
It("uses the default threshold when the only override is NaN", func() {
cfg := detectorConfigFromOptions(&pb.ModelOptions{
ModelFile: "silero_vad.onnx",
Options: []string{"threshold:NaN"},
})
Expect(cfg.Threshold).To(Equal(float32(0.5)))
Expect(cfg.IsValid()).To(Succeed())
})
})
@@ -93,9 +93,33 @@ name: silero-vad
backend: silero-vad
```
Detection parameters can be overridden via model `options` (`key:value` entries):
```yaml
name: silero-vad
backend: silero-vad
options:
- threshold:0.55
- min_silence_duration_ms:50
- speech_pad_ms:450
```
Supported options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `threshold` | float | `0.5` | Speech probability threshold |
| `min_silence_duration_ms` | int | `100` | Minimum silence before ending a speech segment |
| `speech_pad_ms` | int | `30` | Padding added around each speech segment |
Thresholds must be greater than 0 and less than 1. Durations must be nonnegative integers.
Malformed values, negative durations, and NaN thresholds are ignored; the default or last valid value remains in use.
Reload the model (or restart LocalAI) after changing these options.
## Detection Parameters
The Silero VAD backend uses the following internal defaults:
The Silero VAD backend uses the following internal defaults (overridable via `options` above):
- **Sample rate:** 16kHz
- **Threshold:** 0.5
+6
View File
@@ -0,0 +1,6 @@
name: silero-vad
backend: silero-vad
options:
- threshold:0.55
- min_silence_duration_ms:50
- speech_pad_ms:450