mirror of
https://github.com/ikechan8370/chatgpt-plugin.git
synced 2025-12-16 21:37:11 +00:00
662 lines
28 KiB
JavaScript
662 lines
28 KiB
JavaScript
import { downloadFile } from '../utils/common.js'
|
||
import { SunoClient } from '../client/SunoClient.js'
|
||
import { Config } from '../utils/config.js'
|
||
import common from '../../../lib/common/common.js'
|
||
import fs from 'fs'
|
||
import crypto from 'crypto'
|
||
import fetch from 'node-fetch'
|
||
|
||
const Style = [
|
||
{ value: 'Dance', describe: '跳舞' },
|
||
{ value: 'Festive', describe: '节日' },
|
||
{ value: 'Groovy', describe: '槽的' },
|
||
{ value: 'Mid-Tempo', describe: '中速' },
|
||
{ value: 'Syncopated', describe: '切分音' },
|
||
{ value: 'Tipsy', describe: '醉' },
|
||
{ value: 'Dark', describe: '黑暗' },
|
||
{ value: 'Atmospheric', describe: '大气' },
|
||
{ value: 'Cold', describe: '冷' },
|
||
{ value: 'Dark', describe: '黑暗' },
|
||
{ value: 'Doom', describe: '厄运' },
|
||
{ value: 'Dramatic', describe: '戏剧性的' },
|
||
{ value: 'Sinister', describe: '险恶' },
|
||
{ value: 'Eclectic', describe: '折衷' },
|
||
{ value: 'Adjunct', describe: '兼职' },
|
||
{ value: 'Art', describe: '艺术' },
|
||
{ value: 'Capriccio', describe: '狂想曲' },
|
||
{ value: 'Mellifluous', describe: '美化' },
|
||
{ value: 'Nü', describe: 'Nü' },
|
||
{ value: 'Progressive', describe: '进步' },
|
||
{ value: 'Unusual', describe: '异常' },
|
||
{ value: 'Emotion', describe: '情感' },
|
||
{ value: 'Anthemic', describe: '国歌' },
|
||
{ value: 'Emotional', describe: '感情的' },
|
||
{ value: 'Happy', describe: '快乐' },
|
||
{ value: 'Jubilant', describe: '欢腾' },
|
||
{ value: 'Melancholy', describe: '忧郁' },
|
||
{ value: 'Sad', describe: 'Sad' },
|
||
{ value: 'Hard', describe: '硬' },
|
||
{ value: 'Aggressive', describe: '侵略性的' },
|
||
{ value: '积极', describe: '积极' },
|
||
{ value: 'Banger', describe: '爆竹' },
|
||
{ value: 'Power', describe: '权力' },
|
||
{ value: 'Stadium', describe: '体育场' },
|
||
{ value: 'Stomp', describe: '踩' },
|
||
{ value: 'Lyrical', describe: '抒情' },
|
||
{ value: 'Broadway', describe: '百老汇' },
|
||
{ value: 'Cabaret', describe: '酒店' },
|
||
{ value: 'Lounge', describe: '休息室' },
|
||
{ value: 'Operatic', describe: '歌剧' },
|
||
{ value: 'Storytelling', describe: '故事' },
|
||
{ value: 'Torch-Lounge', describe: '火炬酒廊' },
|
||
{ value: 'Theatrical', describe: '戏剧' },
|
||
{ value: 'Troubadour', describe: '吟游诗人' },
|
||
{ value: 'Vegas', describe: '维加斯' },
|
||
{ value: 'Magical', describe: '神奇' },
|
||
{ value: 'Ethereal', describe: '空灵' },
|
||
{ value: 'Majestic', describe: '雄伟' },
|
||
{ value: 'Mysterious', describe: '神秘' },
|
||
{ value: 'Minimal', describe: '极小' },
|
||
{ value: 'Ambient', describe: '氛围' },
|
||
{ value: 'Cinematic', describe: '电影' },
|
||
{ value: 'Heat', describe: '热' },
|
||
{ value: 'Minimal', describe: '极小' },
|
||
{ value: 'Slow', describe: '慢' },
|
||
{ value: 'Sparse', describe: '稀疏' },
|
||
{ value: 'Party', describe: '党' },
|
||
{ value: 'German Schlager', describe: '德国施拉格' },
|
||
{ value: 'Glam', describe: '格南' },
|
||
{ value: 'Glitter', describe: '闪光' },
|
||
{ value: 'Groovy', describe: '槽的' },
|
||
{ value: 'oft', describe: '软' },
|
||
{ value: 'Ambient', describe: '氛围' },
|
||
{ value: 'Bedroom', describe: '卧室' },
|
||
{ value: 'Chillwave', describe: '寒波' },
|
||
{ value: 'Ethereal', describe: '空灵' },
|
||
{ value: 'Intimate', describe: '亲密' },
|
||
{ value: 'Heat', describe: '热' },
|
||
{ value: 'Sadcore', describe: '悲伤' },
|
||
{ value: 'Weird', describe: '奇怪' },
|
||
{ value: 'Carnival', describe: '狂欢节' },
|
||
{ value: 'Distorted', describe: '扭曲' },
|
||
{ value: 'Glitchy', describe: '毛刺' },
|
||
{ value: 'Haunted', describe: '闹鬼的' },
|
||
{ value: 'Hollow', describe: '空心' },
|
||
{ value: 'Musicbox', describe: '音乐盒' },
|
||
{ value: 'Random', describe: '随机' },
|
||
{ value: 'World/Ethnic', describe: '世界/民族' },
|
||
{ value: 'Arabian', describe: '阿拉伯' },
|
||
{ value: 'Bangra', describe: '班格拉' },
|
||
{ value: 'Calypso', describe: '卡吕普索' },
|
||
{ value: 'Chalga', describe: '查尔加' },
|
||
{ value: 'Egyptian', describe: '埃及人' },
|
||
{ value: 'Hindustani', describe: '印度斯坦语' },
|
||
{ value: 'Jewish Music 犹太音乐' },
|
||
{ value: 'Klezmer 克莱兹默' },
|
||
{ value: 'Middle East', describe: '中东' },
|
||
{ value: 'Polka', describe: '波尔卡' },
|
||
{ value: 'Russian Navy Song', describe: '俄罗斯海军之歌' },
|
||
{ value: 'Suomipop', describe: 'Suomipop' },
|
||
{ value: 'Tribal', describe: '部落' }
|
||
]
|
||
const Genre = [
|
||
{ value: 'Country', describe: '乡村' },
|
||
{ value: 'Appalachian', describe: '阿巴拉契亚' },
|
||
{ value: 'Bluegrass', describe: '兰草' },
|
||
{ value: 'Country', describe: '乡村' },
|
||
{ value: 'Folk', describe: '民族' },
|
||
{ value: 'Freak Folk', describe: '怪胎民谣' },
|
||
{ value: 'Western', describe: '西方' },
|
||
{ value: 'Dance', describe: '跳舞' },
|
||
{ value: 'Afro-Cuban', describe: '非裔古巴人' },
|
||
{ value: 'Dance Pop', describe: '流行舞曲' },
|
||
{ value: 'Disco', describe: '迪斯科' },
|
||
{ value: 'Dubstep', describe: 'Dubstep的' },
|
||
{ value: 'Disco Funk', describe: '迪斯科放克' },
|
||
{ value: 'EDM', describe: 'EDM' },
|
||
{ value: 'Electro', describe: '电' },
|
||
{ value: 'High-NRG', describe: '高NRG' },
|
||
{ value: 'House', describe: '房子' },
|
||
{ value: 'Trance', describe: '恍惚' },
|
||
{ value: 'Downtempo', describe: '慢节奏' },
|
||
{ value: 'Ambient', describe: '氛围' },
|
||
{ value: 'Downtempo', describe: '慢节奏' },
|
||
{ value: 'Synthwave', describe: '合成波' },
|
||
{ value: 'Trap', describe: '陷阱' },
|
||
{ value: 'Electronic', describe: '电子的' },
|
||
{ value: 'Ambient', describe: '氛围' },
|
||
{ value: 'Cyberpunk', describe: '赛博朋克' },
|
||
{ value: 'Drum\'n\'bass Drum\'n\'bass', describe: '鼓与贝斯' },
|
||
{ value: 'Dubstep', describe: 'Dubstep的' },
|
||
{ value: 'Electronic', describe: '电子的' },
|
||
{ value: 'Hypnogogical', describe: '催眠' },
|
||
{ value: 'IDM', describe: 'IDM' },
|
||
{ value: 'Phonk', describe: '冯克' },
|
||
{ value: 'Synthpop', describe: '合成流行音乐' },
|
||
{ value: 'Techno', describe: '技术' },
|
||
{ value: 'Trap', describe: '陷阱' },
|
||
{ value: 'Jazz/Soul', describe: '爵士乐/灵魂乐' },
|
||
{ value: 'Bebop', describe: '贝波普' },
|
||
{ value: 'Gospel', describe: '福音' },
|
||
{ value: 'Electro', describe: '电' },
|
||
{ value: 'Frutiger Aero Frutiger', describe: '航空' },
|
||
{ value: 'Jazz', describe: '爵士乐' },
|
||
{ value: 'Latin Jazz', describe: '拉丁爵士乐' },
|
||
{ value: 'RnB', describe: 'RnB' },
|
||
{ value: 'Soul', describe: '灵魂' },
|
||
{ value: 'Latin', describe: '拉丁语' },
|
||
{ value: 'Bossa Nova', describe: '博萨诺瓦' },
|
||
{ value: 'Latin Jazz', describe: '拉丁爵士乐' },
|
||
{ value: 'Forró', describe: 'Forró' },
|
||
{ value: 'Mambo', describe: '曼波' },
|
||
{ value: 'Salsa', describe: '萨尔萨' },
|
||
{ value: 'Tango', describe: '探戈' },
|
||
{ value: 'Reggae', describe: '瑞格乐' },
|
||
{ value: 'Afrobeat', describe: '非洲节拍' },
|
||
{ value: 'Dancehall', describe: '舞厅' },
|
||
{ value: 'Dub', describe: 'Dub' },
|
||
{ value: 'Reggae', describe: '瑞格乐' },
|
||
{ value: 'Reggaeton', describe: '雷鬼' },
|
||
{ value: 'Metal', describe: '金属' },
|
||
{ value: 'Black Metal', describe: '黑色金属' },
|
||
{ value: 'Deathcore', describe: '死亡核心' },
|
||
{ value: 'Death Metal', describe: '死亡金属' },
|
||
{ value: 'Heavy Metal', describe: '重金属' },
|
||
{ value: 'Heavy Metal Trap', describe: '重金属捕集器' },
|
||
{ value: 'Metalcore', describe: '金属芯' },
|
||
{ value: 'Nu Metal', describe: 'Nu Metal(努金属)' },
|
||
{ value: 'Power Metal', describe: '动力金属' },
|
||
{ value: 'Popular', describe: '流行' },
|
||
{ value: 'Pop', describe: 'Pop' },
|
||
{ value: 'Dance Pop', describe: '流行舞曲' },
|
||
{ value: 'Pop Rock', describe: '流行摇滚' },
|
||
{ value: 'Kpop', describe: '韩流' },
|
||
{ value: 'Jpop', describe: '大通' },
|
||
{ value: 'Synthpop', describe: '合成流行音乐' },
|
||
{ value: 'Rock', describe: '摇滚' },
|
||
{ value: 'Classic Rock', describe: '经典摇滚' },
|
||
{ value: 'Blues Rock', describe: '蓝调摇滚' },
|
||
{ value: 'Emo', describe: 'Emo' },
|
||
{ value: 'Glam Rock', describe: '华丽摇滚' },
|
||
{ value: 'Hardcore Punk', describe: '硬核朋克' },
|
||
{ value: 'Indie', describe: '独立' },
|
||
{ value: 'Industrial Rock', describe: '工业摇滚' },
|
||
{ value: 'Punk', describe: '朋克' },
|
||
{ value: 'Rock', describe: '摇滚' },
|
||
{ value: 'Skate Rock', describe: '滑板摇滚' },
|
||
{ value: 'Skatecore', describe: '滑板芯' },
|
||
{ value: 'Suomipop', describe: 'Suomipop' },
|
||
{ value: 'Urban', describe: '都市的' },
|
||
{ value: 'Funk', describe: '恐惧' },
|
||
{ value: 'HipHop', describe: '嘻哈' },
|
||
{ value: 'Phonk', describe: '冯克' },
|
||
{ value: 'Rap', describe: 'Rap' },
|
||
{ value: 'Trap', describe: '陷阱' }
|
||
]
|
||
const Types = [
|
||
{ value: 'Background', describe: '背景' },
|
||
{ value: 'Elevator', describe: '电梯' },
|
||
{ value: 'Jingle', describe: '静乐县' },
|
||
{ value: 'Muzak', describe: '穆扎克' },
|
||
{ value: 'Call to Prayer', describe: '祷告的呼召' },
|
||
{ value: 'Adan', describe: '阿丹' },
|
||
{ value: 'Adjan', describe: '阿让' },
|
||
{ value: 'Call to Prayer', describe: '祷告的呼召' },
|
||
{ value: 'Gregorian Chant', describe: '格里高利圣歌' },
|
||
{ value: 'Character', describe: '字符' },
|
||
{ value: 'I Want Song', describe: '我想要歌' },
|
||
{ value: 'Hero Theme', describe: '英雄主题' },
|
||
{ value: 'Strut', describe: '支柱' },
|
||
{ value: 'March', describe: '三月' },
|
||
{ value: 'Military', describe: '军事' },
|
||
{ value: 'Villain Theme', describe: '反派主题' },
|
||
{ value: 'Children', describe: '孩子' },
|
||
{ value: 'Lullaby', describe: '催眠曲' },
|
||
{ value: 'Nursery Rhyme', describe: '童谣' },
|
||
{ value: 'Sing-along', describe: '跟唱' },
|
||
{ value: 'Toddler', describe: '幼儿' },
|
||
{ value: 'Composer', describe: '作曲家' },
|
||
{ value: 'Adagio', describe: '阿德吉奥' },
|
||
{ value: 'Adjunct', describe: '兼职' },
|
||
{ value: 'Andante', describe: '行板' },
|
||
{ value: 'Allegro', describe: '快板' },
|
||
{ value: 'Capriccio', describe: '狂想曲' },
|
||
{ value: 'Instruments', describe: '仪器' },
|
||
{ value: 'Acoustic Guitar', describe: '木吉他' },
|
||
{ value: 'Bass', describe: '低音' },
|
||
{ value: 'Doublebass', describe: '低音提琴' },
|
||
{ value: 'Electricbass', describe: '电贝司' },
|
||
{ value: 'Electric Guitar', describe: '电吉他' },
|
||
{ value: 'Fingerstyle Guitar', describe: '指弹吉他' },
|
||
{ value: 'Percussion', describe: '击发' },
|
||
{ value: 'Noise', describe: '噪声' },
|
||
{ value: 'Chaotic', describe: '混沌' },
|
||
{ value: 'Distorted', describe: '扭曲' },
|
||
{ value: 'Glitch', describe: '故障' },
|
||
{ value: 'Noise', describe: '噪声' },
|
||
{ value: 'Random', describe: '随机' },
|
||
{ value: 'Stuttering', describe: '口吃' },
|
||
{ value: 'Orchestral', describe: '管弦乐' },
|
||
{ value: 'glissando', describe: 'trombone 长号' },
|
||
{ value: 'legato cello', describe: '大提琴连奏' },
|
||
{ value: 'Orchestral', describe: '管弦乐' },
|
||
{ value: 'spiccato violins', describe: '斯皮卡托小提琴' },
|
||
{ value: 'staccato viola', describe: '断奏中提琴' },
|
||
{ value: 'Symphonic', describe: '交响' },
|
||
{ value: 'Retro', describe: '复古' },
|
||
{ value: '1960s', describe: '1960年代' },
|
||
{ value: 'Barbershop', describe: '理发店' },
|
||
{ value: 'Big Band', describe: '大乐队' },
|
||
{ value: 'Classic', describe: '经典' },
|
||
{ value: 'Doo Wop', describe: '嘟' },
|
||
{ value: 'Girl Group', describe: '女团' },
|
||
{ value: 'Mambo', describe: '曼波' },
|
||
{ value: 'Salooncore', describe: '沙龙核心' },
|
||
{ value: 'Swing', describe: '摆动' },
|
||
{ value: 'Traditional', describe: '传统的' },
|
||
{ value: 'Suffix', describe: '后缀' },
|
||
{ value: '…core', describe: '...核心' },
|
||
{ value: '…jam', describe: '...果酱' },
|
||
{ value: '…out', describe: '...外' },
|
||
{ value: '…wave', describe: '...浪' },
|
||
{ value: 'Traditional', describe: '传统的' },
|
||
{ value: 'Americana', describe: '美洲' },
|
||
{ value: 'Barbershop', describe: '理发店' },
|
||
{ value: 'Christmas Carol', describe: '圣诞颂歌' },
|
||
{ value: 'Traditional', describe: '传统的' },
|
||
{ value: 'Voice', describe: '声音' },
|
||
{ value: 'A Cappella', describe: '无伴奏合唱' },
|
||
{ value: 'Arabian Ornamental', describe: '阿拉伯观赏' },
|
||
{ value: 'Dispassionate', describe: '冷静的' },
|
||
{ value: 'Emotional', describe: '感情的' },
|
||
{ value: 'Ethereal', describe: '空灵' },
|
||
{ value: 'Gregorian chant', describe: '格里高利圣歌' },
|
||
{ value: 'Hindustani', describe: '印度斯坦语' },
|
||
{ value: 'Lounge Singer', describe: '休息室歌手' },
|
||
{ value: 'Melismatic', describe: '旋律' },
|
||
{ value: 'Monotone', describe: '单调' },
|
||
{ value: 'Narration', describe: '叙事' },
|
||
{ value: 'Resonant', describe: '谐振' },
|
||
{ value: 'Spoken Word', describe: '口语' },
|
||
{ value: 'Sprechgesang', describe: 'Sprechgesang' },
|
||
{ value: 'Sultry', describe: '闷热' },
|
||
{ value: 'Scream', describe: '尖叫' },
|
||
{ value: 'Torchy', describe: '火炬' },
|
||
{ value: 'Vocaloid', describe: '声乐' },
|
||
]
|
||
|
||
export default class BingSunoClient {
|
||
constructor(opts) {
|
||
this.opts = opts
|
||
}
|
||
|
||
async replyMsg(song, e) {
|
||
let messages = []
|
||
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))
|
||
await e.reply(await common.makeForwardMsg(e, messages, '音乐合成结果'))
|
||
let retry = 10
|
||
let videoPath
|
||
while (!videoPath && retry >= 0) {
|
||
try {
|
||
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) {
|
||
retry--
|
||
await common.sleep(3000)
|
||
}
|
||
}
|
||
if (videoPath) {
|
||
const data = fs.readFileSync(videoPath)
|
||
await e.reply(segment.video(`base64://${data.toString('base64')}`))
|
||
// 60秒后删除文件避免占用体积
|
||
setTimeout(() => {
|
||
fs.unlinkSync(videoPath)
|
||
}, 60000)
|
||
} else {
|
||
logger.warn(`${song.title}下载视频失败`)
|
||
await this.reply(`${song.title}下载视频失败`)
|
||
}
|
||
}
|
||
|
||
async getSuno(prompt, e) {
|
||
if (prompt.cookie) {
|
||
this.opts.cookies = prompt.cookie
|
||
}
|
||
const sunoResult = await this.getSunoResult(prompt.songtId)
|
||
if (sunoResult) {
|
||
const {
|
||
duration,
|
||
title,
|
||
musicalStyle,
|
||
requestId,
|
||
} = sunoResult
|
||
const generateURL = id => `https://th.bing.com/th?&id=${id}`
|
||
const audioURL = generateURL(`OIG.a_${requestId}`)
|
||
const imageURL = generateURL(`OIG.i_${requestId}`)
|
||
const videoURL = generateURL(`OIG.v_${requestId}`)
|
||
const sunoURL = `https://cdn1.suno.ai/${requestId}.mp4`
|
||
const sunoDisplayResult = {
|
||
title,
|
||
duration,
|
||
musicalStyle,
|
||
audioURL,
|
||
imageURL,
|
||
videoURL,
|
||
sunoURL,
|
||
prompt: prompt.songPrompt
|
||
}
|
||
await e.reply('Suno 生成中,请稍后')
|
||
this.replyMsg(sunoDisplayResult, e)
|
||
} else {
|
||
await e.reply('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 || prompt.lyrics
|
||
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('生成失败,可能是提示词太长或者违规,请检查日志')
|
||
redis.del(`CHATGPT:SUNO:${e.sender.user_id}`)
|
||
return
|
||
}
|
||
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('所有账户余额不足')
|
||
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('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('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) {
|
||
const skey = await this.#getSunoMetadata(requestId)
|
||
if (skey) {
|
||
const sunoMedia = await this.#getSunoMedia(requestId, skey)
|
||
return sunoMedia
|
||
}
|
||
return null
|
||
}
|
||
|
||
async #getSunoMetadata(requestId) {
|
||
const fetchURL = new URL('https://www.bing.com/videos/music')
|
||
const searchParams = new URLSearchParams({
|
||
vdpp: 'suno',
|
||
kseed: '7500',
|
||
SFX: '2',
|
||
q: '',
|
||
iframeid: crypto.randomUUID(),
|
||
requestId,
|
||
})
|
||
fetchURL.search = searchParams.toString()
|
||
const response = await fetch(fetchURL, {
|
||
headers: {
|
||
accept: 'text/html',
|
||
cookie: this.opts.cookies,
|
||
},
|
||
method: 'GET',
|
||
})
|
||
if (response.status === 200) {
|
||
const document = await response.text()
|
||
|
||
const patternSkey = /(?<=skey=)[^&]+/
|
||
const matchSkey = document.match(patternSkey)
|
||
const skey = matchSkey ? matchSkey[0] : null
|
||
|
||
const patternIG = /(?<=IG:"|IG:\s")[0-9A-F]{32}(?=")/
|
||
const matchIG = document.match(patternIG)
|
||
const ig = matchIG ? matchIG[0] : null
|
||
|
||
return { skey, ig }
|
||
} else {
|
||
console.error(`HTTP error! Error: ${response.error}, Status: ${response.status}`)
|
||
return null
|
||
}
|
||
}
|
||
|
||
async #getSunoMedia(requestId, sunoMetadata) {
|
||
let sfx = 1
|
||
const maxTries = 30
|
||
const { skey, ig } = sunoMetadata
|
||
|
||
let rawResponse
|
||
const result = await new Promise((resolve, reject) => {
|
||
const intervalId = setInterval(async () => {
|
||
const fetchURL = new URL('https://www.bing.com/videos/api/custom/music')
|
||
const searchParams = new URLSearchParams({
|
||
skey,
|
||
safesearch: 'Moderate',
|
||
vdpp: 'suno',
|
||
requestId,
|
||
ig,
|
||
iid: 'vsn',
|
||
sfx: sfx.toString(),
|
||
})
|
||
fetchURL.search = searchParams.toString()
|
||
const response = await fetch(fetchURL, {
|
||
headers: {
|
||
accept: '*/*',
|
||
cookie: this.opts.cookies,
|
||
},
|
||
method: 'GET',
|
||
})
|
||
try {
|
||
const body = await response.json()
|
||
rawResponse = JSON.parse(body.RawResponse)
|
||
const { status } = rawResponse
|
||
const done = status === 'complete'
|
||
|
||
if (done) {
|
||
clearInterval(intervalId)
|
||
resolve()
|
||
} else {
|
||
sfx++
|
||
if (sfx === maxTries) {
|
||
reject(new Error('Maximum number of tries exceeded'))
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.log(`获取音乐失败 ${response.status}`)
|
||
reject(new Error(error))
|
||
}
|
||
|
||
}, 2000)
|
||
})
|
||
.then(() => {
|
||
if (rawResponse?.status === 'complete') {
|
||
return {
|
||
duration: rawResponse.duration,
|
||
title: rawResponse.gptPrompt,
|
||
musicalStyle: rawResponse.musicalStyle,
|
||
requestId: rawResponse.id,
|
||
}
|
||
} else {
|
||
throw Error('Suno response could not be completed.')
|
||
}
|
||
})
|
||
.catch((err) => {
|
||
console.error(err)
|
||
return null
|
||
})
|
||
return result
|
||
}
|
||
|
||
extractLyrics(text) {
|
||
// 定义分段关键词
|
||
const sectionKeywords = ['Verse', 'Chorus', 'Bridge', 'Outro', 'End']
|
||
// 初始化lyrics变量
|
||
let lyrics = ''
|
||
// 标记是否开始提取歌词
|
||
let startExtracting = false
|
||
// 将文本按行分割
|
||
const lines = text.split('\n')
|
||
|
||
lines.forEach(line => {
|
||
// 检查每一行是否包含分段关键词
|
||
const sectionFound = sectionKeywords.some(keyword => {
|
||
const regex = new RegExp(`\\[${keyword} \\d+\\]|\\(${keyword} \\d+\\)|\\*\\*${keyword} \\d+\\*\\*`, 'i')
|
||
return regex.test(line)
|
||
})
|
||
// 如果找到第一个分段关键词,开始提取歌词
|
||
if (sectionFound && !startExtracting) {
|
||
startExtracting = true
|
||
}
|
||
// 如果已经开始提取歌词,则添加到lyrics变量中
|
||
if (startExtracting) {
|
||
lyrics += line + '\n'
|
||
}
|
||
})
|
||
return lyrics.trim() // 返回处理过的歌词
|
||
}
|
||
|
||
getRandomElements(arr, count) {
|
||
const shuffled = arr.sort(() => 0.5 - Math.random())
|
||
return shuffled.slice(0, count)
|
||
}
|
||
|
||
generateRandomStyle() {
|
||
const totalItems = 5
|
||
const itemsPerArray = Math.floor(totalItems / 3)
|
||
let remainingItems = totalItems % 3
|
||
|
||
let selectedStyles = this.getRandomElements(Style, itemsPerArray)
|
||
let selectedGenres = this.getRandomElements(Genre, itemsPerArray)
|
||
let selectedTypes = this.getRandomElements(Types, itemsPerArray)
|
||
|
||
if (remainingItems > 0) selectedStyles = selectedStyles.concat(this.getRandomElements(Style, 1)), remainingItems--
|
||
if (remainingItems > 0) selectedGenres = selectedGenres.concat(this.getRandomElements(Genre, 1)), remainingItems--
|
||
|
||
const allSelected = [...selectedStyles, ...selectedGenres, ...selectedTypes]
|
||
return allSelected.map(item => item.value).join(', ')
|
||
}
|
||
}
|