From abdf5298fee6fafa5e703275bca1c0cbbc348ef6 Mon Sep 17 00:00:00 2001 From: RockYang Date: Sun, 28 Jul 2024 10:04:53 +0800 Subject: [PATCH] add function to generate lyrics --- CHANGELOG.md | 2 ++ api/core/app_server.go | 4 ++- api/handler/suno_handler.go | 55 +++++++++++++++++++++++++++--- api/main.go | 2 ++ api/service/dalle/service.go | 3 +- api/service/mj/service.go | 4 +-- api/service/sd/service.go | 4 +-- api/utils/openai.go | 30 +++++++--------- database/update-v4.1.1.sql | 20 ++++------- web/src/assets/css/song.styl | 4 +-- web/src/assets/css/suno.styl | 36 +++++++++++-------- web/src/components/ChatReply.vue | 7 ++-- web/src/components/MusicPlayer.vue | 19 ++++++++--- web/src/views/Home.vue | 13 +++---- web/src/views/Index.vue | 3 +- web/src/views/Register.vue | 2 +- web/src/views/Song.vue | 8 ++--- web/src/views/Suno.vue | 43 ++++++++++++----------- web/src/views/mobile/Profile.vue | 11 +++--- 19 files changed, 168 insertions(+), 102 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abcec219..456f670f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ * 功能优化:增加 session 和系统配置缓存,确保每个页面只进行一次 session 和 get system config 请求 * 功能优化:在应用列表页面,无需先添加模型到用户工作区,可以直接使用 * 功能新增:MJ 绘图失败的任务不会自动删除,而是会在列表页显示失败详细错误信息 +* 功能新增:允许在设置首页纯色背景,背景图片,随机背景图片三种背景模式 * 功能新增:允许在管理后台设置首页显示的导航菜单 +* Bug修复:修复注册页面先显示关闭注册组件,然后再显示注册组件 * 功能新增:增加 Suno 文生歌曲功能 * 功能优化:移除多平台模型支持,统一使用 one-api 接口形式,其他平台的模型需要通过 one-api 接口添加 diff --git a/api/core/app_server.go b/api/core/app_server.go index 9f00601d..3fb32096 100644 --- a/api/core/app_server.go +++ b/api/core/app_server.go @@ -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/") || diff --git a/api/handler/suno_handler.go b/api/handler/suno_handler.go index d004c2b5..4fbf031a 100644 --- a/api/handler/suno_handler.go +++ b/api/handler/suno_handler.go @@ -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) +} diff --git a/api/main.go b/api/main.go index 63911ca0..d8d2cdba 100644 --- a/api/main.go +++ b/api/main.go @@ -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() { diff --git a/api/service/dalle/service.go b/api/service/dalle/service.go index de460e27..7d370767 100644 --- a/api/service/dalle/service.go +++ b/api/service/dalle/service.go @@ -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 diff --git a/api/service/mj/service.go b/api/service/mj/service.go index 2218c851..60b1fc50 100644 --- a/api/service/mj/service.go +++ b/api/service/mj/service.go @@ -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 { diff --git a/api/service/sd/service.go b/api/service/sd/service.go index 468e8b3e..dbb3a3c0 100644 --- a/api/service/sd/service.go +++ b/api/service/sd/service.go @@ -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 { diff --git a/api/utils/openai.go b/api/utils/openai.go index 5a3a83c6..fdbed5f9 100644 --- a/api/utils/openai.go +++ b/api/utils/openai.go @@ -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 的最后使用时间 diff --git a/database/update-v4.1.1.sql b/database/update-v4.1.1.sql index e9ff28cb..e49f9102 100644 --- a/database/update-v4.1.1.sql +++ b/database/update-v4.1.1.sql @@ -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`); \ No newline at end of file diff --git a/web/src/assets/css/song.styl b/web/src/assets/css/song.styl index 04b575f8..ef8c73c2 100644 --- a/web/src/assets/css/song.styl +++ b/web/src/assets/css/song.styl @@ -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 diff --git a/web/src/assets/css/suno.styl b/web/src/assets/css/suno.styl index bf650cf1..ffe39119 100644 --- a/web/src/assets/css/suno.styl +++ b/web/src/assets/css/suno.styl @@ -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 + } + } } \ No newline at end of file diff --git a/web/src/components/ChatReply.vue b/web/src/components/ChatReply.vue index fc4a56c4..74b15760 100644 --- a/web/src/components/ChatReply.vue +++ b/web/src/components/ChatReply.vue @@ -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 diff --git a/web/src/components/MusicPlayer.vue b/web/src/components/MusicPlayer.vue index 434d3883..3b217056 100644 --- a/web/src/components/MusicPlayer.vue +++ b/web/src/components/MusicPlayer.vue @@ -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 = () => { diff --git a/web/src/views/Home.vue b/web/src/views/Home.vue index d348793b..b488443b 100644 --- a/web/src/views/Home.vue +++ b/web/src/views/Home.vue @@ -12,18 +12,18 @@