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>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user