mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-10 15:52:29 -04:00
* feat(diarization): return sound events with include_sounds
A client that wants text, speakers, voice prints and sound events had to
make a diarization call and a separate sound call. Add an include_sounds
request field to /v1/audio/diarization that adds a sounds array of closed
events {start, end, label, confidence}, in seconds.
The parakeet-cpp backend runs a tagger-only scene stream over the clip,
the same stream and thresholds the live path uses, so a clip gives the
same events offline and live. A model with no sound_model companion, or a
backend that does not report sound events, fails with 501 and the stable
code include_sounds_unsupported instead of an empty list. The proto
carries sounds_included so an empty list still means "nothing heard".
The localai-proxy backend forwards the field. Swagger, docs and the
e2e mock backend are updated.
Assisted-by: Claude:claude-sonnet-5-5 [protoc swag go]
* feat(gallery): add parakeet-cpp-multilingual-diarization-speakers-sounds
Same as parakeet-cpp-multilingual-diarization-speakers (TDT 0.6B v3,
Nemotron-3-Diarization, WeSpeaker) plus a CED-Tiny sound_model, so one
model name serves /v1/audio/diarization with include_text,
include_speaker_profiles and include_sounds. It declares the
sound_classification usecase like the realtime scene entries.
Assisted-by: Claude:claude-sonnet-5-5
---------
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
118 lines
5.3 KiB
Go
118 lines
5.3 KiB
Go
package grpcerrors_test
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
|
|
"github.com/mudler/LocalAI/pkg/grpc/grpcerrors"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
func TestGRPCErrors(t *testing.T) {
|
|
RegisterFailHandler(Fail)
|
|
RunSpecs(t, "grpcerrors test suite")
|
|
}
|
|
|
|
var _ = Describe("grpcerrors", func() {
|
|
DescribeTable("IsModelNotLoaded",
|
|
func(err error, want bool) {
|
|
Expect(grpcerrors.IsModelNotLoaded(err)).To(Equal(want))
|
|
},
|
|
Entry("nil", nil, false),
|
|
Entry("typed via constructor", grpcerrors.ModelNotLoaded("parakeet-cpp"), true),
|
|
Entry("typed code only", status.Error(codes.FailedPrecondition, "anything"), true),
|
|
Entry("legacy message (Unknown code)", errors.New("parakeet-cpp: model not loaded"), true),
|
|
Entry("legacy message mixed case", errors.New("Backend: Model Not Loaded"), true),
|
|
Entry("unrelated error", errors.New("context deadline exceeded"), false),
|
|
Entry("unrelated grpc code", status.Error(codes.Unavailable, "connection refused"), false),
|
|
)
|
|
|
|
It("ModelNotLoaded carries FailedPrecondition", func() {
|
|
Expect(status.Code(grpcerrors.ModelNotLoaded("whisper"))).To(Equal(codes.FailedPrecondition))
|
|
})
|
|
|
|
DescribeTable("IsModelMismatch",
|
|
func(err error, want bool) {
|
|
Expect(grpcerrors.IsModelMismatch(err)).To(Equal(want))
|
|
},
|
|
Entry("nil", nil, false),
|
|
Entry("typed via constructor", grpcerrors.ModelMismatch("llama-cpp", "a.gguf", "b.gguf"), true),
|
|
// The sentinel is what makes this detectable, not the code alone.
|
|
// insightface's Embedding returns NOT_FOUND "no face detected" on a
|
|
// PredictOptions RPC (backend/python/insightface/backend.py:127). A
|
|
// code-only check would make the router drop a healthy replica row on
|
|
// every faceless image, so this entry must stay false.
|
|
Entry("insightface no-face NotFound", status.Error(codes.NotFound, "no face detected"), false),
|
|
Entry("insightface no-face in both images",
|
|
status.Error(codes.NotFound, "no face detected in one or both images"), false),
|
|
Entry("unrelated NotFound", status.Error(codes.NotFound, "Job 7 not found"), false),
|
|
// Must not overlap with the model-not-loaded signal, which the router
|
|
// handles identically today but for a different reason.
|
|
Entry("model not loaded is not a mismatch", grpcerrors.ModelNotLoaded("llama-cpp"), false),
|
|
Entry("unrelated error", errors.New("context deadline exceeded"), false),
|
|
)
|
|
|
|
It("ModelMismatch carries NotFound and names both models", func() {
|
|
err := grpcerrors.ModelMismatch("llama-cpp", "a.gguf", "b.gguf")
|
|
Expect(status.Code(err)).To(Equal(codes.NotFound))
|
|
Expect(err.Error()).To(ContainSubstring(grpcerrors.ModelMismatchSentinel))
|
|
Expect(err.Error()).To(ContainSubstring("a.gguf"))
|
|
Expect(err.Error()).To(ContainSubstring("b.gguf"))
|
|
})
|
|
|
|
It("IsModelNotLoaded does not claim a mismatch", func() {
|
|
Expect(grpcerrors.IsModelNotLoaded(grpcerrors.ModelMismatch("llama-cpp", "a", "b"))).To(BeFalse())
|
|
})
|
|
|
|
DescribeTable("IsLiveTranscriptionUnsupported",
|
|
func(err error, want bool) {
|
|
Expect(grpcerrors.IsLiveTranscriptionUnsupported(err)).To(Equal(want))
|
|
},
|
|
Entry("nil", nil, false),
|
|
Entry("typed via constructor", grpcerrors.LiveTranscriptionUnsupported("parakeet-cpp", "not a streaming model"), true),
|
|
Entry("typed code only", status.Error(codes.Unimplemented, "anything"), true),
|
|
Entry("stale stub message (Unknown code)", errors.New("rpc error: method AudioTranscriptionLive unimplemented"), true),
|
|
Entry("unrelated error", errors.New("context deadline exceeded"), false),
|
|
Entry("model not loaded is NOT unsupported", grpcerrors.ModelNotLoaded("parakeet-cpp"), false),
|
|
)
|
|
|
|
It("LiveTranscriptionUnsupported carries Unimplemented, not FailedPrecondition", func() {
|
|
err := grpcerrors.LiveTranscriptionUnsupported("parakeet-cpp", "reason")
|
|
Expect(status.Code(err)).To(Equal(codes.Unimplemented))
|
|
// FailedPrecondition is claimed by IsModelNotLoaded — the two
|
|
// signals must never alias.
|
|
Expect(grpcerrors.IsModelNotLoaded(err)).To(BeFalse())
|
|
})
|
|
|
|
DescribeTable("IsUnimplemented",
|
|
func(err error, want bool) {
|
|
Expect(grpcerrors.IsUnimplemented(err)).To(Equal(want))
|
|
},
|
|
Entry("nil", nil, false),
|
|
Entry("typed code", status.Error(codes.Unimplemented, "method Free not implemented"), true),
|
|
Entry("stale stub message (Unknown code)", errors.New("rpc error: code = Unimplemented desc = "), true),
|
|
Entry("unrelated error", errors.New("context deadline exceeded"), false),
|
|
Entry("unrelated grpc code", status.Error(codes.Unavailable, "connection refused"), false),
|
|
Entry("model not loaded is NOT unimplemented", grpcerrors.ModelNotLoaded("parakeet-cpp"), false),
|
|
)
|
|
|
|
It("StreamTranscriptionUnsupported carries Unimplemented and is not ModelNotLoaded", func() {
|
|
err := grpcerrors.StreamTranscriptionUnsupported("parakeet-cpp", "not a streaming model")
|
|
Expect(status.Code(err)).To(Equal(codes.Unimplemented))
|
|
Expect(grpcerrors.IsModelNotLoaded(err)).To(BeFalse())
|
|
})
|
|
})
|
|
|
|
var _ = Describe("SoundEventsUnsupported", func() {
|
|
It("is Unimplemented and carries the stable code", func() {
|
|
err := grpcerrors.SoundEventsUnsupported("parakeet-cpp", "no sound model")
|
|
Expect(status.Code(err)).To(Equal(codes.Unimplemented))
|
|
Expect(err.Error()).To(ContainSubstring("include_sounds_unsupported"))
|
|
Expect(grpcerrors.SoundEventsUnsupportedCode).To(Equal("include_sounds_unsupported"))
|
|
})
|
|
})
|