sync: migrate ai-operator to Gitea (2026-08-10)
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"ai-operator/internal/ai"
|
||||
"ai-operator/internal/config"
|
||||
"ai-operator/internal/media"
|
||||
)
|
||||
|
||||
func TestTextChunker(t *testing.T) {
|
||||
c := NewTextChunker(10, 80, 20, time.Millisecond, true)
|
||||
chunks := c.Add("Первое предложение. Второе", false)
|
||||
if len(chunks) != 1 || chunks[0] != "Первое предложение." {
|
||||
t.Fatalf("chunks=%v", chunks)
|
||||
}
|
||||
flush := c.Flush()
|
||||
if len(flush) != 1 || flush[0] != "Второе" {
|
||||
t.Fatalf("flush=%v", flush)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaturalizer(t *testing.T) {
|
||||
cfg := config.NaturalnessConfig{Enabled: true, AudioTagsEnabled: true, AllowNonverbalTags: true, MaxAudioTagsPerResponse: 2}
|
||||
got := NaturalizeForVoice("Здравствуйте, меня зовут Жанна. Чем могу помочь?", "ru", cfg)
|
||||
if got == "" || got == "Здравствуйте, меня зовут Жанна. Чем могу помочь?" {
|
||||
t.Fatalf("not naturalized: %q", got)
|
||||
}
|
||||
if stripped := RemoveAudioTags(got); stripped == "" || stripped == got {
|
||||
t.Fatalf("tags not stripped: %q", stripped)
|
||||
}
|
||||
cfg.AllowCough = false
|
||||
if got := NaturalizeForVoice("Первичное подключение газа бесплатно.", "ru", cfg); containsRune(got, "cough") {
|
||||
t.Fatalf("cough added unexpectedly: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamingProviderFakeEndToEnd(t *testing.T) {
|
||||
cfg := config.Config{
|
||||
STT: config.STTConfig{Model: "scribe_realtime_v2", SampleRate: 16000, InputFormat: "pcm_16000"},
|
||||
LLM: config.LLMConfig{StreamChunkMinChars: 10, StreamChunkMaxChars: 160, FirstChunkTimeoutMS: 1, MaxOutputTokens: 100, Temperature: 0.2},
|
||||
Eleven: config.ElevenLabsConfig{VoiceIDRU: "voice", TTSModelID: "eleven_flash_v2_5", TTSOutputFormat: "pcm_16000", TTSSampleRate: 16000},
|
||||
Natural: config.NaturalnessConfig{Enabled: false},
|
||||
Pipeline: config.PipelineConfig{InitialGreeting: false, BargeIn: true, MaxTurnSeconds: 2, TTSStartAfterChars: 20, TTSStartAfterPunctuation: true},
|
||||
}
|
||||
factory := &FakeTTSFactory{}
|
||||
p := NewStreamingProviderWithDeps(cfg, nil, NewFakeSTT(), FakeLLM{}, factory.New)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := p.StartSession(ctx, ai.VoiceSessionConfig{CallID: "c1", SystemPrompt: "prompt"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := p.SendAudio(ctx, media.AudioChunk{Data: []byte{0, 0}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tool := false
|
||||
audio := false
|
||||
for !audio {
|
||||
select {
|
||||
case ev := <-p.Events():
|
||||
if ev.Type == ai.VoiceEventToolCall {
|
||||
tool = true
|
||||
_ = p.SendToolResult(ctx, ai.ToolResult{CallID: ev.CallID, ToolCallID: ev.ToolCall.ID, Result: map[string]any{"answer_text": "Первичное подключение бесплатно."}})
|
||||
}
|
||||
if ev.Type == ai.VoiceEventAssistantAudioDelta {
|
||||
audio = true
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timeout")
|
||||
}
|
||||
}
|
||||
if !tool || !audio || len(factory.Texts) == 0 {
|
||||
t.Fatalf("tool=%t audio=%t texts=%v", tool, audio, factory.Texts)
|
||||
}
|
||||
}
|
||||
|
||||
func containsRune(s, sub string) bool {
|
||||
for i := 0; i+len(sub) <= len(s); i++ {
|
||||
if s[i:i+len(sub)] == sub {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user