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