From b2b348a396964ef9c469f314fb5254dbf4a41dd0 Mon Sep 17 00:00:00 2001 From: Octopus Date: Sat, 26 Sep 2026 03:05:09 +0800 Subject: [PATCH] Update MiniMax music generation handling (#1522) Co-authored-by: Octopus --- internal/audio/minimax/music.go | 50 ++++++++++++++++++++++++---- internal/audio/minimax/music_test.go | 40 +++++++++++++++++++++- 2 files changed, 82 insertions(+), 8 deletions(-) diff --git a/internal/audio/minimax/music.go b/internal/audio/minimax/music.go index 649d925d..576fbda7 100644 --- a/internal/audio/minimax/music.go +++ b/internal/audio/minimax/music.go @@ -3,6 +3,7 @@ package minimax import ( "bytes" "context" + "encoding/hex" "encoding/json" "fmt" "io" @@ -18,8 +19,8 @@ const maxMusicDownloadBytes = 200 * 1024 * 1024 // MusicConfig configures the MiniMax music provider. type MusicConfig struct { APIKey string - APIBase string // default "https://api.minimaxi.chat/v1" - Model string // default "music-2.5+" + APIBase string // default "https://api.minimax.io/v1"; use api.minimaxi.com for China + Model string // default "music-3.0" } // MusicProvider generates music via the MiniMax music generation API. @@ -34,10 +35,10 @@ type MusicProvider struct { // NewMusicProvider returns a MiniMax music provider. func NewMusicProvider(cfg MusicConfig) *MusicProvider { if cfg.APIBase == "" { - cfg.APIBase = "https://api.minimaxi.chat/v1" + cfg.APIBase = "https://api.minimax.io/v1" } if cfg.Model == "" { - cfg.Model = "music-2.5+" + cfg.Model = "music-3.0" } return &MusicProvider{ apiKey: cfg.APIKey, @@ -109,8 +110,9 @@ func (p *MusicProvider) GenerateMusic(ctx context.Context, opts audio.MusicOptio var mmResp struct { Data *struct { - Audio string `json:"audio"` - Music string `json:"music"` + Audio string `json:"audio"` + Music string `json:"music"` + Status int `json:"status"` } `json:"data"` BaseResp *struct { StatusCode int `json:"status_code"` @@ -128,13 +130,28 @@ func (p *MusicProvider) GenerateMusic(ctx context.Context, opts audio.MusicOptio if mmResp.Data == nil { return nil, fmt.Errorf("no data in MiniMax music response") } + if mmResp.Data.Status == 1 { + return nil, fmt.Errorf("MiniMax music generation is still in progress") + } audioURL := mmResp.Data.Audio if audioURL == "" { audioURL = mmResp.Data.Music } if audioURL == "" { - return nil, fmt.Errorf("no audio URL in MiniMax music response") + return nil, fmt.Errorf("no audio data in MiniMax music response") + } + + // music-3.0 may return raw audio as a hexadecimal string even when URL + // output is requested. Preserve URL downloads while accepting both forms. + if !strings.HasPrefix(audioURL, "http://") && !strings.HasPrefix(audioURL, "https://") { + audioBytes, err := decodeHexAudio(audioURL) + if err != nil { + return nil, fmt.Errorf("decode MiniMax music audio: %w", err) + } + return &audio.AudioResult{ + Audio: audioBytes, Extension: "mp3", MimeType: "audio/mpeg", Model: model, Provider: "minimax", + }, nil } // Download the audio file from the returned URL. @@ -167,6 +184,25 @@ func (p *MusicProvider) GenerateMusic(ctx context.Context, opts audio.MusicOptio }, nil } +func decodeHexAudio(value string) ([]byte, error) { + value = strings.TrimSpace(value) + value = strings.TrimPrefix(value, "0x") + if value == "" { + return nil, fmt.Errorf("empty hexadecimal audio payload") + } + if len(value)%2 != 0 { + return nil, fmt.Errorf("hexadecimal audio payload has odd length") + } + decoded, err := hex.DecodeString(value) + if err != nil { + return nil, fmt.Errorf("invalid hexadecimal audio payload: %w", err) + } + if len(decoded) > maxMusicDownloadBytes { + return nil, fmt.Errorf("audio response exceeds %d MB limit", maxMusicDownloadBytes/(1024*1024)) + } + return decoded, nil +} + func truncate(b []byte, n int) string { if len(b) <= n { return string(b) diff --git a/internal/audio/minimax/music_test.go b/internal/audio/minimax/music_test.go index 4d983413..4c3d4c6e 100644 --- a/internal/audio/minimax/music_test.go +++ b/internal/audio/minimax/music_test.go @@ -104,7 +104,7 @@ func TestMusicProvider_GenerateMusic(t *testing.T) { t.Run("vocal_with_lyrics", func(t *testing.T) { apiSrv, dlSrv := makeServers(t, map[string]any{ - "model": "music-2.5+", + "model": "music-3.0", "prompt": "happy pop song", "is_instrumental": false, "lyrics_optimizer": false, @@ -133,6 +133,44 @@ func TestMusicProvider_GenerateMusic(t *testing.T) { } }) + t.Run("hex_audio_payload", func(t *testing.T) { + apiSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{"audio": "4d503344415441", "status": 2}, + "base_resp": map[string]any{"status_code": 0}, + }) + })) + defer apiSrv.Close() + + p := NewMusicProvider(MusicConfig{APIKey: "test-key", APIBase: apiSrv.URL}) + res, err := p.GenerateMusic(context.Background(), audio.MusicOptions{Prompt: "hex response"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if string(res.Audio) != "MP3DATA" { + t.Fatalf("audio bytes: got %q", res.Audio) + } + }) + + t.Run("in_progress_status", func(t *testing.T) { + apiSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{"status": 1}, + "base_resp": map[string]any{"status_code": 0}, + }) + })) + defer apiSrv.Close() + + _, err := NewMusicProvider(MusicConfig{APIKey: "test-key", APIBase: apiSrv.URL}).GenerateMusic( + context.Background(), audio.MusicOptions{Prompt: "pending"}, + ) + if err == nil || !strings.Contains(err.Error(), "still in progress") { + t.Fatalf("expected in-progress error, got %v", err) + } + }) + t.Run("instrumental_flag", func(t *testing.T) { apiSrv, dlSrv := makeServers(t, map[string]any{ "is_instrumental": true,