mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-05 12:34:43 -04:00
* feat(parakeet-cpp): implement the VAD call and add the vad_model option The backend now serves the VAD gRPC call (POST /vad and /v1/vad) with the standalone VAD of libparakeet. It accepts a Silero VAD GGUF as the model file, or an ASR model with a VAD head (Moondream Ultra and Redux). The request audio is float32 PCM at 16 kHz; the response lists the speech segments in seconds, like the silero-vad backend. A model with neither fails the request with the library message. The vad_threshold, vad_min_pause, vad_min_speech, vad_speech_pad and vad_max_segment options tune the segmenter. Unset values keep the defaults of the detector in use, and a bad value fails the load. The vad_model option names a Silero GGUF, resolved against the models directory like the other companion files. It lets any ASR model cut long audio at pauses through parakeet_capi_transcribe_path_json_vad_with, and it implies vad. vad:true alone still uses the model's own head. The new symbols are probed like the existing optional ones. A library without them still loads; the feature that needs one fails with a clear message only when it is used. Assisted-by: Claude:claude-sonnet-5-5 [go test] * feat(gallery): add parakeet-cpp VAD entries and a v3 plus Silero example Add VAD-only entries for the parakeet-cpp backend: the VAD heads of Moondream Redux (packed, CPU) and Ultra (Q8_0), which share their files with the existing ASR entries, and Silero VAD v6.2.3 as a GGUF (MIT, Silero Team). The parakeet-cpp-vad entry installs Silero; it has no variants, because variant ranking prefers the larger build that fits and these are different detectors. Add parakeet-cpp-tdt-0.6b-v3-silero-vad, a v3 entry that sets vad_model so long audio is cut at pauses by Silero. The Silero GGUF entries point at the intended Hugging Face URL of the file; the existing silero-vad entries are unchanged. A test checks the usecases, the shared files and the default entry and the vad_model reference. Assisted-by: Claude:claude-sonnet-5-5 [go test] * docs: describe parakeet-cpp VAD and the vad_model option Document the VAD endpoint on the parakeet-cpp backend (Silero GGUF and the VAD heads of Moondream Ultra and Redux), the vad_* tuning options, and the vad_model option that lets an ASR model without a VAD head cut long audio with Silero. Assisted-by: Claude:claude-sonnet-5-5 * chore(parakeet-cpp): bump parakeet.cpp to 6165e3d Pin the release that adds the standalone VAD (Ultra/Redux head and Silero) and the C API calls the backend now uses. Assisted-by: Claude:claude-sonnet-5-5 --------- Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
116 lines
3.8 KiB
Go
116 lines
3.8 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
|
|
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
|
|
)
|
|
|
|
// vadSampleRate is the rate of VADRequest.audio: the VAD endpoint takes float32
|
|
// PCM at 16 kHz, the rate the other VAD backends (silero-vad, whisper) assume.
|
|
const vadSampleRate = 16000
|
|
|
|
// vadTuning maps a model option to the key of the library's VAD options object.
|
|
// All values are seconds except the threshold. An unset option leaves the
|
|
// default of the detector in use (the VAD head and Silero differ), so no value
|
|
// is repeated here.
|
|
var vadTuning = []struct {
|
|
option, key string
|
|
// min and max bound the value; the library rejects the rest, but failing the
|
|
// load names the option instead of failing every request.
|
|
min, max float64
|
|
minOpen bool // true when min itself is not allowed (the threshold is in (0, 1])
|
|
}{
|
|
{"vad_threshold", "threshold", 0, 1, true},
|
|
{"vad_min_pause", "min_pause", 0, math.Inf(1), true},
|
|
{"vad_min_speech", "min_speech", 0, math.Inf(1), false},
|
|
{"vad_speech_pad", "speech_pad", 0, math.Inf(1), false},
|
|
{"vad_max_segment", "max_segment", 0, math.Inf(1), true},
|
|
}
|
|
|
|
// parseVADTuning reads the vad_threshold, vad_min_pause, vad_min_speech,
|
|
// vad_speech_pad and vad_max_segment model options and returns them as the JSON
|
|
// options object of the C-API, or "" when none is set. A value that does not
|
|
// parse or is out of range fails the load.
|
|
func parseVADTuning(opts *pb.ModelOptions) (string, error) {
|
|
obj := map[string]float64{}
|
|
for _, t := range vadTuning {
|
|
raw := optString(opts, t.option)
|
|
if raw == "" {
|
|
continue
|
|
}
|
|
v, err := strconv.ParseFloat(raw, 64)
|
|
if err != nil || math.IsNaN(v) || math.IsInf(v, 0) {
|
|
return "", fmt.Errorf("parakeet-cpp: option %s: %q is not a number", t.option, raw)
|
|
}
|
|
if v < t.min || (t.minOpen && v == t.min) || v > t.max {
|
|
return "", fmt.Errorf("parakeet-cpp: option %s: %v is out of range", t.option, v)
|
|
}
|
|
obj[t.key] = v
|
|
}
|
|
if len(obj) == 0 {
|
|
return "", nil
|
|
}
|
|
b, err := json.Marshal(obj)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(b), nil
|
|
}
|
|
|
|
// vadDocument is the part of the C-API VAD result the RPC needs.
|
|
type vadDocument struct {
|
|
Segments []struct {
|
|
Start float64 `json:"start"`
|
|
End float64 `json:"end"`
|
|
} `json:"segments"`
|
|
}
|
|
|
|
// VAD runs the standalone voice activity detection of libparakeet on the
|
|
// request audio and returns the speech regions in seconds. It uses the Silero
|
|
// context (the model itself or the vad_model: companion) when there is one, else
|
|
// the VAD head of the ASR model. An ASR model without a head fails with the
|
|
// library's message.
|
|
func (p *ParakeetCpp) VAD(req *pb.VADRequest) (pb.VADResponse, error) {
|
|
if CppVadPcmJSON == nil {
|
|
return pb.VADResponse{}, errors.New("parakeet-cpp: VAD needs a libparakeet.so with parakeet_capi_vad_pcm_json; rebuild the backend against a newer parakeet.cpp")
|
|
}
|
|
p.engineMu.Lock()
|
|
ctx := p.vadCtx
|
|
if ctx == 0 {
|
|
ctx = p.ctxPtr
|
|
}
|
|
if ctx == 0 {
|
|
p.engineMu.Unlock()
|
|
if err := p.notASRError(); err != nil {
|
|
return pb.VADResponse{}, err
|
|
}
|
|
return pb.VADResponse{}, errors.New("parakeet-cpp: no model loaded for VAD")
|
|
}
|
|
cstr := CppVadPcmJSON(ctx, req.GetAudio(), int32(len(req.GetAudio())), vadSampleRate, p.vadOptions)
|
|
var lastErr string
|
|
if cstr == 0 {
|
|
lastErr = CppLastError(ctx)
|
|
}
|
|
p.engineMu.Unlock()
|
|
if cstr == 0 {
|
|
return pb.VADResponse{}, fmt.Errorf("parakeet-cpp: vad failed: %s", lastErr)
|
|
}
|
|
raw := goStringFromCPtr(cstr)
|
|
CppFreeString(cstr)
|
|
|
|
var doc vadDocument
|
|
if err := json.Unmarshal([]byte(raw), &doc); err != nil {
|
|
return pb.VADResponse{}, fmt.Errorf("parakeet-cpp: decode vad json: %w", err)
|
|
}
|
|
segs := make([]*pb.VADSegment, 0, len(doc.Segments))
|
|
for _, s := range doc.Segments {
|
|
segs = append(segs, &pb.VADSegment{Start: float32(s.Start), End: float32(s.End)})
|
|
}
|
|
return pb.VADResponse{Segments: segs}, nil
|
|
}
|