mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 16:23:42 +08:00 
			
		
		
		
	add function to generate lyrics
This commit is contained in:
		@@ -6,7 +6,9 @@
 | 
			
		||||
* 功能优化:增加 session 和系统配置缓存,确保每个页面只进行一次 session 和 get system config 请求
 | 
			
		||||
* 功能优化:在应用列表页面,无需先添加模型到用户工作区,可以直接使用
 | 
			
		||||
* 功能新增:MJ 绘图失败的任务不会自动删除,而是会在列表页显示失败详细错误信息
 | 
			
		||||
* 功能新增:允许在设置首页纯色背景,背景图片,随机背景图片三种背景模式
 | 
			
		||||
* 功能新增:允许在管理后台设置首页显示的导航菜单
 | 
			
		||||
* Bug修复:修复注册页面先显示关闭注册组件,然后再显示注册组件
 | 
			
		||||
* 功能新增:增加 Suno 文生歌曲功能
 | 
			
		||||
* 功能优化:移除多平台模型支持,统一使用 one-api 接口形式,其他平台的模型需要通过 one-api 接口添加
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -83,7 +83,7 @@ func errorHandler(c *gin.Context) {
 | 
			
		||||
		if r := recover(); r != nil {
 | 
			
		||||
			logger.Errorf("Handler Panic: %v", r)
 | 
			
		||||
			debug.PrintStack()
 | 
			
		||||
			c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
 | 
			
		||||
			c.JSON(http.StatusBadRequest, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
 | 
			
		||||
			c.Abort()
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
@@ -225,6 +225,8 @@ func needLogin(c *gin.Context) bool {
 | 
			
		||||
		c.Request.URL.Path == "/api/payment/doPay" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/payment/payWays" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/suno/client" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/suno/Detail" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/suno/play" ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/user/clogin") ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/config/") ||
 | 
			
		||||
 
 | 
			
		||||
@@ -113,7 +113,7 @@ func (h *SunoHandler) Create(c *gin.Context) {
 | 
			
		||||
		RefTaskId:    data.RefTaskId,
 | 
			
		||||
		RefSongId:    data.RefSongId,
 | 
			
		||||
		ExtendSecs:   data.ExtendSecs,
 | 
			
		||||
		Prompt:       data.Prompt,
 | 
			
		||||
		Prompt:       job.Prompt,
 | 
			
		||||
		Tags:         data.Tags,
 | 
			
		||||
		Model:        data.Model,
 | 
			
		||||
		Instrumental: data.Instrumental,
 | 
			
		||||
@@ -265,13 +265,13 @@ func (h *SunoHandler) Update(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
// Detail 歌曲详情
 | 
			
		||||
func (h *SunoHandler) Detail(c *gin.Context) {
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
	if id <= 0 {
 | 
			
		||||
	songId := c.Query("song_id")
 | 
			
		||||
	if songId == "" {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var item model.SunoJob
 | 
			
		||||
	if err := h.DB.Where("id", id).First(&item).Error; err != nil {
 | 
			
		||||
	if err := h.DB.Where("song_id", songId).First(&item).Error; err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -296,3 +296,50 @@ func (h *SunoHandler) Detail(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, itemVo)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Play 增加歌曲播放次数
 | 
			
		||||
func (h *SunoHandler) Play(c *gin.Context) {
 | 
			
		||||
	songId := c.Query("song_id")
 | 
			
		||||
	if songId == "" {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	h.DB.Model(&model.SunoJob{}).Where("song_id", songId).UpdateColumn("play_times", gorm.Expr("play_times + ?", 1))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const genLyricTemplate = `
 | 
			
		||||
你是一位才华横溢的作曲家,拥有丰富的情感和细腻的笔触,你对文字有着独特的感悟力,能将各种情感和意境巧妙地融入歌词中。
 | 
			
		||||
请以【%s】为主题创作一首歌曲,歌曲时间不要太短,3分钟左右,不要输出任何解释性的内容。
 | 
			
		||||
输出格式如下:
 | 
			
		||||
歌曲名称
 | 
			
		||||
第一节:
 | 
			
		||||
{{歌词内容}}
 | 
			
		||||
副歌:
 | 
			
		||||
{{歌词内容}}
 | 
			
		||||
 | 
			
		||||
第二节:
 | 
			
		||||
{{歌词内容}}
 | 
			
		||||
副歌:
 | 
			
		||||
{{歌词内容}}
 | 
			
		||||
 | 
			
		||||
尾声:
 | 
			
		||||
{{歌词内容}}
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
// Lyric 生成歌词
 | 
			
		||||
func (h *SunoHandler) Lyric(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Prompt string `json:"prompt"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(genLyricTemplate, data.Prompt), "gpt-4o-mini")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, content)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -494,6 +494,8 @@ func main() {
 | 
			
		||||
			group.GET("publish", h.Publish)
 | 
			
		||||
			group.POST("update", h.Update)
 | 
			
		||||
			group.GET("detail", h.Detail)
 | 
			
		||||
			group.GET("play", h.Play)
 | 
			
		||||
			group.POST("lyric", h.Lyric)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
 | 
			
		||||
			go func() {
 | 
			
		||||
 
 | 
			
		||||
@@ -110,12 +110,11 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
 | 
			
		||||
	prompt := task.Prompt
 | 
			
		||||
	// translate prompt
 | 
			
		||||
	if utils.HasChinese(prompt) {
 | 
			
		||||
		content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, prompt))
 | 
			
		||||
		content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, prompt), "gpt-4o-mini")
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			prompt = content
 | 
			
		||||
			logger.Debugf("重写后提示词:%s", prompt)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var user model.User
 | 
			
		||||
 
 | 
			
		||||
@@ -72,7 +72,7 @@ func (s *Service) Run() {
 | 
			
		||||
 | 
			
		||||
		// translate prompt
 | 
			
		||||
		if utils.HasChinese(task.Prompt) {
 | 
			
		||||
			content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Prompt))
 | 
			
		||||
			content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Prompt), "gpt-4o-mini")
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				task.Prompt = content
 | 
			
		||||
			} else {
 | 
			
		||||
@@ -81,7 +81,7 @@ func (s *Service) Run() {
 | 
			
		||||
		}
 | 
			
		||||
		// translate negative prompt
 | 
			
		||||
		if task.NegPrompt != "" && utils.HasChinese(task.NegPrompt) {
 | 
			
		||||
			content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.NegPrompt))
 | 
			
		||||
			content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.NegPrompt), "gpt-4o-mini")
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				task.NegPrompt = content
 | 
			
		||||
			} else {
 | 
			
		||||
 
 | 
			
		||||
@@ -63,7 +63,7 @@ func (s *Service) Run() {
 | 
			
		||||
 | 
			
		||||
		// translate prompt
 | 
			
		||||
		if utils.HasChinese(task.Params.Prompt) {
 | 
			
		||||
			content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Params.Prompt))
 | 
			
		||||
			content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Params.Prompt), "gpt-4o-mini")
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				task.Params.Prompt = content
 | 
			
		||||
			} else {
 | 
			
		||||
@@ -73,7 +73,7 @@ func (s *Service) Run() {
 | 
			
		||||
 | 
			
		||||
		// translate negative prompt
 | 
			
		||||
		if task.Params.NegPrompt != "" && utils.HasChinese(task.Params.NegPrompt) {
 | 
			
		||||
			content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.NegPrompt))
 | 
			
		||||
			content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.NegPrompt), "gpt-4o-mini")
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				task.Params.NegPrompt = content
 | 
			
		||||
			} else {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,12 +8,14 @@ package utils
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"github.com/pkoukk/tiktoken-go"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"io"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -43,16 +45,7 @@ type apiRes struct {
 | 
			
		||||
	} `json:"choices"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type apiErrRes struct {
 | 
			
		||||
	Error struct {
 | 
			
		||||
		Code    interface{} `json:"code"`
 | 
			
		||||
		Message string      `json:"message"`
 | 
			
		||||
		Param   interface{} `json:"param"`
 | 
			
		||||
		Type    string      `json:"type"`
 | 
			
		||||
	} `json:"error"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func OpenAIRequest(db *gorm.DB, prompt string) (string, error) {
 | 
			
		||||
func OpenAIRequest(db *gorm.DB, prompt string, modelName string) (string, error) {
 | 
			
		||||
	var apiKey model.ApiKey
 | 
			
		||||
	res := db.Where("type", "chat").Where("enabled", true).First(&apiKey)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
@@ -66,24 +59,27 @@ func OpenAIRequest(db *gorm.DB, prompt string) (string, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var response apiRes
 | 
			
		||||
	var errRes apiErrRes
 | 
			
		||||
	client := req.C()
 | 
			
		||||
	if len(apiKey.ProxyURL) > 5 {
 | 
			
		||||
		client.SetProxyURL(apiKey.ApiURL)
 | 
			
		||||
	}
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
 | 
			
		||||
	r, err := client.R().SetHeader("Content-Type", "application/json").
 | 
			
		||||
		SetHeader("Authorization", "Bearer "+apiKey.Value).
 | 
			
		||||
		SetBody(types.ApiRequest{
 | 
			
		||||
			Model:       "gpt-3.5-turbo",
 | 
			
		||||
			Model:       modelName,
 | 
			
		||||
			Temperature: 0.9,
 | 
			
		||||
			MaxTokens:   1024,
 | 
			
		||||
			Stream:      false,
 | 
			
		||||
			Messages:    messages,
 | 
			
		||||
		}).
 | 
			
		||||
		SetErrorResult(&errRes).
 | 
			
		||||
		SetSuccessResult(&response).Post(apiKey.ApiURL)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return "", fmt.Errorf("error with http request: %v%v%s", err, r.Err, errRes.Error.Message)
 | 
			
		||||
		}).Post(apiURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("请求 OpenAI API失败:%v", err)
 | 
			
		||||
	}
 | 
			
		||||
	body, _ := io.ReadAll(r.Body)
 | 
			
		||||
	err = json.Unmarshal(body, &response)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("解析API数据失败:%v, %s", err, string(body))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更新 API KEY 的最后使用时间
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,18 @@
 | 
			
		||||
CREATE TABLE `chatgpt_suno_jobs` (
 | 
			
		||||
                                     `id` int NOT NULL,
 | 
			
		||||
                                     `user_id` int NOT NULL COMMENT '用户 ID',
 | 
			
		||||
                                     `channel` varchar(100) NOT NULL COMMENT '渠道',
 | 
			
		||||
                                     `title` varchar(100) DEFAULT NULL COMMENT '歌曲标题',
 | 
			
		||||
                                     `type` tinyint(1) DEFAULT '0' COMMENT '任务类型,1:灵感创作,2:自定义创作',
 | 
			
		||||
                                     `task_id` varchar(50) DEFAULT NULL COMMENT '任务 ID',
 | 
			
		||||
                                     `reference_id` char(50) DEFAULT NULL COMMENT '引用任务 ID',
 | 
			
		||||
                                     `ref_task_id` char(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '引用任务 ID',
 | 
			
		||||
                                     `tags` varchar(100) DEFAULT NULL COMMENT '歌曲风格',
 | 
			
		||||
                                     `instrumental` tinyint(1) DEFAULT '0' COMMENT '是否为纯音乐',
 | 
			
		||||
                                     `extend_secs` smallint DEFAULT '0' COMMENT '延长秒数',
 | 
			
		||||
                                     `song_id` varchar(50) DEFAULT NULL COMMENT '要续写的歌曲 ID',
 | 
			
		||||
                                     `ref_song_id` varchar(50) NOT NULL COMMENT '引用的歌曲ID',
 | 
			
		||||
                                     `prompt` varchar(2000) NOT NULL COMMENT '提示词',
 | 
			
		||||
                                     `thumb_img_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '缩略图地址',
 | 
			
		||||
                                     `cover_img_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '封面图地址',
 | 
			
		||||
                                     `cover_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '封面图地址',
 | 
			
		||||
                                     `audio_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '音频地址',
 | 
			
		||||
                                     `model_name` varchar(30) DEFAULT NULL COMMENT '模型地址',
 | 
			
		||||
                                     `progress` smallint DEFAULT '0' COMMENT '任务进度',
 | 
			
		||||
@@ -20,16 +21,9 @@ CREATE TABLE `chatgpt_suno_jobs` (
 | 
			
		||||
                                     `err_msg` varchar(255) DEFAULT NULL COMMENT '错误信息',
 | 
			
		||||
                                     `raw_data` text COMMENT '原始数据',
 | 
			
		||||
                                     `power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力',
 | 
			
		||||
                                     `play_times` int DEFAULT NULL COMMENT '播放次数',
 | 
			
		||||
                                     `created_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表';
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 转储表的索引
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_suno_jobs`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_suno_jobs`
 | 
			
		||||
    ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD UNIQUE KEY `task_id` (`task_id`);
 | 
			
		||||
ALTER TABLE `chatgpt_suno_jobs` ADD PRIMARY KEY (`id`);
 | 
			
		||||
ALTER TABLE `chatgpt_suno_jobs` ADD UNIQUE(`song_id`);
 | 
			
		||||
@@ -62,10 +62,10 @@
 | 
			
		||||
 | 
			
		||||
    .prompt {
 | 
			
		||||
      width 100%
 | 
			
		||||
      height 100%
 | 
			
		||||
      height 500px
 | 
			
		||||
      background-color transparent
 | 
			
		||||
      white-space pre-wrap
 | 
			
		||||
      overflow-y hidden
 | 
			
		||||
      overflow-y auto
 | 
			
		||||
      resize none
 | 
			
		||||
      position relative
 | 
			
		||||
      outline  2px solid transparent
 | 
			
		||||
 
 | 
			
		||||
@@ -44,6 +44,7 @@
 | 
			
		||||
      }
 | 
			
		||||
      .item {
 | 
			
		||||
        margin-bottom: 20px
 | 
			
		||||
        position relative
 | 
			
		||||
 | 
			
		||||
        .create-btn {
 | 
			
		||||
          margin 20px 0
 | 
			
		||||
@@ -118,6 +119,14 @@
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .btn-lyric {
 | 
			
		||||
          position absolute
 | 
			
		||||
          left 10px
 | 
			
		||||
          bottom 10px
 | 
			
		||||
          font-size 12px
 | 
			
		||||
          padding 2px 5px
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .tag-select {
 | 
			
		||||
@@ -268,19 +277,6 @@
 | 
			
		||||
            flex-flow row
 | 
			
		||||
            height 90px
 | 
			
		||||
 | 
			
		||||
            .btn {
 | 
			
		||||
              margin-right 10px
 | 
			
		||||
              background-color #363030
 | 
			
		||||
              border none
 | 
			
		||||
              border-radius 5px
 | 
			
		||||
              padding 5px 10px
 | 
			
		||||
              cursor pointer
 | 
			
		||||
 | 
			
		||||
              &:hover {
 | 
			
		||||
                background-color #5F5958
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .btn-publish {
 | 
			
		||||
              padding 2px 10px
 | 
			
		||||
 | 
			
		||||
@@ -343,7 +339,6 @@
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    .pagination {
 | 
			
		||||
      padding 10px 20px
 | 
			
		||||
      display flex
 | 
			
		||||
@@ -358,4 +353,17 @@
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  .btn {
 | 
			
		||||
    margin-right 10px
 | 
			
		||||
    background-color #363030
 | 
			
		||||
    border none
 | 
			
		||||
    border-radius 5px
 | 
			
		||||
    padding 5px 10px
 | 
			
		||||
    cursor pointer
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color #5F5958
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -339,13 +339,10 @@ const reGenerate = (prompt) => {
 | 
			
		||||
 | 
			
		||||
  .chat-line-reply-chat {
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    width 100%
 | 
			
		||||
    padding-bottom: 1.5rem;
 | 
			
		||||
    padding-top: 1.5rem;
 | 
			
		||||
    padding 1.5rem;
 | 
			
		||||
 | 
			
		||||
    .chat-line-inner {
 | 
			
		||||
      display flex;
 | 
			
		||||
      padding 0 25px;
 | 
			
		||||
      width 100%
 | 
			
		||||
      flex-flow row
 | 
			
		||||
 | 
			
		||||
@@ -364,7 +361,7 @@ const reGenerate = (prompt) => {
 | 
			
		||||
        position: relative;
 | 
			
		||||
        padding: 0;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        max-width 60%
 | 
			
		||||
        max-width 70%
 | 
			
		||||
 | 
			
		||||
        .content-wrapper {
 | 
			
		||||
          display flex
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,7 @@ import {ref, onMounted, watch} from 'vue';
 | 
			
		||||
import {showMessageError} from "@/utils/dialog";
 | 
			
		||||
import {Close} from "@element-plus/icons-vue";
 | 
			
		||||
import {formatTime} from "@/utils/libs";
 | 
			
		||||
import {httpGet} from "@/utils/http"
 | 
			
		||||
 | 
			
		||||
const audio = ref(null);
 | 
			
		||||
const isPlaying = ref(false);
 | 
			
		||||
@@ -55,6 +56,7 @@ const title = ref("")
 | 
			
		||||
const tags = ref("")
 | 
			
		||||
const cover = ref("")
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line no-undef
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  songs: {
 | 
			
		||||
    type: Array,
 | 
			
		||||
@@ -67,7 +69,7 @@ const props = defineProps({
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
// eslint-disable-next-line no-undef
 | 
			
		||||
const emits = defineEmits(['close']);
 | 
			
		||||
const emits = defineEmits(['close','play']);
 | 
			
		||||
 | 
			
		||||
watch(() => props.songs, (newVal) => {
 | 
			
		||||
  loadSong(newVal[songIndex.value]);
 | 
			
		||||
@@ -84,6 +86,7 @@ const loadSong = (song) => {
 | 
			
		||||
  cover.value = song.cover_url
 | 
			
		||||
  audio.value.src = song.audio_url;
 | 
			
		||||
  audio.value.load();
 | 
			
		||||
  isPlaying.value = false
 | 
			
		||||
  audio.value.onloadedmetadata = () => {
 | 
			
		||||
    duration.value = audio.value.duration;
 | 
			
		||||
  };
 | 
			
		||||
@@ -92,15 +95,23 @@ const loadSong = (song) => {
 | 
			
		||||
const togglePlay = () => {
 | 
			
		||||
  if (isPlaying.value) {
 | 
			
		||||
    audio.value.pause();
 | 
			
		||||
    isPlaying.value = false
 | 
			
		||||
  } else {
 | 
			
		||||
    audio.value.play();
 | 
			
		||||
    play()
 | 
			
		||||
  }
 | 
			
		||||
  isPlaying.value = !isPlaying.value;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const play = () => {
 | 
			
		||||
  if (isPlaying.value) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  audio.value.play();
 | 
			
		||||
  isPlaying.value = true;
 | 
			
		||||
  isPlaying.value = true
 | 
			
		||||
  if (audio.value.currentTime === 0) {
 | 
			
		||||
    emits("play")
 | 
			
		||||
    // 增加播放数量
 | 
			
		||||
    httpGet("/api/suno/play",{song_id:props.songs[songIndex.value].song_id}).then().catch()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const prevSong = () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,18 +12,18 @@
 | 
			
		||||
 | 
			
		||||
      <div class="navbar">
 | 
			
		||||
        <el-tooltip
 | 
			
		||||
            v-if="!licenseConfig.de_copy"
 | 
			
		||||
            v-if="!license.de_copy"
 | 
			
		||||
            class="box-item"
 | 
			
		||||
            effect="light"
 | 
			
		||||
            content="部署文档"
 | 
			
		||||
            placement="bottom">
 | 
			
		||||
          <a href="https://ai.r9it.com/docs/install/" class="link-button" target="_blank">
 | 
			
		||||
          <a href="https://docs.geekai.me/install/" class="link-button" target="_blank">
 | 
			
		||||
            <i class="iconfont icon-book"></i>
 | 
			
		||||
          </a>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
 | 
			
		||||
        <el-tooltip
 | 
			
		||||
            v-if="!licenseConfig.de_copy"
 | 
			
		||||
            v-if="!license.de_copy"
 | 
			
		||||
            class="box-item"
 | 
			
		||||
            effect="light"
 | 
			
		||||
            content="项目源码"
 | 
			
		||||
@@ -46,7 +46,7 @@
 | 
			
		||||
                <span class="username">{{ loginUser.nickname }}</span>
 | 
			
		||||
              </el-dropdown-item>
 | 
			
		||||
 | 
			
		||||
              <div  v-if="!licenseConfig.de_copy">
 | 
			
		||||
              <div  v-if="!license.de_copy">
 | 
			
		||||
                <el-dropdown-item>
 | 
			
		||||
                  <i class="iconfont icon-book"></i>
 | 
			
		||||
                  <a :href="docsURL" target="_blank">
 | 
			
		||||
@@ -156,7 +156,7 @@ const loginUser = ref({})
 | 
			
		||||
const version = ref(process.env.VUE_APP_VERSION)
 | 
			
		||||
const routerViewKey = ref(0)
 | 
			
		||||
const showConfigDialog = ref(false)
 | 
			
		||||
const licenseConfig = ref({})
 | 
			
		||||
const license = ref({de_copy: true})
 | 
			
		||||
const docsURL = ref(process.env.VUE_APP_DOCS_URL)
 | 
			
		||||
const gitURL = ref(process.env.VUE_APP_GIT_URL)
 | 
			
		||||
 | 
			
		||||
@@ -205,8 +205,9 @@ onMounted(() => {
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  httpGet("/api/config/license").then(res => {
 | 
			
		||||
    licenseConfig.value = res.data
 | 
			
		||||
    license.value = res.data
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    license.value = {de_copy: false}
 | 
			
		||||
    showMessageError("获取 License 配置:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -73,7 +73,7 @@ if (isMobile()) {
 | 
			
		||||
const title = ref("")
 | 
			
		||||
const logo = ref("")
 | 
			
		||||
const slogan = ref("")
 | 
			
		||||
const license = ref({})
 | 
			
		||||
const license = ref({de_copy: true})
 | 
			
		||||
const winHeight = window.innerHeight - 150
 | 
			
		||||
const isLogin = ref(false)
 | 
			
		||||
const docsURL = ref(process.env.VUE_APP_DOCS_URL)
 | 
			
		||||
@@ -158,6 +158,7 @@ onMounted(() => {
 | 
			
		||||
  httpGet("/api/config/license").then(res => {
 | 
			
		||||
    license.value = res.data
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    license.value = {de_copy: false}
 | 
			
		||||
    ElMessage.error("获取 License 配置失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -196,7 +196,7 @@ const data = ref({
 | 
			
		||||
const enableMobile = ref(false)
 | 
			
		||||
const enableEmail = ref(false)
 | 
			
		||||
const enableUser = ref(false)
 | 
			
		||||
const enableRegister = ref(false)
 | 
			
		||||
const enableRegister = ref(true)
 | 
			
		||||
const activeName = ref("mobile")
 | 
			
		||||
const wxImg = ref("/images/wx.png")
 | 
			
		||||
const licenseConfig = ref({})
 | 
			
		||||
 
 | 
			
		||||
@@ -33,13 +33,13 @@
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="music-player" v-if="playList.length > 0">
 | 
			
		||||
      <music-player :songs="playList" ref="playerRef"/>
 | 
			
		||||
      <music-player :songs="playList" ref="playerRef" @play="song.play_times += 1"/>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {nextTick, onMounted, onUnmounted, ref} from "vue"
 | 
			
		||||
import {onMounted, onUnmounted, ref} from "vue"
 | 
			
		||||
import {useRouter} from "vue-router";
 | 
			
		||||
import {httpGet} from "@/utils/http";
 | 
			
		||||
import {showMessageError} from "@/utils/dialog";
 | 
			
		||||
@@ -54,7 +54,7 @@ const song = ref({title:""})
 | 
			
		||||
const playList = ref([])
 | 
			
		||||
const playerRef = ref(null)
 | 
			
		||||
 | 
			
		||||
httpGet("/api/suno/detail",{id:id}).then(res => {
 | 
			
		||||
httpGet("/api/suno/detail",{song_id:id}).then(res => {
 | 
			
		||||
  song.value = res.data
 | 
			
		||||
  playList.value = [song.value]
 | 
			
		||||
  document.title = song.value?.title+ " | By "+song.value?.user.nickname+" | Suno音乐"
 | 
			
		||||
@@ -84,7 +84,7 @@ const play = () => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const winHeight = ref(window.innerHeight-60)
 | 
			
		||||
const winHeight = ref(window.innerHeight-50)
 | 
			
		||||
const getShareURL = (item) => {
 | 
			
		||||
  return `${location.protocol}//${location.host}/song/${item.id}`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,8 +27,12 @@
 | 
			
		||||
                </template>
 | 
			
		||||
              </el-popover>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="item">
 | 
			
		||||
            <div class="item"
 | 
			
		||||
                 v-loading="generating"
 | 
			
		||||
                 element-loading-text="正在生成歌词..."
 | 
			
		||||
                 element-loading-background="rgba(122, 122, 122, 0.8)">
 | 
			
		||||
              <black-input v-model:value="data.lyrics" type="textarea" :rows="10" placeholder="请在这里输入你自己写的歌词..."/>
 | 
			
		||||
              <button class="btn btn-lyric" @click="createLyric">生成歌词</button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
@@ -146,7 +150,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="center">
 | 
			
		||||
              <div class="title">
 | 
			
		||||
                <a :href="'/song/'+item.id"  target="_blank">{{item.title}}</a>
 | 
			
		||||
                <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.ref_song">
 | 
			
		||||
                    <i class="iconfont icon-link"></i>
 | 
			
		||||
@@ -328,7 +332,6 @@ const editData = ref({title:"",cover:"",id:0})
 | 
			
		||||
 | 
			
		||||
const socket = ref(null)
 | 
			
		||||
const userId = ref(0)
 | 
			
		||||
const heartbeatHandle = ref(null)
 | 
			
		||||
const connect = () => {
 | 
			
		||||
  let host = process.env.VUE_APP_WS_HOST
 | 
			
		||||
  if (host === '') {
 | 
			
		||||
@@ -339,25 +342,9 @@ const connect = () => {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 心跳函数
 | 
			
		||||
  const sendHeartbeat = () => {
 | 
			
		||||
    clearTimeout(heartbeatHandle.value)
 | 
			
		||||
    new Promise((resolve, reject) => {
 | 
			
		||||
      if (socket.value !== null) {
 | 
			
		||||
        socket.value.send(JSON.stringify({type: "heartbeat", content: "ping"}))
 | 
			
		||||
      }
 | 
			
		||||
      resolve("success")
 | 
			
		||||
    }).then(() => {
 | 
			
		||||
      heartbeatHandle.value = setTimeout(() => sendHeartbeat(), 5000)
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const _socket = new WebSocket(host + `/api/suno/client?user_id=${userId.value}`);
 | 
			
		||||
  _socket.addEventListener('open', () => {
 | 
			
		||||
    socket.value = _socket;
 | 
			
		||||
 | 
			
		||||
    // 发送心跳消息
 | 
			
		||||
    sendHeartbeat()
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  _socket.addEventListener('message', event => {
 | 
			
		||||
@@ -564,6 +551,24 @@ const uploadCover = (file) => {
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const generating = ref(false)
 | 
			
		||||
const createLyric = () => {
 | 
			
		||||
  if (data.value.lyrics === "") {
 | 
			
		||||
    return showMessageError("请输入歌词描述")
 | 
			
		||||
  }
 | 
			
		||||
  generating.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
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    showMessageError("歌词生成失败:"+e.message)
 | 
			
		||||
    generating.value = false
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,11 @@
 | 
			
		||||
    <div class="content">
 | 
			
		||||
      <van-form>
 | 
			
		||||
        <div class="avatar">
 | 
			
		||||
          <van-uploader v-model="fileList"
 | 
			
		||||
                        reupload max-count="1"
 | 
			
		||||
                        :deletable="false"
 | 
			
		||||
                        :after-read="afterRead"/>
 | 
			
		||||
          <van-image :src="fileList[0].url" size="80" width="80" fit="cover" round />
 | 
			
		||||
<!--          <van-uploader v-model="fileList"-->
 | 
			
		||||
<!--                        reupload max-count="1"-->
 | 
			
		||||
<!--                        :deletable="false"-->
 | 
			
		||||
<!--                        :after-read="afterRead"/>-->
 | 
			
		||||
        </div>
 | 
			
		||||
        <van-cell-group inset v-model="form">
 | 
			
		||||
          <van-field
 | 
			
		||||
@@ -154,7 +155,7 @@
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {onMounted, ref} from "vue";
 | 
			
		||||
import {showFailToast, showNotify, showSuccessToast, showToast} from "vant";
 | 
			
		||||
import {showFailToast, showNotify, showSuccessToast} from "vant";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import Compressor from 'compressorjs';
 | 
			
		||||
import {dateFormat, isWeChatBrowser, showLoginDialog} from "@/utils/libs";
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user