mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 16:23:42 +08:00 
			
		
		
		
	suno add new function for merging full songs and upload custom music
This commit is contained in:
		@@ -87,11 +87,13 @@ type SunoTask struct {
 | 
			
		||||
	Type         int    `json:"type"`
 | 
			
		||||
	TaskId       string `json:"task_id"`
 | 
			
		||||
	Title        string `json:"title"`
 | 
			
		||||
	RefTaskId    string `json:"ref_task_id"`
 | 
			
		||||
	RefSongId    string `json:"ref_song_id"`
 | 
			
		||||
	RefTaskId    string `json:"ref_task_id,omitempty"`
 | 
			
		||||
	RefSongId    string `json:"ref_song_id,omitempty"`
 | 
			
		||||
	Prompt       string `json:"prompt"` // 提示词/歌词
 | 
			
		||||
	Tags         string `json:"tags"`
 | 
			
		||||
	Model        string `json:"model"`
 | 
			
		||||
	Instrumental bool   `json:"instrumental"` // 是否纯音乐
 | 
			
		||||
	ExtendSecs   int    `json:"extend_secs"`  // 延长秒杀
 | 
			
		||||
	Instrumental bool   `json:"instrumental"`          // 是否纯音乐
 | 
			
		||||
	ExtendSecs   int    `json:"extend_secs,omitempty"` // 延长秒杀
 | 
			
		||||
	SongId       string `json:"song_id,omitempty"`     // 合并歌曲ID
 | 
			
		||||
	AudioURL     string `json:"audio_url"`             // 用户上传音频地址
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@ package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/store/vo"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
@@ -59,23 +58,16 @@ func (h *InviteHandler) Code(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
// List Log 用户邀请记录
 | 
			
		||||
func (h *InviteHandler) List(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Page     int `json:"page"`
 | 
			
		||||
		PageSize int `json:"page_size"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	page := h.GetInt(c, "page", 1)
 | 
			
		||||
	pageSize := h.GetInt(c, "page_size", 20)
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	session := h.DB.Session(&gorm.Session{}).Where("inviter_id = ?", userId)
 | 
			
		||||
	var total int64
 | 
			
		||||
	session.Model(&model.InviteLog{}).Count(&total)
 | 
			
		||||
	var items []model.InviteLog
 | 
			
		||||
	var list = make([]vo.InviteLog, 0)
 | 
			
		||||
	offset := (data.Page - 1) * data.PageSize
 | 
			
		||||
	res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
 | 
			
		||||
	offset := (page - 1) * pageSize
 | 
			
		||||
	res := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&items)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		for _, item := range items {
 | 
			
		||||
			var v vo.InviteLog
 | 
			
		||||
@@ -89,7 +81,7 @@ func (h *InviteHandler) List(c *gin.Context) {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
 | 
			
		||||
	resp.SUCCESS(c, vo.NewPage(total, page, pageSize, list))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Hits 访问邀请码
 | 
			
		||||
 
 | 
			
		||||
@@ -72,15 +72,32 @@ func (h *SunoHandler) Create(c *gin.Context) {
 | 
			
		||||
		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
 | 
			
		||||
		RefTaskId    string `json:"ref_task_id"`         // 续写的任务id
 | 
			
		||||
		ExtendSecs   int    `json:"extend_secs"`         // 续写秒数
 | 
			
		||||
		RefSongId    string `json:"ref_song_id"`         // 续写的歌曲id
 | 
			
		||||
		SongId       string `json:"song_id,omitempty"`   // 要拼接的歌曲id
 | 
			
		||||
		AudioURL     string `json:"audio_url,omitempty"` // 上传自己创作的歌曲
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 歌曲拼接
 | 
			
		||||
	if data.SongId != "" && data.Type == 3 {
 | 
			
		||||
		var song model.SunoJob
 | 
			
		||||
		if err := h.DB.Where("song_id = ?", data.SongId).First(&song).Error; err == nil {
 | 
			
		||||
			data.Instrumental = song.Instrumental
 | 
			
		||||
			data.Model = song.ModelName
 | 
			
		||||
			data.Tags = song.Tags
 | 
			
		||||
		}
 | 
			
		||||
		// 拼接歌词
 | 
			
		||||
		var refSong model.SunoJob
 | 
			
		||||
		if err := h.DB.Where("song_id = ?", data.RefSongId).First(&refSong).Error; err == nil {
 | 
			
		||||
			data.Prompt = fmt.Sprintf("%s\n%s", song.Prompt, refSong.Prompt)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 插入数据库
 | 
			
		||||
	job := model.SunoJob{
 | 
			
		||||
		UserId:       int(h.GetLoginUserId(c)),
 | 
			
		||||
@@ -118,6 +135,8 @@ func (h *SunoHandler) Create(c *gin.Context) {
 | 
			
		||||
		Tags:         data.Tags,
 | 
			
		||||
		Model:        data.Model,
 | 
			
		||||
		Instrumental: data.Instrumental,
 | 
			
		||||
		SongId:       data.SongId,
 | 
			
		||||
		AudioURL:     data.AudioURL,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// update user's power
 | 
			
		||||
 
 | 
			
		||||
@@ -400,7 +400,7 @@ func main() {
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.InviteHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/invite/")
 | 
			
		||||
			group.GET("code", h.Code)
 | 
			
		||||
			group.POST("list", h.List)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.GET("hits", h.Hits)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -82,14 +82,21 @@ func (s *Service) Run() {
 | 
			
		||||
				logger.Errorf("taking task with error: %v", err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			r, err := s.Create(task)
 | 
			
		||||
			var r RespVo
 | 
			
		||||
			if task.Type == 3 && task.SongId != "" { // 歌曲拼接
 | 
			
		||||
				r, err = s.Merge(task)
 | 
			
		||||
			} else if task.Type == 4 && task.AudioURL != "" { // 上传歌曲
 | 
			
		||||
				r, err = s.Upload(task)
 | 
			
		||||
			} else { // 歌曲创作
 | 
			
		||||
				r, err = s.Create(task)
 | 
			
		||||
			}
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Errorf("create task with error: %v", err)
 | 
			
		||||
				s.db.Model(&model.SunoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
 | 
			
		||||
					"err_msg":  err.Error(),
 | 
			
		||||
					"progress": service.FailTaskProgress,
 | 
			
		||||
				})
 | 
			
		||||
				s.notifyQueue.RPush(service.NotifyMessage{UserId: task.UserId, JobId: int(task.Id), Message: service.TaskStatusFailed})
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@@ -138,7 +145,94 @@ func (s *Service) Create(task types.SunoTask) (RespVo, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var res RespVo
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/task/suno/v1/submit/music", apiKey.ApiURL)
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/suno/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数据失败:%v, %s", err, string(body))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != "success" {
 | 
			
		||||
		return RespVo{}, fmt.Errorf("API 返回失败:%s", res.Message)
 | 
			
		||||
	}
 | 
			
		||||
	// update the last_use_at for api key
 | 
			
		||||
	apiKey.LastUsedAt = time.Now().Unix()
 | 
			
		||||
	session.Updates(&apiKey)
 | 
			
		||||
	res.Channel = apiKey.ApiURL
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) Merge(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{}{
 | 
			
		||||
		"clip_id":   task.SongId,
 | 
			
		||||
		"is_infill": false,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var res RespVo
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/suno/submit/concat", 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数据失败:%v, %s", err, string(body))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != "success" {
 | 
			
		||||
		return RespVo{}, fmt.Errorf("API 返回失败:%s", res.Message)
 | 
			
		||||
	}
 | 
			
		||||
	// update the last_use_at for api key
 | 
			
		||||
	apiKey.LastUsedAt = time.Now().Unix()
 | 
			
		||||
	session.Updates(&apiKey)
 | 
			
		||||
	res.Channel = apiKey.ApiURL
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) Upload(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{}{
 | 
			
		||||
		"url": task.AudioURL,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var res RespVo
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/suno/uploads/audio-url", apiKey.ApiURL)
 | 
			
		||||
	logger.Debugf("API URL: %s, request body: %+v", apiURL, reqBody)
 | 
			
		||||
	r, err := req.C().R().
 | 
			
		||||
		SetHeader("Authorization", "Bearer "+apiKey.Value).
 | 
			
		||||
@@ -339,7 +433,7 @@ func (s *Service) QueryTask(taskId string, channel string) (QueryRespVo, error)
 | 
			
		||||
		return QueryRespVo{}, errors.New("no available API KEY for Suno")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/task/suno/v1/fetch/%s", apiKey.ApiURL, taskId)
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/suno/fetch/%s", apiKey.ApiURL, taskId)
 | 
			
		||||
	var res QueryRespVo
 | 
			
		||||
	r, err := req.C().R().SetHeader("Authorization", "Bearer "+apiKey.Value).Get(apiURL)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ type SunoJob struct {
 | 
			
		||||
	UserId       int                    `json:"user_id"`
 | 
			
		||||
	Channel      string                 `json:"channel"`
 | 
			
		||||
	Title        string                 `json:"title"`
 | 
			
		||||
	Type         string                 `json:"type"`
 | 
			
		||||
	Type         int                    `json:"type"`
 | 
			
		||||
	TaskId       string                 `json:"task_id"`
 | 
			
		||||
	RefTaskId    string                 `json:"ref_task_id"`  // 续写的任务id
 | 
			
		||||
	Tags         string                 `json:"tags"`         // 歌曲风格和标签
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,13 @@
 | 
			
		||||
      display flex
 | 
			
		||||
      flex-flow row
 | 
			
		||||
      justify-content: space-between;
 | 
			
		||||
 | 
			
		||||
      .upload-music {
 | 
			
		||||
        .iconfont {
 | 
			
		||||
          margin-right 5px
 | 
			
		||||
          font-size 14px
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .params {
 | 
			
		||||
@@ -85,6 +92,10 @@
 | 
			
		||||
            height 50px
 | 
			
		||||
            border-radius 10px
 | 
			
		||||
          }
 | 
			
		||||
          .icon-mp3 {
 | 
			
		||||
            font-size 42px
 | 
			
		||||
            color #A85295
 | 
			
		||||
          }
 | 
			
		||||
          .title {
 | 
			
		||||
            display flex
 | 
			
		||||
            margin-left 10px
 | 
			
		||||
@@ -266,7 +277,7 @@
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .right {
 | 
			
		||||
          min-width 320px;
 | 
			
		||||
          min-width 350px;
 | 
			
		||||
          font-size 14px
 | 
			
		||||
          padding 0 15px
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
@font-face {
 | 
			
		||||
  font-family: "iconfont"; /* Project id 4125778 */
 | 
			
		||||
  src: url('iconfont.woff2?t=1723593727785') format('woff2'),
 | 
			
		||||
       url('iconfont.woff?t=1723593727785') format('woff'),
 | 
			
		||||
       url('iconfont.ttf?t=1723593727785') format('truetype');
 | 
			
		||||
  src: url('iconfont.woff2?t=1725000514997') format('woff2'),
 | 
			
		||||
       url('iconfont.woff?t=1725000514997') format('woff'),
 | 
			
		||||
       url('iconfont.ttf?t=1725000514997') format('truetype');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.iconfont {
 | 
			
		||||
@@ -13,6 +13,18 @@
 | 
			
		||||
  -moz-osx-font-smoothing: grayscale;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon-merge:before {
 | 
			
		||||
  content: "\e901";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon-upload:before {
 | 
			
		||||
  content: "\e611";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon-concat:before {
 | 
			
		||||
  content: "\e630";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon-email:before {
 | 
			
		||||
  content: "\e670";
 | 
			
		||||
}
 | 
			
		||||
@@ -77,7 +89,7 @@
 | 
			
		||||
  content: "\e608";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon-mp:before {
 | 
			
		||||
.icon-mp3:before {
 | 
			
		||||
  content: "\e6c4";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,10 +1,31 @@
 | 
			
		||||
{
 | 
			
		||||
  "id": "4125778",
 | 
			
		||||
  "name": "chatgpt",
 | 
			
		||||
  "name": "geekai",
 | 
			
		||||
  "font_family": "iconfont",
 | 
			
		||||
  "css_prefix_text": "icon-",
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "glyphs": [
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "8094809",
 | 
			
		||||
      "name": "merge-cells",
 | 
			
		||||
      "font_class": "merge",
 | 
			
		||||
      "unicode": "e901",
 | 
			
		||||
      "unicode_decimal": 59649
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "10278208",
 | 
			
		||||
      "name": "上传",
 | 
			
		||||
      "font_class": "upload",
 | 
			
		||||
      "unicode": "e611",
 | 
			
		||||
      "unicode_decimal": 58897
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "23538484",
 | 
			
		||||
      "name": "拼接",
 | 
			
		||||
      "font_class": "concat",
 | 
			
		||||
      "unicode": "e630",
 | 
			
		||||
      "unicode_decimal": 58928
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "15838472",
 | 
			
		||||
      "name": "email",
 | 
			
		||||
@@ -120,7 +141,7 @@
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "4318807",
 | 
			
		||||
      "name": "mp3",
 | 
			
		||||
      "font_class": "mp",
 | 
			
		||||
      "font_class": "mp3",
 | 
			
		||||
      "unicode": "e6c4",
 | 
			
		||||
      "unicode_decimal": 59076
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -33,11 +33,10 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {onMounted, ref, watch} from "vue";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import {onMounted, ref} from "vue";
 | 
			
		||||
import {httpGet} from "@/utils/http";
 | 
			
		||||
import {ElMessage} from "element-plus";
 | 
			
		||||
import {dateFormat} from "@/utils/libs";
 | 
			
		||||
import {DocumentCopy} from "@element-plus/icons-vue";
 | 
			
		||||
import Clipboard from "clipboard";
 | 
			
		||||
 | 
			
		||||
const items = ref([])
 | 
			
		||||
@@ -60,7 +59,7 @@ onMounted(() => {
 | 
			
		||||
 | 
			
		||||
// 获取数据
 | 
			
		||||
const fetchData = () => {
 | 
			
		||||
  httpPost('/api/invite/list', {page: page.value, page_size: pageSize.value}).then((res) => {
 | 
			
		||||
  httpGet('/api/invite/list', {page: page.value, page_size: pageSize.value}).then((res) => {
 | 
			
		||||
    if (res.data) {
 | 
			
		||||
      items.value = res.data.items
 | 
			
		||||
      total.value = res.data.total
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@
 | 
			
		||||
 | 
			
		||||
        <div class="params">
 | 
			
		||||
          <div class="item-group">
 | 
			
		||||
            <span class="label">循环</span>
 | 
			
		||||
            <span class="label">循环参考图</span>
 | 
			
		||||
            <el-switch  v-model="loop" size="small" style="--el-switch-on-color:#BF78BF;" />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="item-group">
 | 
			
		||||
 
 | 
			
		||||
@@ -211,6 +211,11 @@ const licenseConfig = ref({})
 | 
			
		||||
const enableVerify = ref(false)
 | 
			
		||||
const captchaRef = ref(null)
 | 
			
		||||
 | 
			
		||||
// 记录邀请码点击次数
 | 
			
		||||
if (data.value.invite_code) {
 | 
			
		||||
  httpGet("/api/invite/hits",{code: data.value.invite_code})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
getSystemInfo().then(res => {
 | 
			
		||||
  if (res.data) {
 | 
			
		||||
    title.value = res.data.title
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,20 @@
 | 
			
		||||
        <el-tooltip effect="light" content="定义模式" placement="top">
 | 
			
		||||
          <black-switch  v-model:value="custom" size="large"  />
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
        <el-tooltip effect="light" content="请上传6-60秒的原始音频,检测到人声的音频将仅设为私人音频。" placement="bottom-end">
 | 
			
		||||
          <el-upload
 | 
			
		||||
              class="avatar-uploader"
 | 
			
		||||
              :auto-upload="true"
 | 
			
		||||
              :show-file-list="false"
 | 
			
		||||
              :http-request="uploadAudio"
 | 
			
		||||
              accept=".wav,.mp3"
 | 
			
		||||
          >
 | 
			
		||||
          <el-button class="upload-music" color="#363030" round>
 | 
			
		||||
            <i class="iconfont icon-upload"></i>
 | 
			
		||||
            <span>上传音乐</span>
 | 
			
		||||
          </el-button>
 | 
			
		||||
          </el-upload>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
        <black-select v-model:value="data.model" :options="models" placeholder="请选择模型" style="width: 100px" />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@@ -28,10 +42,10 @@
 | 
			
		||||
              </el-popover>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="item"
 | 
			
		||||
                 v-loading="generating"
 | 
			
		||||
                 v-loading="isGenerating"
 | 
			
		||||
                 element-loading-text="正在生成歌词..."
 | 
			
		||||
                 element-loading-background="rgba(122, 122, 122, 0.8)">
 | 
			
		||||
              <black-input v-model:value="data.lyrics" type="textarea" :rows="10" placeholder="请在这里输入你自己写的歌词..."/>
 | 
			
		||||
              <black-input v-model:value="data.lyrics" type="textarea" :rows="10" :placeholder="promptPlaceholder"/>
 | 
			
		||||
              <button class="btn btn-lyric" @click="createLyric">生成歌词</button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
@@ -137,7 +151,7 @@
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="right-box" v-loading="loading" element-loading-background="rgba(100,100,100,0.3)">
 | 
			
		||||
      <div class="list-box" v-if="!noData">
 | 
			
		||||
        <div v-for="item in list">
 | 
			
		||||
        <div v-for="item in list" :key="item.id">
 | 
			
		||||
          <div class="item" v-if="item.progress === 100">
 | 
			
		||||
            <div class="left">
 | 
			
		||||
              <div class="container">
 | 
			
		||||
@@ -151,13 +165,18 @@
 | 
			
		||||
            <div class="center">
 | 
			
		||||
              <div class="title">
 | 
			
		||||
                <a :href="'/song/'+item.song_id"  target="_blank">{{item.title}}</a>
 | 
			
		||||
                <span class="model">{{item.major_model_version}}</span>
 | 
			
		||||
                <span class="model" v-if="item.major_model_version">{{item.major_model_version}}</span>
 | 
			
		||||
                <span class="model" v-if="item.type === 4">用户上传</span>
 | 
			
		||||
                <span class="model" v-if="item.type === 3">
 | 
			
		||||
                  <i class="iconfont icon-mp3"></i>
 | 
			
		||||
                  完整歌曲
 | 
			
		||||
                </span>
 | 
			
		||||
                <span class="model" v-if="item.ref_song">
 | 
			
		||||
                    <i class="iconfont icon-link"></i>
 | 
			
		||||
                    {{item.ref_song.title}}
 | 
			
		||||
                  </span>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="tags">{{item.tags}}</div>
 | 
			
		||||
              <div class="tags" v-if="item.tags">{{item.tags}}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="right">
 | 
			
		||||
              <div class="tools">
 | 
			
		||||
@@ -178,6 +197,12 @@
 | 
			
		||||
                  </a>
 | 
			
		||||
                </el-tooltip>
 | 
			
		||||
 | 
			
		||||
                <el-tooltip effect="light" content="获取完整歌曲" placement="top" v-if="item.ref_song">
 | 
			
		||||
                  <button class="btn btn-icon" @click="merge(item)">
 | 
			
		||||
                    <i class="iconfont icon-concat"></i>
 | 
			
		||||
                  </button>
 | 
			
		||||
                </el-tooltip>
 | 
			
		||||
 | 
			
		||||
                <el-tooltip effect="light" content="复制歌曲链接" placement="top">
 | 
			
		||||
                  <button class="btn btn-icon copy-link" :data-clipboard-text="getShareURL(item)" >
 | 
			
		||||
                    <i class="iconfont icon-share1"></i>
 | 
			
		||||
@@ -276,13 +301,13 @@ import MusicPlayer from "@/components/MusicPlayer.vue";
 | 
			
		||||
import {compact} from "lodash";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import {showMessageError, showMessageOK} from "@/utils/dialog";
 | 
			
		||||
import Generating from "@/components/ui/Generating.vue";
 | 
			
		||||
import {checkSession} from "@/store/cache";
 | 
			
		||||
import {ElMessage, ElMessageBox} from "element-plus";
 | 
			
		||||
import {formatTime} from "@/utils/libs";
 | 
			
		||||
import Clipboard from "clipboard";
 | 
			
		||||
import BlackDialog from "@/components/ui/BlackDialog.vue";
 | 
			
		||||
import Compressor from "compressorjs";
 | 
			
		||||
import Generating from "@/components/ui/Generating.vue";
 | 
			
		||||
 | 
			
		||||
const winHeight = ref(window.innerHeight - 50)
 | 
			
		||||
const custom = ref(false)
 | 
			
		||||
@@ -329,6 +354,7 @@ const btnText = ref("开始创作")
 | 
			
		||||
const refSong = ref(null)
 | 
			
		||||
const showDialog = ref(false)
 | 
			
		||||
const editData = ref({title:"",cover:"",id:0})
 | 
			
		||||
const promptPlaceholder = ref('请在这里输入你自己写的歌词...')
 | 
			
		||||
 | 
			
		||||
const socket = ref(null)
 | 
			
		||||
const userId = ref(0)
 | 
			
		||||
@@ -422,7 +448,11 @@ const create = () => {
 | 
			
		||||
  data.value.ref_task_id = refSong.value ? refSong.value.task_id : ""
 | 
			
		||||
  data.value.ref_song_id = refSong.value ? refSong.value.song_id : ""
 | 
			
		||||
  data.value.extend_secs = refSong.value ? refSong.value.extend_secs : 0
 | 
			
		||||
  if (custom.value) {
 | 
			
		||||
  if (refSong.value) {
 | 
			
		||||
    if (data.value.extend_secs > refSong.value.duration) {
 | 
			
		||||
      return showMessageError("续写开始时间不能超过原歌曲长度")
 | 
			
		||||
    }
 | 
			
		||||
  } else if (custom.value) {
 | 
			
		||||
    if (data.value.lyrics === "") {
 | 
			
		||||
      return showMessageError("请输入歌词")
 | 
			
		||||
    }
 | 
			
		||||
@@ -434,9 +464,6 @@ const create = () => {
 | 
			
		||||
      return showMessageError("请输入歌曲描述")
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (refSong.value && data.value.extend_secs > refSong.value.duration) {
 | 
			
		||||
    return showMessageError("续写开始时间不能超过原歌曲长度")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  httpPost("/api/suno/create", data.value).then(() => {
 | 
			
		||||
    fetchData(1)
 | 
			
		||||
@@ -446,6 +473,35 @@ const create = () => {
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 拼接歌曲
 | 
			
		||||
const merge = (item) => {
 | 
			
		||||
  httpPost("/api/suno/create", {song_id: item.song_id, type:3}).then(() => {
 | 
			
		||||
    fetchData(1)
 | 
			
		||||
    showMessageOK("创建任务成功")
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    showMessageError("合并歌曲失败:"+e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const uploadAudio = (file) => {
 | 
			
		||||
  const formData = new FormData();
 | 
			
		||||
  formData.append('file', file.file, file.name);
 | 
			
		||||
  // 执行上传操作
 | 
			
		||||
  httpPost('/api/upload', formData).then((res) => {
 | 
			
		||||
    httpPost("/api/suno/create", {audio_url: res.data.url, title:res.data.name, type:4}).then(() => {
 | 
			
		||||
      fetchData(1)
 | 
			
		||||
      showMessageOK("歌曲上传成功")
 | 
			
		||||
    }).catch(e => {
 | 
			
		||||
      showMessageError("歌曲上传失败:"+e.message)
 | 
			
		||||
    })
 | 
			
		||||
    removeRefSong()
 | 
			
		||||
    ElMessage.success({message: "上传成功", duration: 500})
 | 
			
		||||
  }).catch((e) => {
 | 
			
		||||
    ElMessage.error('文件传失败:' + e.message)
 | 
			
		||||
  })
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// 续写歌曲
 | 
			
		||||
const extend = (item) => {
 | 
			
		||||
  refSong.value = item
 | 
			
		||||
@@ -453,6 +509,7 @@ const extend = (item) => {
 | 
			
		||||
  data.value.title = item.title
 | 
			
		||||
  custom.value = true
 | 
			
		||||
  btnText.value = "续写歌曲"
 | 
			
		||||
  promptPlaceholder.value = "输入额外的歌词,根据您之前的歌词来扩展歌曲..."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 更细歌曲
 | 
			
		||||
@@ -485,6 +542,7 @@ watch(() => custom.value, (newValue) => {
 | 
			
		||||
const removeRefSong = () => {
 | 
			
		||||
  refSong.value = null
 | 
			
		||||
  btnText.value = "开始创作"
 | 
			
		||||
  promptPlaceholder.value = "请在这里输入你自己写的歌词..."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const play = (item) => {
 | 
			
		||||
@@ -553,21 +611,21 @@ const uploadCover = (file) => {
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const generating = ref(false)
 | 
			
		||||
const isGenerating = ref(false)
 | 
			
		||||
const createLyric = () => {
 | 
			
		||||
  if (data.value.lyrics === "") {
 | 
			
		||||
    return showMessageError("请输入歌词描述")
 | 
			
		||||
  }
 | 
			
		||||
  generating.value = true
 | 
			
		||||
  isGenerating.value = true
 | 
			
		||||
  httpPost("/api/suno/lyric", {prompt: data.value.lyrics}).then(res => {
 | 
			
		||||
    const lines = res.data.split('\n');
 | 
			
		||||
    data.value.title = lines.shift().replace(/\*/g,"")
 | 
			
		||||
    lines.shift()
 | 
			
		||||
    data.value.lyrics = lines.join('\n');
 | 
			
		||||
    generating.value = false
 | 
			
		||||
    isGenerating.value = false
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    showMessageError("歌词生成失败:"+e.message)
 | 
			
		||||
    generating.value = false
 | 
			
		||||
    isGenerating.value = false
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user