diff --git a/apps/chat.js b/apps/chat.js index c9782f1..190f221 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -33,6 +33,8 @@ import uploadRecord from '../utils/uploadRecord.js' import { SlackClaudeClient } from '../utils/slack/slackClient.js' import { ChatgptManagement } from './management.js' import { getPromptByName } from '../utils/prompts.js' +import Translate from '../utils/baiduTranslate.js' +import emojiStrip from 'emoji-strip' try { await import('keyv') } catch (err) { @@ -220,6 +222,7 @@ export class chatgpt extends plugin { async destroyConversations (e) { const userData = await getUserData(e.user_id) const use = (userData.mode === 'default' ? null : userData.mode) || await redis.get('CHATGPT:USE') + await redis.del(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`) if (use === 'claude') { // let client = new SlackClaudeClient({ // slackUserToken: Config.slackUserToken, @@ -367,6 +370,7 @@ export class chatgpt extends plugin { switch (use) { case 'claude': { let cs = await redis.keys('CHATGPT:SLACK_CONVERSATION:*') + let we = await redis.keys('CHATGPT:WRONG_EMOTION:*') for (let i = 0; i < cs.length; i++) { await redis.del(cs[i]) if (Config.debug) { @@ -374,10 +378,14 @@ export class chatgpt extends plugin { } deleted++ } + for (const element of we) { + await redis.del(element) + } break } case 'bing': { let cs = await redis.keys('CHATGPT:CONVERSATIONS_BING:*') + let we = await redis.keys('CHATGPT:WRONG_EMOTION:*') for (let i = 0; i < cs.length; i++) { await redis.del(cs[i]) if (Config.debug) { @@ -385,6 +393,9 @@ export class chatgpt extends plugin { } deleted++ } + for (const element of we) { + await redis.del(element) + } break } case 'api': { @@ -519,7 +530,7 @@ export class chatgpt extends plugin { } break case 'azure': - if (!Config.azureKey) { + if (!Config.azureTTSKey) { await this.reply('您没有配置Azure Key,请前往锅巴面板进行配置') return } @@ -538,6 +549,7 @@ export class chatgpt extends plugin { userSetting = JSON.parse(userSetting) } userSetting.useTTS = true + userSetting.usePicture = false await redis.set(`CHATGPT:USER:${e.sender.user_id}`, JSON.stringify(userSetting)) await this.reply('ChatGPT回复已转换为语音模式') } @@ -611,7 +623,8 @@ export class chatgpt extends plugin { userSetting.ttsRoleAzure = chosen[0].code await redis.set(`CHATGPT:USER:${e.sender.user_id}`, JSON.stringify(userSetting)) // Config.azureTTSSpeaker = chosen[0].code - await this.reply(`您的默认语音角色已被设置为”${speaker}-${chosen[0].gender}-${chosen[0].languageDetail}“`) + const supportEmotion = AzureTTS.supportConfigurations.find(config => config.name === speaker)?.emotion + await this.reply(`您的默认语音角色已被设置为 ${speaker}-${chosen[0].gender}-${chosen[0].languageDetail} ${supportEmotion && Config.azureTTSEmotion ? ',此角色支持多情绪配置,建议重新使用设定并结束对话以获得最佳体验!' : ''}`) } break } @@ -845,6 +858,25 @@ export class chatgpt extends plugin { await this.reply('我正在思考如何回复你,请稍等', true, { recallMsg: 8 }) } } + const emotionFlag = await redis.get(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`) + let userReplySetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`) + userReplySetting = !userReplySetting + ? getDefaultReplySetting() + : JSON.parse(userReplySetting) + // 图片模式就不管了,降低抱歉概率 + if (Config.ttsMode === 'azure' && Config.enhanceAzureTTSEmotion && userReplySetting.useTTS === true && await AzureTTS.getEmotionPrompt(e)) { + switch (emotionFlag) { + case '1': + prompt += '(上一次回复没有添加情绪,请确保接下来的对话正确使用情绪和情绪格式,回复时忽略此内容。)' + break + case '2': + prompt += '(不要使用给出情绪范围的词和错误的情绪格式,请确保接下来的对话正确选择情绪,回复时忽略此内容。)' + break + case '3': + prompt += '(不要给出多个情绪[]项,请确保接下来的对话给且只给出一个正确情绪项,回复时忽略此内容。)' + break + } + } logger.info(`chatgpt prompt: ${prompt}`) let previousConversation let conversation = {} @@ -955,7 +987,57 @@ export class chatgpt extends plugin { await e.reply('没有任何回复', true) return } - // 分离内容和情绪 + let emotion, emotionDegree + if (Config.ttsMode === 'azure' && (use === 'claude' || use === 'bing') && await AzureTTS.getEmotionPrompt(e)) { + let ttsRoleAzure = userReplySetting.ttsRoleAzure + const emotionReg = /\[\s*['`’‘]?(\w+)[`’‘']?\s*[,,、]\s*([\d.]+)\s*\]/ + const emotionTimes = response.match(/\[\s*['`’‘]?(\w+)[`’‘']?\s*[,,、]\s*([\d.]+)\s*\]/g) + const emotionMatch = response.match(emotionReg) + if (emotionMatch) { + const [startIndex, endIndex] = [ + emotionMatch.index, + emotionMatch.index + emotionMatch[0].length - 1 + ] + const ttsArr = + response.length / 2 < endIndex + ? [response.substring(startIndex), response.substring(0, startIndex)] + : [ + response.substring(0, endIndex + 1), + response.substring(endIndex + 1) + ] + const match = ttsArr[0].match(emotionReg) + response = ttsArr[1].replace(/\n/, '').trim() + if (match) { + [emotion, emotionDegree] = [match[1], match[2]] + const configuration = AzureTTS.supportConfigurations.find( + (config) => config.code === ttsRoleAzure + ) + const supportedEmotions = + configuration.emotion && Object.keys(configuration.emotion) + if (supportedEmotions && supportedEmotions.includes(emotion)) { + logger.warn(`角色 ${ttsRoleAzure} 支持 ${emotion} 情绪.`) + await redis.set(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`, '0') + } else { + logger.warn(`角色 ${ttsRoleAzure} 不支持 ${emotion} 情绪.`) + await redis.set(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`, '2') + } + logger.info(`情绪: ${emotion}, 程度: ${emotionDegree}`) + if (emotionTimes.length > 1) { + logger.warn('回复包含多个情绪项') + // 处理包含多个情绪项的情况,后续可以考虑实现单次回复多情绪的配置 + response = response.replace(/\[\s*['`’‘]?(\w+)[`’‘']?\s*[,,、]\s*([\d.]+)\s*\]/g, '').trim() + await redis.set(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`, '3') + } + } else { + // 使用了正则匹配外的奇奇怪怪的符号 + logger.warn('情绪格式错误') + await redis.set(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`, '2') + } + } else { + logger.warn('回复不包含情绪') + await redis.set(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`, '1') + } + } if (Config.sydneyMood) { let tempResponse = completeJSON(response) if (tempResponse.text) response = tempResponse.text @@ -1013,6 +1095,9 @@ export class chatgpt extends plugin { ttsRegex = '' } ttsResponse = response.replace(ttsRegex, '') + ttsResponse = emojiStrip(ttsResponse) + // 处理多行回复有时候只会读第一行和azure语音会读出一些标点符号的问题 + ttsResponse = ttsResponse.replace(/[-:_;*;\n]/g, ',') // 先把文字回复发出去,避免过久等待合成语音 if (Config.alsoSendText || ttsResponse.length > Config.ttsAutoFallbackThreshold) { if (Config.ttsMode === 'vits-uma-genshin-honkai' && ttsResponse.length > Config.ttsAutoFallbackThreshold) { @@ -1028,6 +1113,22 @@ export class chatgpt extends plugin { } let wav if (Config.ttsMode === 'vits-uma-genshin-honkai' && Config.ttsSpace) { + if (Config.autoJapanese && (_.isEmpty(Config.baiduTranslateAppId) || _.isEmpty(Config.baiduTranslateSecret))) { + await this.reply('请检查翻译配置是否正确。') + return false + } + if (Config.autoJapanese) { + try { + const translate = new Translate({ + appid: Config.baiduTranslateAppId, + secret: Config.baiduTranslateSecret + }) + ttsResponse = await translate(ttsResponse, '日') + } catch (err) { + logger.error(err) + await this.reply(err.message + '\n将使用原始文本合成语音...') + } + } try { wav = await generateAudio(ttsResponse, speaker, '中日混合(中文用[ZH][ZH]包裹起来,日文用[JA][JA]包裹起来)') } catch (err) { @@ -1035,9 +1136,14 @@ export class chatgpt extends plugin { await this.reply('合成语音发生错误~') } } else if (Config.ttsMode === 'azure' && Config.azureTTSKey) { + let ssml = AzureTTS.generateSsml(ttsResponse, { + speaker, + emotion, + emotionDegree + }) wav = await AzureTTS.generateAudio(ttsResponse, { speaker - }) + }, await ssml) } else if (Config.ttsMode === 'voicevox' && Config.voicevoxSpace) { wav = await VoiceVoxTTS.generateAudio(ttsResponse, { speaker @@ -1573,7 +1679,11 @@ export class chatgpt extends plugin { // 如果是新对话 if (Config.slackClaudeEnableGlobalPreset && (useCast?.slack || Config.slackClaudeGlobalPreset)) { // 先发送设定 - await client.sendMessage(useCast?.slack || Config.slackClaudeGlobalPreset, e) + let prompt = (useCast?.slack || Config.slackClaudeGlobalPreset) + await client.sendMessage(prompt, e) + // 处理可能由情绪参数导致的设定超限问题 + await client.sendMessage(await AzureTTS.getEmotionPrompt(e), e) + logger.info('claudeFirst:', prompt) } } let text = await client.sendMessage(prompt, e) @@ -1624,6 +1734,7 @@ export class chatgpt extends plugin { if (err.message?.indexOf('context_length_exceeded') > 0) { logger.warn(err) await redis.del(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`) + await redis.del(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`) await e.reply('字数超限啦,将为您自动结束本次对话。') return null } else { @@ -1648,6 +1759,7 @@ export class chatgpt extends plugin { // 如果有对话进行中,先删除 logger.info('开启Claude新对话,但旧对话未结束,自动结束上一次对话') await redis.del(`CHATGPT:SLACK_CONVERSATION:${e.sender.user_id}`) + await redis.del(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`) } response = await client.sendMessage('', e) await e.reply(response, true) @@ -1661,9 +1773,11 @@ export class chatgpt extends plugin { // 如果有对话进行中,先删除 logger.info('开启Claude新对话,但旧对话未结束,自动结束上一次对话') await redis.del(`CHATGPT:SLACK_CONVERSATION:${e.sender.user_id}`) + await redis.del(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`) } logger.info('send preset: ' + preset.content) - response = await client.sendMessage(preset.content, e) + response = await client.sendMessage(preset.content, e) + + await client.sendMessage(await AzureTTS.getEmotionPrompt(e), e) await e.reply(response, true) } } diff --git a/apps/entertainment.js b/apps/entertainment.js index 8dc3b88..9d45a9b 100644 --- a/apps/entertainment.js +++ b/apps/entertainment.js @@ -8,7 +8,8 @@ import fetch from 'node-fetch' import { mkdirs } from '../utils/common.js' import uploadRecord from '../utils/uploadRecord.js' import { makeWordcloud } from '../utils/wordcloud/wordcloud.js' - +import Translate, { transMap } from '../utils/baiduTranslate.js' +import _ from 'lodash' let useSilk = false try { await import('node-silk') @@ -20,7 +21,7 @@ export class Entertainment extends plugin { constructor (e) { super({ name: 'ChatGPT-Plugin 娱乐小功能', - dsc: '让你的聊天更有趣!现已支持主动打招呼和表情合成小功能!', + dsc: '让你的聊天更有趣!现已支持主动打招呼、表情合成、群聊词云统计与文本翻译小功能!', event: 'message', priority: 500, rule: [ @@ -41,6 +42,10 @@ export class Entertainment extends plugin { { reg: '^#?(今日词云|群友在聊什么)$', fnc: 'wordcloud' + }, + { + reg: '^#((?:寄批踢)?翻.*|chatgpt翻译帮助)', + fnc: 'translate' } ] }) @@ -55,6 +60,42 @@ export class Entertainment extends plugin { ] } + async translate (e) { + if (e.msg.trim() === '#chatgpt翻译帮助') { + await this.reply('支持中、日、文(文言文)、英、俄、韩语言之间的文本翻译功能,"寄批踢"为可选前缀' + + '\n示例:1. #寄批踢翻英 你好' + + '\t2. #翻中 你好' + + '\t3. #寄批踢翻文 hello') + return + } + if (_.isEmpty(Config.baiduTranslateAppId) || _.isEmpty(Config.baiduTranslateSecret)) { + this.reply('请检查翻译配置是否正确。') + return + } + const regExp = /(#(?:寄批踢)?翻(.))(.*)/ + const msg = e.msg.trim() + const match = msg.match(regExp) + let result = '' + if (!(match[2] in transMap)) { + e.reply('输入格式有误或暂不支持该语言,' + + '\n当前支持:中、日、文(文言文)、英、俄、韩。', e.isGroup + ) + return + } + const PendingText = match[3] + try { + const translate = new Translate({ + appid: Config.baiduTranslateAppId, + secret: Config.baiduTranslateSecret + }) + result = await translate(PendingText, match[2]) + } catch (err) { + logger.error(err) + result = err.message + } + await this.reply(result, e.isGroup) + } + async wordcloud (e) { if (e.isGroup) { let groupId = e.group_id @@ -248,8 +289,8 @@ export class Entertainment extends plugin { return false } else { Config.initiativeChatGroups = Config.initiativeChatGroups - .filter(group => group.trim() !== '') - .concat(validGroups) + .filter(group => group.trim() !== '') + .concat(validGroups) } if (typeof paramArray[2] === 'undefined' && typeof paramArray[3] === 'undefined') { replyMsg = `已更新打招呼设置:\n${!e.isGroup ? '群号:' + Config.initiativeChatGroups.join(', ') + '\n' : ''}间隔时间:${Config.helloInterval}小时\n触发概率:${Config.helloProbability}%` diff --git a/apps/history.js b/apps/history.js index e7ab30b..3c27859 100644 --- a/apps/history.js +++ b/apps/history.js @@ -22,7 +22,8 @@ export class history extends plugin { rule: [ { reg: '^#(chatgpt|ChatGPT)(导出)?聊天记录$', - fnc: 'history' + fnc: 'history', + permission: 'master' } ] }) diff --git a/apps/management.js b/apps/management.js index 8b21e48..0359213 100644 --- a/apps/management.js +++ b/apps/management.js @@ -1,13 +1,23 @@ import plugin from '../../../lib/plugins/plugin.js' import { Config } from '../utils/config.js' import { exec } from 'child_process' -import { checkPnpm, formatDuration, parseDuration, getPublicIP, renderUrl } from '../utils/common.js' +import { + checkPnpm, + formatDuration, + parseDuration, + getPublicIP, + renderUrl, + makeForwardMsg, + getDefaultReplySetting +} from '../utils/common.js' import SydneyAIClient from '../utils/SydneyAIClient.js' -import { convertSpeaker, speakers } from '../utils/tts.js' +import { convertSpeaker, speakers as vitsRoleList } from '../utils/tts.js' import md5 from 'md5' import path from 'path' import fs from 'fs' import loader from '../../../lib/plugins/loader.js' +import { supportConfigurations as voxRoleList } from '../utils/tts/voicevox.js' +import { supportConfigurations as azureRoleList } from '../utils/tts/microsoft-azure.js' let isWhiteList = true export class ChatgptManagement extends plugin { constructor (e) { @@ -212,11 +222,62 @@ export class ChatgptManagement extends plugin { { reg: '^#chatgpt(对话|管理|娱乐|绘图|人物设定|聊天记录)?指令表(帮助)?', fnc: 'commandHelp' + }, + { + reg: '^#语音切换.*', + fnc: 'ttsSwitch', + permission: 'master' + }, + { + reg: '^#chatgpt角色列表$', + fnc: 'getTTSRoleList' } ] }) } + async getTTSRoleList (e) { + let userReplySetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`) + userReplySetting = !userReplySetting + ? getDefaultReplySetting() + : JSON.parse(userReplySetting) + if (!userReplySetting.useTTS) return + let ttsMode = Config.ttsMode + let roleList = [] + if (ttsMode === 'vits-uma-genshin-honkai') { + const [firstHalf, secondHalf] = [vitsRoleList.slice(0, Math.floor(vitsRoleList.length / 2)).join('、'), vitsRoleList.slice(Math.floor(vitsRoleList.length / 2)).join('、')] + const [chunk1, chunk2] = [firstHalf.match(/[^、]+(?:、[^、]+){0,30}/g), secondHalf.match(/[^、]+(?:、[^、]+){0,30}/g)] + const list = [await makeForwardMsg(e, chunk1, `${Config.ttsMode}角色列表1`), await makeForwardMsg(e, chunk2, `${Config.ttsMode}角色列表2`)] + roleList = await makeForwardMsg(e, list, `${Config.ttsMode}角色列表`) + await this.reply(roleList) + return + } else if (ttsMode === 'voicevox') { + roleList = voxRoleList.map(item => item.name).join('、') + } else if (ttsMode === 'azure') { + roleList = azureRoleList.map(item => item.name).join('、') + } + if (roleList.length > 300) { + let chunks = roleList.match(/[^、]+(?:、[^、]+){0,30}/g) + roleList = await makeForwardMsg(e, chunks, `${Config.ttsMode}角色列表`) + } + await this.reply(roleList) + } + async ttsSwitch (e) { + let regExp = /#语音切换(.*)/ + let ttsMode = e.msg.match(regExp)[1] + if (['vits', 'azure', 'voicevox'].includes(ttsMode)) { + if (ttsMode === 'vits') { + Config.ttsMode = 'vits-uma-genshin-honkai' + } else { + Config.ttsMode = ttsMode + } + await this.reply(`语音回复已切换至${Config.ttsMode}模式${Config.ttsMode === 'azure' ? ',建议重新开始对话以获得更好的对话效果!' : ''}`) + } else { + await this.reply('暂不支持此模式,当前支持vits,azure,voicevox。') + } + return false + } + async commandHelp (e) { if (!this.e.isMaster) { return this.reply('你没有权限') } if (e.msg.trim() === '#chatgpt指令表帮助') { @@ -267,8 +328,8 @@ export class ChatgptManagement extends plugin { prompts.push(generatePrompt(plugin, commands)) } } - - await this.reply(prompts.join('\n')) + let msg = await makeForwardMsg(e, prompts, e.msg.slice(1)) + await this.reply(msg) return true } @@ -479,7 +540,7 @@ export class ChatgptManagement extends plugin { Config.defaultTTSRole = '' } else { const ttsRole = convertSpeaker(speaker) - if (speakers.includes(ttsRole)) { + if (vitsRoleList.includes(ttsRole)) { Config.defaultTTSRole = ttsRole replyMsg = `ChatGPT默认语音角色已被设置为“${ttsRole}”` } else { diff --git a/apps/prompts.js b/apps/prompts.js index 1860648..5d4d173 100644 --- a/apps/prompts.js +++ b/apps/prompts.js @@ -4,6 +4,7 @@ import _ from 'lodash' import { Config } from '../utils/config.js' import { getMasterQQ, limitString, makeForwardMsg, maskQQ } from '../utils/common.js' import { deleteOnePrompt, getPromptByName, readPrompts, saveOnePrompt } from '../utils/prompts.js' +import AzureTTS from "../utils/tts/microsoft-azure.js"; export class help extends plugin { constructor (e) { super({ @@ -160,7 +161,12 @@ export class help extends plugin { } if (keyMap[use]) { - Config[keyMap[use]] = prompt.content + if (Config.ttsMode === 'azure') { + Config[keyMap[use]] = prompt.content + '\n' + await AzureTTS.getEmotionPrompt(e) + logger.warn(Config[keyMap[use]]) + } else { + Config[keyMap[use]] = prompt.content + } await redis.set(`CHATGPT:PROMPT_USE_${use}`, promptName) await e.reply(`你当前正在使用${use}模式,已将该模式设定应用为"${promptName}"。更该设定后建议结束对话以使设定更好生效`, true) } else { diff --git a/config/config.example.json b/config/config.example.json index 59a51aa..577faca 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -72,5 +72,11 @@ "enablePrivateChat": false, "groupWhitelist": [], "groupBlacklist": [], - "ttsRegex": "/匹配规则/匹配模式" + "ttsRegex": "/匹配规则/匹配模式", + "baiduTranslateAppId": "", + "baiduTranslateSecret": "", + "azureTTSSpeaker": "zh-CN-XiaochenNeural", + "azureTTSEmotion": false, + "enhanceAzureTTSEmotion": false, + "autoJapanese": false } \ No newline at end of file diff --git a/guoba.support.js b/guoba.support.js index 7cc2b87..1d90bba 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -98,29 +98,16 @@ export function supportGuoba () { }, { field: 'defaultTTSRole', - label: '语音模式默认角色(vits-uma-genshin-honkai)', + label: 'vits默认角色', bottomHelpMessage: 'vits-uma-genshin-honkai语音模式下,未指定角色时使用的角色。若留空,将使用随机角色回复。若用户通过指令指定了角色,将忽略本设定', component: 'Select', componentProps: { options: speakers.concat('随机').map(s => { return { label: s, value: s } }) } }, - { - field: 'voicevoxTTSSpeaker', - label: '语音模式默认角色(VoiceVox)', - bottomHelpMessage: 'VoiceVox语音模式下,未指定角色时使用的角色。若留空,将使用随机角色回复。若用户通过指令指定了角色,将忽略本设定', - component: 'Select', - componentProps: { - options: VoiceVoxTTS.supportConfigurations.map(item => { - return item.styles.map(style => { - return `${item.name}-${style.name}` - }).concat(item.name) - }).flat().concat('随机').map(s => { return { label: s, value: s } }) - } - }, { field: 'azureTTSSpeaker', - label: '语音模式默认角色(微软Azure)', + label: 'Azure默认角色', bottomHelpMessage: '微软Azure语音模式下,未指定角色时使用的角色。若用户通过指令指定了角色,将忽略本设定', component: 'Select', componentProps: { @@ -132,6 +119,19 @@ export function supportGuoba () { }) } }, + { + field: 'voicevoxTTSSpeaker', + label: 'VoiceVox默认角色', + bottomHelpMessage: 'VoiceVox语音模式下,未指定角色时使用的角色。若留空,将使用随机角色回复。若用户通过指令指定了角色,将忽略本设定', + component: 'Select', + componentProps: { + options: VoiceVoxTTS.supportConfigurations.map(item => { + return item.styles.map(style => { + return `${item.name}-${style.name}` + }).concat(item.name) + }).flat().concat('随机').map(s => { return { label: s, value: s } }) + } + }, { field: 'ttsRegex', label: '语音过滤正则表达式', @@ -155,6 +155,14 @@ export function supportGuoba () { bottomHelpMessage: '语音模式下,同时发送文字版,避免音质较低听不懂', component: 'Switch' }, + { + field: 'autoJapanese', + label: 'vits模式日语输出', + bottomHelpMessage: '使用vits语音时,将机器人的文字回复翻译成日文后获取语音。' + + '需要填写下方的翻译配置,配置文档:http://api.fanyi.baidu.com/doc/21 ' + + '填写配置后另外支持通过本插件使用文字翻译功能,发送"#chatgpt翻译帮助"查看使用方法。', + component: 'Switch' + }, { field: 'autoUsePicture', label: '长文本自动转图片', @@ -557,6 +565,16 @@ export function supportGuoba () { bottomHelpMessage: '可注册2captcha实现跳过验证码,收费服务但很便宜。否则可能会遇到验证码而卡住', component: 'InputPassword' }, + { + field: 'baiduTranslateAppId', + label: '百度翻译应用ID', + component: 'Input' + }, + { + field: 'baiduTranslateSecret', + label: '百度翻译密钥', + component: 'Input' + }, { field: 'ttsSpace', label: 'vits-uma-genshin-honkai语音转换API地址', @@ -580,6 +598,18 @@ export function supportGuoba () { bottomHelpMessage: '例如japaneast', component: 'Input' }, + { + field: 'azureTTSEmotion', + label: 'Azure情绪多样化', + bottomHelpMessage: '切换角色后使用"#chatgpt使用设定xxx"重新开始对话以更新不同角色的情绪配置。支持使用不同的说话风格回复,各个角色支持说话风格详情:https://speech.microsoft.com/portal/voicegallery', + component: 'Switch' + }, + { + field: 'enhanceAzureTTSEmotion', + label: 'Azure情绪纠正', + bottomHelpMessage: '当机器人未使用或使用了不支持的说话风格时,将在对话中提醒机器人。注意:bing模式开启此项后有概率增大触发抱歉的机率,且不要单独开启此项。', + component: 'Switch' + }, { field: 'huggingFaceReverseProxy', label: '语音转换huggingface反代', diff --git a/package.json b/package.json index 5b21142..37ee843 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@slack/bolt": "^3.13.0", "@waylaidwanderer/chatgpt-api": "^1.33.2", "asn1.js": "^5.0.0", + "axios": "^1.3.6", "chatgpt": "^5.1.1", "delay": "^5.0.0", "diff": "^5.1.0", @@ -18,13 +19,15 @@ "https-proxy-agent": "5.0.1", "keyv": "^4.5.2", "keyv-file": "^0.2.0", + "md5-node": "^1.0.1", "microsoft-cognitiveservices-speech-sdk": "^1.27.0", "node-fetch": "^3.3.1", "openai": "^3.2.1", "random": "^4.1.0", "undici": "^5.21.0", "uuid": "^9.0.0", - "ws": "^8.13.0" + "ws": "^8.13.0", + "emoji-strip": "^1.0.1" }, "optionalDependencies": { "@node-rs/jieba": "^1.6.2", diff --git a/utils/baiduTranslate.js b/utils/baiduTranslate.js new file mode 100644 index 0000000..6c9223e --- /dev/null +++ b/utils/baiduTranslate.js @@ -0,0 +1,141 @@ +import md5 from 'md5-node' +import axios from 'axios' + +// noinspection NonAsciiCharacters +export const transMap = { 中: 'zh', 日: 'jp', 文: 'wyw', 英: 'en', 俄: 'ru', 韩: 'kr' } +const errOr = { + 52001: '请求超时,请重试。', + 52002: '系统错误,请重试。', + 52003: '未授权用户,请检查appid是否正确或者服务是否开通。', + 54000: '必填参数为空,请检查是否少传参数。', + 54001: '签名错误,请检查您的签名生成方法。', + 54003: '访问频率受限,请降低您的调用频率,或进行身份认证后切换为高级版/尊享版。', + 54004: '账户余额不足,请前往管理控制台为账户充值。', + 54005: '长query请求频繁,请降低长query的发送频率,3s后再试。', + 58000: '客户端IP非法,检查个人资料里填写的IP地址是否正确,可前往开发者信息-基本信息修改。', + 58001: '译文语言方向不支持,检查译文语言是否在语言列表里。', + 58002: '服务当前已关闭,请前往管理控制台开启服务。', + 90107: '认证未通过或未生效,请前往我的认证查看认证进度。' +} +function Translate (config) { + this.requestNumber = 0 // 请求次数 + this.config = { + showProgress: 1, // 是否显示进度 + requestNumber: 1, // 最大请求次数 + agreement: 'http', // 协议 + ...config + } + this.baiduApi = `${this.config.agreement}://api.fanyi.baidu.com/api/trans/vip/translate` + + // 拼接url + this.createUrl = (domain, form) => { + let result = domain + '?' + for (let key in form) { + result += `${key}=${form[key]}&` + } + return result.slice(0, result.length - 1) + } + + this.translate = async (value, ...params) => { + let result = '' + let from = 'auto' + let to = 'en' + + if (params.length === 1) { + to = transMap[params[0]] || to + } else if (params.length === 2) { + from = transMap[params[0]] || from + to = transMap[params[1]] || to + } + if (typeof value === 'string') { + const res = await this.requestApi(value, { from, to }) + result = res[0].dst + } + + if (Array.isArray(value) || Object.prototype.toString.call(value) === '[object Object]') { + result = await this._createObjValue(value, { from, to }) + } + + return result + } + + this.requestApi = (value, params) => { + if (this.requestNumber >= this.config.requestNumber) { + return new Promise((resolve) => { + setTimeout(() => { + this.requestApi(value, params).then((res) => { + resolve(res) + }) + }, 1000) + }) + } + + this.requestNumber++ + + const { appid, secret } = this.config + const q = value + const salt = Math.random() + const sign = md5(`${appid}${q}${salt}${secret}`) + + const fromData = { + q: encodeURIComponent(q), + sign, + appid, + salt, + from: params.from || 'auto', + to: params.to || 'en' + } + + const fanyiApi = this.createUrl(this.baiduApi, fromData) + + return new Promise((resolve, reject) => { + axios + .get(fanyiApi) + .then(({ data: res }) => { + if (!res.error_code) { + const resList = res.trans_result + resolve(resList) + } else { + const errCode = res.error_code + if (errOr[errCode]) { + reject(new Error('翻译出错了~' + errOr[errCode])) + } else { + reject(new Error('翻译出错了~' + errCode)) + } + } + }) + .finally(() => { + setTimeout(() => { + this.requestNumber-- + }, 1000) + }) + }) + } + // 递归翻译数组或对象 + this._createObjValue = async (value, parames) => { + let index = 0 + const obj = Array.isArray(value) ? [] : {} + const strDatas = Array.isArray(value) ? value : Object.values(value) + const reqData = strDatas + .filter((item) => typeof item === 'string') // 过滤字符串 + .join('\n') + const res = reqData ? await this.requestApi(reqData, parames) : [] + for (let key in value) { + if (typeof value[key] === 'string') { + obj[key] = res[index].dst + index++ + } + if ( + Array.isArray(value[key]) || + Object.prototype.toString.call(value[key]) === '[object Object]' + ) { + obj[key] = await this.translate(value[key], parames) // 递归翻译 + } + } + return obj + } + + return this.translate +} + +export default Translate diff --git a/utils/config.js b/utils/config.js index 0e604d1..55014cc 100644 --- a/utils/config.js +++ b/utils/config.js @@ -111,8 +111,12 @@ const defaultConfig = { azureTTSSpeaker: 'zh-CN-XiaochenNeural', voicevoxSpace: '', voicevoxTTSSpeaker: '护士机器子T', + baiduTranslateAppId: '', + baiduTranslateSecret: '', + azureTTSEmotion: false, + enhanceAzureTTSEmotion: false, + autoJapanese: false, version: 'v2.5.8' - } const _path = process.cwd() let config = {} diff --git a/utils/tts/microsoft-azure.js b/utils/tts/microsoft-azure.js index aff8fe0..9eae62f 100644 --- a/utils/tts/microsoft-azure.js +++ b/utils/tts/microsoft-azure.js @@ -1,50 +1,110 @@ - import crypto from 'crypto' -import { mkdirs } from '../common.js' +import { getDefaultReplySetting, mkdirs } from '../common.js' import { Config } from '../config.js' + let sdk try { sdk = (await import('microsoft-cognitiveservices-speech-sdk')).default } catch (err) { logger.warn('未安装microsoft-cognitiveservices-speech-sdk,无法使用微软Azure语音源') } -async function generateAudio (text, option = {}) { +async function generateAudio (text, option = {}, ssml = '') { if (!sdk) { throw new Error('未安装microsoft-cognitiveservices-speech-sdk,无法使用微软Azure语音源') } let subscriptionKey = Config.azureTTSKey let serviceRegion = Config.azureTTSRegion + let speechConfig = sdk.SpeechConfig.fromSubscription(subscriptionKey, serviceRegion) const _path = process.cwd() mkdirs(`${_path}/data/chatgpt/tts/azure`) let filename = `${_path}/data/chatgpt/tts/azure/${crypto.randomUUID()}.wav` let audioConfig = sdk.AudioConfig.fromAudioFileOutput(filename) - let speechConfig = sdk.SpeechConfig.fromSubscription(subscriptionKey, serviceRegion) - // speechConfig.speechSynthesisLanguage = option?.language || 'zh-CN' - logger.info('using speaker: ' + option?.speaker || 'zh-CN-YunyeNeural') - speechConfig.speechSynthesisVoiceName = option?.speaker || 'zh-CN-YunyeNeural' - let synthesizer = new sdk.SpeechSynthesizer(speechConfig, audioConfig) + let synthesizer + if (ssml) { + synthesizer = new sdk.SpeechSynthesizer(speechConfig, audioConfig) + await speakSsmlAsync(synthesizer, ssml) + } else { + speechConfig.speechSynthesisLanguage = option?.language || 'zh-CN' + logger.info('using speaker: ' + option?.speaker || 'zh-CN-YunyeNeural') + speechConfig.speechSynthesisVoiceName = option?.speaker || 'zh-CN-YunyeNeural' + synthesizer = new sdk.SpeechSynthesizer(speechConfig, audioConfig) + await speakTextAsync(synthesizer, text) + } + console.log('synthesis finished.') + synthesizer.close() + synthesizer = undefined + return filename +} + +async function speakTextAsync (synthesizer, text) { return new Promise((resolve, reject) => { synthesizer.speakTextAsync(text, result => { if (result.reason === sdk.ResultReason.SynthesizingAudioCompleted) { - console.log('synthesis finished.') + logger.info('speakTextAsync: true') + resolve() } else { console.error('Speech synthesis canceled, ' + result.errorDetails + '\nDid you update the subscription info?') + reject(result.errorDetails) } - synthesizer.close() - synthesizer = undefined - resolve(filename) }, err => { console.error('err - ' + err) - synthesizer.close() - synthesizer = undefined reject(err) }) }) } -const supportConfigurations = [ +async function speakSsmlAsync (synthesizer, ssml) { + return new Promise((resolve, reject) => { + synthesizer.speakSsmlAsync(ssml, result => { + if (result.reason === sdk.ResultReason.SynthesizingAudioCompleted) { + logger.info('speakSsmlAsync: true') + resolve() + } else { + console.error('Speech synthesis canceled, ' + result.errorDetails + + '\nDid you update the subscription info?') + reject(result.errorDetails) + } + }, err => { + console.error('err - ' + err) + reject(err) + }) + }) +} +async function generateSsml (text, option = {}) { + const voiceName = option.speaker || 'zh-CN-YunyeNeural' + const expressAs = option.emotion ? `` : '' + return ` + + ${expressAs}${text}${expressAs ? '' : ''} + + ` +} +async function getEmotionPrompt (e) { + if (!Config.azureTTSEmotion) return '' + let userReplySetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`) + userReplySetting = !userReplySetting + ? getDefaultReplySetting() + : JSON.parse(userReplySetting) + let emotionPrompt = '' + let ttsRoleAzure = userReplySetting.ttsRoleAzure + const configuration = Config.ttsMode === 'azure' ? supportConfigurations.find(config => config.code === ttsRoleAzure) : '' + if (configuration !== '' && configuration.emotion) { + // 0-1 感觉没啥区别,说实话只有1和2听得出差别。。 + emotionPrompt = `\n在回复的最开始使用[]在其中表示你这次回复的情绪风格和程度(1-2),最小单位0.1 + \n例如:['angry',2]表示你极度愤怒 + \n这是情绪参考值,禁止使用给出范围以外的词,且单次回复只需要给出一个情绪表示 + \n${JSON.stringify(configuration.emotion)} + \n另外,不要在情绪[]前后使用回车换行,如果你明白上面的设定,请回复’好的,我明白了‘并在后续的对话中严格执行此设定。` + // logger.warn('emotionPrompt:', `${JSON.stringify(configuration.emotion)}`) + } else { + return '' + } + return emotionPrompt +} +export const supportConfigurations = [ { code: 'zh-CN-liaoning-XiaobeiNeural', name: '晓北', @@ -78,35 +138,86 @@ const supportConfigurations = [ name: '晓晓', language: 'zh-CN', languageDetail: '中文(普通话,简体)', - gender: '女' + gender: '女', + emotion: { + affectionate: '温暖、亲切的语气', + angry: '生气和厌恶的语气', + assistant: '数字助理用的是热情而轻松的语气', + calm: '沉着冷静的态度说话。语气、音调和韵律统一', + chat: '表达轻松随意的语气', + cheerful: '表达积极愉快的语气', + customerservice: '以友好热情的语气为客户提供支持', + disgruntled: '轻蔑、抱怨的语气,表现不悦和蔑视', + excited: '乐观、充满希望的语气,发生了美好的事情', + fearful: '恐惧、紧张的语气,说话人处于紧张和不安的状态', + friendly: '愉快、怡人、温暖、真诚、关切的语气', + gentle: '温和、礼貌、愉快的语气,音调和音量较低', + lyrical: '以优美又带感伤的方式表达情感', + newscast: '以正式专业的语气叙述新闻', + 'poetry-reading': '读诗时带情感和节奏的语气', + sad: '表达悲伤语气', + serious: '严肃、命令的语气' + } }, { code: 'zh-CN-YunxiNeural', name: '云希', language: 'zh-CN', - languageDetail: '中文(普通话,简体)', - gender: '男' + languageDetail: '中文 (普通话,简体)', + gender: '男', + emotion: { + angry: '表达生气和愤怒的语气', + assistant: '数字助理使用热情而轻松的语气', + chat: '表达轻松随意的语气', + cheerful: '表达积极愉快的语气', + depressed: '表达沮丧、消沉的语气', + disgruntled: '表达不满、不悦的语气', + embarrassed: '表达尴尬、难为情的语气', + fearful: '表达害怕、恐惧的语气', + 'narration-relaxed': '以轻松、自然的语气叙述', + newscast: '用于新闻播报,表现出庄重、严谨的语气', + sad: '表达悲伤、失落的语气', + serious: '表现出认真、严肃的语气' + } }, { code: 'zh-CN-YunyangNeural', name: '云扬', language: 'zh-CN', - languageDetail: '中文(普通话,简体)', - gender: '男' + languageDetail: '中文 (普通话,简体)', + gender: '男', + emotion: { + customerservice: '以亲切友好的语气为客户提供支持', + 'narration-professional': '以专业、稳重的语气讲述', + 'newscast-casual': '以轻松自然的语气播报新闻' + } }, { code: 'zh-CN-YunyeNeural', name: '云野', language: 'zh-CN', languageDetail: '中文(普通话,简体)', - gender: '男' + gender: '男', + emotion: { + angry: '表达愤怒和不满的语气', + calm: '以冷静的态度说话,不带过多情绪', + cheerful: '表达快乐和积极的语气', + disgruntled: '表达不满和不满足的语气', + embarrassed: '表达不自在或难堪的语气', + fearful: '表达害怕和不安的语气', + sad: '表达悲伤和失落的语气', + serious: '以认真和严肃的态度说话' + } }, { code: 'zh-CN-XiaoshuangNeural', name: '晓双', language: 'zh-CN', languageDetail: '中文(普通话,简体)', - gender: '女' + gender: '女', + emotion: { + chat: '表达轻松随意的语气' + } }, { code: 'zh-CN-XiaoyouNeural', @@ -141,56 +252,126 @@ const supportConfigurations = [ name: '晓墨', language: 'zh-CN', languageDetail: '中文(普通话,简体)', - gender: '女' + gender: '女', + emotion: { + affectionate: '温暖、亲切的语气', + angry: '生气和厌恶的语气', + calm: '沉着冷静的态度说话。语气、音调和韵律统一', + cheerful: '表达积极愉快的语气', + depressed: '调低音调和音量来表达忧郁、沮丧的语气', + disgruntled: '轻蔑、抱怨的语气,表现不悦和蔑视', + embarrassed: '在说话者感到不舒适时表达不确定、犹豫的语气', + envious: '当你渴望别人拥有的东西时,表达一种钦佩的语气', + fearful: '恐惧、紧张的语气,说话人处于紧张和不安的状态', + gentle: '温和、礼貌、愉快的语气,音调和音量较低', + sad: '表达悲伤语气', + serious: '严肃、命令的语气' + } }, { code: 'zh-CN-XiaoxuanNeural', name: '晓萱', language: 'zh-CN', languageDetail: '中文(普通话,简体)', - gender: '女' + gender: '女', + emotion: { + angry: '生气和厌恶的语气', + calm: '沉着冷静的态度说话。语气、音调和韵律统一', + cheerful: '表达积极愉快的语气', + depressed: '调低音调和音量来表达忧郁、沮丧的语气', + disgruntled: '轻蔑、抱怨的语气,表现不悦和蔑视', + fearful: '恐惧、紧张的语气,说话人处于紧张和不安的状态', + gentle: '温和、礼貌、愉快的语气,音调和音量较低', + serious: '严肃、命令的语气' + } }, { code: 'zh-CN-XiaohanNeural', name: '晓涵', language: 'zh-CN', languageDetail: '中文(普通话,简体)', - gender: '女' + gender: '女', + emotion: { + affectionate: '温暖、亲切的语气', + angry: '生气和厌恶的语气', + calm: '沉着冷静的态度说话。语气、音调和韵律统一', + cheerful: '表达积极愉快的语气', + disgruntled: '轻蔑、抱怨的语气,表现不悦和蔑视', + embarrassed: '在说话者感到不舒适时表达不确定、犹豫的语气', + fearful: '恐惧、紧张的语气,说话人处于紧张和不安的状态', + gentle: '温和、礼貌、愉快的语气,音调和音量较低', + sad: '表达悲伤语气', + serious: '严肃、命令的语气' + } }, { code: 'zh-CN-XiaoruiNeural', name: '晓睿', language: 'zh-CN', languageDetail: '中文(普通话,简体)', - gender: '女' + gender: '女', + emotion: { + angry: '生气和厌恶的语气', + calm: '沉着冷静的态度说话。语气、音调和韵律统一', + fearful: '恐惧、紧张的语气,说话人处于紧张和不安的状态', + sad: '表达悲伤语气' + } }, { code: 'zh-CN-XiaomengNeural', name: '晓梦', language: 'zh-CN', languageDetail: '中文(普通话,简体)', - gender: '女' + gender: '女', + emotion: { + chat: '表达轻松随意的语气' + } }, { code: 'zh-CN-XiaoyiNeural', name: '晓伊', language: 'zh-CN', languageDetail: '中文(普通话,简体)', - gender: '女' + gender: '女', + emotion: { + angry: '生气和厌恶的语气', + affectionate: '温暖、亲切的语气', + cheerful: '表达积极愉快的语气', + gentle: '温和、礼貌、愉快的语气,音调和音量较低', + sad: '表达悲伤语气', + serious: '严肃、命令的语气' + } }, { code: 'zh-CN-XiaozhenNeural', name: '晓甄', language: 'zh-CN', languageDetail: '中文(普通话,简体)', - gender: '女' + gender: '女', + emotion: { + angry: '生气和厌恶的语气', + cheerful: '表达积极愉快的语气', + disgruntled: '轻蔑、抱怨的语气,表现不悦和蔑视', + fearful: '恐惧、紧张的语气,说话人处于紧张和不安的状态', + sad: '表达悲伤语气', + serious: '严肃、命令的语气' + } }, { code: 'zh-CN-YunfengNeural', name: '云枫', language: 'zh-CN', languageDetail: '中文(普通话,简体)', - gender: '男' + gender: '男', + emotion: { + angry: '生气和厌恶的语气', + cheerful: '表达积极愉快的语气', + depressed: '调低音调和音量来表达忧郁、沮丧的语气', + disgruntled: '轻蔑、抱怨的语气,表现不悦和蔑视', + fearful: '恐惧、紧张的语气,说话人处于紧张和不安的状态', + sad: '表达悲伤语气', + serious: '严肃、命令的语气' + } }, { code: 'zh-CN-YunhaoNeural', @@ -204,21 +385,44 @@ const supportConfigurations = [ name: '云健', language: 'zh-CN', languageDetail: '中文(普通话,简体)', - gender: '男' + gender: '男', + emotion: { + 'narration-relaxed': '以轻松、自然的语气进行叙述', + 'sports-commentary': '在解说体育比赛时,使用专业而自信的语气', + 'sports-commentary-excited': '在解说激动人心的体育比赛时,使用兴奋和激动的语气' + } }, { code: 'zh-CN-YunxiaNeural', name: '云夏', language: 'zh-CN', - languageDetail: '中文(普通话,简体)', - gender: '男' + languageDetail: '中文 (普通话,简体)', + gender: '男', + emotion: { + angry: '生气和厌恶的语气', + calm: '沉着冷静的态度说话。语气、音调和韵律统一', + cheerful: '表达积极愉快的语气', + fearful: '表达害怕、紧张的语气', + sad: '表达悲伤和失落的语气' + } }, { code: 'zh-CN-YunzeNeural', name: '云泽', language: 'zh-CN', - languageDetail: '中文(普通话,简体)', - gender: '男' + languageDetail: '中文 (普通话,简体)', + gender: '男', + emotion: { + angry: '用愤怒的语气表达强烈的不满和愤怒', + calm: '以冷静、沉着的语气说话,表现出稳重、深思熟虑的态度', + cheerful: '表达愉快和轻松的情绪', + depressed: '用沉闷、低落的语气表达消极、悲伤的情绪', + disgruntled: '表达不满和不高兴的情绪', + 'documentary-narration': '用一种客观、中立的语气讲述事实和事件', + fearful: '表达害怕、不安的情绪', + sad: '用悲伤的语气表达悲伤和失落', + serious: '以严肃的语气和态度表现出对事情的重视和认真对待' + } }, { code: 'zh-HK-HiuGaaiNeural', @@ -240,7 +444,480 @@ const supportConfigurations = [ language: 'zh-CN', languageDetail: '中文(粤语,繁体)', gender: '男' + }, + { + code: 'en-GB-AbbiNeural', + name: 'Abbi', + language: 'en-GB', + languageDetail: '英语(英国)', + gender: 'female' + }, + { + code: 'en-GB-AlfieNeural', + name: 'Alfie', + language: 'en-GB', + languageDetail: '英语(英国)', + gender: 'male' + }, + { + code: 'en-GB-BellaNeural', + name: 'Bella', + language: 'en-GB', + languageDetail: '英语(英国)', + gender: 'female' + }, + { + code: 'en-GB-ElliotNeural', + name: 'Elliot', + language: 'en-GB', + languageDetail: '英语(英国)', + gender: 'male' + }, + { + code: 'en-GB-EthanNeural', + name: 'Ethan', + language: 'en-GB', + languageDetail: '英语(英国)', + gender: 'male' + }, + { + code: 'en-GB-HollieNeural', + name: 'Hollie', + language: 'en-GB', + languageDetail: '英语(英国)', + gender: 'female' + }, + { + code: 'en-GB-LibbyNeural', + name: 'Libby', + language: 'en-GB', + languageDetail: '英语(英国)', + gender: 'female' + }, + { + code: 'en-GB-MaisieNeural', + name: 'Maisie', + language: 'en-GB', + languageDetail: '英语(英国)', + gender: 'female' + }, + { + code: 'en-GB-NoahNeural', + name: 'Noah', + language: 'en-GB', + languageDetail: '英语(英国)', + gender: 'male' + }, + { + code: 'en-GB-OliverNeural', + name: 'Oliver', + language: 'en-GB', + languageDetail: '英语(英国)', + gender: 'male' + }, + { + code: 'en-GB-OliviaNeural', + name: 'Olivia', + language: 'en-GB', + languageDetail: '英语(英国)', + gender: 'female' + }, + { + code: 'en-GB-RyanNeural', + name: 'Ryan', + language: 'en-GB', + languageDetail: '英语(英国)', + gender: 'male', + emotion: { + chat: '表达轻松随意的语气', + cheerful: '表达积极愉快的语气' + + } + }, + { + code: 'en-GB-SoniaNeural', + name: 'Sonia', + language: 'en-GB', + languageDetail: '英语(英国)', + gender: 'female', + emotion: { + cheerful: '表达积极愉快的语气', + sad: '表达悲伤语气' + + } + }, + { + code: 'en-GB-ThomasNeural', + name: 'Thomas', + language: 'en-GB', + languageDetail: '英语(英国)', + gender: 'male' + }, + { + code: 'ja-JP-AoiNeural', + name: '葵', + language: 'ja-JP', + languageDetail: '日语(日本)', + gender: '女' + }, + { + code: 'ja-JP-DaichiNeural', + name: '大地', + language: 'ja-JP', + languageDetail: '日语(日本)', + gender: '男' + }, + { + code: 'ja-JP-KeitaNeural', + name: '慶太', + language: 'ja-JP', + languageDetail: '日语(日本)', + gender: '男' + }, + { + code: 'ja-JP-MayuNeural', + name: '真由', + language: 'ja-JP', + languageDetail: '日语(日本)', + gender: '女' + }, + { + code: 'ja-JP-NanamiNeural', + name: '七海', + language: 'ja-JP', + languageDetail: '日语(日本)', + gender: '女', + emotion: { + chat: '表达轻松随意的语气', + cheerful: '表达积极愉快的语气', + customerservice: '以友好热情的语气为客户提供支持' + } + }, + { + code: 'ja-JP-NaokiNeural', + name: '直樹', + language: 'ja-JP', + languageDetail: '日语(日本)', + gender: '男' + }, + { + code: 'ja-JP-ShioriNeural', + name: '栞', + language: 'ja-JP', + languageDetail: '日语(日本)', + gender: '女' + }, + { + code: 'en-US-AIGenerate1Neural1', + name: 'AI Generate 1', + language: 'en-US', + languageDetail: 'English (United States)', + gender: '男' + }, + { + code: 'en-US-AIGenerate2Neural1', + name: 'AI Generate 2', + language: 'en-US', + languageDetail: 'English (United States)', + gender: '女' + }, + { + code: 'en-US-AmberNeural', + name: 'Amber', + language: 'en-US', + languageDetail: 'English (United States)', + gender: '女' + }, + { + code: 'en-US-AnaNeural', + name: 'Ana', + language: 'en-US', + languageDetail: 'English (United States)', + gender: '女性、儿童' + }, + { + code: 'en-US-AriaNeural', + name: 'Aria', + language: 'en-US', + languageDetail: 'English (United States)', + gender: '女', + emotion: { + angry: '生气和厌恶的语气', + cheerful: '表达积极愉快的语气', + excited: '乐观、充满希望的语气,发生了美好的事情', + friendly: '愉快、怡人、温暖、真诚、关切的语气', + hopeful: '温暖且渴望的语气。像是会有好事发生', + sad: '表达悲伤语气', + shouting: '就像从遥远的地方说话或在外面说话', + terrified: '非常害怕的语气,语速快且声音颤抖。不稳定的疯狂状态', + unfriendly: '表达一种冷淡无情的语气', + whispering: '说话非常柔和,发出的声音小且温柔', + chat: '表达轻松随意的语气', + customerservice: '以友好热情的语气为客户提供支持', + empathetic: '表达关心和理解', + 'narration-professional': '以专业、客观的语气朗读内容', + 'newscast-casual': '以通用、随意的语气发布一般新闻', + 'newscast-formal': '以正式、自信和权威的语气发布新闻' + } + }, + { + code: 'en-US-AshleyNeural', + name: 'Ashley', + language: 'en-US', + languageDetail: 'English (United States)', + gender: '女' + }, + { + code: 'en-US-BrandonNeural', + name: 'Brandon', + language: 'en-US', + languageDetail: 'English (United States)', + gender: '男' + }, + { + code: 'en-US-ChristopherNeural', + name: 'Christopher', + language: 'en-US', + languageDetail: 'English (United States)', + gender: '男' + }, + { + code: 'en-US-CoraNeural', + name: 'Cora', + language: 'en-US', + languageDetail: 'English (United States)', + gender: '女' + }, + { + code: 'en-US-DavisNeural', + name: 'Davis', + language: 'en-US', + languageDetail: 'English (United States)', + gender: '男', + emotion: { + angry: '生气和厌恶的语气', + cheerful: '表达积极愉快的语气', + excited: '乐观、充满希望的语气,发生了美好的事情', + friendly: '愉快、怡人、温暖、真诚、关切的语气', + hopeful: '温暖且渴望的语气。像是会有好事发生', + sad: '表达悲伤语气', + shouting: '就像从遥远的地方说话或在外面说话', + terrified: '非常害怕的语气,语速快且声音颤抖。不稳定的疯狂状态', + unfriendly: '表达一种冷淡无情的语气', + whispering: '说话非常柔和,发出的声音小且温柔' + } + }, + { + code: 'en-US-ElizabethNeural', + name: 'Elizabeth', + language: 'en-US', + languageDetail: 'English (United States)', + gender: '女' + }, + { + code: 'en-US-EricNeural', + name: 'Eric', + language: 'en-US', + languageDetail: 'English (United States)', + gender: '男' + }, + { + code: 'en-US-GuyNeural', + name: 'Guy', + language: 'en-US', + languageDetail: 'English (United States)', + gender: '男', + emotion: { + angry: '生气和厌恶的语气', + cheerful: '表达积极愉快的语气', + excited: '乐观、充满希望的语气,发生了美好的事情', + friendly: '愉快、怡人、温暖、真诚、关切的语气', + hopeful: '温暖且渴望的语气。像是会有好事发生', + sad: '表达悲伤语气', + shouting: '就像从遥远的地方说话或在外面说话', + terrified: '非常害怕的语气,语速快且声音颤抖。不稳定的疯狂状态', + unfriendly: '表达一种冷淡无情的语气', + whispering: '说话非常柔和,发出的声音小且温柔', + newscast: '以正式专业的语气叙述新闻' + + } + }, + { + code: 'en-US-JacobNeural', + name: 'Jacob', + language: 'en-US', + languageDetail: 'English (United States)', + gender: '男' + }, + { + code: 'en-US-JaneNeural', + name: 'Jane', + language: 'en-US', + languageDetail: 'English (United States)', + gender: '女', + emotion: { + angry: '生气和厌恶的语气', + cheerful: '表达积极愉快的语气', + excited: '乐观、充满希望的语气,发生了美好的事情', + friendly: '愉快、怡人、温暖、真诚、关切的语气', + hopeful: '温暖且渴望的语气。像是会有好事发生', + sad: '表达悲伤语气', + shouting: '就像从遥远的地方说话或在外面说话', + terrified: '非常害怕的语气,语速快且声音颤抖。不稳定的疯狂状态', + unfriendly: '表达一种冷淡无情的语气', + whispering: '说话非常柔和,发出的声音小且温柔' + } + }, + { + code: 'en-US-JasonNeural', + name: 'Jason', + language: 'en-US', + languageDetail: '英语(美国)', + gender: 'male', + emotion: { + angry: '生气和厌恶的语气', + cheerful: '表达积极愉快的语气', + excited: '乐观、充满希望的语气,发生了美好的事情', + friendly: '愉快、怡人、温暖、真诚、关切的语气', + hopeful: '温暖且渴望的语气。像是会有好事发生', + sad: '表达悲伤语气', + shouting: '就像从遥远的地方说话或在外面说话', + terrified: '非常害怕的语气,语速快且声音颤抖。不稳定的疯狂状态', + unfriendly: '表达一种冷淡无情的语气', + whispering: '说话非常柔和,发出的声音小且温柔' + } + }, + { + code: 'en-US-JennyMultilingualNeural3', + name: 'Jenny', + language: 'en-US', + languageDetail: '英语(美国)', + gender: 'female' + }, + { + code: 'en-US-JennyNeural', + name: 'Jenny', + language: 'en-US', + languageDetail: '英语(美国)', + gender: 'female', + emotion: { + angry: '生气和厌恶的语气', + cheerful: '表达积极愉快的语气', + excited: '乐观、充满希望的语气,发生了美好的事情', + friendly: '愉快、怡人、温暖、真诚、关切的语气', + hopeful: '温暖且渴望的语气。像是会有好事发生', + sad: '表达悲伤语气', + shouting: '就像从遥远的地方说话或在外面说话', + terrified: '非常害怕的语气,语速快且声音颤抖。不稳定的疯狂状态', + unfriendly: '表达一种冷淡无情的语气', + whispering: '说话非常柔和,发出的声音小且温柔', + assistant: '数字助理用的是热情而轻松的语气', + chat: '表达轻松随意的语气', + customerservice: '以友好热情的语气为客户提供支持', + newscast: '以正式专业的语气叙述新闻' + + } + }, + { + code: 'en-US-MichelleNeural', + name: 'Michelle', + language: 'en-US', + languageDetail: '英语(美国)', + gender: 'female' + }, + { + code: 'en-US-MonicaNeural', + name: 'Monica', + language: 'en-US', + languageDetail: '英语(美国)', + gender: 'female' + }, + { + code: 'en-US-NancyNeural', + name: 'Nancy', + language: 'en-US', + languageDetail: '英语(美国)', + gender: 'female', + emotion: { + angry: '生气和厌恶的语气', + cheerful: '表达积极愉快的语气', + excited: '乐观、充满希望的语气,发生了美好的事情', + friendly: '愉快、怡人、温暖、真诚、关切的语气', + hopeful: '温暖且渴望的语气。像是会有好事发生', + sad: '表达悲伤语气', + shouting: '就像从遥远的地方说话或在外面说话', + terrified: '非常害怕的语气,语速快且声音颤抖。不稳定的疯狂状态', + unfriendly: '表达一种冷淡无情的语气', + whispering: '说话非常柔和,发出的声音小且温柔' + } + }, + { + code: 'en-US-RogerNeural', + name: 'Roger', + language: 'en-US', + languageDetail: '英语(美国)', + gender: 'male' + }, + { + code: 'en-US-SaraNeural', + name: 'Sara', + language: 'en-US', + languageDetail: '英语(美国)', + gender: 'female', + emotion: { + angry: '生气和厌恶的语气', + cheerful: '表达积极愉快的语气', + excited: '乐观、充满希望的语气,发生了美好的事情', + friendly: '愉快、怡人、温暖、真诚、关切的语气', + hopeful: '温暖且渴望的语气。像是会有好事发生', + sad: '表达悲伤语气', + shouting: '就像从遥远的地方说话或在外面说话', + terrified: '非常害怕的语气,语速快且声音颤抖。不稳定的疯狂状态', + unfriendly: '表达一种冷淡无情的语气', + whispering: '说话非常柔和,发出的声音小且温柔' + + } + }, + { + code: 'en-US-SteffanNeural', + name: 'Steffan', + language: 'en-US', + languageDetail: '英语(美国)', + gender: 'male' + }, + { + code: 'en-US-TonyNeural', + name: 'Tony', + language: 'en-US', + languageDetail: '英语(美国)', + gender: 'male', + emotion: { + angry: '生气和厌恶的语气', + cheerful: '表达积极愉快的语气', + excited: '乐观、充满希望的语气,发生了美好的事情', + friendly: '愉快、怡人、温暖、真诚、关切的语气', + hopeful: '温暖且渴望的语气。像是会有好事发生', + sad: '表达悲伤语气', + shouting: '就像从遥远的地方说话或在外面说话', + terrified: '非常害怕的语气,语速快且声音颤抖。不稳定的疯狂状态', + unfriendly: '表达一种冷淡无情的语气', + whispering: '说话非常柔和,发出的声音小且温柔' + } + }, + { + code: 'en-IN-NeerjaNeural', + name: 'Neerja', + language: 'en', + languageDetail: '英语(印度)', + gender: 'female' + }, + { + code: 'en-IN-PrabhatNeural', + name: 'Prabhat', + language: 'en', + languageDetail: '英语(印度)', + gender: 'male' } ] -export default { generateAudio, supportConfigurations } +export default { generateAudio, generateSsml, getEmotionPrompt, supportConfigurations } diff --git a/utils/tts/voicevox.js b/utils/tts/voicevox.js index 62d94bb..75b1036 100644 --- a/utils/tts/voicevox.js +++ b/utils/tts/voicevox.js @@ -73,7 +73,7 @@ async function generateAudio (text, options = {}) { return Buffer.from(synthesisResponseData) } -const supportConfigurations = [ +export const supportConfigurations = [ { supported_features: { permitted_synthesis_morphing: 'SELF_ONLY' }, name: '四国めたん', diff --git a/utils/uploadRecord.js b/utils/uploadRecord.js index 74bfdc2..e3d7809 100644 --- a/utils/uploadRecord.js +++ b/utils/uploadRecord.js @@ -8,7 +8,8 @@ import stream from 'stream' import crypto from 'crypto' import child_process from 'child_process' import { Config } from './config.js' -import {mkdirs} from "./common.js"; +import path from 'path' +import { mkdirs } from './common.js' let module try { module = await import('oicq') @@ -248,9 +249,14 @@ async function getPttBuffer (file, ffmpeg = 'ffmpeg') { } async function audioTrans (file, ffmpeg = 'ffmpeg') { + const tmpfile = path.join(TMP_DIR, uuid()) + const cmd = IS_WIN + ? `${ffmpeg} -i "${file}" -f s16le -ac 1 -ar 24000 "${tmpfile}"` + : `exec ${ffmpeg} -i "${file}" -f s16le -ac 1 -ar 24000 "${tmpfile}"` return new Promise((resolve, reject) => { - const tmpfile = TMP_DIR + '/' + (0, uuid)(); - (0, child_process.exec)(`${ffmpeg} -i "${file}" -f s16le -ac 1 -ar 24000 "${tmpfile}"`, async (error, stdout, stderr) => { + // 隐藏windows下调用ffmpeg的cmd弹窗 + const options = IS_WIN ? { windowsHide: true, stdio: 'ignore' } : {} + child_process.exec(cmd, options, async (error, stdout, stderr) => { try { resolve(pcm2slk(fs.readFileSync(tmpfile))) } catch {