本文由 ChatMoney团队出品
前言
在科技与艺术的交汇处,AI音乐创作正以其独特的魅力,引领着音乐产业的一次革命。不久前,AI音乐的浪潮席卷了整个创意领域,激发了无数音乐爱好者和技术开发者的无限想象。在这场音乐与科技的盛宴中,主流的AI音乐平台suno无疑成为了焦点,尽管它尚未对外开放API服务,但这并未阻止我们探索的脚步。
今天,我们将踏上一段奇妙的旅程,用Go语言这把精准而强大的工具,尝试构建一个简易的suno-api。
开发前准备
- golang开发环境。
- golang版本:1.21.0
- 可登录suno的环境
- 获取suno平台cookie
开发过程
1.安装gin框架
go get -u github.com/gin-gonic/gin
2.封装suno请求
- 相关结构体
package internal
import "time"
// GenerateReq generate-Req
type GenerateReq struct {
GptDescriptionPrompt string `json:"gpt_description_prompt"`
Prompt string `json:"prompt"`
Mv string `json:"mv"`
Title string `json:"title"`
Tags string `json:"tags"`
}
// GenerateResp generate-Resp
type GenerateResp struct {
BatchSize int `json:"batch_size"`
Clips []Clips `json:"clips"`
CreatedAt time.Time `json:"created_at"`
ID string `json:"id"`
Status string `json:"status"`
Metadata `json:"metadata"`
MajorModelVersion string `json:"major_model_version"`
}
// TokenResp token-Response
type TokenResp struct {
Jwt string
Object string
}
// SidResp session-resp
type SidResp struct {
Response struct {
Object string `json:"object"`
ID string `json:"id"`
Sessions []Session `json:"sessions"`
SignIn interface{} `json:"sign_in"`
SignUp interface{} `json:"sign_up"`
LastActiveSessionID string `json:"last_active_session_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} `json:"response"`
Client interface{} `json:"client"`
}
// Clips clips
type Clips struct {
Detail string `json:"detail"`
Id string `json:"id"`
VideoUrl string `json:"video_url"`
AudioUrl string `json:"audio_url"`
ImageUrl string `json:"image_url"`
ImageLargeUrl string `json:"image_large_url"`
MajorModelVersion string `json:"major_model_version"`
ModelName string `json:"model_name"`
Metadata *Metadata `json:"metadata"`
IsLiked bool `json:"is_liked"`
UserId string `json:"user_id"`
IsTrashed bool `json:"is_trashed"`
Reaction interface{} `json:"reaction"`
CreatedAt time.Time `json:"created_at"`
Status string `json:"status"`
Title string `json:"title"`
PlayCount int `json:"play_count"`
UpvoteCount int `json:"upvote_count"`
IsPublic bool `json:"is_public"`
}
// Metadata Metadata
type Metadata struct {
Tags string `json:"tags"`
Prompt string `json:"prompt"`
GptDescriptionPrompt string `json:"gpt_description_prompt"`
AudioPromptId interface{} `json:"audio_prompt_id"`
History interface{} `json:"history"`
ConcatHistory interface{} `json:"concat_history"`
Type string `json:"type"`
Duration float64 `json:"duration"`
RefundCredits bool `json:"refund_credits"`
Stream bool `json:"stream"`
ErrorType interface{} `json:"error_type"`
ErrorMessage interface{} `json:"error_message"`
}
// Session sessionId
type Session struct {
Object string `json:"object"`
ID string `json:"id"`
}
- 获取suno的token
- 实现逻辑为通过suno的cookie获取到sessionid,后通过sessionid获取token.
// GetToken getToken
func (s *service) GetToken(cookieString string) (token string, err error) {
var cookies []*http.Cookie
cookies = s.parseCookieString(cookieString)
jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
sidUrl, _ := url.Parse(GetSidUrl)
jar.SetCookies(sidUrl, cookies)
client := &http.Client{
Jar: jar,
}
sidResp, err := client.Get(sidUrl.String())
if err != nil {
return "", err
}
defer sidResp.Body.Close()
var sidResponse SidResp
_ = json.NewDecoder(sidResp.Body).Decode(&sidResponse)
sid := ""
if len(sidResponse.Response.Sessions) > 0 {
sid = sidResponse.Response.Sessions[0].ID
fmt.Println(sidResponse.Response.Sessions[0].ID)
} else {
err = errors.New("获取sessionId失败:Response.Sessions为空")
return "", err
}
getTokenUrl := fmt.Sprintf("%s/%s/tokens?_clerk_js_version=4.72.0-snapshot.vc141245", GetTokenUrl, sid)
tokenResp, err := client.Post(getTokenUrl, ContentType, nil)
if err != nil {
return "", err
}
defer tokenResp.Body.Close()
var tokenResponse TokenResp
bodyBytes, err := io.ReadAll(tokenResp.Body)
if err != nil {
return "", fmt.Errorf("error reading response body: %v", err)
}
if tokenResp.StatusCode != http.StatusOK {
fmt.Printf("Error decoding JSON response: %vnResponse body: %sn", err, bodyBytes)
fmt.Printf("请求失败,状态码: %d, 响应内容: %sn", tokenResp.StatusCode, bodyBytes)
return "", errors.New(fmt.Sprintf("请求失败,状态码: %d, 响应内容: %sn", tokenResp.StatusCode, bodyBytes))
}
err = json.Unmarshal(bodyBytes, &tokenResponse)
if err != nil {
return "", err
}
if tokenResponse.Jwt == "" {
return "", errors.New("获取token失败:token为空")
}
return tokenResponse.Jwt, nil
}
// parseCookieString 格式化cookie
func (s *service) parseCookieString(cookieString string) []*http.Cookie {
var cookies []*http.Cookie
for _, cookiePair := range strings.Split(cookieString, "; ") {
parts := strings.Split(cookiePair, "=")
if len(parts) == 2 {
cookies = append(cookies, &http.Cookie{Name: parts[0], Value: parts[1]})
}
}
return cookies
}
- 生成歌曲提交
// Generate 发起生成请求
func (s *service) Generate(prompt string) (response GenerateResp, err error) {
// token校验
if err = s.checkToken(); err != nil {
return
}
// 携带token发起请求
genReq := GenerateReq{
GptDescriptionPrompt: prompt,
Mv: "chirp-v3-0",
}
reqData, _ := json.Marshal(genReq)
genMusicUrl := BaseUrl + "/api/generate/v2/"
req, _ := http.NewRequest("POST", genMusicUrl, bytes.NewBuffer(reqData))
req.Header.Set("Authorization", "Bearer "+Token)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
var genResponse GenerateResp
err = json.NewDecoder(resp.Body).Decode(&genResponse)
if err != nil {
return
}
return genResponse, nil
}
- 查询歌曲详情
// GetFeed 查询详情
func (s *service) GetFeed(ids []string) (result []Clips, err error) {
// token校验
if err = s.checkToken(); err != nil {
return
}
idsFormat := url.PathEscape(strings.Join(ids, ","))
feedUrl := fmt.Sprintf("%s/api/feed/?ids=%s", BaseUrl, idsFormat)
req, _ := http.NewRequest("GET", feedUrl, nil)
req.Header.Set("Authorization", "Bearer "+Token)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading response body: %v", err)
}
err = json.Unmarshal(bodyBytes, &result)
if err != nil {
fmt.Printf("Error decoding JSON response: %vnResponse body: %sn", err, bodyBytes)
return nil, err
}
return result, nil
}
3.控制器
// GenMusic 提交音乐请求
func (h *Handler) GenMusic(c *gin.Context) {
var genReq genReq
if err := c.ShouldBindJSON(&genReq); err != nil {
errResponse(c, err.Error(), "Params Fail")
return
}
token, err := SunoService.GetToken(cookieString)
if err != nil {
errResponse(c, err.Error(), "Request Failed")
return
}
SunoService.SetToken(token)
res, err := SunoService.Generate(genReq.Prompt)
if err != nil {
errResponse(c, err.Error(), "Request Failed")
return
}
okResponse(c, res)
return
}
// GetFeed 查询详情
func (h *Handler) GetFeed(c *gin.Context) {
var feedReq feedReq
if err := c.ShouldBindJSON(&feedReq); err != nil {
errResponse(c, err.Error(), "Params Fail")
return
}
token, err := SunoService.GetToken(cookieString)
if err != nil {
errResponse(c, err.Error(), "Request Failed")
return
}
SunoService.SetToken(token)
data, err := SunoService.GetFeed(feedReq.ClipsIds)
if err != nil {
errResponse(c, err.Error(), "Request Failed")
return
}
okResponse(c, data)
return
}
postman测试
项目启动
cd cmd
go run main.go
生成音乐
查询音乐详情
结语
虽然目前这个suno-api仅揭开了其神秘的面纱,实现了基础的生成及详情查询功能,但它所蕴含的潜力与未来的发展空间却是无限的。如果我们能够为这个API增添更多的功能和优化,它将变得更加强大和完善。比如,通过配置文件来管理cookie,让它们更加安全和持久;增加cookie的保活机制,让用户体验更加流畅;记录每一次的请求日志,让问题追踪和系统监控变得更加简单;实施接口限流策略,保障系统在高并发情况下的稳定性等等。希望后续能够激发更多的创意火花,创造出更多动人心弦的旋律。
感谢这段旅程有你的陪伴,让我们一起期待下一次更加精彩的启程。
完整代码
关于我们
本文由ChatMoney团队出品,ChatMoney专注于AI应用落地与变现,我们提供全套、持续更新的AI源码系统与可执行的变现方案,致力于帮助更多人利用AI来变现,欢迎进入ChatMoney获取更多AI变现方案!