d22e02fa2e
#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>
121 lines
3.3 KiB
Go
121 lines
3.3 KiB
Go
// gummycheck:1B/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 帧的推流间隔 ms(50=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
|