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

111 lines
2.8 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 文件路径")
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)
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))
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(50 * time.Millisecond) // 2x 实时推流
}
_ = sess.Close()
<-done
fmt.Printf("首个结果延迟 %v(含 2x 推流速度因素)\n", firstPartial.Round(time.Millisecond))
}
// 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