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 @@