Files
voice_test/asr_volc.go
T

218 lines
6.0 KiB
Go
Raw 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.
package main
import (
"bytes"
"compress/gzip"
"context"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
"github.com/gorilla/websocket"
)
// volcASR: 火山引擎大模型流式语音识别(Doubao-Seed-ASR-Streaming / sauc bigmodel)。
// 二进制帧: [4B header][可选4B sequence][4B payload size][payload]
// header: ver<<4|hdrUnits, msgType<<4|flags, ser<<4|comp, 0x00
const (
volcURL = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel"
msgFullClient = 0x1
msgAudioOnly = 0x2
msgFullServer = 0x9
msgError = 0xF
flagNoSeq = 0x0 // full client request 用
flagLast = 0x2 // 客户端最后一包音频(负包,无序号)
flagNegWithSeq = 0x3 // 服务端最终帧(负包+序号)
serJSON = 0x1
compNone = 0x0
compGzip = 0x1
)
type volcASR struct {
cfg agentCfg
conn *websocket.Conn
out chan asrEvent
wmu sync.Mutex
once sync.Once
emitted map[int]bool // 已作为 final 发出的 utterance 下标
}
func newVolcASR(cfg agentCfg) *volcASR {
return &volcASR{cfg: cfg, out: make(chan asrEvent, 64), emitted: map[int]bool{}}
}
func (v *volcASR) start(ctx context.Context) error {
hdr := http.Header{}
if v.cfg.VolcAppKey != "" { // 新版控制台:单一 APP Key
hdr.Set("X-Api-Key", v.cfg.VolcAppKey)
} else if v.cfg.VolcAppID != "" && v.cfg.VolcAccessToken != "" { // 旧版控制台:APP ID + Access Token
hdr.Set("X-Api-App-Key", v.cfg.VolcAppID)
hdr.Set("X-Api-Access-Key", v.cfg.VolcAccessToken)
} else {
return fmt.Errorf("未配置火山凭证:新版控制台设 VOLC_ASR_APP_KEY,旧版设 VOLC_ASR_APP_ID + VOLC_ASR_ACCESS_TOKEN(均需在语音技术控制台获取,非 VOLCENGINE_ACCESS_KEY")
}
hdr.Set("X-Api-Resource-Id", v.cfg.VolcResourceID)
hdr.Set("X-Api-Request-Id", uuid())
hdr.Set("X-Api-Connect-Id", uuid())
hdr.Set("X-Api-Sequence", "-1")
conn, resp, err := websocket.DefaultDialer.DialContext(ctx, volcURL, hdr)
if err != nil {
logid := ""
if resp != nil {
logid = resp.Header.Get("X-Tt-Logid")
}
return fmt.Errorf("连接火山 ASR (401/403 多为凭证或资源未开通, logid=%s): %w", logid, err)
}
v.conn = conn
req := map[string]any{
"user": map[string]any{"uid": "voice-test-agent"},
"audio": map[string]any{"format": "pcm", "rate": 16000, "bits": 16, "channel": 1},
"request": map[string]any{
"model_name": "bigmodel",
"enable_itn": true,
"enable_punc": true,
"result_type": "full",
"show_utterances": true,
},
}
body, _ := json.Marshal(req)
if err := v.conn.WriteMessage(websocket.BinaryMessage, volcFrame(msgFullClient, flagNoSeq, serJSON, compNone, body)); err != nil {
return fmt.Errorf("发送 full client request: %w", err)
}
go v.readLoop()
return nil
}
// volcFrame 构造一帧二进制协议。
func volcFrame(msgType, flags, ser, comp byte, payload []byte) []byte {
buf := bytes.NewBuffer(make([]byte, 0, 12+len(payload)))
buf.WriteByte(0x11) // ver=1, header=1*4B
buf.WriteByte(msgType<<4 | flags)
buf.WriteByte(ser<<4 | comp)
buf.WriteByte(0x00)
binary.Write(buf, binary.BigEndian, uint32(len(payload)))
buf.Write(payload)
return buf.Bytes()
}
func (v *volcASR) readLoop() {
defer v.closeUpstream()
for {
mt, data, err := v.conn.ReadMessage()
if err != nil {
v.emit(asrEvent{Typ: "error", Error: fmt.Sprintf("连接关闭: %v", err)})
return
}
if mt != websocket.BinaryMessage || len(data) < 8 {
continue
}
msgType, flags := data[1]>>4, data[1]&0x0F
comp := data[2] & 0x0F
off := int(data[0]&0x0F) * 4
if flags != 0 { // 带 sequence 的响应帧(1=正常 / 2、3=负包结束)
if len(data) < off+4 {
continue
}
off += 4 // sequence 值不用
}
var code uint32
if msgType == msgError {
if len(data) < off+4 {
continue
}
code = binary.BigEndian.Uint32(data[off : off+4])
off += 4
}
if len(data) < off+4 {
continue
}
size := int(binary.BigEndian.Uint32(data[off : off+4]))
payload := data[off+4 : off+4+size]
if comp == compGzip {
if r, err := gzip.NewReader(bytes.NewReader(payload)); err == nil {
payload, _ = io.ReadAll(r)
}
}
switch msgType {
case msgFullServer:
var resp struct {
Code int `json:"code"`
Message string `json:"message"`
Result struct {
Text string `json:"text"`
Utterances []struct {
Text string `json:"text"`
Definite bool `json:"definite"`
} `json:"utterances"`
} `json:"result"`
}
if json.Unmarshal(payload, &resp) != nil {
continue
}
if resp.Code != 0 && resp.Code != 20000000 {
v.emit(asrEvent{Typ: "error", Code: fmt.Sprint(resp.Code), Error: resp.Message})
return
}
// 句级 finalutterance 固化即发出(连续对话不依赖结束帧)
for i, u := range resp.Result.Utterances {
if u.Definite && !v.emitted[i] {
v.emitted[i] = true
v.emit(asrEvent{Typ: "final", Text: u.Text})
}
}
// partial:最后一个未固化 utterance
if n := len(resp.Result.Utterances); n > 0 && !resp.Result.Utterances[n-1].Definite {
v.emit(asrEvent{Typ: "partial", Text: resp.Result.Utterances[n-1].Text})
}
// 服务端最终帧(负包):流结束
if flags == flagLast || flags == flagNegWithSeq {
return
}
case msgError:
v.emit(asrEvent{Typ: "error", Code: fmt.Sprint(code), Error: string(payload)})
return
}
}
}
func (v *volcASR) emit(e asrEvent) {
select {
case v.out <- e:
default:
}
}
func (v *volcASR) sendAudio(b []byte) error {
v.wmu.Lock()
defer v.wmu.Unlock()
return v.conn.WriteMessage(websocket.BinaryMessage, volcFrame(msgAudioOnly, 0, 0, compNone, b))
}
func (v *volcASR) finish() error {
// 负包:flags=2,空 payload
v.wmu.Lock()
defer v.wmu.Unlock()
return v.conn.WriteMessage(websocket.BinaryMessage, volcFrame(msgAudioOnly, flagLast, 0, compNone, nil))
}
func (v *volcASR) events() <-chan asrEvent { return v.out }
func (v *volcASR) close() { v.closeUpstream() }
func (v *volcASR) closeUpstream() {
v.once.Do(func() {
if v.conn != nil {
v.conn.Close()
}
close(v.out)
})
}