初始化导入 new-api 源码
This commit is contained in:
402
relay/channel/volcengine/adaptor.go
Normal file
402
relay/channel/volcengine/adaptor.go
Normal file
@@ -0,0 +1,402 @@
|
||||
package volcengine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
channelconstant "github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/relay/channel"
|
||||
"github.com/QuantumNous/new-api/relay/channel/claude"
|
||||
"github.com/QuantumNous/new-api/relay/channel/openai"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
"github.com/QuantumNous/new-api/relay/constant"
|
||||
"github.com/QuantumNous/new-api/setting/model_setting"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
const (
|
||||
contextKeyTTSRequest = "volcengine_tts_request"
|
||||
contextKeyResponseFormat = "response_format"
|
||||
)
|
||||
|
||||
type Adaptor struct {
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
|
||||
//TODO implement me
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, req *dto.ClaudeRequest) (any, error) {
|
||||
if _, ok := channelconstant.ChannelSpecialBases[info.ChannelBaseUrl]; ok {
|
||||
adaptor := claude.Adaptor{}
|
||||
return adaptor.ConvertClaudeRequest(c, info, req)
|
||||
}
|
||||
adaptor := openai.Adaptor{}
|
||||
return adaptor.ConvertClaudeRequest(c, info, req)
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
|
||||
if info.RelayMode != constant.RelayModeAudioSpeech {
|
||||
return nil, errors.New("unsupported audio relay mode")
|
||||
}
|
||||
|
||||
appID, token, err := parseVolcengineAuth(info.ApiKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
voiceType := mapVoiceType(request.Voice)
|
||||
speedRatio := lo.FromPtrOr(request.Speed, 0.0)
|
||||
encoding := mapEncoding(request.ResponseFormat)
|
||||
|
||||
c.Set(contextKeyResponseFormat, encoding)
|
||||
|
||||
volcRequest := VolcengineTTSRequest{
|
||||
App: VolcengineTTSApp{
|
||||
AppID: appID,
|
||||
Token: token,
|
||||
Cluster: "volcano_tts",
|
||||
},
|
||||
User: VolcengineTTSUser{
|
||||
UID: "openai_relay_user",
|
||||
},
|
||||
Audio: VolcengineTTSAudio{
|
||||
VoiceType: voiceType,
|
||||
Encoding: encoding,
|
||||
SpeedRatio: speedRatio,
|
||||
Rate: 24000,
|
||||
},
|
||||
Request: VolcengineTTSReqInfo{
|
||||
ReqID: generateRequestID(),
|
||||
Text: request.Input,
|
||||
Operation: "submit",
|
||||
Model: info.OriginModelName,
|
||||
},
|
||||
}
|
||||
|
||||
if len(request.Metadata) > 0 {
|
||||
if err = json.Unmarshal(request.Metadata, &volcRequest); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling metadata to volcengine request: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.Set(contextKeyTTSRequest, volcRequest)
|
||||
|
||||
if volcRequest.Request.Operation == "submit" {
|
||||
info.IsStream = true
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(volcRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshalling volcengine request: %w", err)
|
||||
}
|
||||
|
||||
return bytes.NewReader(jsonData), nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
|
||||
switch info.RelayMode {
|
||||
case constant.RelayModeImagesGenerations:
|
||||
return request, nil
|
||||
// 根据官方文档,并没有发现豆包生图支持表单请求:https://www.volcengine.com/docs/82379/1824121
|
||||
//case constant.RelayModeImagesEdits:
|
||||
//
|
||||
// var requestBody bytes.Buffer
|
||||
// writer := multipart.NewWriter(&requestBody)
|
||||
//
|
||||
// writer.WriteField("model", request.Model)
|
||||
//
|
||||
// formData := c.Request.PostForm
|
||||
// for key, values := range formData {
|
||||
// if key == "model" {
|
||||
// continue
|
||||
// }
|
||||
// for _, value := range values {
|
||||
// writer.WriteField(key, value)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if err := c.Request.ParseMultipartForm(32 << 20); err != nil {
|
||||
// return nil, errors.New("failed to parse multipart form")
|
||||
// }
|
||||
//
|
||||
// if c.Request.MultipartForm != nil && c.Request.MultipartForm.File != nil {
|
||||
// var imageFiles []*multipart.FileHeader
|
||||
// var exists bool
|
||||
//
|
||||
// if imageFiles, exists = c.Request.MultipartForm.File["image"]; !exists || len(imageFiles) == 0 {
|
||||
// if imageFiles, exists = c.Request.MultipartForm.File["image[]"]; !exists || len(imageFiles) == 0 {
|
||||
// foundArrayImages := false
|
||||
// for fieldName, files := range c.Request.MultipartForm.File {
|
||||
// if strings.HasPrefix(fieldName, "image[") && len(files) > 0 {
|
||||
// foundArrayImages = true
|
||||
// for _, file := range files {
|
||||
// imageFiles = append(imageFiles, file)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if !foundArrayImages && (len(imageFiles) == 0) {
|
||||
// return nil, errors.New("image is required")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// for i, fileHeader := range imageFiles {
|
||||
// file, err := fileHeader.Open()
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("failed to open image file %d: %w", i, err)
|
||||
// }
|
||||
// defer file.Close()
|
||||
//
|
||||
// fieldName := "image"
|
||||
// if len(imageFiles) > 1 {
|
||||
// fieldName = "image[]"
|
||||
// }
|
||||
//
|
||||
// mimeType := detectImageMimeType(fileHeader.Filename)
|
||||
//
|
||||
// h := make(textproto.MIMEHeader)
|
||||
// h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, fieldName, fileHeader.Filename))
|
||||
// h.Set("Content-Type", mimeType)
|
||||
//
|
||||
// part, err := writer.CreatePart(h)
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("create form part failed for image %d: %w", i, err)
|
||||
// }
|
||||
//
|
||||
// if _, err := io.Copy(part, file); err != nil {
|
||||
// return nil, fmt.Errorf("copy file failed for image %d: %w", i, err)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if maskFiles, exists := c.Request.MultipartForm.File["mask"]; exists && len(maskFiles) > 0 {
|
||||
// maskFile, err := maskFiles[0].Open()
|
||||
// if err != nil {
|
||||
// return nil, errors.New("failed to open mask file")
|
||||
// }
|
||||
// defer maskFile.Close()
|
||||
//
|
||||
// mimeType := detectImageMimeType(maskFiles[0].Filename)
|
||||
//
|
||||
// h := make(textproto.MIMEHeader)
|
||||
// h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="mask"; filename="%s"`, maskFiles[0].Filename))
|
||||
// h.Set("Content-Type", mimeType)
|
||||
//
|
||||
// maskPart, err := writer.CreatePart(h)
|
||||
// if err != nil {
|
||||
// return nil, errors.New("create form file failed for mask")
|
||||
// }
|
||||
//
|
||||
// if _, err := io.Copy(maskPart, maskFile); err != nil {
|
||||
// return nil, errors.New("copy mask file failed")
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// return nil, errors.New("no multipart form data found")
|
||||
// }
|
||||
//
|
||||
// writer.Close()
|
||||
// c.Request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
// return bytes.NewReader(requestBody.Bytes()), nil
|
||||
|
||||
default:
|
||||
return request, nil
|
||||
}
|
||||
}
|
||||
|
||||
func detectImageMimeType(filename string) string {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg":
|
||||
return "image/jpeg"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".webp":
|
||||
return "image/webp"
|
||||
default:
|
||||
if strings.HasPrefix(ext, ".jp") {
|
||||
return "image/jpeg"
|
||||
}
|
||||
return "image/png"
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
baseUrl := info.ChannelBaseUrl
|
||||
if baseUrl == "" {
|
||||
baseUrl = channelconstant.ChannelBaseURLs[channelconstant.ChannelTypeVolcEngine]
|
||||
}
|
||||
specialPlan, hasSpecialPlan := channelconstant.ChannelSpecialBases[baseUrl]
|
||||
|
||||
switch info.RelayFormat {
|
||||
case types.RelayFormatClaude:
|
||||
if hasSpecialPlan && specialPlan.ClaudeBaseURL != "" {
|
||||
return fmt.Sprintf("%s/v1/messages", specialPlan.ClaudeBaseURL), nil
|
||||
}
|
||||
if strings.HasPrefix(info.UpstreamModelName, "bot") {
|
||||
return fmt.Sprintf("%s/api/v3/bots/chat/completions", baseUrl), nil
|
||||
}
|
||||
return fmt.Sprintf("%s/api/v3/chat/completions", baseUrl), nil
|
||||
default:
|
||||
switch info.RelayMode {
|
||||
case constant.RelayModeChatCompletions:
|
||||
if hasSpecialPlan && specialPlan.OpenAIBaseURL != "" {
|
||||
return fmt.Sprintf("%s/chat/completions", specialPlan.OpenAIBaseURL), nil
|
||||
}
|
||||
if strings.HasPrefix(info.UpstreamModelName, "bot") {
|
||||
return fmt.Sprintf("%s/api/v3/bots/chat/completions", baseUrl), nil
|
||||
}
|
||||
return fmt.Sprintf("%s/api/v3/chat/completions", baseUrl), nil
|
||||
case constant.RelayModeEmbeddings:
|
||||
return fmt.Sprintf("%s/api/v3/embeddings", baseUrl), nil
|
||||
//豆包的图生图也走generations接口: https://www.volcengine.com/docs/82379/1824121
|
||||
case constant.RelayModeImagesGenerations, constant.RelayModeImagesEdits:
|
||||
return fmt.Sprintf("%s/api/v3/images/generations", baseUrl), nil
|
||||
//case constant.RelayModeImagesEdits:
|
||||
// return fmt.Sprintf("%s/api/v3/images/edits", baseUrl), nil
|
||||
case constant.RelayModeRerank:
|
||||
return fmt.Sprintf("%s/api/v3/rerank", baseUrl), nil
|
||||
case constant.RelayModeResponses:
|
||||
return fmt.Sprintf("%s/api/v3/responses", baseUrl), nil
|
||||
case constant.RelayModeAudioSpeech:
|
||||
if baseUrl == channelconstant.ChannelBaseURLs[channelconstant.ChannelTypeVolcEngine] {
|
||||
return "wss://openspeech.bytedance.com/api/v1/tts/ws_binary", nil
|
||||
}
|
||||
return fmt.Sprintf("%s/v1/audio/speech", baseUrl), nil
|
||||
default:
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("unsupported relay mode: %d", info.RelayMode)
|
||||
}
|
||||
|
||||
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
|
||||
channel.SetupApiRequestHeader(info, c, req)
|
||||
|
||||
if info.RelayMode == constant.RelayModeAudioSpeech {
|
||||
parts := strings.Split(info.ApiKey, "|")
|
||||
if len(parts) == 2 {
|
||||
req.Set("Authorization", "Bearer;"+parts[1])
|
||||
}
|
||||
req.Set("Content-Type", "application/json")
|
||||
return nil
|
||||
} else if info.RelayMode == constant.RelayModeImagesEdits {
|
||||
req.Set("Content-Type", gin.MIMEJSON)
|
||||
}
|
||||
|
||||
req.Set("Authorization", "Bearer "+info.ApiKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
|
||||
if request == nil {
|
||||
return nil, errors.New("request is nil")
|
||||
}
|
||||
|
||||
if !model_setting.ShouldPreserveThinkingSuffix(info.OriginModelName) &&
|
||||
strings.HasSuffix(info.UpstreamModelName, "-thinking") &&
|
||||
strings.HasPrefix(info.UpstreamModelName, "deepseek") {
|
||||
info.UpstreamModelName = strings.TrimSuffix(info.UpstreamModelName, "-thinking")
|
||||
request.Model = info.UpstreamModelName
|
||||
request.THINKING = json.RawMessage(`{"type": "enabled"}`)
|
||||
}
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) {
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
|
||||
if info.RelayMode == constant.RelayModeAudioSpeech {
|
||||
baseUrl := info.ChannelBaseUrl
|
||||
if baseUrl == "" {
|
||||
baseUrl = channelconstant.ChannelBaseURLs[channelconstant.ChannelTypeVolcEngine]
|
||||
}
|
||||
|
||||
if baseUrl == channelconstant.ChannelBaseURLs[channelconstant.ChannelTypeVolcEngine] {
|
||||
if info.IsStream {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return channel.DoApiRequest(a, c, info, requestBody)
|
||||
}
|
||||
|
||||
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
|
||||
if info.RelayFormat == types.RelayFormatClaude {
|
||||
if _, ok := channelconstant.ChannelSpecialBases[info.ChannelBaseUrl]; ok {
|
||||
adaptor := claude.Adaptor{}
|
||||
return adaptor.DoResponse(c, resp, info)
|
||||
}
|
||||
}
|
||||
|
||||
if info.RelayMode == constant.RelayModeAudioSpeech {
|
||||
encoding := mapEncoding(c.GetString(contextKeyResponseFormat))
|
||||
if info.IsStream {
|
||||
volcRequestInterface, exists := c.Get(contextKeyTTSRequest)
|
||||
if !exists {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
errors.New("volcengine TTS request not found in context"),
|
||||
types.ErrorCodeBadRequestBody,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
volcRequest, ok := volcRequestInterface.(VolcengineTTSRequest)
|
||||
if !ok {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
errors.New("invalid volcengine TTS request type"),
|
||||
types.ErrorCodeBadRequestBody,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
// Get the WebSocket URL
|
||||
requestURL, urlErr := a.GetRequestURL(info)
|
||||
if urlErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
urlErr,
|
||||
types.ErrorCodeBadRequestBody,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
return handleTTSWebSocketResponse(c, requestURL, volcRequest, info, encoding)
|
||||
}
|
||||
return handleTTSResponse(c, resp, info, encoding)
|
||||
}
|
||||
|
||||
adaptor := openai.Adaptor{}
|
||||
usage, err = adaptor.DoResponse(c, resp, info)
|
||||
return
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetModelList() []string {
|
||||
return ModelList
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetChannelName() string {
|
||||
return ChannelName
|
||||
}
|
||||
19
relay/channel/volcengine/constants.go
Normal file
19
relay/channel/volcengine/constants.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package volcengine
|
||||
|
||||
var ModelList = []string{
|
||||
"Doubao-pro-128k",
|
||||
"Doubao-pro-32k",
|
||||
"Doubao-pro-4k",
|
||||
"Doubao-lite-128k",
|
||||
"Doubao-lite-32k",
|
||||
"Doubao-lite-4k",
|
||||
"Doubao-embedding",
|
||||
"doubao-seedream-4-0-250828",
|
||||
"seedream-4-0-250828",
|
||||
"doubao-seedance-1-0-pro-250528",
|
||||
"seedance-1-0-pro-250528",
|
||||
"doubao-seed-1-6-thinking-250715",
|
||||
"seed-1-6-thinking-250715",
|
||||
}
|
||||
|
||||
var ChannelName = "volcengine"
|
||||
533
relay/channel/volcengine/protocols.go
Normal file
533
relay/channel/volcengine/protocols.go
Normal file
@@ -0,0 +1,533 @@
|
||||
package volcengine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type (
|
||||
EventType int32
|
||||
MsgType uint8
|
||||
MsgTypeFlagBits uint8
|
||||
VersionBits uint8
|
||||
HeaderSizeBits uint8
|
||||
SerializationBits uint8
|
||||
CompressionBits uint8
|
||||
)
|
||||
|
||||
const (
|
||||
MsgTypeFlagNoSeq MsgTypeFlagBits = 0
|
||||
MsgTypeFlagPositiveSeq MsgTypeFlagBits = 0b1
|
||||
MsgTypeFlagNegativeSeq MsgTypeFlagBits = 0b11
|
||||
MsgTypeFlagWithEvent MsgTypeFlagBits = 0b100
|
||||
)
|
||||
|
||||
const (
|
||||
Version1 VersionBits = iota + 1
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderSize4 HeaderSizeBits = iota + 1
|
||||
)
|
||||
|
||||
const (
|
||||
SerializationJSON SerializationBits = 0b1
|
||||
)
|
||||
|
||||
const (
|
||||
CompressionNone CompressionBits = 0
|
||||
)
|
||||
|
||||
const (
|
||||
MsgTypeFullClientRequest MsgType = 0b1
|
||||
MsgTypeAudioOnlyClient MsgType = 0b10
|
||||
MsgTypeFullServerResponse MsgType = 0b1001
|
||||
MsgTypeAudioOnlyServer MsgType = 0b1011
|
||||
MsgTypeFrontEndResultServer MsgType = 0b1100
|
||||
MsgTypeError MsgType = 0b1111
|
||||
)
|
||||
|
||||
func (t MsgType) String() string {
|
||||
switch t {
|
||||
case MsgTypeFullClientRequest:
|
||||
return "MsgType_FullClientRequest"
|
||||
case MsgTypeAudioOnlyClient:
|
||||
return "MsgType_AudioOnlyClient"
|
||||
case MsgTypeFullServerResponse:
|
||||
return "MsgType_FullServerResponse"
|
||||
case MsgTypeAudioOnlyServer:
|
||||
return "MsgType_AudioOnlyServer"
|
||||
case MsgTypeError:
|
||||
return "MsgType_Error"
|
||||
case MsgTypeFrontEndResultServer:
|
||||
return "MsgType_FrontEndResultServer"
|
||||
default:
|
||||
return fmt.Sprintf("MsgType_(%d)", t)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
EventType_None EventType = 0
|
||||
|
||||
EventType_StartConnection EventType = 1
|
||||
EventType_FinishConnection EventType = 2
|
||||
|
||||
EventType_ConnectionStarted EventType = 50
|
||||
EventType_ConnectionFailed EventType = 51
|
||||
EventType_ConnectionFinished EventType = 52
|
||||
|
||||
EventType_StartSession EventType = 100
|
||||
EventType_CancelSession EventType = 101
|
||||
EventType_FinishSession EventType = 102
|
||||
|
||||
EventType_SessionStarted EventType = 150
|
||||
EventType_SessionCanceled EventType = 151
|
||||
EventType_SessionFinished EventType = 152
|
||||
EventType_SessionFailed EventType = 153
|
||||
|
||||
EventType_UsageResponse EventType = 154
|
||||
|
||||
EventType_TaskRequest EventType = 200
|
||||
EventType_UpdateConfig EventType = 201
|
||||
|
||||
EventType_AudioMuted EventType = 250
|
||||
|
||||
EventType_SayHello EventType = 300
|
||||
|
||||
EventType_TTSSentenceStart EventType = 350
|
||||
EventType_TTSSentenceEnd EventType = 351
|
||||
EventType_TTSResponse EventType = 352
|
||||
EventType_TTSEnded EventType = 359
|
||||
EventType_PodcastRoundStart EventType = 360
|
||||
EventType_PodcastRoundResponse EventType = 361
|
||||
EventType_PodcastRoundEnd EventType = 362
|
||||
|
||||
EventType_ASRInfo EventType = 450
|
||||
EventType_ASRResponse EventType = 451
|
||||
EventType_ASREnded EventType = 459
|
||||
|
||||
EventType_ChatTTSText EventType = 500
|
||||
|
||||
EventType_ChatResponse EventType = 550
|
||||
EventType_ChatEnded EventType = 559
|
||||
|
||||
EventType_SourceSubtitleStart EventType = 650
|
||||
EventType_SourceSubtitleResponse EventType = 651
|
||||
EventType_SourceSubtitleEnd EventType = 652
|
||||
|
||||
EventType_TranslationSubtitleStart EventType = 653
|
||||
EventType_TranslationSubtitleResponse EventType = 654
|
||||
EventType_TranslationSubtitleEnd EventType = 655
|
||||
)
|
||||
|
||||
func (t EventType) String() string {
|
||||
switch t {
|
||||
case EventType_None:
|
||||
return "EventType_None"
|
||||
case EventType_StartConnection:
|
||||
return "EventType_StartConnection"
|
||||
case EventType_FinishConnection:
|
||||
return "EventType_FinishConnection"
|
||||
case EventType_ConnectionStarted:
|
||||
return "EventType_ConnectionStarted"
|
||||
case EventType_ConnectionFailed:
|
||||
return "EventType_ConnectionFailed"
|
||||
case EventType_ConnectionFinished:
|
||||
return "EventType_ConnectionFinished"
|
||||
case EventType_StartSession:
|
||||
return "EventType_StartSession"
|
||||
case EventType_CancelSession:
|
||||
return "EventType_CancelSession"
|
||||
case EventType_FinishSession:
|
||||
return "EventType_FinishSession"
|
||||
case EventType_SessionStarted:
|
||||
return "EventType_SessionStarted"
|
||||
case EventType_SessionCanceled:
|
||||
return "EventType_SessionCanceled"
|
||||
case EventType_SessionFinished:
|
||||
return "EventType_SessionFinished"
|
||||
case EventType_SessionFailed:
|
||||
return "EventType_SessionFailed"
|
||||
case EventType_UsageResponse:
|
||||
return "EventType_UsageResponse"
|
||||
case EventType_TaskRequest:
|
||||
return "EventType_TaskRequest"
|
||||
case EventType_UpdateConfig:
|
||||
return "EventType_UpdateConfig"
|
||||
case EventType_AudioMuted:
|
||||
return "EventType_AudioMuted"
|
||||
case EventType_SayHello:
|
||||
return "EventType_SayHello"
|
||||
case EventType_TTSSentenceStart:
|
||||
return "EventType_TTSSentenceStart"
|
||||
case EventType_TTSSentenceEnd:
|
||||
return "EventType_TTSSentenceEnd"
|
||||
case EventType_TTSResponse:
|
||||
return "EventType_TTSResponse"
|
||||
case EventType_TTSEnded:
|
||||
return "EventType_TTSEnded"
|
||||
case EventType_PodcastRoundStart:
|
||||
return "EventType_PodcastRoundStart"
|
||||
case EventType_PodcastRoundResponse:
|
||||
return "EventType_PodcastRoundResponse"
|
||||
case EventType_PodcastRoundEnd:
|
||||
return "EventType_PodcastRoundEnd"
|
||||
case EventType_ASRInfo:
|
||||
return "EventType_ASRInfo"
|
||||
case EventType_ASRResponse:
|
||||
return "EventType_ASRResponse"
|
||||
case EventType_ASREnded:
|
||||
return "EventType_ASREnded"
|
||||
case EventType_ChatTTSText:
|
||||
return "EventType_ChatTTSText"
|
||||
case EventType_ChatResponse:
|
||||
return "EventType_ChatResponse"
|
||||
case EventType_ChatEnded:
|
||||
return "EventType_ChatEnded"
|
||||
case EventType_SourceSubtitleStart:
|
||||
return "EventType_SourceSubtitleStart"
|
||||
case EventType_SourceSubtitleResponse:
|
||||
return "EventType_SourceSubtitleResponse"
|
||||
case EventType_SourceSubtitleEnd:
|
||||
return "EventType_SourceSubtitleEnd"
|
||||
case EventType_TranslationSubtitleStart:
|
||||
return "EventType_TranslationSubtitleStart"
|
||||
case EventType_TranslationSubtitleResponse:
|
||||
return "EventType_TranslationSubtitleResponse"
|
||||
case EventType_TranslationSubtitleEnd:
|
||||
return "EventType_TranslationSubtitleEnd"
|
||||
default:
|
||||
return fmt.Sprintf("EventType_(%d)", t)
|
||||
}
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Version VersionBits
|
||||
HeaderSize HeaderSizeBits
|
||||
MsgType MsgType
|
||||
MsgTypeFlag MsgTypeFlagBits
|
||||
Serialization SerializationBits
|
||||
Compression CompressionBits
|
||||
|
||||
EventType EventType
|
||||
SessionID string
|
||||
ConnectID string
|
||||
Sequence int32
|
||||
ErrorCode uint32
|
||||
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func NewMessageFromBytes(data []byte) (*Message, error) {
|
||||
if len(data) < 3 {
|
||||
return nil, fmt.Errorf("data too short: expected at least 3 bytes, got %d", len(data))
|
||||
}
|
||||
|
||||
typeAndFlag := data[1]
|
||||
|
||||
msg, err := NewMessage(MsgType(typeAndFlag>>4), MsgTypeFlagBits(typeAndFlag&0b00001111))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := msg.Unmarshal(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func NewMessage(msgType MsgType, flag MsgTypeFlagBits) (*Message, error) {
|
||||
return &Message{
|
||||
MsgType: msgType,
|
||||
MsgTypeFlag: flag,
|
||||
Version: Version1,
|
||||
HeaderSize: HeaderSize4,
|
||||
Serialization: SerializationJSON,
|
||||
Compression: CompressionNone,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *Message) String() string {
|
||||
switch m.MsgType {
|
||||
case MsgTypeAudioOnlyServer, MsgTypeAudioOnlyClient:
|
||||
if m.MsgTypeFlag == MsgTypeFlagPositiveSeq || m.MsgTypeFlag == MsgTypeFlagNegativeSeq {
|
||||
return fmt.Sprintf("%s, %s, Sequence: %d, PayloadSize: %d", m.MsgType, m.EventType, m.Sequence, len(m.Payload))
|
||||
}
|
||||
return fmt.Sprintf("%s, %s, PayloadSize: %d", m.MsgType, m.EventType, len(m.Payload))
|
||||
case MsgTypeError:
|
||||
return fmt.Sprintf("%s, %s, ErrorCode: %d, Payload: %s", m.MsgType, m.EventType, m.ErrorCode, string(m.Payload))
|
||||
default:
|
||||
if m.MsgTypeFlag == MsgTypeFlagPositiveSeq || m.MsgTypeFlag == MsgTypeFlagNegativeSeq {
|
||||
return fmt.Sprintf("%s, %s, Sequence: %d, Payload: %s",
|
||||
m.MsgType, m.EventType, m.Sequence, string(m.Payload))
|
||||
}
|
||||
return fmt.Sprintf("%s, %s, Payload: %s", m.MsgType, m.EventType, string(m.Payload))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) Marshal() ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
header := []uint8{
|
||||
uint8(m.Version)<<4 | uint8(m.HeaderSize),
|
||||
uint8(m.MsgType)<<4 | uint8(m.MsgTypeFlag),
|
||||
uint8(m.Serialization)<<4 | uint8(m.Compression),
|
||||
}
|
||||
|
||||
headerSize := 4 * int(m.HeaderSize)
|
||||
if padding := headerSize - len(header); padding > 0 {
|
||||
header = append(header, make([]uint8, padding)...)
|
||||
}
|
||||
|
||||
if err := binary.Write(buf, binary.BigEndian, header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
writers, err := m.writers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, write := range writers {
|
||||
if err := write(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (m *Message) Unmarshal(data []byte) error {
|
||||
buf := bytes.NewBuffer(data)
|
||||
|
||||
versionAndHeaderSize, err := buf.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Version = VersionBits(versionAndHeaderSize >> 4)
|
||||
m.HeaderSize = HeaderSizeBits(versionAndHeaderSize & 0b00001111)
|
||||
|
||||
_, err = buf.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serializationCompression, err := buf.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Serialization = SerializationBits(serializationCompression & 0b11110000)
|
||||
m.Compression = CompressionBits(serializationCompression & 0b00001111)
|
||||
|
||||
headerSize := 4 * int(m.HeaderSize)
|
||||
readSize := 3
|
||||
if paddingSize := headerSize - readSize; paddingSize > 0 {
|
||||
if n, err := buf.Read(make([]byte, paddingSize)); err != nil || n < paddingSize {
|
||||
return fmt.Errorf("insufficient header bytes: expected %d, got %d", paddingSize, n)
|
||||
}
|
||||
}
|
||||
|
||||
readers, err := m.readers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, read := range readers {
|
||||
if err := read(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := buf.ReadByte(); err != io.EOF {
|
||||
return fmt.Errorf("unexpected data after message: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Message) writers() (writers []func(*bytes.Buffer) error, _ error) {
|
||||
if m.MsgTypeFlag == MsgTypeFlagWithEvent {
|
||||
writers = append(writers, m.writeEvent, m.writeSessionID)
|
||||
}
|
||||
|
||||
switch m.MsgType {
|
||||
case MsgTypeFullClientRequest, MsgTypeFullServerResponse, MsgTypeFrontEndResultServer, MsgTypeAudioOnlyClient, MsgTypeAudioOnlyServer:
|
||||
if m.MsgTypeFlag == MsgTypeFlagPositiveSeq || m.MsgTypeFlag == MsgTypeFlagNegativeSeq {
|
||||
writers = append(writers, m.writeSequence)
|
||||
}
|
||||
case MsgTypeError:
|
||||
writers = append(writers, m.writeErrorCode)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported message type: %d", m.MsgType)
|
||||
}
|
||||
|
||||
writers = append(writers, m.writePayload)
|
||||
return writers, nil
|
||||
}
|
||||
|
||||
func (m *Message) writeEvent(buf *bytes.Buffer) error {
|
||||
return binary.Write(buf, binary.BigEndian, m.EventType)
|
||||
}
|
||||
|
||||
func (m *Message) writeSessionID(buf *bytes.Buffer) error {
|
||||
switch m.EventType {
|
||||
case EventType_StartConnection, EventType_FinishConnection,
|
||||
EventType_ConnectionStarted, EventType_ConnectionFailed:
|
||||
return nil
|
||||
}
|
||||
|
||||
size := len(m.SessionID)
|
||||
if int64(size) > math.MaxUint32 {
|
||||
return fmt.Errorf("session ID size (%d) exceeds max(uint32)", size)
|
||||
}
|
||||
|
||||
if err := binary.Write(buf, binary.BigEndian, uint32(size)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf.WriteString(m.SessionID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Message) writeSequence(buf *bytes.Buffer) error {
|
||||
return binary.Write(buf, binary.BigEndian, m.Sequence)
|
||||
}
|
||||
|
||||
func (m *Message) writeErrorCode(buf *bytes.Buffer) error {
|
||||
return binary.Write(buf, binary.BigEndian, m.ErrorCode)
|
||||
}
|
||||
|
||||
func (m *Message) writePayload(buf *bytes.Buffer) error {
|
||||
size := len(m.Payload)
|
||||
if int64(size) > math.MaxUint32 {
|
||||
return fmt.Errorf("payload size (%d) exceeds max(uint32)", size)
|
||||
}
|
||||
|
||||
if err := binary.Write(buf, binary.BigEndian, uint32(size)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf.Write(m.Payload)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Message) readers() (readers []func(*bytes.Buffer) error, _ error) {
|
||||
switch m.MsgType {
|
||||
case MsgTypeFullClientRequest, MsgTypeFullServerResponse, MsgTypeFrontEndResultServer, MsgTypeAudioOnlyClient, MsgTypeAudioOnlyServer:
|
||||
if m.MsgTypeFlag == MsgTypeFlagPositiveSeq || m.MsgTypeFlag == MsgTypeFlagNegativeSeq {
|
||||
readers = append(readers, m.readSequence)
|
||||
}
|
||||
case MsgTypeError:
|
||||
readers = append(readers, m.readErrorCode)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported message type: %d", m.MsgType)
|
||||
}
|
||||
|
||||
if m.MsgTypeFlag == MsgTypeFlagWithEvent {
|
||||
readers = append(readers, m.readEvent, m.readSessionID, m.readConnectID)
|
||||
}
|
||||
|
||||
readers = append(readers, m.readPayload)
|
||||
return readers, nil
|
||||
}
|
||||
|
||||
func (m *Message) readEvent(buf *bytes.Buffer) error {
|
||||
return binary.Read(buf, binary.BigEndian, &m.EventType)
|
||||
}
|
||||
|
||||
func (m *Message) readSessionID(buf *bytes.Buffer) error {
|
||||
switch m.EventType {
|
||||
case EventType_StartConnection, EventType_FinishConnection,
|
||||
EventType_ConnectionStarted, EventType_ConnectionFailed,
|
||||
EventType_ConnectionFinished:
|
||||
return nil
|
||||
}
|
||||
|
||||
var size uint32
|
||||
if err := binary.Read(buf, binary.BigEndian, &size); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if size > 0 {
|
||||
m.SessionID = string(buf.Next(int(size)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Message) readConnectID(buf *bytes.Buffer) error {
|
||||
switch m.EventType {
|
||||
case EventType_ConnectionStarted, EventType_ConnectionFailed,
|
||||
EventType_ConnectionFinished:
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
var size uint32
|
||||
if err := binary.Read(buf, binary.BigEndian, &size); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if size > 0 {
|
||||
m.ConnectID = string(buf.Next(int(size)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Message) readSequence(buf *bytes.Buffer) error {
|
||||
return binary.Read(buf, binary.BigEndian, &m.Sequence)
|
||||
}
|
||||
|
||||
func (m *Message) readErrorCode(buf *bytes.Buffer) error {
|
||||
return binary.Read(buf, binary.BigEndian, &m.ErrorCode)
|
||||
}
|
||||
|
||||
func (m *Message) readPayload(buf *bytes.Buffer) error {
|
||||
var size uint32
|
||||
if err := binary.Read(buf, binary.BigEndian, &size); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if size > 0 {
|
||||
m.Payload = buf.Next(int(size))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReceiveMessage(conn *websocket.Conn) (*Message, error) {
|
||||
mt, frame, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mt != websocket.BinaryMessage && mt != websocket.TextMessage {
|
||||
return nil, fmt.Errorf("unexpected Websocket message type: %d", mt)
|
||||
}
|
||||
msg, err := NewMessageFromBytes(frame)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func FullClientRequest(conn *websocket.Conn, payload []byte) error {
|
||||
msg, err := NewMessage(MsgTypeFullClientRequest, MsgTypeFlagNoSeq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg.Payload = payload
|
||||
frame, err := msg.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return conn.WriteMessage(websocket.BinaryMessage, frame)
|
||||
}
|
||||
305
relay/channel/volcengine/tts.go
Normal file
305
relay/channel/volcengine/tts.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package volcengine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type VolcengineTTSRequest struct {
|
||||
App VolcengineTTSApp `json:"app"`
|
||||
User VolcengineTTSUser `json:"user"`
|
||||
Audio VolcengineTTSAudio `json:"audio"`
|
||||
Request VolcengineTTSReqInfo `json:"request"`
|
||||
}
|
||||
|
||||
type VolcengineTTSApp struct {
|
||||
AppID string `json:"appid"`
|
||||
Token string `json:"token"`
|
||||
Cluster string `json:"cluster"`
|
||||
}
|
||||
|
||||
type VolcengineTTSUser struct {
|
||||
UID string `json:"uid"`
|
||||
}
|
||||
|
||||
type VolcengineTTSAudio struct {
|
||||
VoiceType string `json:"voice_type"`
|
||||
Encoding string `json:"encoding"`
|
||||
SpeedRatio float64 `json:"speed_ratio"`
|
||||
Rate int `json:"rate"`
|
||||
Bitrate int `json:"bitrate,omitempty"`
|
||||
LoudnessRatio float64 `json:"loudness_ratio,omitempty"`
|
||||
EnableEmotion bool `json:"enable_emotion,omitempty"`
|
||||
Emotion string `json:"emotion,omitempty"`
|
||||
EmotionScale float64 `json:"emotion_scale,omitempty"`
|
||||
ExplicitLanguage string `json:"explicit_language,omitempty"`
|
||||
ContextLanguage string `json:"context_language,omitempty"`
|
||||
}
|
||||
|
||||
type VolcengineTTSReqInfo struct {
|
||||
ReqID string `json:"reqid"`
|
||||
Text string `json:"text"`
|
||||
Operation string `json:"operation"`
|
||||
Model string `json:"model,omitempty"`
|
||||
TextType string `json:"text_type,omitempty"`
|
||||
SilenceDuration float64 `json:"silence_duration,omitempty"`
|
||||
WithTimestamp interface{} `json:"with_timestamp,omitempty"`
|
||||
ExtraParam *VolcengineTTSExtraParam `json:"extra_param,omitempty"`
|
||||
}
|
||||
|
||||
type VolcengineTTSExtraParam struct {
|
||||
DisableMarkdownFilter bool `json:"disable_markdown_filter,omitempty"`
|
||||
EnableLatexTn bool `json:"enable_latex_tn,omitempty"`
|
||||
MuteCutThreshold string `json:"mute_cut_threshold,omitempty"`
|
||||
MuteCutRemainMs string `json:"mute_cut_remain_ms,omitempty"`
|
||||
DisableEmojiFilter bool `json:"disable_emoji_filter,omitempty"`
|
||||
UnsupportedCharRatioThresh float64 `json:"unsupported_char_ratio_thresh,omitempty"`
|
||||
AigcWatermark bool `json:"aigc_watermark,omitempty"`
|
||||
CacheConfig *VolcengineTTSCacheConfig `json:"cache_config,omitempty"`
|
||||
}
|
||||
|
||||
type VolcengineTTSCacheConfig struct {
|
||||
TextType int `json:"text_type,omitempty"`
|
||||
UseCache bool `json:"use_cache,omitempty"`
|
||||
}
|
||||
|
||||
type VolcengineTTSResponse struct {
|
||||
ReqID string `json:"reqid"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Sequence int `json:"sequence"`
|
||||
Data string `json:"data"`
|
||||
Addition *VolcengineTTSAdditionInfo `json:"addition,omitempty"`
|
||||
}
|
||||
|
||||
type VolcengineTTSAdditionInfo struct {
|
||||
Duration string `json:"duration"`
|
||||
}
|
||||
|
||||
var openAIToVolcengineVoiceMap = map[string]string{
|
||||
"alloy": "zh_male_M392_conversation_wvae_bigtts",
|
||||
"echo": "zh_male_wenhao_mars_bigtts",
|
||||
"fable": "zh_female_tianmei_mars_bigtts",
|
||||
"onyx": "zh_male_zhibei_mars_bigtts",
|
||||
"nova": "zh_female_shuangkuaisisi_mars_bigtts",
|
||||
"shimmer": "zh_female_cancan_mars_bigtts",
|
||||
}
|
||||
|
||||
var responseFormatToEncodingMap = map[string]string{
|
||||
"mp3": "mp3",
|
||||
"opus": "ogg_opus",
|
||||
"aac": "mp3",
|
||||
"flac": "mp3",
|
||||
"wav": "wav",
|
||||
"pcm": "pcm",
|
||||
}
|
||||
|
||||
func parseVolcengineAuth(apiKey string) (appID, token string, err error) {
|
||||
parts := strings.Split(apiKey, "|")
|
||||
if len(parts) != 2 {
|
||||
return "", "", errors.New("invalid api key format, expected: appid|access_token")
|
||||
}
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
|
||||
func mapVoiceType(openAIVoice string) string {
|
||||
if voice, ok := openAIToVolcengineVoiceMap[openAIVoice]; ok {
|
||||
return voice
|
||||
}
|
||||
return openAIVoice
|
||||
}
|
||||
|
||||
func mapEncoding(responseFormat string) string {
|
||||
if encoding, ok := responseFormatToEncodingMap[responseFormat]; ok {
|
||||
return encoding
|
||||
}
|
||||
return "mp3"
|
||||
}
|
||||
|
||||
func getContentTypeByEncoding(encoding string) string {
|
||||
contentTypeMap := map[string]string{
|
||||
"mp3": "audio/mpeg",
|
||||
"ogg_opus": "audio/ogg",
|
||||
"wav": "audio/wav",
|
||||
"pcm": "audio/pcm",
|
||||
}
|
||||
if ct, ok := contentTypeMap[encoding]; ok {
|
||||
return ct
|
||||
}
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
func handleTTSResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, encoding string) (usage any, err *types.NewAPIError) {
|
||||
body, readErr := io.ReadAll(resp.Body)
|
||||
if readErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
errors.New("failed to read volcengine response"),
|
||||
types.ErrorCodeReadResponseBodyFailed,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var volcResp VolcengineTTSResponse
|
||||
if unmarshalErr := json.Unmarshal(body, &volcResp); unmarshalErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
errors.New("failed to parse volcengine response"),
|
||||
types.ErrorCodeBadResponseBody,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
if volcResp.Code != 3000 {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
errors.New(volcResp.Message),
|
||||
types.ErrorCodeBadResponse,
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
}
|
||||
|
||||
audioData, decodeErr := base64.StdEncoding.DecodeString(volcResp.Data)
|
||||
if decodeErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
errors.New("failed to decode audio data"),
|
||||
types.ErrorCodeBadResponseBody,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
contentType := getContentTypeByEncoding(encoding)
|
||||
c.Header("Content-Type", contentType)
|
||||
c.Data(http.StatusOK, contentType, audioData)
|
||||
|
||||
usage = &dto.Usage{
|
||||
PromptTokens: info.GetEstimatePromptTokens(),
|
||||
CompletionTokens: 0,
|
||||
TotalTokens: info.GetEstimatePromptTokens(),
|
||||
}
|
||||
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
func generateRequestID() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
func handleTTSWebSocketResponse(c *gin.Context, requestURL string, volcRequest VolcengineTTSRequest, info *relaycommon.RelayInfo, encoding string) (usage any, err *types.NewAPIError) {
|
||||
_, token, parseErr := parseVolcengineAuth(info.ApiKey)
|
||||
if parseErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
parseErr,
|
||||
types.ErrorCodeChannelInvalidKey,
|
||||
http.StatusUnauthorized,
|
||||
)
|
||||
}
|
||||
|
||||
header := http.Header{}
|
||||
header.Set("Authorization", fmt.Sprintf("Bearer;%s", token))
|
||||
|
||||
conn, resp, dialErr := websocket.DefaultDialer.DialContext(context.Background(), requestURL, header)
|
||||
if dialErr != nil {
|
||||
if resp != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("failed to connect to websocket: %w, status: %d", dialErr, resp.StatusCode),
|
||||
types.ErrorCodeBadResponseStatusCode,
|
||||
http.StatusBadGateway,
|
||||
)
|
||||
}
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("failed to connect to websocket: %w", dialErr),
|
||||
types.ErrorCodeBadResponseStatusCode,
|
||||
http.StatusBadGateway,
|
||||
)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
payload, marshalErr := json.Marshal(volcRequest)
|
||||
if marshalErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("failed to marshal request: %w", marshalErr),
|
||||
types.ErrorCodeBadRequestBody,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
if sendErr := FullClientRequest(conn, payload); sendErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("failed to send request: %w", sendErr),
|
||||
types.ErrorCodeBadRequestBody,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
contentType := getContentTypeByEncoding(encoding)
|
||||
c.Header("Content-Type", contentType)
|
||||
c.Header("Transfer-Encoding", "chunked")
|
||||
|
||||
for {
|
||||
msg, recvErr := ReceiveMessage(conn)
|
||||
if recvErr != nil {
|
||||
if websocket.IsCloseError(recvErr, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
|
||||
break
|
||||
}
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("failed to receive message: %w", recvErr),
|
||||
types.ErrorCodeBadResponse,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
switch msg.MsgType {
|
||||
case MsgTypeError:
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("received error from server: code=%d, %s", msg.ErrorCode, string(msg.Payload)),
|
||||
types.ErrorCodeBadResponse,
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
case MsgTypeFrontEndResultServer:
|
||||
continue
|
||||
case MsgTypeAudioOnlyServer:
|
||||
if len(msg.Payload) > 0 {
|
||||
if _, writeErr := c.Writer.Write(msg.Payload); writeErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("failed to write audio data: %w", writeErr),
|
||||
types.ErrorCodeBadResponse,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
c.Writer.Flush()
|
||||
}
|
||||
|
||||
if msg.Sequence < 0 {
|
||||
c.Status(http.StatusOK)
|
||||
usage = &dto.Usage{
|
||||
PromptTokens: info.GetEstimatePromptTokens(),
|
||||
CompletionTokens: 0,
|
||||
TotalTokens: info.GetEstimatePromptTokens(),
|
||||
}
|
||||
return usage, nil
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
usage = &dto.Usage{
|
||||
PromptTokens: info.GetEstimatePromptTokens(),
|
||||
CompletionTokens: 0,
|
||||
TotalTokens: info.GetEstimatePromptTokens(),
|
||||
}
|
||||
return usage, nil
|
||||
}
|
||||
Reference in New Issue
Block a user