-
Notifications
You must be signed in to change notification settings - Fork 11.6k
增加MiniMax语音合成TTS支持 #2081
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
Merged
creamlike1024
merged 4 commits into
QuantumNous:main
from
feitianbubu:pr/add-miniMax-tts
Oct 20, 2025
Merged
增加MiniMax语音合成TTS支持 #2081
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| package minimax | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
|
|
||
| "github.com/QuantumNous/new-api/dto" | ||
| "github.com/QuantumNous/new-api/relay/channel" | ||
| "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/types" | ||
|
|
||
| "github.com/gin-gonic/gin" | ||
| ) | ||
|
|
||
| type Adaptor struct { | ||
| } | ||
|
|
||
| func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) { | ||
| return nil, errors.New("not implemented") | ||
| } | ||
|
|
||
| func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, req *dto.ClaudeRequest) (any, error) { | ||
| return nil, errors.New("not implemented") | ||
| } | ||
|
|
||
| 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") | ||
| } | ||
|
|
||
| voiceID := request.Voice | ||
| speed := request.Speed | ||
| outputFormat := request.ResponseFormat | ||
|
|
||
| minimaxRequest := MiniMaxTTSRequest{ | ||
| Model: info.OriginModelName, | ||
| Text: request.Input, | ||
| VoiceSetting: VoiceSetting{ | ||
| VoiceID: voiceID, | ||
| Speed: speed, | ||
| }, | ||
| AudioSetting: &AudioSetting{ | ||
| Format: outputFormat, | ||
| }, | ||
| OutputFormat: outputFormat, | ||
| } | ||
|
|
||
| // 同步扩展字段的厂商自定义metadata | ||
| if len(request.Metadata) > 0 { | ||
| if err := json.Unmarshal(request.Metadata, &minimaxRequest); err != nil { | ||
| return nil, fmt.Errorf("error unmarshalling metadata to minimax request: %w", err) | ||
| } | ||
| } | ||
|
|
||
| jsonData, err := json.Marshal(minimaxRequest) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("error marshalling minimax request: %w", err) | ||
| } | ||
| if outputFormat != "hex" { | ||
| outputFormat = "url" | ||
| } | ||
|
|
||
| c.Set("response_format", outputFormat) | ||
|
|
||
| // Debug: log the request structure | ||
| // fmt.Printf("MiniMax TTS Request: %s\n", string(jsonData)) | ||
|
|
||
| return bytes.NewReader(jsonData), nil | ||
| } | ||
|
|
||
| func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { | ||
| return request, nil | ||
| } | ||
|
|
||
| func (a *Adaptor) Init(info *relaycommon.RelayInfo) { | ||
| } | ||
|
|
||
| func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { | ||
| return GetRequestURL(info) | ||
| } | ||
|
|
||
| func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { | ||
| channel.SetupApiRequestHeader(info, c, req) | ||
| 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") | ||
| } | ||
| 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 nil, errors.New("not implemented") | ||
| } | ||
|
|
||
| func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { | ||
| 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.RelayMode == constant.RelayModeAudioSpeech { | ||
| return handleTTSResponse(c, resp, info) | ||
| } | ||
|
|
||
| adaptor := openai.Adaptor{} | ||
| return adaptor.DoResponse(c, resp, info) | ||
| } | ||
|
|
||
| func (a *Adaptor) GetModelList() []string { | ||
| return ModelList | ||
| } | ||
|
|
||
| func (a *Adaptor) GetChannelName() string { | ||
| return ChannelName | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,194 @@ | ||
| package minimax | ||
|
|
||
| import ( | ||
| "encoding/hex" | ||
| "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" | ||
| ) | ||
|
|
||
| type MiniMaxTTSRequest struct { | ||
| Model string `json:"model"` | ||
| Text string `json:"text"` | ||
| Stream bool `json:"stream,omitempty"` | ||
| StreamOptions *StreamOptions `json:"stream_options,omitempty"` | ||
| VoiceSetting VoiceSetting `json:"voice_setting"` | ||
| PronunciationDict *PronunciationDict `json:"pronunciation_dict,omitempty"` | ||
| AudioSetting *AudioSetting `json:"audio_setting,omitempty"` | ||
| TimbreWeights []TimbreWeight `json:"timbre_weights,omitempty"` | ||
| LanguageBoost string `json:"language_boost,omitempty"` | ||
| VoiceModify *VoiceModify `json:"voice_modify,omitempty"` | ||
| SubtitleEnable bool `json:"subtitle_enable,omitempty"` | ||
| OutputFormat string `json:"output_format,omitempty"` | ||
| AigcWatermark bool `json:"aigc_watermark,omitempty"` | ||
| } | ||
|
|
||
| type StreamOptions struct { | ||
| ExcludeAggregatedAudio bool `json:"exclude_aggregated_audio,omitempty"` | ||
| } | ||
|
|
||
| type VoiceSetting struct { | ||
| VoiceID string `json:"voice_id"` | ||
| Speed float64 `json:"speed,omitempty"` | ||
| Vol float64 `json:"vol,omitempty"` | ||
| Pitch int `json:"pitch,omitempty"` | ||
| Emotion string `json:"emotion,omitempty"` | ||
| TextNormalization bool `json:"text_normalization,omitempty"` | ||
| LatexRead bool `json:"latex_read,omitempty"` | ||
| } | ||
|
|
||
| type PronunciationDict struct { | ||
| Tone []string `json:"tone,omitempty"` | ||
| } | ||
|
|
||
| type AudioSetting struct { | ||
| SampleRate int `json:"sample_rate,omitempty"` | ||
| Bitrate int `json:"bitrate,omitempty"` | ||
| Format string `json:"format,omitempty"` | ||
| Channel int `json:"channel,omitempty"` | ||
| ForceCbr bool `json:"force_cbr,omitempty"` | ||
| } | ||
|
|
||
| type TimbreWeight struct { | ||
| VoiceID string `json:"voice_id"` | ||
| Weight int `json:"weight"` | ||
| } | ||
|
|
||
| type VoiceModify struct { | ||
| Pitch int `json:"pitch,omitempty"` | ||
| Intensity int `json:"intensity,omitempty"` | ||
| Timbre int `json:"timbre,omitempty"` | ||
| SoundEffects string `json:"sound_effects,omitempty"` | ||
| } | ||
|
|
||
| type MiniMaxTTSResponse struct { | ||
| Data MiniMaxTTSData `json:"data"` | ||
| ExtraInfo MiniMaxExtraInfo `json:"extra_info"` | ||
| TraceID string `json:"trace_id"` | ||
| BaseResp MiniMaxBaseResp `json:"base_resp"` | ||
| } | ||
|
|
||
| type MiniMaxTTSData struct { | ||
| Audio string `json:"audio"` | ||
| Status int `json:"status"` | ||
| } | ||
|
|
||
| type MiniMaxExtraInfo struct { | ||
| UsageCharacters int64 `json:"usage_characters"` | ||
| } | ||
|
|
||
| type MiniMaxBaseResp struct { | ||
| StatusCode int64 `json:"status_code"` | ||
| StatusMsg string `json:"status_msg"` | ||
| } | ||
|
|
||
| func getContentTypeByFormat(format string) string { | ||
| contentTypeMap := map[string]string{ | ||
| "mp3": "audio/mpeg", | ||
| "wav": "audio/wav", | ||
| "flac": "audio/flac", | ||
| "aac": "audio/aac", | ||
| "pcm": "audio/pcm", | ||
| } | ||
| if ct, ok := contentTypeMap[format]; ok { | ||
| return ct | ||
| } | ||
| return "audio/mpeg" // default to mp3 | ||
| } | ||
|
|
||
| func handleTTSResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) { | ||
| body, readErr := io.ReadAll(resp.Body) | ||
| if readErr != nil { | ||
| return nil, types.NewErrorWithStatusCode( | ||
| fmt.Errorf("failed to read minimax response: %w", readErr), | ||
| types.ErrorCodeReadResponseBodyFailed, | ||
| http.StatusInternalServerError, | ||
| ) | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| // Parse response | ||
| var minimaxResp MiniMaxTTSResponse | ||
| if unmarshalErr := json.Unmarshal(body, &minimaxResp); unmarshalErr != nil { | ||
| return nil, types.NewErrorWithStatusCode( | ||
| fmt.Errorf("failed to unmarshal minimax TTS response: %w", unmarshalErr), | ||
| types.ErrorCodeBadResponseBody, | ||
| http.StatusInternalServerError, | ||
| ) | ||
| } | ||
|
|
||
| // Check base_resp status code | ||
| if minimaxResp.BaseResp.StatusCode != 0 { | ||
| return nil, types.NewErrorWithStatusCode( | ||
| fmt.Errorf("minimax TTS error: %d - %s", minimaxResp.BaseResp.StatusCode, minimaxResp.BaseResp.StatusMsg), | ||
| types.ErrorCodeBadResponse, | ||
| http.StatusBadRequest, | ||
| ) | ||
| } | ||
|
|
||
| // Check if we have audio data | ||
| if minimaxResp.Data.Audio == "" { | ||
| return nil, types.NewErrorWithStatusCode( | ||
| fmt.Errorf("no audio data in minimax TTS response"), | ||
| types.ErrorCodeBadResponse, | ||
| http.StatusBadRequest, | ||
| ) | ||
| } | ||
|
|
||
| if strings.HasPrefix(minimaxResp.Data.Audio, "http") { | ||
| c.Redirect(http.StatusFound, minimaxResp.Data.Audio) | ||
| } else { | ||
| // Handle hex-encoded audio data | ||
| audioData, decodeErr := hex.DecodeString(minimaxResp.Data.Audio) | ||
| if decodeErr != nil { | ||
| return nil, types.NewErrorWithStatusCode( | ||
| fmt.Errorf("failed to decode hex audio data: %w", decodeErr), | ||
| types.ErrorCodeBadResponse, | ||
| http.StatusInternalServerError, | ||
| ) | ||
| } | ||
|
|
||
| // Determine content type - default to mp3 | ||
| contentType := "audio/mpeg" | ||
|
|
||
| c.Data(http.StatusOK, contentType, audioData) | ||
| } | ||
|
|
||
| usage = &dto.Usage{ | ||
| PromptTokens: info.PromptTokens, | ||
| CompletionTokens: 0, | ||
| TotalTokens: int(minimaxResp.ExtraInfo.UsageCharacters), | ||
| } | ||
|
|
||
| return usage, nil | ||
| } | ||
|
|
||
| func handleChatCompletionResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) { | ||
| body, readErr := io.ReadAll(resp.Body) | ||
| if readErr != nil { | ||
| return nil, types.NewErrorWithStatusCode( | ||
| errors.New("failed to read minimax response"), | ||
| types.ErrorCodeReadResponseBodyFailed, | ||
| http.StatusInternalServerError, | ||
| ) | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| // Set response headers | ||
| for key, values := range resp.Header { | ||
| for _, value := range values { | ||
| c.Header(key, value) | ||
| } | ||
| } | ||
|
|
||
| c.Data(resp.StatusCode, "application/json", body) | ||
| return nil, nil | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
This helper function is never used.
The
getContentTypeByFormatfunction is defined but never called. Line 160 inhandleTTSResponsehardcodescontentType = "audio/mpeg"instead of using this function to determine the correct content type based on the audio format.Apply this diff to use the helper function properly:
} else { // Handle hex-encoded audio data audioData, decodeErr := hex.DecodeString(minimaxResp.Data.Audio) if decodeErr != nil { return nil, types.NewErrorWithStatusCode( fmt.Errorf("failed to decode hex audio data: %w", decodeErr), types.ErrorCodeBadResponse, http.StatusInternalServerError, ) } - // Determine content type - default to mp3 - contentType := "audio/mpeg" + // Determine content type from response format + format := "mp3" // default + if val, exists := c.Get("audio_format"); exists { + if f, ok := val.(string); ok { + format = f + } + } + contentType := getContentTypeByFormat(format) c.Data(http.StatusOK, contentType, audioData) }Note: You'll also need to store the audio format in the Gin context (similar to how
response_formatis stored at adaptor.go line 69) so it's available when serving the response.🤖 Prompt for AI Agents