mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-09-17 16:56:38 +08:00
add put url file for oss interface
This commit is contained in:
parent
e17dcf4d5f
commit
59301df073
@ -180,6 +180,7 @@ type SystemConfig struct {
|
|||||||
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
|
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
|
||||||
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
|
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
|
||||||
DallPower int `json:"dall_power,omitempty"` // DALLE3 绘图消耗算力
|
DallPower int `json:"dall_power,omitempty"` // DALLE3 绘图消耗算力
|
||||||
|
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
|
||||||
|
|
||||||
WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址
|
WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址
|
||||||
|
|
||||||
|
@ -80,14 +80,18 @@ type DallTask struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SunoTask struct {
|
type SunoTask struct {
|
||||||
Id int `json:"id"`
|
Id uint `json:"id"`
|
||||||
UserId string `json:"user_id"`
|
Channel string `json:"channel"`
|
||||||
|
UserId int `json:"user_id"`
|
||||||
Type int `json:"type"`
|
Type int `json:"type"`
|
||||||
TaskId string `json:"task_id"`
|
TaskId string `json:"task_id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
ReferenceId string `json:"reference_id"`
|
RefTaskId string `json:"ref_task_id"`
|
||||||
Prompt string `json:"prompt"`
|
RefSongId string `json:"ref_song_id"`
|
||||||
|
Lyrics string `json:"lyrics"` // 歌词:自定义模式
|
||||||
|
Prompt string `json:"prompt"` // 提示词:灵感模式
|
||||||
Tags string `json:"tags"`
|
Tags string `json:"tags"`
|
||||||
|
Model string `json:"model"`
|
||||||
Instrumental bool `json:"instrumental"` // 是否纯音乐
|
Instrumental bool `json:"instrumental"` // 是否纯音乐
|
||||||
ExtendSecs int `json:"extend_secs"` // 延长秒杀
|
ExtendSecs int `json:"extend_secs"` // 延长秒杀
|
||||||
}
|
}
|
||||||
|
@ -469,7 +469,7 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, sessi
|
|||||||
} else {
|
} else {
|
||||||
client = http.DefaultClient
|
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))
|
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
|
||||||
return client.Do(request)
|
return client.Do(request)
|
||||||
}
|
}
|
||||||
@ -607,7 +607,7 @@ func (h *ChatHandler) extractImgUrl(text string) string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
newImgURL, err := h.uploadManager.GetUploadHandler().PutImg(imageURL, false)
|
newImgURL, err := h.uploadManager.GetUploadHandler().PutUrlFile(imageURL, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("error with download image: ", err)
|
logger.Error("error with download image: ", err)
|
||||||
continue
|
continue
|
||||||
|
@ -8,13 +8,22 @@ package handler
|
|||||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"geekai/core"
|
"geekai/core"
|
||||||
|
"geekai/core/types"
|
||||||
|
"geekai/service/suno"
|
||||||
|
"geekai/store/model"
|
||||||
|
"geekai/store/vo"
|
||||||
|
"geekai/utils"
|
||||||
|
"geekai/utils/resp"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SunoHandler struct {
|
type SunoHandler struct {
|
||||||
BaseHandler
|
BaseHandler
|
||||||
|
service *suno.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSunoHandler(app *core.AppServer, db *gorm.DB) *SunoHandler {
|
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) {
|
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) {
|
func (h *SunoHandler) List(c *gin.Context) {
|
||||||
|
16
api/main.go
16
api/main.go
@ -23,6 +23,7 @@ import (
|
|||||||
"geekai/service/payment"
|
"geekai/service/payment"
|
||||||
"geekai/service/sd"
|
"geekai/service/sd"
|
||||||
"geekai/service/sms"
|
"geekai/service/sms"
|
||||||
|
"geekai/service/suno"
|
||||||
"geekai/service/wx"
|
"geekai/service/wx"
|
||||||
"geekai/store"
|
"geekai/store"
|
||||||
"io"
|
"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.NewAlipayService),
|
||||||
fx.Provide(payment.NewHuPiPay),
|
fx.Provide(payment.NewHuPiPay),
|
||||||
fx.Provide(payment.NewJPayService),
|
fx.Provide(payment.NewJPayService),
|
||||||
@ -475,6 +482,15 @@ func main() {
|
|||||||
group.GET("remove", h.Remove)
|
group.GET("remove", h.Remove)
|
||||||
group.GET("publish", h.Publish)
|
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) {
|
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
|
||||||
go func() {
|
go func() {
|
||||||
err := s.Run(db)
|
err := s.Run(db)
|
||||||
|
@ -166,7 +166,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
|||||||
Style: task.Style,
|
Style: task.Style,
|
||||||
Quality: task.Quality,
|
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").
|
r, err := s.httpClient.R().SetHeader("Content-Type", "application/json").
|
||||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||||
SetBody(reqBody).
|
SetBody(reqBody).
|
||||||
@ -259,7 +259,7 @@ func (s *Service) DownloadImages() {
|
|||||||
|
|
||||||
func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string, error) {
|
func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string, error) {
|
||||||
// sava image
|
// sava image
|
||||||
imgURL, err := s.uploadManager.GetUploadHandler().PutImg(orgURL, false)
|
imgURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(orgURL, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -139,7 +139,7 @@ func (p *ServicePool) DownloadImages() {
|
|||||||
if strings.HasPrefix(v.OrgURL, "https://cdn.discordapp.com") {
|
if strings.HasPrefix(v.OrgURL, "https://cdn.discordapp.com") {
|
||||||
proxy = true
|
proxy = true
|
||||||
}
|
}
|
||||||
imgURL, err := p.uploaderManager.GetUploadHandler().PutImg(v.OrgURL, proxy)
|
imgURL, err := p.uploaderManager.GetUploadHandler().PutUrlFile(v.OrgURL, proxy)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("error with download image %s, %v", v.OrgURL, err)
|
logger.Errorf("error with download image %s, %v", v.OrgURL, err)
|
||||||
|
@ -84,25 +84,25 @@ func (s AliYunOss) PutFile(ctx *gin.Context, name string) (File, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s AliYunOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
func (s AliYunOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||||
var imageData []byte
|
var fileData []byte
|
||||||
var err error
|
var err error
|
||||||
if useProxy {
|
if useProxy {
|
||||||
imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
|
fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
|
||||||
} else {
|
} else {
|
||||||
imageData, err = utils.DownloadImage(imageURL, "")
|
fileData, err = utils.DownloadImage(fileURL, "")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error with download image: %v", err)
|
return "", fmt.Errorf("error with download image: %v", err)
|
||||||
}
|
}
|
||||||
parse, err := url.Parse(imageURL)
|
parse, err := url.Parse(fileURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||||
}
|
}
|
||||||
fileExt := utils.GetImgExt(parse.Path)
|
fileExt := utils.GetImgExt(parse.Path)
|
||||||
objectKey := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -57,8 +57,8 @@ func (s LocalStorage) PutFile(ctx *gin.Context, name string) (File, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s LocalStorage) PutImg(imageURL string, useProxy bool) (string, error) {
|
func (s LocalStorage) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||||
parse, err := url.Parse(imageURL)
|
parse, err := url.Parse(fileURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
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 {
|
if useProxy {
|
||||||
err = utils.DownloadFile(imageURL, filePath, s.proxyURL)
|
err = utils.DownloadFile(fileURL, filePath, s.proxyURL)
|
||||||
} else {
|
} else {
|
||||||
err = utils.DownloadFile(imageURL, filePath, "")
|
err = utils.DownloadFile(fileURL, filePath, "")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error with download image: %v", err)
|
return "", fmt.Errorf("error with download image: %v", err)
|
||||||
|
@ -44,18 +44,18 @@ func NewMiniOss(appConfig *types.AppConfig) (MiniOss, error) {
|
|||||||
return MiniOss{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil
|
return MiniOss{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s MiniOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
func (s MiniOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||||
var imageData []byte
|
var fileData []byte
|
||||||
var err error
|
var err error
|
||||||
if useProxy {
|
if useProxy {
|
||||||
imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
|
fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
|
||||||
} else {
|
} else {
|
||||||
imageData, err = utils.DownloadImage(imageURL, "")
|
fileData, err = utils.DownloadImage(fileURL, "")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error with download image: %v", err)
|
return "", fmt.Errorf("error with download image: %v", err)
|
||||||
}
|
}
|
||||||
parse, err := url.Parse(imageURL)
|
parse, err := url.Parse(fileURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
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(),
|
context.Background(),
|
||||||
s.config.Bucket,
|
s.config.Bucket,
|
||||||
filename,
|
filename,
|
||||||
strings.NewReader(string(imageData)),
|
strings.NewReader(string(fileData)),
|
||||||
int64(len(imageData)),
|
int64(len(fileData)),
|
||||||
minio.PutObjectOptions{ContentType: "image/png"})
|
minio.PutObjectOptions{ContentType: "image/png"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -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) {
|
func (s QinNiuOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||||
var imageData []byte
|
var fileData []byte
|
||||||
var err error
|
var err error
|
||||||
if useProxy {
|
if useProxy {
|
||||||
imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
|
fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
|
||||||
} else {
|
} else {
|
||||||
imageData, err = utils.DownloadImage(imageURL, "")
|
fileData, err = utils.DownloadImage(fileURL, "")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error with download image: %v", err)
|
return "", fmt.Errorf("error with download image: %v", err)
|
||||||
}
|
}
|
||||||
parse, err := url.Parse(imageURL)
|
parse, err := url.Parse(fileURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
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{}
|
ret := storage.PutRet{}
|
||||||
extra := storage.PutExtra{}
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ type File struct {
|
|||||||
}
|
}
|
||||||
type Uploader interface {
|
type Uploader interface {
|
||||||
PutFile(ctx *gin.Context, name string) (File, error)
|
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)
|
PutBase64(imageData string) (string, error)
|
||||||
Delete(fileURL string) error
|
Delete(fileURL string) error
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package dalle
|
package suno
|
||||||
|
|
||||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||||
@ -8,11 +8,17 @@ package dalle
|
|||||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"geekai/core/types"
|
"geekai/core/types"
|
||||||
logger2 "geekai/logger"
|
logger2 "geekai/logger"
|
||||||
"geekai/service/oss"
|
"geekai/service/oss"
|
||||||
"geekai/store"
|
"geekai/store"
|
||||||
|
"geekai/store/model"
|
||||||
|
"geekai/utils"
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/imroc/req/v3"
|
"github.com/imroc/req/v3"
|
||||||
@ -57,10 +63,230 @@ func (s *Service) Run() {
|
|||||||
continue
|
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
|
||||||
}
|
}
|
||||||
|
@ -5,14 +5,16 @@ import "time"
|
|||||||
type SunoJob struct {
|
type SunoJob struct {
|
||||||
Id uint `gorm:"primarykey;column:id"`
|
Id uint `gorm:"primarykey;column:id"`
|
||||||
UserId int
|
UserId int
|
||||||
|
Channel string // 频道
|
||||||
Title string
|
Title string
|
||||||
Type string
|
Type int
|
||||||
TaskId string
|
TaskId string
|
||||||
ReferenceId string // 续写的任务id
|
RefTaskId string // 续写的任务id
|
||||||
Tags string // 歌曲风格和标签
|
Tags string // 歌曲风格和标签
|
||||||
Instrumental bool // 是否生成纯音乐
|
Instrumental bool // 是否生成纯音乐
|
||||||
ExtendSecs int // 续写秒数
|
ExtendSecs int // 续写秒数
|
||||||
SongId int // 续写的歌曲id
|
SongId string // 续写的歌曲id
|
||||||
|
RefSongId string
|
||||||
Prompt string // 提示词
|
Prompt string // 提示词
|
||||||
ThumbImgURL string // 缩略图 URL
|
ThumbImgURL string // 缩略图 URL
|
||||||
CoverImgURL string // 封面图 URL
|
CoverImgURL string // 封面图 URL
|
||||||
|
@ -3,27 +3,29 @@ package vo
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type SunoJob struct {
|
type SunoJob struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
UserId int `json:"user_id"`
|
UserId int `json:"user_id"`
|
||||||
Title string `json:"title"`
|
Channel string `json:"channel"`
|
||||||
Type string `json:"type"`
|
Title string `json:"title"`
|
||||||
TaskId string `json:"task_id"`
|
Type string `json:"type"`
|
||||||
ReferenceId string `json:"reference_id"` // 续写的任务id
|
TaskId string `json:"task_id"`
|
||||||
Tags string `json:"tags"` // 歌曲风格和标签
|
RefTaskId string `json:"ref_task_id"` // 续写的任务id
|
||||||
Instrumental bool `json:"instrumental"` // 是否生成纯音乐
|
Tags string `json:"tags"` // 歌曲风格和标签
|
||||||
ExtendSecs int `json:"extend_secs"` // 续写秒数
|
Instrumental bool `json:"instrumental"` // 是否生成纯音乐
|
||||||
SongId int `json:"song_id"` // 续写的歌曲id
|
ExtendSecs int `json:"extend_secs"` // 续写秒数
|
||||||
Prompt string `json:"prompt"` // 提示词
|
SongId string `json:"song_id"` // 续写的歌曲id
|
||||||
ThumbImgURL string `json:"thumb_img_url"` // 缩略图 URL
|
RefSongId string `json:"ref_song_id"` // 续写的歌曲id
|
||||||
CoverImgURL string `json:"cover_img_url"` // 封面图 URL
|
Prompt string `json:"prompt"` // 提示词
|
||||||
AudioURL string `json:"audio_url"` // 音频 URL
|
ThumbImgURL string `json:"thumb_img_url"` // 缩略图 URL
|
||||||
ModelName string `json:"model_name"` // 模型名称
|
CoverImgURL string `json:"cover_img_url"` // 封面图 URL
|
||||||
Progress int `json:"progress"` // 任务进度
|
AudioURL string `json:"audio_url"` // 音频 URL
|
||||||
Duration int `json:"duration"` // 银屏时长,秒
|
ModelName string `json:"model_name"` // 模型名称
|
||||||
Publish bool `json:"publish"` // 是否发布
|
Progress int `json:"progress"` // 任务进度
|
||||||
ErrMsg string `json:"err_msg"` // 错误信息
|
Duration int `json:"duration"` // 银屏时长,秒
|
||||||
RawData string `json:"raw_data"` // 原始数据 json
|
Publish bool `json:"publish"` // 是否发布
|
||||||
Power int `json:"power"` // 消耗算力
|
ErrMsg string `json:"err_msg"` // 错误信息
|
||||||
|
RawData map[string]interface{} `json:"raw_data"` // 原始数据 json
|
||||||
|
Power int `json:"power"` // 消耗算力
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
.right-box {
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import {ref} from "vue";
|
import {ref, watch} from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
value : {
|
value : {
|
||||||
@ -43,6 +43,9 @@ const props = defineProps({
|
|||||||
default: 1024
|
default: 1024
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
watch(() => props.value, (newValue) => {
|
||||||
|
model.value = newValue
|
||||||
|
})
|
||||||
const model = ref(props.value)
|
const model = ref(props.value)
|
||||||
const emits = defineEmits(['update:value']);
|
const emits = defineEmits(['update:value']);
|
||||||
const onInput = (value) => {
|
const onInput = (value) => {
|
||||||
|
@ -10,11 +10,11 @@
|
|||||||
|
|
||||||
<div class="params">
|
<div class="params">
|
||||||
<div class="pure-music">
|
<div class="pure-music">
|
||||||
<span class="switch"><black-switch v-model:value="instrumental" size="default" /></span>
|
<span class="switch"><black-switch v-model:value="data.instrumental" size="default" /></span>
|
||||||
<span class="text">纯音乐</span>
|
<span class="text">纯音乐</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="custom">
|
<div v-if="custom">
|
||||||
<div class="item-group" v-if="!instrumental">
|
<div class="item-group" v-if="!data.instrumental">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="text">歌词</span>
|
<span class="text">歌词</span>
|
||||||
<el-popover placement="right"
|
<el-popover placement="right"
|
||||||
@ -44,20 +44,20 @@
|
|||||||
</el-icon>
|
</el-icon>
|
||||||
</template>
|
</template>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
<div class="tag-select">
|
|
||||||
<el-tag
|
|
||||||
v-for="tag in tags"
|
|
||||||
:key="tag"
|
|
||||||
:type="tag === data.tags ? 'success' : ''"
|
|
||||||
:hit="tag === data.tags"
|
|
||||||
style="margin-right: 10px"
|
|
||||||
@click="data.tags = tag"
|
|
||||||
>{{ tag }}</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<black-input v-model:value="data.tags" type="textarea" :maxlength="120" :rows="3" placeholder="请输入音乐风格,多个风格之间用英文逗号隔开..."/>
|
<black-input v-model:value="data.tags" type="textarea" :maxlength="120" :rows="3" placeholder="请输入音乐风格,多个风格之间用英文逗号隔开..."/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tag-select">
|
||||||
|
<div class="inner">
|
||||||
|
<span
|
||||||
|
class="tag"
|
||||||
|
@click="selectTag(tag)"
|
||||||
|
v-for="tag in tags"
|
||||||
|
:key="tag.value">{{ tag.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item-group">
|
<div class="item-group">
|
||||||
@ -93,7 +93,7 @@
|
|||||||
</el-popover>
|
</el-popover>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<black-input v-model:value="data.lyrics" type="textarea" :rows="10" placeholder="例如:一首关于鸟人的摇滚歌曲..."/>
|
<black-input v-model:value="data.prompt" type="textarea" :rows="10" placeholder="例如:一首关于鸟人的摇滚歌曲..."/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -182,21 +182,41 @@ import BlackSwitch from "@/components/ui/BlackSwitch.vue";
|
|||||||
import BlackInput from "@/components/ui/BlackInput.vue";
|
import BlackInput from "@/components/ui/BlackInput.vue";
|
||||||
import MusicPlayer from "@/components/MusicPlayer.vue";
|
import MusicPlayer from "@/components/MusicPlayer.vue";
|
||||||
import {compact} from "lodash";
|
import {compact} from "lodash";
|
||||||
|
import {httpPost} from "@/utils/http";
|
||||||
|
import {showMessageError, showMessageOK} from "@/utils/dialog";
|
||||||
|
import {showSuccessToast} from "vant";
|
||||||
|
|
||||||
const winHeight = ref(window.innerHeight - 50)
|
const winHeight = ref(window.innerHeight - 50)
|
||||||
const custom = ref(false)
|
const custom = ref(false)
|
||||||
const instrumental = ref(false)
|
|
||||||
const models = ref([
|
const models = ref([
|
||||||
{label: "v3.0", value: "chirp-v3-0"},
|
{label: "v3.0", value: "chirp-v3-0"},
|
||||||
{label: "v3.5", value:"chirp-v3-5"}
|
{label: "v3.5", value:"chirp-v3-5"}
|
||||||
])
|
])
|
||||||
const tags = ref([])
|
const tags = ref([
|
||||||
|
{label: "女声", value: "female vocals"},
|
||||||
|
{label: "男声", value: "male vocals"},
|
||||||
|
{label: "流行", value: "pop"},
|
||||||
|
{label: "摇滚", value: "rock"},
|
||||||
|
{label: "硬摇滚", value: "hard rock"},
|
||||||
|
{label: "电音", value: "electronic"},
|
||||||
|
{label: "金属", value: "metal"},
|
||||||
|
{label: "重金属", value: "heavy metal"},
|
||||||
|
{label: "节拍", value: "beat"},
|
||||||
|
{label: "弱拍", value: "upbeat"},
|
||||||
|
{label: "合成器", value: "synth"},
|
||||||
|
{label: "吉他", value: "guitar"},
|
||||||
|
{label: "钢琴", value: "piano"},
|
||||||
|
{label: "小提琴", value: "violin"},
|
||||||
|
{label: "贝斯", value: "bass"},
|
||||||
|
{label: "嘻哈", value: "hip hop"},
|
||||||
|
])
|
||||||
const data = ref({
|
const data = ref({
|
||||||
model: "chirp-v3-0",
|
model: "chirp-v3-0",
|
||||||
tags: "",
|
tags: "",
|
||||||
lyrics: "",
|
lyrics: "",
|
||||||
prompt: "",
|
prompt: "",
|
||||||
title: ""
|
title: "",
|
||||||
|
instrumental:false
|
||||||
})
|
})
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const noData = ref(false)
|
const noData = ref(false)
|
||||||
@ -247,7 +267,14 @@ const list = ref([
|
|||||||
])
|
])
|
||||||
|
|
||||||
const create = () => {
|
const create = () => {
|
||||||
|
data.value.type = custom.value ? 2 : 1
|
||||||
console.log(data.value)
|
console.log(data.value)
|
||||||
|
httpPost("/api/suno/create", data.value).then(res => {
|
||||||
|
console.log(res)
|
||||||
|
showMessageOK("创建任务成功")
|
||||||
|
}).catch(e => {
|
||||||
|
showMessageError("创建任务失败:"+e.message)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const play = (item) => {
|
const play = (item) => {
|
||||||
@ -261,6 +288,14 @@ const duration = (secs) => {
|
|||||||
const seconds = secs%60
|
const seconds = secs%60
|
||||||
return `${minutes}:${seconds}`
|
return `${minutes}:${seconds}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectTag = (tag) => {
|
||||||
|
if (data.value.tags.length + tag.value.length >= 119) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.value.tags = compact([...data.value.tags.split(","), tag.value]).join(",")
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
|
@ -254,6 +254,9 @@
|
|||||||
<el-form-item label="DALL-E-3算力" prop="dall_power">
|
<el-form-item label="DALL-E-3算力" prop="dall_power">
|
||||||
<el-input v-model.number="system['dall_power']" placeholder="使用DALL-E-3画一张图消耗算力"/>
|
<el-input v-model.number="system['dall_power']" placeholder="使用DALL-E-3画一张图消耗算力"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="Suno 算力" prop="suno_power">
|
||||||
|
<el-input v-model.number="system['suno_power']" placeholder="使用 Suno 生成一首音乐消耗算力"/>
|
||||||
|
</el-form-item>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="众筹支付">
|
<el-tab-pane label="众筹支付">
|
||||||
<el-form-item label="启用众筹功能" prop="enabled_reward">
|
<el-form-item label="启用众筹功能" prop="enabled_reward">
|
||||||
|
Loading…
Reference in New Issue
Block a user