feat: 支持suno:#suno+prompt#创作歌曲+prompt

This commit is contained in:
ikechan8370 2024-02-21 14:38:13 +08:00
parent 63edc9403c
commit eea0748de7
7 changed files with 268 additions and 2 deletions

View file

@ -324,10 +324,14 @@ export class dalle extends plugin {
const index = bingTokens.findIndex(element => element.Token === bingToken)
bingTokens[index].Usage += 1
await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingTokens))
let cookie
if (bingToken.includes('=')) {
cookie = bingToken
}
let client = new BingDrawClient({
baseUrl: Config.sydneyReverseProxy,
userToken: bingToken
userToken: bingToken,
cookies: cookie
})
await redis.set(`CHATGPT:DRAW:${e.sender.user_id}`, 'c', { EX: 30 })
try {

83
apps/vocal.js Normal file
View file

@ -0,0 +1,83 @@
import plugin from '../../../lib/plugins/plugin.js'
import { SunoClient } from '../client/SunoClient.js'
import { Config } from '../utils/config.js'
import { downloadFile } from '../utils/common.js'
import common from '../../../lib/common/common.js'
export class Vocal extends plugin {
constructor (e) {
super({
name: 'ChatGPT-Plugin 音乐合成',
dsc: '基于Suno等AI的饮月生成',
event: 'message',
priority: 500,
rule: [
{
reg: '^#((创作)?歌曲|suno|Suno)',
fnc: 'createSong',
permission: 'master'
}
]
})
this.task = [
{
// 设置十分钟左右的浮动
cron: '0/1 * * * ?',
// cron: '*/2 * * * *',
name: '保持suno心跳',
fnc: this.heartbeat.bind(this)
}
]
}
async heartbeat (e) {
let sessTokens = Config.sunoSessToken.split(',')
let clientTokens = Config.sunoClientToken.split(',')
for (let i = 0; i < sessTokens.length; i++) {
let sessToken = sessTokens[i]
let clientToken = clientTokens[i]
if (sessToken && clientToken) {
let client = new SunoClient({ sessToken, clientToken })
await client.heartbeat()
}
}
}
async createSong (e) {
if (!Config.sunoClientToken || !Config.sunoSessToken) {
await e.reply('未配置Suno Token')
return true
}
await e.reply('正在生成,请稍后')
try {
let sessTokens = Config.sunoSessToken.split(',')
let clientTokens = Config.sunoClientToken.split(',')
let tried = 0
while (tried < sessTokens.length) {
let index = tried
let sess = sessTokens[index]
let clientToken = clientTokens[index]
let client = new SunoClient({ sessToken: sess, clientToken })
let { credit, email } = await client.queryCredit()
if (credit < 10) {
tried++
logger.info(`账户${email}余额不足,尝试下一个账户`)
continue
}
let description = e.msg.replace(/#((创作)?歌曲|suno|Suno)/, '')
let songs = await client.createSong(description)
let messages = ['提示词:' + description]
for (let song of songs) {
messages.push(`歌名:${song.title}, 风格: ${song.metadata.tags}, 长度: ${song.metadata.duration}\n歌词:\n${song.metadata.prompt}`)
messages.push(segment.image(song.image_url))
let videoPath = await downloadFile(song.video_url, `suno/${song.title}.mp4`)
messages.push(segment.video(videoPath))
}
await e.reply(common.makeForwardMsg(e, messages, '音乐合成结果'))
}
} catch (err) {
console.error(err)
await e.reply('生成失败,请查看日志')
}
}
}

142
client/SunoClient.js Normal file
View file

@ -0,0 +1,142 @@
import { newFetch } from '../utils/proxy.js'
import common from '../../../lib/common/common.js'
import { decrypt } from '../utils/jwt.js'
import { FormData } from 'node-fetch'
export class SunoClient {
constructor (options) {
this.options = options
this.sessToken = options.sessToken
this.clientToken = options.clientToken
if (!this.clientToken || !this.sessToken) {
throw new Error('Token is required')
}
}
async getToken () {
let lastToken = this.sessToken
let payload = decrypt(lastToken)
let sid = JSON.parse(payload).sid
logger.mark('sid: ' + sid)
let tokenRes = await newFetch(`https://clerk.suno.ai/v1/client/sessions/${sid}/tokens/api?_clerk_js_version=4.70.0`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Cookie: `__client=${this.clientToken};`,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
Origin: 'https://app.suno.ai',
Referer: 'https://app.suno.ai/create/'
}
})
let tokenData = await tokenRes.json()
let token = tokenData.jwt
logger.info('new token got: ' + token)
return token
}
async createSong (description) {
let sess = await this.getToken()
let createRes = await newFetch('https://studio-api.suno.ai/api/generate/v2/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${sess}`,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
Origin: 'https://app.suno.ai',
Referer: 'https://app.suno.ai/create/',
Cookie: `__sess=${sess}`
},
body: JSON.stringify({ gpt_description_prompt: description, mv: 'chirp-v2-engine-v13', prompt: '' })
})
if (createRes.status !== 200) {
console.log(await createRes.json())
throw new Error('Failed to create song ' + createRes.status)
}
let createData = await createRes.json()
let ids = createData?.clips?.map(clip => clip.id)
let queryUrl = `https://studio-api.suno.ai/api/feed/?ids=${ids[0]}%2C${ids[1]}`
let allDone = false; let songs = []
while (!allDone) {
let queryRes = await newFetch(queryUrl, {
headers: {
Authorization: `Bearer ${sess}`
}
})
if (queryRes.status !== 200) {
throw new Error('Failed to query song')
}
let queryData = await queryRes.json()
logger.debug(queryData)
allDone = queryData.every(clip => clip.status === 'complete')
songs = queryData
await common.sleep(1000)
}
return songs
}
async queryUser (sess) {
if (!sess) {
sess = await this.getToken()
}
let userRes = await newFetch('https://studio-api.suno.ai/api/session/', {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${sess}`,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
Origin: 'https://app.suno.ai',
Referer: 'https://app.suno.ai/create/',
Cookie: `__sess=${sess}`
}
})
let userData = await userRes.json()
logger.debug(userData)
let user = userData?.user.email
return user
}
async queryCredit () {
let sess = await this.getToken()
let infoRes = await newFetch('https://studio-api.suno.ai/api/billing/info/', {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${sess}`,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
Origin: 'https://app.suno.ai',
Referer: 'https://app.suno.ai/create/',
Cookie: `__sess=${sess}`
}
})
let infoData = await infoRes.json()
logger.debug(infoData)
let credit = infoData?.total_credits_left
let email = await this.queryUser(sess)
return {
email, credit
}
}
async heartbeat () {
let lastToken = this.sessToken
let payload = decrypt(lastToken)
let sid = JSON.parse(payload).sid
logger.mark('sid: ' + sid)
let heartbeatUrl = `https://clerk.suno.ai/v1/client/sessions/${sid}/touch?_clerk_js_version=4.70.0`
let heartbeatRes = await fetch(heartbeatUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Cookie: `__client=${this.clientToken};`,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
Origin: 'https://app.suno.ai',
Referer: 'https://app.suno.ai/create/'
},
body: 'active_organization_id='
})
logger.debug(await heartbeatRes.text())
if (heartbeatRes.status === 200) {
logger.debug('heartbeat success')
return true
}
}
}

