Files
wangjia 40760aa884
ci / server (push) Failing after 14s
ci / design-tokens (push) Failing after 11s
dudu MVP:五端语音输入法初始提交
- server:Go 网关(WS 流式识别中继/计费配额/微信登录支付 mock/反馈/埋点),gummy provider 已真实联调
- desktop:Tauri 2(全局快捷键 push-to-talk/浮层/托盘/设置/登录购买/反馈/首启引导)
- android:Compose 主 App + IME(键盘内录音直传)
- ios:App + 键盘扩展(1A spike 实证键盘内不可录音,走 deep link 听写)
- design/design-pipeline:设计系统 + token 导出 iOS/Android 主题
- doc:前后端设计文档(HTML);web:官网宣传页;todo:任务看板

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:38:37 +08:00

99 lines
2.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}