package asr import ( "context" "sync" "dudu/server/pkg/protocol" ) // MockProvider 测试/开发用:按收到的音频量逐字"识别"出预置句子, // 行为模拟 Gummy:每帧产出 partial,句读处产出 final。 type MockProvider struct { // Script 轮换使用的识别文本 Script []string mu sync.Mutex idx int } func NewMock() *MockProvider { return &MockProvider{Script: []string{ "帮我把这份周报整理一下,重点突出本周的进展。", "好的,我马上把文件发给你,大概十分钟之内。", }} } func (m *MockProvider) Name() string { return "mock" } func (m *MockProvider) StartSession(ctx context.Context, cfg SessionConfig) (Session, error) { m.mu.Lock() sentence := []rune(m.Script[m.idx%len(m.Script)]) m.idx++ m.mu.Unlock() s := &mockSession{ sentence: sentence, results: make(chan Result, 64), done: make(chan struct{}), } return s, nil } type mockSession struct { sentence []rune results chan Result pos int // 已"识别"的字数 committed int // 已 final 的字数 bytes int closeOnce sync.Once done chan struct{} mu sync.Mutex } // 每帧(100ms 音频)识别出 1 个字;遇句读(,。)将其前内容定稿为 final。 func (s *mockSession) SendAudio(pcm []byte) error { s.mu.Lock() defer s.mu.Unlock() select { case <-s.done: return nil default: } s.bytes += len(pcm) for s.pos < len(s.sentence) && s.bytes >= (s.pos+1)*protocol.FrameBytes { s.pos++ ch := s.sentence[s.pos-1] endMs := int64(s.bytes) * 1000 / 32000 // 模拟 provider 侧时间戳=实收音频时长 if ch == ',' || ch == '。' { s.emit(Result{Text: string(s.sentence[s.committed:s.pos]), IsFinal: true, EndTimeMs: endMs}) s.committed = s.pos } else { s.emit(Result{Text: string(s.sentence[s.committed:s.pos]), EndTimeMs: endMs}) } } return nil } func (s *mockSession) emit(r Result) { select { case s.results <- r: default: // 测试场景下不阻塞 } } func (s *mockSession) Results() <-chan Result { return s.results } // Close flush 未定稿部分为 final 并关闭通道。 func (s *mockSession) Close() error { s.closeOnce.Do(func() { s.mu.Lock() if s.pos > s.committed { s.emit(Result{Text: string(s.sentence[s.committed:s.pos]), IsFinal: true, EndTimeMs: int64(s.bytes) * 1000 / 32000}) s.committed = s.pos } close(s.done) close(s.results) s.mu.Unlock() }) return nil }