Files
dudu/server/cmd/gummycheck/main.go
wangjia d22e02fa2e
ci / server (push) Failing after 9s
ci / design-tokens (push) Failing after 10s
fix(#23)+feat(#24): stop 后不再下发周期 usage 帧;gummy 预连接 spare 会话
#23 修复(服务端根治,五端客户端提交信号自动变安全):
- finish 先冻结会话并停 usageLoop,再 flush provider——此前 usageLoop 在
  waitResults(最长 3s)期间继续 tick,周期 usage 帧会抢在尾部 final 之前
  下发,客户端以 partial 文本提前上屏、丢失 final 修正(12B 实测 8s 会话
  4/4 复现;修复后 6/6 归零)
- 桌面端收尾兜底超时 350ms → 800ms(实测 8s 会话 final flush 需 350~540ms,
  350ms 会截丢尾 final)

#24 预连接(12B 调优):
- GummyProvider 常备一条已完成 run-task 握手的 spare 会话,start 直取,
  后台异步补位 + 40s 定期换新(DashScope 空闲 60s 断连,实测 60s 存活/
  120s Idle timeout,留余量 45s)
- 实测网关 start 处理 120~670ms → 3~4ms;消除 dial 抖动(实测 60ms~3.7s)
  对 first_partial 尾部的放大

gummycheck 增加 -model / -pace / -idle 参数,支持模型对比与闲置存活实验

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:44:25 +08:00

121 lines
3.3 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.
// gummycheck1B/6B 实测工具——用真实 DashScope Key 验证 gummy provider 全链路。
// 用法(key 经 stdin 传入,不落盘):
//
// rbw get dashscope-api-key | go run ./cmd/gummycheck -wav /path/to/16k-mono-16bit.wav
package main
import (
"bufio"
"bytes"
"context"
"encoding/binary"
"flag"
"fmt"
"os"
"strings"
"time"
"dudu/server/internal/asr"
)
func main() {
wavPath := flag.String("wav", "", "16kHz/16bit/mono wav 文件路径")
model := flag.String("model", "gummy-realtime-v1", "DashScope 实时识别模型")
paceMs := flag.Int("pace", 50, "每 100ms 帧的推流间隔 ms50=2x 加速,100=实时)")
idleSec := flag.Int("idle", 0, "会话建立后先闲置 N 秒再推流(测预连接会话的存活时间)")
flag.Parse()
if *wavPath == "" {
fmt.Fprintln(os.Stderr, "用法: rbw get dashscope-api-key | gummycheck -wav test.wav")
os.Exit(2)
}
sc := bufio.NewScanner(os.Stdin)
if !sc.Scan() {
fmt.Fprintln(os.Stderr, "stdin 未读到 API key")
os.Exit(2)
}
key := strings.TrimSpace(sc.Text())
pcm, err := readWavData(*wavPath)
if err != nil {
fmt.Fprintln(os.Stderr, "读 wav 失败:", err)
os.Exit(1)
}
fmt.Printf("音频 %d 字节 ≈ %.1fs\n", len(pcm), float64(len(pcm))/32000)
p := asr.NewGummy(key)
p.Model = *model
fmt.Printf("模型 %s · 推流间隔 %dms/帧\n", *model, *paceMs)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
start := time.Now()
sess, err := p.StartSession(ctx, asr.SessionConfig{SampleRate: 16000, SessionID: "gummycheck"})
if err != nil {
fmt.Fprintln(os.Stderr, "StartSession 失败:", err)
os.Exit(1)
}
fmt.Printf("会话建立 %v\n", time.Since(start).Round(time.Millisecond))
if *idleSec > 0 {
fmt.Printf("闲置 %ds...\n", *idleSec)
time.Sleep(time.Duration(*idleSec) * time.Second)
start = time.Now() // 重新计时,测闲置后的识别延迟
}
done := make(chan struct{})
var firstPartial time.Duration
go func() {
defer close(done)
for r := range sess.Results() {
if r.Err != nil {
fmt.Println("ERROR:", r.Err)
return
}
if firstPartial == 0 {
firstPartial = time.Since(start)
}
tag := "partial"
if r.IsFinal {
tag = "FINAL "
}
fmt.Printf("[%6.2fs] %s end=%dms %s\n", time.Since(start).Seconds(), tag, r.EndTimeMs, r.Text)
}
}()
const frame = 3200
for off := 0; off < len(pcm); off += frame {
end := min(off+frame, len(pcm))
if err := sess.SendAudio(pcm[off:end]); err != nil {
fmt.Fprintln(os.Stderr, "SendAudio 失败:", err)
os.Exit(1)
}
time.Sleep(time.Duration(*paceMs) * time.Millisecond)
}
_ = sess.Close()
<-done
fmt.Printf("首个结果延迟 %v(推流 %dms/帧)\n", firstPartial.Round(time.Millisecond), *paceMs)
}
// readWavData 提取 wav 的 data chunk(仅支持 PCM)。
func readWavData(path string) ([]byte, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
if len(b) < 44 || string(b[0:4]) != "RIFF" || string(b[8:12]) != "WAVE" {
return nil, fmt.Errorf("不是 wav 文件")
}
off := 12
for off+8 <= len(b) {
id := string(b[off : off+4])
size := int(binary.LittleEndian.Uint32(b[off+4 : off+8]))
if id == "data" {
return b[off+8 : min(off+8+size, len(b))], nil
}
off += 8 + size
}
return nil, fmt.Errorf("未找到 data chunk")
}
var _ = bytes.MinRead