View file

@ -0,0 +1,11 @@
import { SunoClient } from '../SunoClient.js'
async function test () {
const options = {
}
let client = new SunoClient(options)
let res = await client.createSong('guacamole')
console.log(res)
}
test()

View file

@ -792,6 +792,22 @@ export function supportGuoba () {
bottomHelpMessage: '对https://generativelanguage.googleapis.com的反代',
component: 'Input'
},
{
label: '以下为Suno音乐合成的配置。',
component: 'Divider'
},
{
field: 'sunoSessToken',
label: 'sunoSessToken',
bottomHelpMessage: 'suno的__sess token需要与sunoClientToken一一对应数量相同多个用逗号隔开',
component: 'InputTextArea'
},
{
field: 'sunoClientToken',
label: 'sunoClientToken',
bottomHelpMessage: 'suno的__client token需要与sunoSessToken一一对应数量相同多个用逗号隔开',
component: 'InputTextArea'
},
{
label: '以下为杂七杂八的配置',
component: 'Divider'

View file

@ -175,6 +175,8 @@ const defaultConfig = {
// origin: https://generativelanguage.googleapis.com
geminiBaseUrl: 'https://gemini.ikechan8370.com',
chatglmRefreshToken: '',
sunoSessToken: '',
sunoClientToken: '',
version: 'v2.7.10'
}
const _path = process.cwd()

8
utils/jwt.js Normal file
View file

@ -0,0 +1,8 @@
export function decrypt (jwtToken) {
const [encodedHeader, encodedPayload, signature] = jwtToken.split('.')
const decodedHeader = Buffer.from(encodedHeader, 'base64').toString('utf-8')
const decodedPayload = Buffer.from(encodedPayload, 'base64').toString('utf-8')
return decodedPayload
}