-
Notifications
You must be signed in to change notification settings - Fork 11.1k
新增支持豆包语音合成2.0功能 #2067
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
新增支持豆包语音合成2.0功能 #2067
Changes from all commits
fe68488
58c4da0
735386c
8965fc4
6a24c37
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,208 @@ | ||||||||||||||||||||||||||||||
| package volcengine | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||
| "encoding/base64" | ||||||||||||||||||||||||||||||
| "encoding/json" | ||||||||||||||||||||||||||||||
| "errors" | ||||||||||||||||||||||||||||||
| "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" | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+107
to
+113
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix inconsistent error message. Line 110's error message says Apply this diff to fix the error message: 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 "", "", errors.New("invalid api key format, expected: AppId|AccessToken")
}
return parts[0], parts[1], nil
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func mapVoiceType(openAIVoice string) string { | ||||||||||||||||||||||||||||||
| if voice, ok := openAIToVolcengineVoiceMap[openAIVoice]; ok { | ||||||||||||||||||||||||||||||
| return voice | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return openAIVoice | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // [0.1,2],默认为 1,通常保留一位小数即可 | ||||||||||||||||||||||||||||||
| func mapSpeedRatio(speed float64) float64 { | ||||||||||||||||||||||||||||||
| if speed == 0 { | ||||||||||||||||||||||||||||||
| return 1.0 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if speed < 0.1 { | ||||||||||||||||||||||||||||||
| return 0.1 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if speed > 2.0 { | ||||||||||||||||||||||||||||||
| return 2.0 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return speed | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| 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.PromptTokens, | ||||||||||||||||||||||||||||||
| CompletionTokens: 0, | ||||||||||||||||||||||||||||||
| TotalTokens: info.PromptTokens, | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return usage, nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func generateRequestID() string { | ||||||||||||||||||||||||||||||
| return uuid.New().String() | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Review format mapping for potential user expectation mismatch.
Lines 101-102 map both
"aac"and"flac"to"mp3". When users request these formats, they'll receive MP3 audio instead, which could cause issues if the client expects the requested format. Consider either:🌐 Web query:
💡 Result:
Yes. Volcengine's TTS supports AAC and FLAC as output formats (response_format can be mp3, opus, aac, flac, wav, pcm). [1][2]
Sources:
[1] developer.volcengine.com TTS article (response_format list).
[2] TTS API docs (response_format: mp3, opus, aac, flac, wav, pcm).
Fix incorrect format mappings: aac and flac should not map to mp3.
Volcengine's TTS API supports aac and flac as native output formats, but lines 101-102 incorrectly map both to "mp3". This causes users requesting these formats to receive MP3 audio instead, creating a format mismatch.
Correct these mappings:
"aac": "aac""flac": "flac"🤖 Prompt for AI Agents