From eea0748de77f4600449b811929f25a95eafec608 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Wed, 21 Feb 2024 14:38:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81suno=EF=BC=9A`#suno+p?= =?UTF-8?q?rompt`=E6=88=96`#=E5=88=9B=E4=BD=9C=E6=AD=8C=E6=9B=B2+prompt`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/draw.js | 8 +- apps/vocal.js | 83 ++++++++++++++++++++ client/SunoClient.js | 142 ++++++++++++++++++++++++++++++++++ client/test/SunoClientTest.js | 11 +++ guoba.support.js | 16 ++++ utils/config.js | 2 + utils/jwt.js | 8 ++ 7 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 apps/vocal.js create mode 100644 client/SunoClient.js create mode 100644 client/test/SunoClientTest.js create mode 100644 utils/jwt.js diff --git a/apps/draw.js b/apps/draw.js index 339d29c..d28a706 100644 --- a/apps/draw.js +++ b/apps/draw.js @@ -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 { diff --git a/apps/vocal.js b/apps/vocal.js new file mode 100644 index 0000000..d0d5a7d --- /dev/null +++ b/apps/vocal.js @@ -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('生成失败,请查看日志') + } + } +} diff --git a/client/SunoClient.js b/client/SunoClient.js new file mode 100644 index 0000000..a37c791 --- /dev/null +++ b/client/SunoClient.js @@ -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 + } + } +} diff --git a/client/test/SunoClientTest.js b/client/test/SunoClientTest.js new file mode 100644 index 0000000..35661ef --- /dev/null +++ b/client/test/SunoClientTest.js @@ -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() diff --git a/guoba.support.js b/guoba.support.js index 4b118a6..ab6bf59 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -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' diff --git a/utils/config.js b/utils/config.js index beccf06..df31894 100644 --- a/utils/config.js +++ b/utils/config.js @@ -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() diff --git a/utils/jwt.js b/utils/jwt.js new file mode 100644 index 0000000..1af66f3 --- /dev/null +++ b/utils/jwt.js @@ -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 +}