sync: migrate ai-operator to Gitea (2026-08-10)
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
package call
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"ai-operator/internal/ai"
|
||||
"ai-operator/internal/media"
|
||||
)
|
||||
|
||||
type pumpMedia struct {
|
||||
audio chan media.AudioChunk
|
||||
sent [][]byte
|
||||
flushed int
|
||||
}
|
||||
|
||||
func newPumpMedia() *pumpMedia {
|
||||
return &pumpMedia{audio: make(chan media.AudioChunk, 4)}
|
||||
}
|
||||
func (m *pumpMedia) Audio() <-chan media.AudioChunk { return m.audio }
|
||||
func (m *pumpMedia) SendAudio(ctx context.Context, data []byte) error {
|
||||
m.sent = append(m.sent, append([]byte(nil), data...))
|
||||
return nil
|
||||
}
|
||||
func (m *pumpMedia) FlushMedia(ctx context.Context) error { m.flushed++; return nil }
|
||||
func (m *pumpMedia) Close(ctx context.Context) error { close(m.audio); return nil }
|
||||
|
||||
type pumpProvider struct {
|
||||
sent []media.AudioChunk
|
||||
}
|
||||
|
||||
func (p *pumpProvider) StartSession(ctx context.Context, config ai.VoiceSessionConfig) error {
|
||||
return nil
|
||||
}
|
||||
func (p *pumpProvider) SendAudio(ctx context.Context, chunk media.AudioChunk) error {
|
||||
p.sent = append(p.sent, media.AudioChunk{CallID: chunk.CallID, Data: append([]byte(nil), chunk.Data...), Codec: chunk.Codec, Timestamp: chunk.Timestamp})
|
||||
return nil
|
||||
}
|
||||
func (p *pumpProvider) SendToolResult(ctx context.Context, result ai.ToolResult) error { return nil }
|
||||
func (p *pumpProvider) Close(ctx context.Context) error { return nil }
|
||||
func (p *pumpProvider) Events() <-chan ai.VoiceEvent { return nil }
|
||||
func (p *pumpProvider) Stats() ai.VoiceProviderStats { return ai.VoiceProviderStats{} }
|
||||
|
||||
func TestAudioPumpForwardsAsteriskAudioToProvider(t *testing.T) {
|
||||
pm := newPumpMedia()
|
||||
pp := &pumpProvider{}
|
||||
pump := &AudioPump{CallID: "c1", Media: pm, Provider: pp, AsteriskSampleRate: 16000, ProviderInputSampleRate: 24000, ProviderOutputSampleRate: 24000}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
if err := pump.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pm.audio <- media.AudioChunk{Data: []byte{0, 0, 1, 0, 2, 0, 3, 0}, Codec: media.CodecSLIN16, Timestamp: time.Now()}
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for len(pp.sent) == 0 && time.Now().Before(deadline) {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if len(pp.sent) != 1 {
|
||||
t.Fatalf("provider chunks=%d", len(pp.sent))
|
||||
}
|
||||
if len(pp.sent[0].Data) <= 8 {
|
||||
t.Fatalf("expected 16k->24k resample to increase bytes, got %d", len(pp.sent[0].Data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAudioPumpForwardsProviderAudioToAsterisk(t *testing.T) {
|
||||
pm := newPumpMedia()
|
||||
pp := &pumpProvider{}
|
||||
pump := &AudioPump{CallID: "c1", Media: pm, Provider: pp, AsteriskSampleRate: 16000, ProviderInputSampleRate: 24000, ProviderOutputSampleRate: 24000}
|
||||
in := []byte{0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0}
|
||||
if err := pump.HandleProviderEvent(context.Background(), ai.VoiceEvent{Type: ai.VoiceEventAssistantAudioDelta, Audio: in}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(pm.sent) != 1 {
|
||||
t.Fatalf("media sent chunks=%d", len(pm.sent))
|
||||
}
|
||||
if len(pm.sent[0]) >= len(in) {
|
||||
t.Fatalf("expected 24k->16k resample to reduce bytes, got %d from %d", len(pm.sent[0]), len(in))
|
||||
}
|
||||
pump.mu.Lock()
|
||||
pump.suppressInputUntil = time.Now().Add(-time.Second)
|
||||
pump.mu.Unlock()
|
||||
if err := pump.HandleProviderEvent(context.Background(), ai.VoiceEvent{Type: ai.VoiceEventInterruption}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pm.flushed != 1 {
|
||||
t.Fatalf("flush count=%d", pm.flushed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAudioPumpSuppressesInputDuringAssistantPlayback(t *testing.T) {
|
||||
pm := newPumpMedia()
|
||||
pp := &pumpProvider{}
|
||||
pump := &AudioPump{CallID: "c1", Media: pm, Provider: pp, AsteriskSampleRate: 16000, ProviderInputSampleRate: 24000, ProviderOutputSampleRate: 24000}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
if err := pump.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pump.HandleProviderEvent(context.Background(), ai.VoiceEvent{Type: ai.VoiceEventAssistantAudioDelta, Audio: []byte{0, 0, 1, 0, 2, 0, 3, 0}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pm.audio <- media.AudioChunk{Data: []byte{0, 0, 1, 0, 2, 0, 3, 0}, Codec: media.CodecSLIN16, Timestamp: time.Now()}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if len(pp.sent) != 0 {
|
||||
t.Fatalf("provider received echo audio chunks=%d", len(pp.sent))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAudioPumpIgnoresEchoInterruptionDuringAssistantPlayback(t *testing.T) {
|
||||
pm := newPumpMedia()
|
||||
pp := &pumpProvider{}
|
||||
pump := &AudioPump{CallID: "c1", Media: pm, Provider: pp, AsteriskSampleRate: 16000, ProviderInputSampleRate: 24000, ProviderOutputSampleRate: 24000}
|
||||
if err := pump.HandleProviderEvent(context.Background(), ai.VoiceEvent{Type: ai.VoiceEventAssistantAudioDelta, Audio: []byte{0, 0, 1, 0, 2, 0, 3, 0}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pump.HandleProviderEvent(context.Background(), ai.VoiceEvent{Type: ai.VoiceEventInterruption}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pm.flushed != 0 {
|
||||
t.Fatalf("unexpected flush during assistant echo suppression: %d", pm.flushed)
|
||||
}
|
||||
pump.mu.Lock()
|
||||
pump.suppressInputUntil = time.Now().Add(-time.Second)
|
||||
pump.mu.Unlock()
|
||||
if err := pump.HandleProviderEvent(context.Background(), ai.VoiceEvent{Type: ai.VoiceEventInterruption}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pm.flushed != 1 {
|
||||
t.Fatalf("flush count=%d", pm.flushed)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user