diff --git a/api/core/types/config.go b/api/core/types/config.go
index 8b1341c9..67845571 100644
--- a/api/core/types/config.go
+++ b/api/core/types/config.go
@@ -180,6 +180,7 @@ type SystemConfig struct {
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
DallPower int `json:"dall_power,omitempty"` // DALLE3 绘图消耗算力
+ SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址
diff --git a/api/core/types/task.go b/api/core/types/task.go
index 552b7666..95e7fc9f 100644
--- a/api/core/types/task.go
+++ b/api/core/types/task.go
@@ -80,14 +80,18 @@ type DallTask struct {
}
type SunoTask struct {
- Id int `json:"id"`
- UserId string `json:"user_id"`
+ Id uint `json:"id"`
+ Channel string `json:"channel"`
+ UserId int `json:"user_id"`
Type int `json:"type"`
TaskId string `json:"task_id"`
Title string `json:"title"`
- ReferenceId string `json:"reference_id"`
- Prompt string `json:"prompt"`
+ RefTaskId string `json:"ref_task_id"`
+ RefSongId string `json:"ref_song_id"`
+ Lyrics string `json:"lyrics"` // 歌词:自定义模式
+ Prompt string `json:"prompt"` // 提示词:灵感模式
Tags string `json:"tags"`
+ Model string `json:"model"`
Instrumental bool `json:"instrumental"` // 是否纯音乐
ExtendSecs int `json:"extend_secs"` // 延长秒杀
}
diff --git a/api/handler/chatimpl/chat_handler.go b/api/handler/chatimpl/chat_handler.go
index 43cae8d2..c21556b2 100644
--- a/api/handler/chatimpl/chat_handler.go
+++ b/api/handler/chatimpl/chat_handler.go
@@ -469,7 +469,7 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, sessi
} else {
client = http.DefaultClient
}
- logger.Debugf("Sending %s request, ApiURL:%s, API KEY:%s, PROXY: %s, Model: %s", session.Model.Platform, apiKey.ApiURL, apiURL, apiKey.ProxyURL, req.Model)
+ logger.Debugf("Sending %s request, Channel:%s, API KEY:%s, PROXY: %s, Model: %s", session.Model.Platform, apiKey.ApiURL, apiURL, apiKey.ProxyURL, req.Model)
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
return client.Do(request)
}
@@ -607,7 +607,7 @@ func (h *ChatHandler) extractImgUrl(text string) string {
continue
}
- newImgURL, err := h.uploadManager.GetUploadHandler().PutImg(imageURL, false)
+ newImgURL, err := h.uploadManager.GetUploadHandler().PutUrlFile(imageURL, false)
if err != nil {
logger.Error("error with download image: ", err)
continue
diff --git a/api/handler/suno_handler.go b/api/handler/suno_handler.go
index 5419ddea..98398b9d 100644
--- a/api/handler/suno_handler.go
+++ b/api/handler/suno_handler.go
@@ -8,13 +8,22 @@ package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
+ "fmt"
"geekai/core"
+ "geekai/core/types"
+ "geekai/service/suno"
+ "geekai/store/model"
+ "geekai/store/vo"
+ "geekai/utils"
+ "geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
+ "time"
)
type SunoHandler struct {
BaseHandler
+ service *suno.Service
}
func NewSunoHandler(app *core.AppServer, db *gorm.DB) *SunoHandler {
@@ -48,6 +57,80 @@ func (h *SunoHandler) Client(c *gin.Context) {
func (h *SunoHandler) Create(c *gin.Context) {
+ var data struct {
+ Prompt string `json:"prompt"`
+ Instrumental bool `json:"instrumental"`
+ Lyrics string `json:"lyrics"`
+ Model string `json:"model"`
+ Tags string `json:"tags"`
+ Title string `json:"title"`
+ Type int `json:"type"`
+ RefTaskId string `json:"ref_task_id"` // 续写的任务id
+ ExtendSecs int `json:"extend_secs"` // 续写秒数
+ RefSongId string `json:"ref_song_id"` // 续写的歌曲id
+ }
+ if err := c.ShouldBindJSON(&data); err != nil {
+ resp.ERROR(c, types.InvalidArgs)
+ return
+ }
+
+ // 插入数据库
+ job := model.SunoJob{
+ UserId: int(h.GetLoginUserId(c)),
+ Prompt: data.Prompt,
+ Instrumental: data.Instrumental,
+ ModelName: data.Model,
+ Tags: data.Tags,
+ Title: data.Title,
+ Type: data.Type,
+ RefSongId: data.RefSongId,
+ RefTaskId: data.RefTaskId,
+ ExtendSecs: data.ExtendSecs,
+ Power: h.App.SysConfig.SunoPower,
+ }
+ tx := h.DB.Create(&job)
+ if tx.Error != nil {
+ resp.ERROR(c, tx.Error.Error())
+ return
+ }
+
+ // 创建任务
+ h.service.PushTask(types.SunoTask{
+ Id: job.Id,
+ UserId: job.UserId,
+ Type: job.Type,
+ Title: job.Title,
+ Lyrics: data.Lyrics,
+ RefTaskId: data.RefTaskId,
+ RefSongId: data.RefSongId,
+ ExtendSecs: data.ExtendSecs,
+ Prompt: data.Prompt,
+ Tags: data.Tags,
+ Model: data.Model,
+ Instrumental: data.Instrumental,
+ })
+
+ // update user's power
+ tx = h.DB.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
+ // 记录算力变化日志
+ if tx.Error == nil && tx.RowsAffected > 0 {
+ user, _ := h.GetLoginUser(c)
+ h.DB.Create(&model.PowerLog{
+ UserId: user.Id,
+ Username: user.Username,
+ Type: types.PowerConsume,
+ Amount: job.Power,
+ Balance: user.Power - job.Power,
+ Mark: types.PowerSub,
+ Model: job.ModelName,
+ Remark: fmt.Sprintf("Suno 文生歌曲,%s", job.ModelName),
+ CreatedAt: time.Now(),
+ })
+ }
+
+ var itemVo vo.SunoJob
+ _ = utils.CopyObject(job, &itemVo)
+ resp.SUCCESS(c, itemVo)
}
func (h *SunoHandler) List(c *gin.Context) {
diff --git a/api/main.go b/api/main.go
index 3de8eceb..21d26be5 100644
--- a/api/main.go
+++ b/api/main.go
@@ -23,6 +23,7 @@ import (
"geekai/service/payment"
"geekai/service/sd"
"geekai/service/sms"
+ "geekai/service/suno"
"geekai/service/wx"
"geekai/store"
"io"
@@ -209,6 +210,12 @@ func main() {
}
}),
+ fx.Provide(suno.NewService),
+ fx.Invoke(func(s *suno.Service) {
+ s.Run()
+ s.SyncTaskProgress()
+ }),
+
fx.Provide(payment.NewAlipayService),
fx.Provide(payment.NewHuPiPay),
fx.Provide(payment.NewJPayService),
@@ -475,6 +482,15 @@ func main() {
group.GET("remove", h.Remove)
group.GET("publish", h.Publish)
}),
+ fx.Provide(handler.NewSunoHandler),
+ fx.Invoke(func(s *core.AppServer, h *handler.SunoHandler) {
+ group := s.Engine.Group("/api/suno")
+ group.Any("client", h.Client)
+ group.POST("create", h.Create)
+ group.GET("list", h.List)
+ group.GET("remove", h.Remove)
+ group.GET("publish", h.Publish)
+ }),
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
go func() {
err := s.Run(db)
diff --git a/api/service/dalle/service.go b/api/service/dalle/service.go
index 4225182f..de460e27 100644
--- a/api/service/dalle/service.go
+++ b/api/service/dalle/service.go
@@ -166,7 +166,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
Style: task.Style,
Quality: task.Quality,
}
- logger.Infof("Sending %s request, ApiURL:%s, API KEY:%s, BODY: %+v", apiKey.Platform, apiURL, apiKey.Value, reqBody)
+ logger.Infof("Sending %s request, Channel:%s, API KEY:%s, BODY: %+v", apiKey.Platform, apiURL, apiKey.Value, reqBody)
r, err := s.httpClient.R().SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+apiKey.Value).
SetBody(reqBody).
@@ -259,7 +259,7 @@ func (s *Service) DownloadImages() {
func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string, error) {
// sava image
- imgURL, err := s.uploadManager.GetUploadHandler().PutImg(orgURL, false)
+ imgURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(orgURL, false)
if err != nil {
return "", err
}
diff --git a/api/service/mj/pool.go b/api/service/mj/pool.go
index d6ab1f60..dca71a76 100644
--- a/api/service/mj/pool.go
+++ b/api/service/mj/pool.go
@@ -139,7 +139,7 @@ func (p *ServicePool) DownloadImages() {
if strings.HasPrefix(v.OrgURL, "https://cdn.discordapp.com") {
proxy = true
}
- imgURL, err := p.uploaderManager.GetUploadHandler().PutImg(v.OrgURL, proxy)
+ imgURL, err := p.uploaderManager.GetUploadHandler().PutUrlFile(v.OrgURL, proxy)
if err != nil {
logger.Errorf("error with download image %s, %v", v.OrgURL, err)
diff --git a/api/service/oss/aliyun_oss.go b/api/service/oss/aliyun_oss.go
index 00dcc8d7..d36ad139 100644
--- a/api/service/oss/aliyun_oss.go
+++ b/api/service/oss/aliyun_oss.go
@@ -84,25 +84,25 @@ func (s AliYunOss) PutFile(ctx *gin.Context, name string) (File, error) {
}, nil
}
-func (s AliYunOss) PutImg(imageURL string, useProxy bool) (string, error) {
- var imageData []byte
+func (s AliYunOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
+ var fileData []byte
var err error
if useProxy {
- imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
+ fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
} else {
- imageData, err = utils.DownloadImage(imageURL, "")
+ fileData, err = utils.DownloadImage(fileURL, "")
}
if err != nil {
return "", fmt.Errorf("error with download image: %v", err)
}
- parse, err := url.Parse(imageURL)
+ parse, err := url.Parse(fileURL)
if err != nil {
return "", fmt.Errorf("error with parse image URL: %v", err)
}
fileExt := utils.GetImgExt(parse.Path)
objectKey := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
// 上传文件字节数据
- err = s.bucket.PutObject(objectKey, bytes.NewReader(imageData))
+ err = s.bucket.PutObject(objectKey, bytes.NewReader(fileData))
if err != nil {
return "", err
}
diff --git a/api/service/oss/localstorage.go b/api/service/oss/localstorage.go
index f64ff055..642f3d0d 100644
--- a/api/service/oss/localstorage.go
+++ b/api/service/oss/localstorage.go
@@ -57,8 +57,8 @@ func (s LocalStorage) PutFile(ctx *gin.Context, name string) (File, error) {
}, nil
}
-func (s LocalStorage) PutImg(imageURL string, useProxy bool) (string, error) {
- parse, err := url.Parse(imageURL)
+func (s LocalStorage) PutUrlFile(fileURL string, useProxy bool) (string, error) {
+ parse, err := url.Parse(fileURL)
if err != nil {
return "", fmt.Errorf("error with parse image URL: %v", err)
}
@@ -69,9 +69,9 @@ func (s LocalStorage) PutImg(imageURL string, useProxy bool) (string, error) {
}
if useProxy {
- err = utils.DownloadFile(imageURL, filePath, s.proxyURL)
+ err = utils.DownloadFile(fileURL, filePath, s.proxyURL)
} else {
- err = utils.DownloadFile(imageURL, filePath, "")
+ err = utils.DownloadFile(fileURL, filePath, "")
}
if err != nil {
return "", fmt.Errorf("error with download image: %v", err)
diff --git a/api/service/oss/minio_oss.go b/api/service/oss/minio_oss.go
index 5eaca499..d095127b 100644
--- a/api/service/oss/minio_oss.go
+++ b/api/service/oss/minio_oss.go
@@ -44,18 +44,18 @@ func NewMiniOss(appConfig *types.AppConfig) (MiniOss, error) {
return MiniOss{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil
}
-func (s MiniOss) PutImg(imageURL string, useProxy bool) (string, error) {
- var imageData []byte
+func (s MiniOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
+ var fileData []byte
var err error
if useProxy {
- imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
+ fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
} else {
- imageData, err = utils.DownloadImage(imageURL, "")
+ fileData, err = utils.DownloadImage(fileURL, "")
}
if err != nil {
return "", fmt.Errorf("error with download image: %v", err)
}
- parse, err := url.Parse(imageURL)
+ parse, err := url.Parse(fileURL)
if err != nil {
return "", fmt.Errorf("error with parse image URL: %v", err)
}
@@ -65,8 +65,8 @@ func (s MiniOss) PutImg(imageURL string, useProxy bool) (string, error) {
context.Background(),
s.config.Bucket,
filename,
- strings.NewReader(string(imageData)),
- int64(len(imageData)),
+ strings.NewReader(string(fileData)),
+ int64(len(fileData)),
minio.PutObjectOptions{ContentType: "image/png"})
if err != nil {
return "", err
diff --git a/api/service/oss/qiniu_oss.go b/api/service/oss/qiniu_oss.go
index 703b6d78..310be7cf 100644
--- a/api/service/oss/qiniu_oss.go
+++ b/api/service/oss/qiniu_oss.go
@@ -93,18 +93,18 @@ func (s QinNiuOss) PutFile(ctx *gin.Context, name string) (File, error) {
}
-func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) {
- var imageData []byte
+func (s QinNiuOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
+ var fileData []byte
var err error
if useProxy {
- imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
+ fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
} else {
- imageData, err = utils.DownloadImage(imageURL, "")
+ fileData, err = utils.DownloadImage(fileURL, "")
}
if err != nil {
return "", fmt.Errorf("error with download image: %v", err)
}
- parse, err := url.Parse(imageURL)
+ parse, err := url.Parse(fileURL)
if err != nil {
return "", fmt.Errorf("error with parse image URL: %v", err)
}
@@ -113,7 +113,7 @@ func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) {
ret := storage.PutRet{}
extra := storage.PutExtra{}
// 上传文件字节数据
- err = s.uploader.Put(context.Background(), &ret, s.putPolicy.UploadToken(s.mac), key, bytes.NewReader(imageData), int64(len(imageData)), &extra)
+ err = s.uploader.Put(context.Background(), &ret, s.putPolicy.UploadToken(s.mac), key, bytes.NewReader(fileData), int64(len(fileData)), &extra)
if err != nil {
return "", err
}
diff --git a/api/service/oss/uploader.go b/api/service/oss/uploader.go
index 435e22d7..d4caf835 100644
--- a/api/service/oss/uploader.go
+++ b/api/service/oss/uploader.go
@@ -23,7 +23,7 @@ type File struct {
}
type Uploader interface {
PutFile(ctx *gin.Context, name string) (File, error)
- PutImg(imageURL string, useProxy bool) (string, error)
+ PutUrlFile(url string, useProxy bool) (string, error)
PutBase64(imageData string) (string, error)
Delete(fileURL string) error
}
diff --git a/api/service/suno/service.go b/api/service/suno/service.go
index 8f28302a..df1e5876 100644
--- a/api/service/suno/service.go
+++ b/api/service/suno/service.go
@@ -1,4 +1,4 @@
-package dalle
+package suno
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
@@ -8,11 +8,17 @@ package dalle
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
+ "encoding/json"
+ "errors"
+ "fmt"
"geekai/core/types"
logger2 "geekai/logger"
"geekai/service/oss"
"geekai/store"
+ "geekai/store/model"
+ "geekai/utils"
"github.com/go-redis/redis/v8"
+ "io"
"time"
"github.com/imroc/req/v3"
@@ -57,10 +63,230 @@ func (s *Service) Run() {
continue
}
+ r, err := s.Create(task)
+ if err != nil {
+ logger.Errorf("create task with error: %v", err)
+ s.db.UpdateColumns(map[string]interface{}{
+ "err_msg": err.Error(),
+ "progress": 101,
+ })
+ continue
+ }
+
+ // 更新任务信息
+ s.db.Model(&model.SunoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
+ "task_id": r.Data,
+ "channel": r.Channel,
+ })
}
}()
}
-func (s *Service) Create(task types.SunoTask) {
-
+type RespVo struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+ Data string `json:"data"`
+ Channel string `json:"channel,omitempty"`
+}
+
+func (s *Service) Create(task types.SunoTask) (RespVo, error) {
+ // 读取 API KEY
+ var apiKey model.ApiKey
+ session := s.db.Session(&gorm.Session{}).Where("type", "suno").Where("enabled", true)
+ if task.Channel != "" {
+ session = session.Where("api_url", task.Channel)
+ }
+ tx := session.Order("last_used_at DESC").First(&apiKey)
+ if tx.Error != nil {
+ return RespVo{}, errors.New("no available API KEY for Suno")
+ }
+
+ reqBody := map[string]interface{}{
+ "task_id": task.TaskId,
+ "continue_clip_id": task.RefSongId,
+ "continue_at": task.ExtendSecs,
+ "make_instrumental": task.Instrumental,
+ }
+ // 灵感模式
+ if task.Type == 1 {
+ reqBody["gpt_description_prompt"] = task.Prompt
+ } else { // 自定义模式
+ reqBody["prompt"] = task.Lyrics
+ reqBody["tags"] = task.Tags
+ reqBody["mv"] = task.Model
+ reqBody["title"] = task.Title
+ }
+
+ var res RespVo
+ apiURL := fmt.Sprintf("%s/task/suno/v1/submit/music", apiKey.ApiURL)
+ logger.Debugf("API URL: %s, request body: %+v", apiURL, reqBody)
+ r, err := req.C().R().
+ SetHeader("Authorization", "Bearer "+apiKey.Value).
+ SetBody(reqBody).
+ Post(apiURL)
+ if err != nil {
+ return RespVo{}, fmt.Errorf("请求 API 出错:%v", err)
+ }
+
+ body, _ := io.ReadAll(r.Body)
+ err = json.Unmarshal(body, &res)
+ if err != nil {
+ return RespVo{}, fmt.Errorf("解析API数据失败:%s", string(body))
+ }
+ res.Channel = apiKey.ApiURL
+ return res, nil
+}
+
+// SyncTaskProgress 异步拉取任务
+func (s *Service) SyncTaskProgress() {
+ go func() {
+ var jobs []model.SunoJob
+ for {
+ res := s.db.Where("progress < ?", 100).Where("task_id <> ?", "").Find(&jobs)
+ if res.Error != nil {
+ continue
+ }
+
+ for _, job := range jobs {
+ task, err := s.QueryTask(job.TaskId, job.Channel)
+ if err != nil {
+ logger.Errorf("query task with error: %v", err)
+ continue
+ }
+
+ if task.Code != "success" {
+ logger.Errorf("query task with error: %v", task.Message)
+ continue
+ }
+
+ logger.Debugf("task: %+v", task.Data.Status)
+ // 任务完成,删除旧任务插入两条新任务
+ if task.Data.Status == "SUCCESS" {
+ var jobId = job.Id
+ var flag = false
+ tx := s.db.Begin()
+ for _, v := range task.Data.Data {
+ job.Id = 0
+ job.Progress = 100
+ job.Title = v.Title
+ job.SongId = v.Id
+ job.Duration = int(v.Metadata.Duration)
+ job.Prompt = v.Metadata.Prompt
+ job.Tags = v.Metadata.Tags
+ job.ModelName = v.ModelName
+ job.RawData = utils.JsonEncode(v)
+
+ // 下载图片和音频
+ thumbURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.ImageUrl, true)
+ if err != nil {
+ logger.Errorf("download image with error: %v", err)
+ continue
+ }
+ coverURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.ImageLargeUrl, true)
+ if err != nil {
+ logger.Errorf("download image with error: %v", err)
+ continue
+ }
+ audioURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.AudioUrl, true)
+ if err != nil {
+ logger.Errorf("download audio with error: %v", err)
+ continue
+ }
+ job.ThumbImgURL = thumbURL
+ job.CoverImgURL = coverURL
+ job.AudioURL = audioURL
+
+ if err = tx.Create(&job).Error; err != nil {
+ logger.Error("create job with error: %v", err)
+ tx.Rollback()
+ break
+ }
+ flag = true
+ }
+
+ // 删除旧任务
+ if flag {
+ if err = tx.Delete(&model.SunoJob{}, "id = ?", jobId).Error; err != nil {
+ logger.Error("create job with error: %v", err)
+ tx.Rollback()
+ continue
+ }
+ }
+
+ tx.Commit()
+
+ } else if task.Data.FailReason != "" {
+ job.Progress = 101
+ job.ErrMsg = task.Data.FailReason
+ s.db.Updates(&job)
+ }
+ }
+
+ time.Sleep(time.Second * 10)
+ }
+ }()
+}
+
+type QueryRespVo struct {
+ Code string `json:"code"`
+ Message string `json:"message"`
+ Data struct {
+ TaskId string `json:"task_id"`
+ Action string `json:"action"`
+ Status string `json:"status"`
+ FailReason string `json:"fail_reason"`
+ SubmitTime int `json:"submit_time"`
+ StartTime int `json:"start_time"`
+ FinishTime int `json:"finish_time"`
+ Progress string `json:"progress"`
+ Data []struct {
+ Id string `json:"id"`
+ Title string `json:"title"`
+ Status string `json:"status"`
+ Metadata struct {
+ Tags string `json:"tags"`
+ Type string `json:"type"`
+ Prompt string `json:"prompt"`
+ Stream bool `json:"stream"`
+ Duration float64 `json:"duration"`
+ ErrorMessage interface{} `json:"error_message"`
+ } `json:"metadata"`
+ AudioUrl string `json:"audio_url"`
+ ImageUrl string `json:"image_url"`
+ VideoUrl string `json:"video_url"`
+ ModelName string `json:"model_name"`
+ DisplayName string `json:"display_name"`
+ ImageLargeUrl string `json:"image_large_url"`
+ MajorModelVersion string `json:"major_model_version"`
+ } `json:"data"`
+ } `json:"data"`
+}
+
+func (s *Service) QueryTask(taskId string, channel string) (QueryRespVo, error) {
+ // 读取 API KEY
+ var apiKey model.ApiKey
+ tx := s.db.Session(&gorm.Session{}).Where("type", "suno").
+ Where("api_url", channel).
+ Where("enabled", true).
+ Order("last_used_at DESC").First(&apiKey)
+ if tx.Error != nil {
+ return QueryRespVo{}, errors.New("no available API KEY for Suno")
+ }
+
+ apiURL := fmt.Sprintf("%s/task/suno/v1/fetch/%s", apiKey.ApiURL, taskId)
+ var res QueryRespVo
+ r, err := req.C().R().SetHeader("Authorization", "Bearer "+apiKey.Value).Get(apiURL)
+
+ if err != nil {
+ return QueryRespVo{}, fmt.Errorf("请求 API 失败:%v", err)
+ }
+
+ defer r.Body.Close()
+ body, _ := io.ReadAll(r.Body)
+ err = json.Unmarshal(body, &res)
+ if err != nil {
+ return QueryRespVo{}, fmt.Errorf("解析API数据失败:%s", string(body))
+ }
+
+ return res, nil
}
diff --git a/api/store/model/suno_job.go b/api/store/model/suno_job.go
index 7add83e7..61d36377 100644
--- a/api/store/model/suno_job.go
+++ b/api/store/model/suno_job.go
@@ -5,14 +5,16 @@ import "time"
type SunoJob struct {
Id uint `gorm:"primarykey;column:id"`
UserId int
+ Channel string // 频道
Title string
- Type string
+ Type int
TaskId string
- ReferenceId string // 续写的任务id
+ RefTaskId string // 续写的任务id
Tags string // 歌曲风格和标签
Instrumental bool // 是否生成纯音乐
ExtendSecs int // 续写秒数
- SongId int // 续写的歌曲id
+ SongId string // 续写的歌曲id
+ RefSongId string
Prompt string // 提示词
ThumbImgURL string // 缩略图 URL
CoverImgURL string // 封面图 URL
diff --git a/api/store/vo/suno_job.go b/api/store/vo/suno_job.go
index cb76fc13..014148cf 100644
--- a/api/store/vo/suno_job.go
+++ b/api/store/vo/suno_job.go
@@ -3,27 +3,29 @@ package vo
import "time"
type SunoJob struct {
- Id uint `json:"id"`
- UserId int `json:"user_id"`
- Title string `json:"title"`
- Type string `json:"type"`
- TaskId string `json:"task_id"`
- ReferenceId string `json:"reference_id"` // 续写的任务id
- Tags string `json:"tags"` // 歌曲风格和标签
- Instrumental bool `json:"instrumental"` // 是否生成纯音乐
- ExtendSecs int `json:"extend_secs"` // 续写秒数
- SongId int `json:"song_id"` // 续写的歌曲id
- Prompt string `json:"prompt"` // 提示词
- ThumbImgURL string `json:"thumb_img_url"` // 缩略图 URL
- CoverImgURL string `json:"cover_img_url"` // 封面图 URL
- AudioURL string `json:"audio_url"` // 音频 URL
- ModelName string `json:"model_name"` // 模型名称
- Progress int `json:"progress"` // 任务进度
- Duration int `json:"duration"` // 银屏时长,秒
- Publish bool `json:"publish"` // 是否发布
- ErrMsg string `json:"err_msg"` // 错误信息
- RawData string `json:"raw_data"` // 原始数据 json
- Power int `json:"power"` // 消耗算力
+ Id uint `json:"id"`
+ UserId int `json:"user_id"`
+ Channel string `json:"channel"`
+ Title string `json:"title"`
+ Type string `json:"type"`
+ TaskId string `json:"task_id"`
+ RefTaskId string `json:"ref_task_id"` // 续写的任务id
+ Tags string `json:"tags"` // 歌曲风格和标签
+ Instrumental bool `json:"instrumental"` // 是否生成纯音乐
+ ExtendSecs int `json:"extend_secs"` // 续写秒数
+ SongId string `json:"song_id"` // 续写的歌曲id
+ RefSongId string `json:"ref_song_id"` // 续写的歌曲id
+ Prompt string `json:"prompt"` // 提示词
+ ThumbImgURL string `json:"thumb_img_url"` // 缩略图 URL
+ CoverImgURL string `json:"cover_img_url"` // 封面图 URL
+ AudioURL string `json:"audio_url"` // 音频 URL
+ ModelName string `json:"model_name"` // 模型名称
+ Progress int `json:"progress"` // 任务进度
+ Duration int `json:"duration"` // 银屏时长,秒
+ Publish bool `json:"publish"` // 是否发布
+ ErrMsg string `json:"err_msg"` // 错误信息
+ RawData map[string]interface{} `json:"raw_data"` // 原始数据 json
+ Power int `json:"power"` // 消耗算力
CreatedAt time.Time
}
diff --git a/web/src/assets/css/suno.styl b/web/src/assets/css/suno.styl
index becbd661..2ac02bc0 100644
--- a/web/src/assets/css/suno.styl
+++ b/web/src/assets/css/suno.styl
@@ -67,6 +67,30 @@
}
}
}
+
+ .tag-select {
+ position relative
+ overflow-x auto
+ overflow-y hidden
+ width 100%
+
+ .inner {
+ display flex
+ flex-flow row
+ padding-bottom 10px
+
+ .tag {
+ margin-right 10px
+ word-break keep-all
+ background-color #312C2C
+ color #e1e1e1
+ border-radius 5px
+ padding 3px 6px
+ cursor pointer
+ font-size 13px
+ }
+ }
+ }
}
}
.right-box {
diff --git a/web/src/components/ui/BlackInput.vue b/web/src/components/ui/BlackInput.vue
index 63fa9d0f..1c3a495d 100644
--- a/web/src/components/ui/BlackInput.vue
+++ b/web/src/components/ui/BlackInput.vue
@@ -19,7 +19,7 @@