Files
Blizzard 4ee4a91a51 feat(voice): 火山 ASR 流式识别客户端 + 会话接线(Phase 1 耳朵)
voice/asr.go: 连 wss://openspeech.bytedance.com/api/v3/sauc/bigmodel,新版 API Key 鉴权
(Authorization: Bearer + X-Api-Resource-Id + Connect-Id),发初始配置帧(bigmodel/zh/ITN/
标点/VAD),PushAudio 流式喂 PCM、Finish 收尾;读 goroutine 解析响应(result 支持数组/对象/
字符串,type=final 为最终)推入 Results 通道。

handler/voice.go 接线:onAudio→PushAudio、start→重开识别、end→Finish;起 goroutine 把
转写 send(transcript) 实时回推客户端。加 writeMu 串行化写(读循环与 ASR 结果 goroutine
都写同一 WS,gorilla 禁并发写)。连接结束 stopASR 收尾。

带单测(配置JSON字段/result三形态解析)。真识别需部署联调(要真连火山);API Key 是否
还需 X-Api-App-Key 联调若 401 再补。上行接线(final→SubmitTask)与 TTS 是下一步。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:27:32 +08:00

54 lines
1.7 KiB
Go

package voice
import (
"encoding/json"
"testing"
)
// 初始配置 JSON 关键字段必须对(错了火山直接拒识别)。
func TestBuildASRRequest(t *testing.T) {
var r asrReq
if err := json.Unmarshal(buildASRRequest("u1"), &r); err != nil {
t.Fatalf("配置 JSON 不合法: %v", err)
}
if r.User.UID != "u1" {
t.Fatalf("uid 应 u1,得 %q", r.User.UID)
}
if r.Audio.Rate != 16000 || r.Audio.Bits != 16 || r.Audio.Channel != 1 {
t.Fatalf("音频参数应 16k/16bit/单声道,得 %+v", r.Audio)
}
if r.Request.ModelName != "bigmodel" || !r.Request.VAD.VadEnable {
t.Fatalf("model/vad 配置错:%+v", r.Request)
}
// 空 uid 兜底
var r2 asrReq
_ = json.Unmarshal(buildASRRequest(""), &r2)
if r2.User.UID == "" {
t.Fatal("空 uid 应兜底非空")
}
}
// result 三种形态(数组/对象/字符串)都要能取出文本;final 由 type 决定。
func TestParseASRResult(t *testing.T) {
cases := []struct {
payload string
wantText string
wantFinal bool
}{
{`{"type":"interim","result":[{"text":"你好"}]}`, "你好", false},
{`{"type":"final","result":[{"text":"你好"},{"text":"世界"}]}`, "你好世界", true},
{`{"type":"final","result":{"text":"单对象"}}`, "单对象", true},
{`{"type":"interim","result":"纯字符串"}`, "纯字符串", false},
}
for _, c := range cases {
text, final, ok := parseASRResult([]byte(c.payload))
if !ok || text != c.wantText || final != c.wantFinal {
t.Fatalf("parse(%s)=%q,%v,%v want %q,%v", c.payload, text, final, ok, c.wantText, c.wantFinal)
}
}
// 坏 JSON 不 panic、ok=false
if _, _, ok := parseASRResult([]byte("not json")); ok {
t.Fatal("坏 JSON 应 ok=false")
}
}