diff --git a/model/core.js b/model/core.js index c9db5df..031e4d8 100644 --- a/model/core.js +++ b/model/core.js @@ -253,16 +253,20 @@ class Core { } } opt.onSunoCreateRequest = prompt => { - logger.mark(`开始生成内容:Suno ${prompt.songtId}`) + logger.mark(`开始生成内容:Suno ${prompt.songtId || ''}`) let client = new BingSunoClient({ cookies: cookies }) redis.set(`CHATGPT:SUNO:${e.sender.user_id}`, 'c', { EX: 30 }).then(() => { try { - if (Config.bingLocalSuno) { + if (Config.bingSuno == 'local') { // 调用本地Suno配置进行歌曲生成 client.getLocalSuno(prompt, e) + } else if (Config.bingSuno == 'api' && Config.bingSunoApi) { + // 调用第三方Suno配置进行歌曲生成 + client.getApiSuno(prompt, e) } else { + // 调用Bing Suno进行歌曲生成 client.getSuno(prompt, e) } } catch (err) { diff --git a/resources/view/setting_view.json b/resources/view/setting_view.json index cac3509..ee932a8 100644 --- a/resources/view/setting_view.json +++ b/resources/view/setting_view.json @@ -575,6 +575,16 @@ "label": "允许生成图像等内容", "data": "enableGenerateContents" }, + { + "type": "check", + "label": "允许生成歌曲等内容", + "data": "enableGenerateSuno" + }, + { + "type": "check", + "label": "伪造歌曲生成", + "data": "enableGenerateSunoForger" + }, { "type": "url", "label": "必应验证码pass服务", @@ -588,10 +598,16 @@ "data": "bingAPDraw" }, { - "type": "check", - "label": "第三方歌曲生成", - "placeholder": "使用AP插件代替Bing进行绘图", - "data": "bingLocalSuno" + "type": "select", + "label": "歌曲生成模式", + "data": "bingSuno", + "items": [ { "label": "Bing", "value": "bing" }, { "label": "本地", "value": "local" }, { "label": "第三方", "value": "api" } ] + }, + { + "type": "url", + "label": "第三方歌曲生成API地址", + "placeholder": "https://github.com/gcui-art/suno-api的api地址", + "data": "bingSunoApi" }, { "type": "textarea", @@ -1118,7 +1134,14 @@ "label": "Sydney模式接受首条信息超时时间", "placeholder": "超过该时间阈值未收到Bing的任何消息,则断开本次连接并重试", "data": "sydneyFirstMessageTimeout" + }, + { + "type": "number", + "label": "SunoApi获取超时时间", + "placeholder": "使用sunoApi获取数据时超时时间,单位秒", + "data": "sunoApiTimeout" } + ] }, { diff --git a/utils/BingSuno.js b/utils/BingSuno.js index 8ccccde..b126148 100644 --- a/utils/BingSuno.js +++ b/utils/BingSuno.js @@ -5,7 +5,6 @@ import common from '../../../lib/common/common.js' import fs from 'fs' import crypto from 'crypto' import fetch from 'node-fetch' -import lodash from 'lodash' export default class BingSunoClient { constructor(opts) { @@ -14,14 +13,14 @@ export default class BingSunoClient { async replyMsg(song, e) { let messages = [] - messages.push(`歌名:${song.title}\n风格: ${song.musicalStyle}\n长度: ${song.duration}秒\n歌词:\n${song.prompt}\n`) + messages.push(`歌名:${song.title}\n风格: ${song.musicalStyle}\n歌词:\n${song.prompt}\n`) messages.push(`音频链接:${song.audioURL}\n视频链接:${song.videoURL}\n封面链接:${song.imageURL}\n`) messages.push(segment.image(song.imageURL)) let retry = 3 let videoPath while (!videoPath && retry >= 0) { try { - videoPath = await downloadFile(song.video_url, `suno/${song.title}.mp4`, false, false, { + videoPath = await downloadFile(song.videoURL, `suno/${song.title}.mp4`, false, false, { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' }) } catch (err) { @@ -47,7 +46,6 @@ export default class BingSunoClient { if (prompt.cookie) { this.opts.cookies = prompt.cookie } - const sunoResult = await this.getSunoResult(prompt.songtId) if (sunoResult) { const { @@ -72,78 +70,154 @@ export default class BingSunoClient { prompt: prompt.songPrompt } await e.reply('Bing Suno 生成中,请稍后') - replyMsg(sunoDisplayResult, e) + this.replyMsg(sunoDisplayResult, e) } else { await e.reply('Bing Suno 数据获取失败') redis.del(`CHATGPT:SUNO:${e.sender.user_id}`) } + redis.del(`CHATGPT:SUNO:${e.sender.user_id}`) } async getLocalSuno(prompt, e) { if (!Config.sunoClientToken || !Config.sunoSessToken) { await e.reply('未配置Suno Token') + redis.del(`CHATGPT:SUNO:${e.sender.user_id}`) return true - } - let description = prompt.songPrompt - await e.reply('正在生成,请稍后') - try { + } + let description = prompt.songPrompt + 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() - logger.info({ credit, email }) - if (credit < 10) { - tried++ - logger.info(`账户${email}余额不足,尝试下一个账户`) - continue - } - - let songs = await client.createSong(description) - if (!songs || songs.length === 0) { - e.reply('生成失败,可能是提示词太长或者违规,请检查日志') - return - } - let messages = ['提示词:' + description] - for (let song of songs) { - messages.push(`歌名:${song.title}\n风格: ${song.metadata.tags}\n长度: ${lodash.round(song.metadata.duration, 0)}秒\n歌词:\n${song.metadata.prompt}\n`) - messages.push(`音频链接:${song.audio_url}\n视频链接:${song.video_url}\n封面链接:${song.image_url}\n`) - messages.push(segment.image(song.image_url)) - let retry = 3 - let videoPath - while (!videoPath && retry >= 0) { - try { - videoPath = await downloadFile(song.video_url, `suno/${song.title}.mp4`, false, false, { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' - }) - } catch (err) { - retry-- - await common.sleep(1000) - } + let index = tried + let sess = sessTokens[index] + let clientToken = clientTokens[index] + let client = new SunoClient({ sessToken: sess, clientToken }) + let { credit, email } = await client.queryCredit() + logger.info({ credit, email }) + if (credit < 10) { + tried++ + logger.info(`账户${email}余额不足,尝试下一个账户`) + continue } - if (videoPath) { - const data = fs.readFileSync(videoPath) - messages.push(segment.video(`base64://${data.toString('base64')}`)) - // 60秒后删除文件避免占用体积 - setTimeout(() => { - fs.unlinkSync(videoPath) - }, 60000) - } else { - logger.warn(`${song.title}下载视频失败,仅发送视频链接`) + + let songs = await client.createSong(description) + if (!songs || songs.length === 0) { + e.reply('生成失败,可能是提示词太长或者违规,请检查日志') + redis.del(`CHATGPT:SUNO:${e.sender.user_id}`) + return } - } - await e.reply(await common.makeForwardMsg(e, messages, '音乐合成结果')) - return true + let messages = ['提示词:' + description] + for (let song of songs) { + messages.push(`歌名:${song.title}\n风格: ${song.metadata.tags}\n歌词:\n${song.metadata.prompt}\n`) + messages.push(`音频链接:${song.audio_url}\n视频链接:${song.video_url}\n封面链接:${song.image_url}\n`) + messages.push(segment.image(song.image_url)) + let retry = 3 + let videoPath + while (!videoPath && retry >= 0) { + try { + videoPath = await downloadFile(song.video_url, `suno/${song.title}.mp4`, false, false, { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' + }) + } catch (err) { + retry-- + await common.sleep(1000) + } + } + if (videoPath) { + const data = fs.readFileSync(videoPath) + messages.push(segment.video(`base64://${data.toString('base64')}`)) + // 60秒后删除文件避免占用体积 + setTimeout(() => { + fs.unlinkSync(videoPath) + }, 60000) + } else { + logger.warn(`${song.title}下载视频失败,仅发送视频链接`) + } + } + await e.reply(await common.makeForwardMsg(e, messages, '音乐合成结果')) + redis.del(`CHATGPT:SUNO:${e.sender.user_id}`) + return true } await e.reply('所有账户余额不足') - } catch (err) { + redis.del(`CHATGPT:SUNO:${e.sender.user_id}`) + } catch (err) { console.error(err) await e.reply('生成失败,请查看日志') - } + redis.del(`CHATGPT:SUNO:${e.sender.user_id}`) + } + } + + async getApiSuno(prompt, e) { + if (!Config.bingSunoApi) { + await e.reply('未配置 Suno API') + return + } + const responseId = await fetch(`${Config.bingSunoApi}/api/custom_generate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + "prompt": prompt.songPrompt || prompt.lyrics, + "tags": prompt.tags || "pop", + "title": prompt.title || e.sender.card || e.sender.nickname, + "make_instrumental": false, + "wait_audio": false + }) + }) + const sunoId = await responseId.json() + if (sunoId[0]?.id) { + await e.reply('Bing Suno 生成中,请稍后') + let timeoutTimes = Config.sunoApiTimeout + let timer = setInterval(async () => { + const response = await fetch(`${Config.bingSunoApi}/api/get?ids=${sunoId[0]?.id}`, { + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }) + if (!response.ok) { + await e.reply('Bing Suno 数据获取失败') + logger.error(response.error.message) + redis.del(`CHATGPT:SUNO:${e.sender.user_id}`) + clearInterval(timer) + timer = null + throw new Error(`HTTP error! status: ${response.status}`) + } + const result = await response.json() + if (result[0].status == 'complete') { + const sunoResult = result[0] + const title = sunoResult.title + const audioURL = sunoResult.audio_url + const imageURL = sunoResult.image_url + const videoURL = sunoResult.video_url + const musicalStyle = sunoResult.tags + const prompt = sunoResult.lyric + const sunoURL = `https://cdn1.suno.ai/${sunoResult.id}.mp4` + const sunoDisplayResult = { + title, + musicalStyle, + audioURL, + imageURL, + videoURL, + sunoURL, + prompt + } + this.replyMsg(sunoDisplayResult, e) + clearInterval(timer) + } else if (timeoutTimes === 0) { + await e.reply('❌Suno 生成超时', true) + clearInterval(timer) + timer = null + } else { + logger.info('等待Suno生成中: ' + timeoutTimes) + timeoutTimes-- + } + }, 3000) + } } async getSunoResult(requestId) { @@ -210,7 +284,6 @@ export default class BingSunoClient { sfx: sfx.toString(), }) fetchURL.search = searchParams.toString() - const response = await fetch(fetchURL, { headers: { accept: '*/*', diff --git a/utils/SydneyAIClient.js b/utils/SydneyAIClient.js index a94acad..dcc585d 100644 --- a/utils/SydneyAIClient.js +++ b/utils/SydneyAIClient.js @@ -7,7 +7,7 @@ import fetch, { import crypto from 'crypto' import WebSocket from 'ws' import { Config } from './config.js' -import { formatDate, getMasterQQ, isCN, getUserData, limitString } from './common.js' +import { formatDate, getMasterQQ, isCN, getUserData, limitString, extractMarkdownJson } from './common.js' import moment from 'moment' import { getProxy } from './proxy.js' import common from '../../../lib/common/common.js' @@ -337,7 +337,8 @@ export default class SydneyAIClient { const text = (useCast?.bing || Config.sydney).replaceAll(namePlaceholder, botName || defaultBotName) + ((Config.enableGroupContext && groupId) ? groupContextTip : '') + ((Config.enforceMaster && master) ? masterTip : '') + - (Config.sydneyMood ? moodTip : '') + (Config.sydneyMood ? moodTip : '') + + ((!Config.enableGenerateSuno && Config.bingSuno != 'bing' && Config.enableGenerateSunoForger) ? 'If I ask you to generate music or write songs, you need to reply with information suitable for Suno to generate music. The returned message is in JSON format, with a structure of {"option": "Suno", "tags": "style", "title": "title of the song", "lyrics": "lyrics"}.' : '') if (!text) { previousMessages = pm } else { @@ -387,7 +388,7 @@ export default class SydneyAIClient { } let optionsSets = getOptionSet(Config.toneStyle, Config.enableGenerateContents) let source = 'cib-ccp'; let gptId = 'copilot' - if ((!Config.sydneyEnableSearch && !Config.enableGenerateContents) || toSummaryFileContent?.content) { + if ((!Config.sydneyEnableSearch && !Config.enableGenerateContents && !Config.enableGenerateSuno) || toSummaryFileContent?.content) { optionsSets.push(...['nosearchall']) } if (isPro) { @@ -504,7 +505,7 @@ export default class SydneyAIClient { } } } - if (Config.enableGenerateContents){ + if (Config.enableGenerateSuno){ argument0.plugins.push({ "id": "22b7f79d-8ea4-437e-b5fd-3e21f09f7bc1", "category": 1 @@ -714,7 +715,6 @@ export default class SydneyAIClient { suggestedResponsesSoFar = message.suggestedResponses } if (messages[0].contentType === 'SUNO') { - console.log() onSunoCreateRequest({ songtId: messages[0]?.hiddenText.split('=')[1], songPrompt: messages[0]?.text, @@ -835,6 +835,16 @@ export default class SydneyAIClient { message.adaptiveCards = adaptiveCardsSoFar message.text = replySoFar.join('') } + // 伪造歌曲生成 + if (Config.enableGenerateSunoForger) { + const sunoList = extractMarkdownJson(message.text) + for (let suno of sunoList) { + if (suno.option == 'Suno') { + logger.info(`开始生成歌曲${suno.tags}`) + onSunoCreateRequest(suno) + } + } + } resolve({ message, conversationExpiryTime: event?.item?.conversationExpiryTime @@ -1046,7 +1056,7 @@ function getOptionSet (tone, generateContent = false) { ]) break } - if (Config.enableGenerateContents){ + if (Config.enableGenerateSuno){ optionset.push(...[ '014CB21D', 'B3FF9F21' diff --git a/utils/common.js b/utils/common.js index ca33569..f73d74f 100644 --- a/utils/common.js +++ b/utils/common.js @@ -1242,3 +1242,36 @@ function maskString (str) { return firstThreeChars + maskedChars + lastThreeChars } + +/** + * generated by ai + * @param text + * @returns {array} + */ +export function extractMarkdownJson(text) { + const lines = text.split('\n') + const mdJson = [] + let currentObj = null + + lines.forEach(line => { + if (line.startsWith('```json') && !currentObj) { + // 开始一个新的JSON对象 + currentObj = { json: '' } + } else if (line.startsWith('```') && currentObj) { + // 结束当前的JSON对象 + try { + // 尝试将JSON字符串转换为对象 + currentObj.json = JSON.parse(currentObj.json) + mdJson.push(currentObj) + currentObj = null + } catch (e) { + console.error('JSON解析错误:', e) + } + } else if (currentObj) { + // 将行添加到当前的JSON对象 + currentObj.json += line + } + }) + + return mdJson.map(obj => obj.json) +} \ No newline at end of file diff --git a/utils/config.js b/utils/config.js index 830dbf5..f78cd3b 100644 --- a/utils/config.js +++ b/utils/config.js @@ -81,6 +81,7 @@ const defaultConfig = { defaultTimeoutMs: 120000, chromeTimeoutMS: 120000, sydneyFirstMessageTimeout: 40000, + sunoApiTimeout: 60, ttsSpace: '', // https://114514.201666.xyz huggingFaceReverseProxy: '', @@ -104,7 +105,8 @@ const defaultConfig = { sydneyApologyIgnored: true, enforceMaster: false, bingAPDraw: false, - bingLocalSuno: false, + bingSuno: 'bing', + bingSunoApi: '', serverPort: 3321, serverHost: '', viewHost: '', @@ -151,6 +153,8 @@ const defaultConfig = { enhanceAzureTTSEmotion: false, autoJapanese: false, enableGenerateContents: false, + enableGenerateSuno: false, + enableGenerateSunoForger: false, amapKey: '', azSerpKey: '', serpSource: 'ikechan8370',