163 lines
5.8 KiB
Go
163 lines
5.8 KiB
Go
package backend
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/core/schema"
|
|
"github.com/mudler/LocalAI/core/trace"
|
|
"github.com/mudler/LocalAI/pkg/grpc/proto"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("liveEventFromProto", func() {
|
|
It("maps deltas, eou flags and words (ns -> duration)", func() {
|
|
ev := liveEventFromProto(&proto.TranscriptLiveResponse{
|
|
Delta: "hello ",
|
|
Eou: true,
|
|
Words: []*proto.TranscriptWord{
|
|
{Start: int64(100 * time.Millisecond), End: int64(400 * time.Millisecond), Text: "hello"},
|
|
},
|
|
})
|
|
Expect(ev.Delta).To(Equal("hello "))
|
|
Expect(ev.Eou).To(BeTrue())
|
|
Expect(ev.Words).To(HaveLen(1))
|
|
Expect(ev.Words[0].Text).To(Equal("hello"))
|
|
Expect(ev.Words[0].Start).To(Equal(100 * time.Millisecond))
|
|
Expect(ev.Words[0].End).To(Equal(400 * time.Millisecond))
|
|
Expect(ev.Final).To(BeNil())
|
|
})
|
|
|
|
It("maps the terminal final result including the eou flag", func() {
|
|
ev := liveEventFromProto(&proto.TranscriptLiveResponse{
|
|
FinalResult: &proto.TranscriptResult{
|
|
Text: "hello world",
|
|
Duration: 1.5,
|
|
Eou: true,
|
|
Segments: []*proto.TranscriptSegment{{Id: 0, Text: "hello world"}},
|
|
},
|
|
})
|
|
Expect(ev.Final).NotTo(BeNil())
|
|
Expect(ev.Final.Text).To(Equal("hello world"))
|
|
Expect(ev.Final.Duration).To(BeNumerically("~", 1.5, 1e-6))
|
|
Expect(ev.Final.Eou).To(BeTrue())
|
|
Expect(ev.Final.Segments).To(HaveLen(1))
|
|
})
|
|
|
|
It("yields an empty event for a bare ready ack (filtered by the recv loop)", func() {
|
|
ev := liveEventFromProto(&proto.TranscriptLiveResponse{Ready: true})
|
|
Expect(ev.Delta).To(BeEmpty())
|
|
Expect(ev.Eou).To(BeFalse())
|
|
Expect(ev.Words).To(BeEmpty())
|
|
Expect(ev.Final).To(BeNil())
|
|
})
|
|
|
|
It("maps the eob backchannel flag separately from eou", func() {
|
|
ev := liveEventFromProto(&proto.TranscriptLiveResponse{Delta: "uh-huh", Eob: true})
|
|
Expect(ev.Eob).To(BeTrue())
|
|
Expect(ev.Eou).To(BeFalse())
|
|
})
|
|
})
|
|
|
|
// liveTraceState is what makes streaming-only pipelines visible on the
|
|
// Traces page: without it a semantic_vad session with retranscribe off
|
|
// produced no transcription trace at all. One trace per session (= one per
|
|
// realtime turn), recorded at Close.
|
|
var _ = Describe("liveTraceState", func() {
|
|
var appConfig *config.ApplicationConfig
|
|
|
|
BeforeEach(func() {
|
|
appConfig = &config.ApplicationConfig{
|
|
EnableTracing: true,
|
|
TracingMaxItems: 64,
|
|
}
|
|
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems, appConfig.TracingMaxBodyBytes)
|
|
trace.ClearBackendTraces()
|
|
})
|
|
|
|
modelCfg := func() config.ModelConfig {
|
|
cfg := config.ModelConfig{Backend: "parakeet-cpp"}
|
|
cfg.Name = "parakeet-live"
|
|
return cfg
|
|
}
|
|
|
|
It("is disabled (nil) when tracing is off, and nil receivers are no-ops", func() {
|
|
appConfig.EnableTracing = false
|
|
ts := newLiveTraceState(modelCfg(), appConfig, "en")
|
|
Expect(ts).To(BeNil())
|
|
|
|
// The session calls these unconditionally; nil must be safe.
|
|
ts.addPCM([]float32{0.5})
|
|
ts.observe(LiveTranscriptionEvent{Eou: true})
|
|
ts.record(nil)
|
|
Consistently(trace.GetBackendTraces, "100ms", "20ms").Should(BeEmpty())
|
|
})
|
|
|
|
It("records one transcription trace with text, eou event counts and audio snippet at Close", func() {
|
|
ts := newLiveTraceState(modelCfg(), appConfig, "en")
|
|
Expect(ts).NotTo(BeNil())
|
|
|
|
// One second of a loud-ish constant tone so the snippet has signal.
|
|
pcm := make([]float32, liveSampleRate)
|
|
for i := range pcm {
|
|
pcm[i] = 0.25
|
|
}
|
|
ts.addPCM(pcm)
|
|
ts.observe(LiveTranscriptionEvent{Delta: "hello "})
|
|
ts.observe(LiveTranscriptionEvent{Delta: "world", Eou: true})
|
|
ts.observe(LiveTranscriptionEvent{Final: &schema.TranscriptionResult{Text: "hello world", Eou: true}})
|
|
|
|
ts.record(nil)
|
|
|
|
Eventually(trace.GetBackendTraces).Should(HaveLen(1))
|
|
got := trace.GetBackendTraces()[0]
|
|
Expect(got.Type).To(Equal(trace.BackendTraceTranscription))
|
|
Expect(got.ModelName).To(Equal("parakeet-live"))
|
|
Expect(got.Backend).To(Equal("parakeet-cpp"))
|
|
Expect(got.Summary).To(ContainSubstring("hello world"))
|
|
Expect(got.Data["source"]).To(Equal("live_stream"))
|
|
Expect(got.Data["result_text"]).To(Equal("hello world"))
|
|
// The live FinalResult no longer carries a terminal eou flag; the
|
|
// per-feed eou_events count is what the trace records instead.
|
|
Expect(got.Data).NotTo(HaveKey("eou"))
|
|
Expect(got.Data["eou_events"]).To(Equal(1))
|
|
Expect(got.Data["delta_events"]).To(Equal(2))
|
|
Expect(got.Data["audio_duration_s"]).To(BeNumerically("~", 1.0, 0.01))
|
|
Expect(got.Data["audio_wav_base64"]).NotTo(BeEmpty())
|
|
Expect(got.Error).To(BeEmpty())
|
|
})
|
|
|
|
It("caps the stored snippet but keeps counting the full fed duration", func() {
|
|
ts := newLiveTraceState(modelCfg(), appConfig, "")
|
|
|
|
// Feed past the snippet cap in two chunks (cap + one extra second).
|
|
ts.addPCM(make([]float32, trace.MaxSnippetSeconds*liveSampleRate))
|
|
ts.addPCM(make([]float32, liveSampleRate))
|
|
|
|
Expect(len(ts.pcm)).To(Equal(trace.MaxSnippetSeconds * liveSampleRate * 2))
|
|
Expect(ts.fedSamples).To(Equal((trace.MaxSnippetSeconds + 1) * liveSampleRate))
|
|
|
|
ts.record(nil)
|
|
Eventually(trace.GetBackendTraces).Should(HaveLen(1))
|
|
got := trace.GetBackendTraces()[0]
|
|
Expect(got.Data["audio_duration_s"]).To(BeNumerically("~", float64(trace.MaxSnippetSeconds+1), 0.01))
|
|
Expect(got.Data["audio_snippet_s"]).To(BeNumerically("~", float64(trace.MaxSnippetSeconds), 0.01))
|
|
})
|
|
|
|
It("clamps out-of-range float samples instead of wrapping", func() {
|
|
ts := newLiveTraceState(modelCfg(), appConfig, "")
|
|
ts.addPCM([]float32{2.0, -2.0})
|
|
Expect(ts.pcm).To(Equal([]byte{0xff, 0x7f, 0x00, 0x80})) // 32767, -32768
|
|
})
|
|
|
|
It("stamps the close error on the trace", func() {
|
|
ts := newLiveTraceState(modelCfg(), appConfig, "")
|
|
ts.record(errors.New("stream torn down"))
|
|
|
|
Eventually(trace.GetBackendTraces).Should(HaveLen(1))
|
|
Expect(trace.GetBackendTraces()[0].Error).To(Equal("stream torn down"))
|
|
})
|
|
})
|