diff --git a/src/utils/plugins/main/voice/amazonPollyVoice.js b/src/utils/plugins/main/voice/amazonPollyVoice.js index 948fa657..f459b114 100644 --- a/src/utils/plugins/main/voice/amazonPollyVoice.js +++ b/src/utils/plugins/main/voice/amazonPollyVoice.js @@ -3,184 +3,208 @@ const fs = require("fs"); const crypto = require("crypto"); const axios = require("axios"); const getAudioPath = async (text, speed, dirPath, config) => { - let audioName = new Date().getTime() + ".mp3"; - if (!fs.existsSync(path.join(dirPath, "tts"))) { - fs.mkdirSync(path.join(dirPath, "tts")); - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - else { - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - return path.join(dirPath, "tts", audioName); + let audioName = new Date().getTime() + ".mp3"; + if (!fs.existsSync(path.join(dirPath, "tts"))) { + fs.mkdirSync(path.join(dirPath, "tts")); + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } else { + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } + return path.join(dirPath, "tts", audioName); }; // AWS Signature Version 4 signing helpers const getSignatureKey = (crypto, key, dateStamp, regionName, serviceName) => { - const kDate = crypto - .createHmac("sha256", "AWS4" + key) - .update(dateStamp) - .digest(); - const kRegion = crypto - .createHmac("sha256", kDate) - .update(regionName) - .digest(); - const kService = crypto - .createHmac("sha256", kRegion) - .update(serviceName) - .digest(); - const kSigning = crypto - .createHmac("sha256", kService) - .update("aws4_request") - .digest(); - return kSigning; + const kDate = crypto + .createHmac("sha256", "AWS4" + key) + .update(dateStamp) + .digest(); + const kRegion = crypto + .createHmac("sha256", kDate) + .update(regionName) + .digest(); + const kService = crypto + .createHmac("sha256", kRegion) + .update(serviceName) + .digest(); + const kSigning = crypto + .createHmac("sha256", kService) + .update("aws4_request") + .digest(); + return kSigning; }; const getAmzDateTime = () => { - const now = new Date(); - const amzDate = now - .toISOString() - .replace(/[:\-]|\.\d{3}/g, "") - .slice(0, 15) + "Z"; - const dateStamp = amzDate.slice(0, 8); - return { amzDate, dateStamp }; + const now = new Date(); + const amzDate = + now + .toISOString() + .replace(/[:\-]|\.\d{3}/g, "") + .slice(0, 15) + "Z"; + const dateStamp = amzDate.slice(0, 8); + return { amzDate, dateStamp }; }; const getTTSAudio = async (text, speed, config) => { - let region = config.region || "us-east-1"; - let accessKeyId = config.accessKeyId || ""; - let secretAccessKey = config.secretAccessKey || ""; - let voiceId = config.voiceId || "Joanna"; - let engine = config.engine || "neural"; - // speed is a multiplier: 1.0 = normal, e.g. 0.5 = slow, 2.0 = fast - // Amazon Polly SSML rate accepts: x-slow, slow, medium, fast, x-fast, or N% - let rate = speed ? `${Math.round(speed * 100)}%` : "100%"; - const service = "polly"; - const host = `polly.${region}.amazonaws.com`; - const endpoint = `https://${host}/v1/speech`; - const method = "POST"; - const contentType = "application/json"; - const requestBody = JSON.stringify({ - Engine: engine, - OutputFormat: "mp3", - Text: `${text}`, - TextType: "ssml", - VoiceId: voiceId, - }); - const { amzDate, dateStamp } = getAmzDateTime(); - const payloadHash = crypto - .createHash("sha256") - .update(requestBody) - .digest("hex"); - const canonicalHeaders = `content-type:${contentType}\n` + - `host:${host}\n` + - `x-amz-date:${amzDate}\n`; - const signedHeaders = "content-type;host;x-amz-date"; - const canonicalRequest = [ - method, - "/v1/speech", - "", - canonicalHeaders, - signedHeaders, - payloadHash, - ].join("\n"); - const algorithm = "AWS4-HMAC-SHA256"; - const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`; - const stringToSign = [ - algorithm, - amzDate, - credentialScope, - crypto.createHash("sha256").update(canonicalRequest).digest("hex"), - ].join("\n"); - const signingKey = getSignatureKey(crypto, secretAccessKey, dateStamp, region, service); - const signature = crypto - .createHmac("sha256", signingKey) - .update(stringToSign) - .digest("hex"); - const authorizationHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, ` + - `SignedHeaders=${signedHeaders}, Signature=${signature}`; - return new Promise((resolve, reject) => { - axios - .post(endpoint, requestBody, { - headers: { - "Content-Type": contentType, - "X-Amz-Date": amzDate, - Authorization: authorizationHeader, - }, - responseType: "arraybuffer", - }) - .then((response) => { - resolve(response.data); - }) - .catch((error) => { - reject(error); - }); - }); + let region = config.region || "us-east-1"; + let accessKeyId = config.accessKeyId || ""; + let secretAccessKey = config.secretAccessKey || ""; + let voiceId = config.voiceId || "Joanna"; + let engine = config.engine || "neural"; + // speed is a multiplier: 1.0 = normal, e.g. 0.5 = slow, 2.0 = fast + // Amazon Polly SSML rate accepts: x-slow, slow, medium, fast, x-fast, or N% + let rate = speed ? `${Math.round(speed * 100)}%` : "100%"; + const service = "polly"; + const host = `polly.${region}.amazonaws.com`; + const endpoint = `https://${host}/v1/speech`; + const method = "POST"; + const contentType = "application/json"; + const requestBody = JSON.stringify({ + Engine: engine, + OutputFormat: "mp3", + Text: `${text}`, + TextType: "ssml", + VoiceId: voiceId, + }); + const { amzDate, dateStamp } = getAmzDateTime(); + const payloadHash = crypto + .createHash("sha256") + .update(requestBody) + .digest("hex"); + const canonicalHeaders = + `content-type:${contentType}\n` + + `host:${host}\n` + + `x-amz-date:${amzDate}\n`; + const signedHeaders = "content-type;host;x-amz-date"; + const canonicalRequest = [ + method, + "/v1/speech", + "", + canonicalHeaders, + signedHeaders, + payloadHash, + ].join("\n"); + const algorithm = "AWS4-HMAC-SHA256"; + const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`; + const stringToSign = [ + algorithm, + amzDate, + credentialScope, + crypto.createHash("sha256").update(canonicalRequest).digest("hex"), + ].join("\n"); + const signingKey = getSignatureKey( + crypto, + secretAccessKey, + dateStamp, + region, + service + ); + const signature = crypto + .createHmac("sha256", signingKey) + .update(stringToSign) + .digest("hex"); + const authorizationHeader = + `${algorithm} Credential=${accessKeyId}/${credentialScope}, ` + + `SignedHeaders=${signedHeaders}, Signature=${signature}`; + return new Promise((resolve, reject) => { + axios + .post(endpoint, requestBody, { + headers: { + "Content-Type": contentType, + "X-Amz-Date": amzDate, + Authorization: authorizationHeader, + }, + responseType: "arraybuffer", + }) + .then((response) => { + resolve(response.data); + }) + .catch((error) => { + reject(error); + }); + }); }; const getTTSVoice = async (config) => { - let region = config.region || "us-east-1"; - let accessKeyId = config.accessKeyId || ""; - let secretAccessKey = config.secretAccessKey || ""; - const service = "polly"; - const host = `polly.${region}.amazonaws.com`; - const method = "GET"; - const path = "/v1/voices"; - const queryString = "Engine=neural"; - const { amzDate, dateStamp } = getAmzDateTime(); - const payloadHash = crypto.createHash("sha256").update("").digest("hex"); - const canonicalHeaders = `host:${host}\n` + `x-amz-date:${amzDate}\n`; - const signedHeaders = "host;x-amz-date"; - const canonicalRequest = [ - method, - path, - queryString, - canonicalHeaders, - signedHeaders, - payloadHash, - ].join("\n"); - const algorithm = "AWS4-HMAC-SHA256"; - const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`; - const stringToSign = [ - algorithm, - amzDate, - credentialScope, - crypto.createHash("sha256").update(canonicalRequest).digest("hex"), - ].join("\n"); - const signingKey = getSignatureKey(crypto, secretAccessKey, dateStamp, region, service); - const signature = crypto - .createHmac("sha256", signingKey) - .update(stringToSign) - .digest("hex"); - const authorizationHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, ` + - `SignedHeaders=${signedHeaders}, Signature=${signature}`; - const url = `https://${host}${path}?${queryString}`; - return new Promise((resolve, reject) => { - axios - .get(url, { - headers: { - "X-Amz-Date": amzDate, - Authorization: authorizationHeader, - }, - }) - .then((response) => { - let voices = response.data.Voices; - resolve(voices.map((voice) => { - return { - name: voice.Id, - gender: voice.Gender.toLowerCase(), - locale: voice.LanguageCode, - displayName: `Amazon Polly - ${voice.Name} (${voice.LanguageCode}) - ${voice.Gender}`, - plugin: "amazon-polly-voice-plugin", - config: { - ...config, - voiceId: voice.Id, - engine: (voice.SupportedEngines || []).includes("neural") - ? "neural" - : "standard", - }, - }; - })); - }) - .catch((error) => { - reject(error); - }); - }); + let region = config.region || "us-east-1"; + let accessKeyId = config.accessKeyId || ""; + let secretAccessKey = config.secretAccessKey || ""; + const service = "polly"; + const host = `polly.${region}.amazonaws.com`; + const method = "GET"; + const path = "/v1/voices"; + const queryString = "Engine=neural"; + const { amzDate, dateStamp } = getAmzDateTime(); + const payloadHash = crypto.createHash("sha256").update("").digest("hex"); + const canonicalHeaders = `host:${host}\n` + `x-amz-date:${amzDate}\n`; + const signedHeaders = "host;x-amz-date"; + const canonicalRequest = [ + method, + path, + queryString, + canonicalHeaders, + signedHeaders, + payloadHash, + ].join("\n"); + const algorithm = "AWS4-HMAC-SHA256"; + const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`; + const stringToSign = [ + algorithm, + amzDate, + credentialScope, + crypto.createHash("sha256").update(canonicalRequest).digest("hex"), + ].join("\n"); + const signingKey = getSignatureKey( + crypto, + secretAccessKey, + dateStamp, + region, + service + ); + const signature = crypto + .createHmac("sha256", signingKey) + .update(stringToSign) + .digest("hex"); + const authorizationHeader = + `${algorithm} Credential=${accessKeyId}/${credentialScope}, ` + + `SignedHeaders=${signedHeaders}, Signature=${signature}`; + const url = `https://${host}${path}?${queryString}`; + return new Promise((resolve, reject) => { + axios + .get(url, { + headers: { + "X-Amz-Date": amzDate, + Authorization: authorizationHeader, + }, + }) + .then((response) => { + let voices = response.data.Voices; + console.log("Amazon Polly voices fetched:", voices); + resolve( + voices.map((voice) => { + return { + name: voice.Id, + gender: voice.Gender.toLowerCase(), + locale: voice.LanguageCode, + displayName: `Amazon Polly - ${voice.Name} (${voice.LanguageCode}) - ${voice.Gender}`, + plugin: "amazon-polly-voice-plugin", + config: { + ...config, + voiceId: voice.Id, + engine: (voice.SupportedEngines || []).includes("neural") + ? "neural" + : "standard", + }, + }; + }) + ); + }) + .catch((error) => { + reject(error); + }); + }); }; module.exports = { getAudioPath, getTTSVoice }; diff --git a/src/utils/plugins/main/voice/chatTTSUIVoice.js b/src/utils/plugins/main/voice/chatTTSUIVoice.js index a62b4ed9..562c5fd5 100644 --- a/src/utils/plugins/main/voice/chatTTSUIVoice.js +++ b/src/utils/plugins/main/voice/chatTTSUIVoice.js @@ -1,37 +1,40 @@ const axios = require("axios"); const qs = require("qs"); const getAudioPath = async (text, speed, dirPath, config) => { - let url = config.url || "http://127.0.0.1:9966/tts"; - let prompt = config.prompt || ""; - let voice = config.voice || "3333"; - let temperature = config.temperature || 0.3; - let top_p = config.top_p || 0.7; - let top_k = config.top_k || 20; - let skip_refine = config.skip_refine || 0; - let custom_voice = config.custom_voice || 0; - return new Promise((resolve, reject) => { - axios.post(url, qs.stringify({ - text: text, - prompt: prompt, - voice: voice, - temperature: temperature, - top_p: top_p, - top_k: top_k, - skip_refine: skip_refine, - custom_voice: custom_voice - })) - .then(response => { - if (response.data.code == 0) { - resolve(response.data.audio_files[0].filename); - } - else { - resolve(""); - } + let url = config.url || "http://127.0.0.1:9966/tts"; + let prompt = config.prompt || ""; + let voice = config.voice || "3333"; + let temperature = config.temperature || 0.3; + let top_p = config.top_p || 0.7; + let top_k = config.top_k || 20; + let skip_refine = config.skip_refine || 0; + let custom_voice = config.custom_voice || 0; + return new Promise((resolve, reject) => { + axios + .post( + url, + qs.stringify({ + text: text, + prompt: prompt, + voice: voice, + temperature: temperature, + top_p: top_p, + top_k: top_k, + skip_refine: skip_refine, + custom_voice: custom_voice, }) - .catch(error => { - reject(error); - }); - }); + ) + .then((response) => { + if (response.data.code == 0) { + resolve(response.data.audio_files[0].filename); + } else { + resolve(""); + } + }) + .catch((error) => { + reject(error); + }); + }); }; module.exports = { getAudioPath }; diff --git a/src/utils/plugins/main/voice/chatTTSVoice.js b/src/utils/plugins/main/voice/chatTTSVoice.js index da9f7cde..3526cedb 100644 --- a/src/utils/plugins/main/voice/chatTTSVoice.js +++ b/src/utils/plugins/main/voice/chatTTSVoice.js @@ -3,41 +3,47 @@ const fs = require("fs"); const axios = require("axios"); const JSZip = require("jszip"); const getAudioPath = async (text, speed, dirPath, config) => { - let audioName = new Date().getTime() + ".mp3"; - if (!fs.existsSync(path.join(dirPath, "tts"))) { - fs.mkdirSync(path.join(dirPath, "tts")); - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - else { - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - return path.join(dirPath, "tts", audioName); + let audioName = new Date().getTime() + ".mp3"; + if (!fs.existsSync(path.join(dirPath, "tts"))) { + fs.mkdirSync(path.join(dirPath, "tts")); + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } else { + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } + return path.join(dirPath, "tts", audioName); }; const getTTSAudio = async (text, speed, config) => { - let url = config.url || "http://127.0.0.1:8000/generate_voice"; - // main infer params - const body = config.body; - // refine text params - const params_refine_text = config.params_refine_text; - // Add params_refine_text to body - body.params_refine_text = params_refine_text; - // infer code params - const params_infer_code = config.params_infer_code; - // Add params_infer_code to body - body.params_infer_code = params_infer_code; - body.text = [text]; - body.stream = false; - return new Promise((resolve, reject) => { - axios.post(url, body, { responseType: "arraybuffer" }) - .then(async (response) => { - const zip = await JSZip.loadAsync(response.data); - const firstEntry = Object.values(zip.files)[0]; - resolve(await firstEntry.async("nodebuffer")); - }) - .catch(error => { - reject(error); - }); - }); + let url = config.url || "http://127.0.0.1:8000/generate_voice"; + // main infer params + const body = config.body; + // refine text params + const params_refine_text = config.params_refine_text; + // Add params_refine_text to body + body.params_refine_text = params_refine_text; + // infer code params + const params_infer_code = config.params_infer_code; + // Add params_infer_code to body + body.params_infer_code = params_infer_code; + body.text = [text]; + body.stream = false; + return new Promise((resolve, reject) => { + axios + .post(url, body, { responseType: "arraybuffer" }) + .then(async (response) => { + const zip = await JSZip.loadAsync(response.data); + const firstEntry = Object.values(zip.files)[0]; + resolve(await firstEntry.async("nodebuffer")); + }) + .catch((error) => { + reject(error); + }); + }); }; module.exports = { getAudioPath }; diff --git a/src/utils/plugins/main/voice/coquiTTSVoice.js b/src/utils/plugins/main/voice/coquiTTSVoice.js index 1885b437..f93d320c 100644 --- a/src/utils/plugins/main/voice/coquiTTSVoice.js +++ b/src/utils/plugins/main/voice/coquiTTSVoice.js @@ -2,47 +2,58 @@ const path = require("path"); const fs = require("fs"); const axios = require("axios"); const getAudioPath = async (text, speed, dirPath, config) => { - let audioName = new Date().getTime() + ".wav"; - if (!fs.existsSync(path.join(dirPath, "tts"))) { - fs.mkdirSync(path.join(dirPath, "tts")); - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - else { - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - return path.join(dirPath, "tts", audioName); + let audioName = new Date().getTime() + ".wav"; + if (!fs.existsSync(path.join(dirPath, "tts"))) { + fs.mkdirSync(path.join(dirPath, "tts")); + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } else { + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } + return path.join(dirPath, "tts", audioName); }; const objectToQueryString = (obj) => { - const queryParams = []; - for (let key in obj) { - if (obj.hasOwnProperty(key)) { - const value = obj[key]; - const encodedKey = encodeURIComponent(key); - const encodedValue = encodeURIComponent(value); - queryParams.push(`${encodedKey}=${encodedValue}`); - } + const queryParams = []; + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + const value = obj[key]; + const encodedKey = encodeURIComponent(key); + const encodedValue = encodeURIComponent(value); + queryParams.push(`${encodedKey}=${encodedValue}`); } - return queryParams.join("&"); + } + return queryParams.join("&"); }; const getTTSAudio = async (text, speed, config) => { - let url = config.url || "http://127.0.0.1:5002/api/tts"; - let speaker_id = config.speaker_id || ""; - let language_id = config.language_id || ""; - let style_wav = config.style_wav || ""; - return new Promise((resolve, reject) => { - axios.get(url + "?" + objectToQueryString({ + let url = config.url || "http://127.0.0.1:5002/api/tts"; + let speaker_id = config.speaker_id || ""; + let language_id = config.language_id || ""; + let style_wav = config.style_wav || ""; + return new Promise((resolve, reject) => { + axios + .get( + url + + "?" + + objectToQueryString({ text, speaker_id, language_id, - style_wav - }), { responseType: "arraybuffer" }) - .then(response => { - resolve(response.data); - }) - .catch(error => { - reject(error); - }); - }); + style_wav, + }), + { responseType: "arraybuffer" } + ) + .then((response) => { + resolve(response.data); + }) + .catch((error) => { + reject(error); + }); + }); }; module.exports = { getAudioPath }; diff --git a/src/utils/plugins/main/voice/elevenlabsTTSVoice.js b/src/utils/plugins/main/voice/elevenlabsTTSVoice.js index b1d72b1e..0c3569c8 100644 --- a/src/utils/plugins/main/voice/elevenlabsTTSVoice.js +++ b/src/utils/plugins/main/voice/elevenlabsTTSVoice.js @@ -2,84 +2,96 @@ const path = require("path"); const fs = require("fs"); const axios = require("axios"); const getAudioPath = async (text, speed, dirPath, config) => { - let audioName = new Date().getTime() + ".mp3"; - if (!fs.existsSync(path.join(dirPath, "tts"))) { - fs.mkdirSync(path.join(dirPath, "tts")); - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - else { - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - return path.join(dirPath, "tts", audioName); + let audioName = new Date().getTime() + ".mp3"; + if (!fs.existsSync(path.join(dirPath, "tts"))) { + fs.mkdirSync(path.join(dirPath, "tts")); + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } else { + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } + return path.join(dirPath, "tts", audioName); }; const getTTSAudio = async (text, speed, config) => { - let apiKey = config.apiKey || ""; - let voiceId = config.voiceId || "JBFqnCBsd6RMkjVDRZzb"; - let modelId = config.modelId || "eleven_multilingual_v2"; - let outputFormat = "mp3_44100_128"; - // speed: 0.7 ~ 1.2, default 1.0 - let voiceSettings = { - speed: speed || 1.0, - }; - let url = `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}?output_format=${outputFormat}`; - return new Promise((resolve, reject) => { - axios - .post(url, { - text: text, - model_id: modelId, - voice_settings: voiceSettings, - }, { - headers: { - "xi-api-key": apiKey, - "Content-Type": "application/json", - }, - responseType: "arraybuffer", - }) - .then((response) => { - resolve(response.data); - }) - .catch((error) => { - reject(error); - }); - }); + let apiKey = config.apiKey || ""; + let voiceId = config.voiceId || "JBFqnCBsd6RMkjVDRZzb"; + let modelId = config.modelId || "eleven_multilingual_v2"; + let outputFormat = "mp3_44100_128"; + // speed: 0.7 ~ 1.2, default 1.0 + let voiceSettings = { + speed: speed || 1.0, + }; + let url = `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}?output_format=${outputFormat}`; + return new Promise((resolve, reject) => { + axios + .post( + url, + { + text: text, + model_id: modelId, + voice_settings: voiceSettings, + }, + { + headers: { + "xi-api-key": apiKey, + "Content-Type": "application/json", + }, + responseType: "arraybuffer", + } + ) + .then((response) => { + resolve(response.data); + }) + .catch((error) => { + reject(error); + }); + }); }; const getTTSVoice = async (config) => { - let apiKey = config.apiKey || ""; - let url = `https://api.elevenlabs.io/v2/voices?page_size=100`; - return new Promise((resolve, reject) => { - axios - .get(url, { - headers: { - "xi-api-key": apiKey, - }, - }) - .then((response) => { - let voices = response.data.voices; - resolve(voices.map((voice) => { - let labels = voice.labels || {}; - let gender = labels.gender || "unknown"; - let locale = labels.language || - (voice.verified_languages && - voice.verified_languages[0] && - voice.verified_languages[0].locale) || - ""; - return { - name: voice.voice_id, - gender: gender.toLowerCase(), - locale: locale, - displayName: `ElevenLabs - ${voice.name}${locale ? ` (${locale})` : ""}`, - plugin: "elevenlabs-tts-voice-plugin", - config: { - ...config, - voiceId: voice.voice_id, - }, - }; - })); - }) - .catch((error) => { - reject(error); - }); - }); + let apiKey = config.apiKey || ""; + let url = `https://api.elevenlabs.io/v2/voices?page_size=100`; + return new Promise((resolve, reject) => { + axios + .get(url, { + headers: { + "xi-api-key": apiKey, + }, + }) + .then((response) => { + let voices = response.data.voices; + resolve( + voices.map((voice) => { + let labels = voice.labels || {}; + let gender = labels.gender || "unknown"; + let locale = + labels.language || + (voice.verified_languages && + voice.verified_languages[0] && + voice.verified_languages[0].locale) || + ""; + return { + name: voice.voice_id, + gender: gender.toLowerCase(), + locale: locale, + displayName: `ElevenLabs - ${voice.name}${locale ? ` (${locale})` : ""}`, + plugin: "elevenlabs-tts-voice-plugin", + config: { + ...config, + voiceId: voice.voice_id, + }, + }; + }) + ); + }) + .catch((error) => { + reject(error); + }); + }); }; module.exports = { getAudioPath, getTTSVoice }; diff --git a/src/utils/plugins/main/voice/grokTTSVoice.js b/src/utils/plugins/main/voice/grokTTSVoice.js index 1b54ea0d..adf85ce0 100644 --- a/src/utils/plugins/main/voice/grokTTSVoice.js +++ b/src/utils/plugins/main/voice/grokTTSVoice.js @@ -2,96 +2,101 @@ const path = require("path"); const fs = require("fs"); const axios = require("axios"); const getAudioPath = async (text, speed, dirPath, config) => { - let audioName = new Date().getTime() + ".mp3"; - if (!fs.existsSync(path.join(dirPath, "tts"))) { - fs.mkdirSync(path.join(dirPath, "tts")); - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - else { - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - return path.join(dirPath, "tts", audioName); + let audioName = new Date().getTime() + ".mp3"; + if (!fs.existsSync(path.join(dirPath, "tts"))) { + fs.mkdirSync(path.join(dirPath, "tts")); + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } else { + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } + return path.join(dirPath, "tts", audioName); }; const getTTSAudio = async (text, speed, config) => { - let apiKey = config.apiKey || ""; - let voiceId = config.voiceId || "eve"; - let language = config.language || "auto"; - let url = "https://api.x.ai/v1/tts"; - let body = { - text: text, - voice_id: voiceId, - language: language, - output_format: { - codec: "mp3", - sample_rate: 24000, - bit_rate: 128000, + let apiKey = config.apiKey || ""; + let voiceId = config.voiceId || "eve"; + let language = config.language || "auto"; + let url = "https://api.x.ai/v1/tts"; + let body = { + text: text, + voice_id: voiceId, + language: language, + output_format: { + codec: "mp3", + sample_rate: 24000, + bit_rate: 128000, + }, + }; + return new Promise((resolve, reject) => { + axios + .post(url, body, { + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", }, - }; - return new Promise((resolve, reject) => { - axios - .post(url, body, { - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", - }, - responseType: "arraybuffer", - }) - .then((response) => { - resolve(response.data); - }) - .catch((error) => { - reject(error); - }); - }); + responseType: "arraybuffer", + }) + .then((response) => { + resolve(response.data); + }) + .catch((error) => { + reject(error); + }); + }); }; const getTTSVoice = async (config) => { - const voiceList = [ - { voiceId: "eve", name: "Eve", gender: "female" }, - { voiceId: "ara", name: "Ara", gender: "female" }, - { voiceId: "rex", name: "Rex", gender: "male" }, - { voiceId: "sal", name: "Sal", gender: "male" }, - { voiceId: "leo", name: "Leo", gender: "male" }, - ]; - const languageList = [ - { code: "en", name: "English" }, - { code: "ar-EG", name: "Arabic (Egypt)" }, - { code: "ar-SA", name: "Arabic (Saudi Arabia)" }, - { code: "ar-AE", name: "Arabic (UAE)" }, - { code: "bn", name: "Bengali" }, - { code: "zh", name: "Chinese (Simplified)" }, - { code: "fr", name: "French" }, - { code: "de", name: "German" }, - { code: "hi", name: "Hindi" }, - { code: "id", name: "Indonesian" }, - { code: "it", name: "Italian" }, - { code: "ja", name: "Japanese" }, - { code: "ko", name: "Korean" }, - { code: "pt-BR", name: "Portuguese (Brazil)" }, - { code: "pt-PT", name: "Portuguese (Portugal)" }, - { code: "ru", name: "Russian" }, - { code: "es-MX", name: "Spanish (Mexico)" }, - { code: "es-ES", name: "Spanish (Spain)" }, - { code: "tr", name: "Turkish" }, - { code: "vi", name: "Vietnamese" }, - ]; - const result = []; - for (const voice of voiceList) { - for (const lang of languageList) { - result.push({ - name: voice.voiceId, - gender: voice.gender, - locale: lang.code, - displayName: `Grok TTS - ${voice.name} (${lang.name})`, - plugin: "grok-tts-voice-plugin", - config: { - ...config, - voiceId: voice.voiceId, - language: lang.code, - }, - }); - } + const voiceList = [ + { voiceId: "eve", name: "Eve", gender: "female" }, + { voiceId: "ara", name: "Ara", gender: "female" }, + { voiceId: "rex", name: "Rex", gender: "male" }, + { voiceId: "sal", name: "Sal", gender: "male" }, + { voiceId: "leo", name: "Leo", gender: "male" }, + ]; + const languageList = [ + { code: "en", name: "English" }, + { code: "ar-EG", name: "Arabic (Egypt)" }, + { code: "ar-SA", name: "Arabic (Saudi Arabia)" }, + { code: "ar-AE", name: "Arabic (UAE)" }, + { code: "bn", name: "Bengali" }, + { code: "zh", name: "Chinese (Simplified)" }, + { code: "fr", name: "French" }, + { code: "de", name: "German" }, + { code: "hi", name: "Hindi" }, + { code: "id", name: "Indonesian" }, + { code: "it", name: "Italian" }, + { code: "ja", name: "Japanese" }, + { code: "ko", name: "Korean" }, + { code: "pt-BR", name: "Portuguese (Brazil)" }, + { code: "pt-PT", name: "Portuguese (Portugal)" }, + { code: "ru", name: "Russian" }, + { code: "es-MX", name: "Spanish (Mexico)" }, + { code: "es-ES", name: "Spanish (Spain)" }, + { code: "tr", name: "Turkish" }, + { code: "vi", name: "Vietnamese" }, + ]; + const result = []; + for (const voice of voiceList) { + for (const lang of languageList) { + result.push({ + name: voice.voiceId, + gender: voice.gender, + locale: lang.code, + displayName: `Grok TTS - ${voice.name} (${lang.name})`, + plugin: "grok-tts-voice-plugin", + config: { + ...config, + voiceId: voice.voiceId, + language: lang.code, + }, + }); } - return result; + } + return result; }; module.exports = { getAudioPath, getTTSVoice }; diff --git a/src/utils/plugins/main/voice/mimoTTSVoice.js b/src/utils/plugins/main/voice/mimoTTSVoice.js index ccb3ffc5..9aece60b 100644 --- a/src/utils/plugins/main/voice/mimoTTSVoice.js +++ b/src/utils/plugins/main/voice/mimoTTSVoice.js @@ -2,118 +2,129 @@ const path = require("path"); const fs = require("fs"); const axios = require("axios"); const getAudioPath = async (text, speed, dirPath, config) => { - let audioName = new Date().getTime() + ".wav"; - if (!fs.existsSync(path.join(dirPath, "tts"))) { - fs.mkdirSync(path.join(dirPath, "tts")); - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - else { - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - return path.join(dirPath, "tts", audioName); + let audioName = new Date().getTime() + ".wav"; + if (!fs.existsSync(path.join(dirPath, "tts"))) { + fs.mkdirSync(path.join(dirPath, "tts")); + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } else { + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } + return path.join(dirPath, "tts", audioName); }; const getTTSAudio = async (text, speed, config) => { - let apiKey = config.apiKey || ""; - let voiceName = config.voiceName || "mimo_default"; - let url = "https://api.xiaomimimo.com/v1/chat/completions"; - return new Promise((resolve, reject) => { - axios - .post(url, { - model: "mimo-v2.5-tts", - messages: [ - { - role: "assistant", - content: text, - }, - ], - audio: { - format: "mp3", - voice: voiceName, + let apiKey = config.apiKey || ""; + let voiceName = config.voiceName || "mimo_default"; + let url = "https://api.xiaomimimo.com/v1/chat/completions"; + return new Promise((resolve, reject) => { + axios + .post( + url, + { + model: "mimo-v2.5-tts", + messages: [ + { + role: "assistant", + content: text, }, - }, { - headers: { - "api-key": apiKey, - "Content-Type": "application/json", - }, - }) - .then((response) => { - let audioData = response.data.choices[0].message.audio.data; - let audioBuffer = Buffer.from(audioData, "base64"); - resolve(audioBuffer); - }) - .catch((error) => { - reject(error); - }); - }); + ], + audio: { + format: "mp3", + voice: voiceName, + }, + }, + { + headers: { + "api-key": apiKey, + "Content-Type": "application/json", + }, + } + ) + .then((response) => { + let audioData = response.data.choices[0].message.audio.data; + let audioBuffer = Buffer.from(audioData, "base64"); + resolve(audioBuffer); + }) + .catch((error) => { + reject(error); + }); + }); }; const getTTSVoice = async (config) => { - const voices = [ - { - name: "mimo_default", - displayName: "MiMo Default", - gender: "female", - locale: "zh-CN", - }, - { - name: "冰糖", - displayName: "冰糖 (Bingtang) - Female", - gender: "female", - locale: "zh-CN", - }, - { - name: "茉莉", - displayName: "茉莉 (Moli) - Female", - gender: "female", - locale: "zh-CN", - }, - { - name: "苏打", - displayName: "苏打 (Suda) - Male", - gender: "male", - locale: "zh-CN", - }, - { - name: "白桦", - displayName: "白桦 (Baihua)- Male", - gender: "male", - locale: "zh-CN", - }, - { - name: "Mia", - displayName: "Mia - Female", - gender: "female", - locale: "en-US", - }, - { - name: "Chloe", - displayName: "Chloe - Female", - gender: "female", - locale: "en-US", - }, - { - name: "Milo", - displayName: "Milo - Male", - gender: "male", - locale: "en-US", - }, - { - name: "Dean", - displayName: "Dean - Male", - gender: "male", - locale: "en-US", - }, - ]; - return Promise.resolve(voices.map((voice) => ({ - name: voice.name, - gender: voice.gender, - locale: voice.locale, - displayName: `MiMo TTS - ${voice.displayName} (${voice.locale})`, - plugin: "mimo-tts-voice-plugin", - config: { - ...config, - voiceName: voice.name, - }, - }))); + const voices = [ + { + name: "mimo_default", + displayName: "MiMo Default", + gender: "female", + locale: "zh-CN", + }, + { + name: "冰糖", + displayName: "冰糖 (Bingtang) - Female", + gender: "female", + locale: "zh-CN", + }, + { + name: "茉莉", + displayName: "茉莉 (Moli) - Female", + gender: "female", + locale: "zh-CN", + }, + { + name: "苏打", + displayName: "苏打 (Suda) - Male", + gender: "male", + locale: "zh-CN", + }, + { + name: "白桦", + displayName: "白桦 (Baihua)- Male", + gender: "male", + locale: "zh-CN", + }, + { + name: "Mia", + displayName: "Mia - Female", + gender: "female", + locale: "en-US", + }, + { + name: "Chloe", + displayName: "Chloe - Female", + gender: "female", + locale: "en-US", + }, + { + name: "Milo", + displayName: "Milo - Male", + gender: "male", + locale: "en-US", + }, + { + name: "Dean", + displayName: "Dean - Male", + gender: "male", + locale: "en-US", + }, + ]; + return Promise.resolve( + voices.map((voice) => ({ + name: voice.name, + gender: voice.gender, + locale: voice.locale, + displayName: `MiMo TTS - ${voice.displayName} (${voice.locale})`, + plugin: "mimo-tts-voice-plugin", + config: { + ...config, + voiceName: voice.name, + }, + })) + ); }; module.exports = { getAudioPath, getTTSVoice }; diff --git a/src/utils/plugins/main/voice/minimaxTTSVoice.js b/src/utils/plugins/main/voice/minimaxTTSVoice.js index 64ab6e10..fab283cd 100644 --- a/src/utils/plugins/main/voice/minimaxTTSVoice.js +++ b/src/utils/plugins/main/voice/minimaxTTSVoice.js @@ -2,185 +2,185 @@ const path = require("path"); const fs = require("fs"); const axios = require("axios"); const getAudioPath = async (text, speed, dirPath, config) => { - let audioName = new Date().getTime() + ".mp3"; - if (!fs.existsSync(path.join(dirPath, "tts"))) { - fs.mkdirSync(path.join(dirPath, "tts")); - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - else { - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - return path.join(dirPath, "tts", audioName); + let audioName = new Date().getTime() + ".mp3"; + if (!fs.existsSync(path.join(dirPath, "tts"))) { + fs.mkdirSync(path.join(dirPath, "tts")); + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } else { + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } + return path.join(dirPath, "tts", audioName); }; const getTTSAudio = async (text, speed, config) => { - let apiKey = config.apiKey || ""; - let model = config.model || "speech-02-hd"; - let voiceName = config.voiceName || "English_expressive_narrator"; - // speed range: 0.5 ~ 2.0, default 1.0 - let speedValue = speed ? Math.min(2.0, Math.max(0.5, speed)) : 1.0; - let url = config.baseUrl + "/v1/t2a_v2"; - return new Promise((resolve, reject) => { - axios - .post(url, { - model: model, - text: text, - stream: false, - voice_setting: { - voice_id: voiceName, - speed: speedValue, - vol: 1, - pitch: 0, - }, - audio_setting: { - sample_rate: 32000, - bitrate: 128000, - format: "mp3", - channel: 1, - }, - output_format: "hex", - }, { - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", - }, - }) - .then((response) => { - let hexAudio = response.data && response.data.data && response.data.data.audio; - if (!hexAudio) { - reject("No audio data in response"); - return; - } - resolve(Buffer.from(hexAudio, "hex")); - }) - .catch((error) => { - reject(error); - }); - }); + let apiKey = config.apiKey || ""; + let model = config.model || "speech-02-hd"; + let voiceName = config.voiceName || "English_expressive_narrator"; + // speed range: 0.5 ~ 2.0, default 1.0 + let speedValue = speed ? Math.min(2.0, Math.max(0.5, speed)) : 1.0; + let url = config.baseUrl + "/v1/t2a_v2"; + return new Promise((resolve, reject) => { + axios + .post( + url, + { + model: model, + text: text, + stream: false, + voice_setting: { + voice_id: voiceName, + speed: speedValue, + vol: 1, + pitch: 0, + }, + audio_setting: { + sample_rate: 32000, + bitrate: 128000, + format: "mp3", + channel: 1, + }, + output_format: "hex", + }, + { + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + } + ) + .then((response) => { + let hexAudio = + response.data && response.data.data && response.data.data.audio; + if (!hexAudio) { + reject("No audio data in response"); + return; + } + resolve(Buffer.from(hexAudio, "hex")); + }) + .catch((error) => { + reject(error); + }); + }); }; const getLocaleFromVoiceId = (voiceId) => { - if (voiceId.startsWith("English_") || voiceId.startsWith("English ")) - return "en-US"; - if (voiceId.startsWith("Chinese (Mandarin)") || - voiceId.startsWith("Mandarin_")) - return "zh-CN"; - if (voiceId.startsWith("Cantonese")) - return "zh-HK"; - if (voiceId.startsWith("Japanese_")) - return "ja-JP"; - if (voiceId.startsWith("Korean_")) - return "ko-KR"; - if (voiceId.startsWith("Spanish_")) - return "es-ES"; - if (voiceId.startsWith("Portuguese_")) - return "pt-BR"; - if (voiceId.startsWith("French_")) - return "fr-FR"; - if (voiceId.startsWith("German_")) - return "de-DE"; - if (voiceId.startsWith("Russian_")) - return "ru-RU"; - if (voiceId.startsWith("Italian_")) - return "it-IT"; - if (voiceId.startsWith("Dutch_")) - return "nl-NL"; - if (voiceId.startsWith("Arabic_")) - return "ar"; - if (voiceId.startsWith("Turkish_")) - return "tr-TR"; - if (voiceId.startsWith("Ukrainian_")) - return "uk-UA"; - if (voiceId.startsWith("Thai_")) - return "th-TH"; - if (voiceId.startsWith("Polish_")) - return "pl-PL"; - if (voiceId.startsWith("Romanian_")) - return "ro-RO"; - if (voiceId.startsWith("Greek_") || voiceId.startsWith("greek_")) - return "el-GR"; - if (voiceId.startsWith("Czech_") || voiceId.startsWith("czech_")) - return "cs-CZ"; - if (voiceId.startsWith("Finnish_") || voiceId.startsWith("finnish_")) - return "fi-FI"; - if (voiceId.startsWith("Hindi_") || voiceId.startsWith("hindi_")) - return "hi-IN"; - if (voiceId.startsWith("Indonesian_")) - return "id-ID"; - if (voiceId.startsWith("Vietnamese_")) - return "vi-VN"; + if (voiceId.startsWith("English_") || voiceId.startsWith("English ")) return "en-US"; + if ( + voiceId.startsWith("Chinese (Mandarin)") || + voiceId.startsWith("Mandarin_") + ) + return "zh-CN"; + if (voiceId.startsWith("Cantonese")) return "zh-HK"; + if (voiceId.startsWith("Japanese_")) return "ja-JP"; + if (voiceId.startsWith("Korean_")) return "ko-KR"; + if (voiceId.startsWith("Spanish_")) return "es-ES"; + if (voiceId.startsWith("Portuguese_")) return "pt-BR"; + if (voiceId.startsWith("French_")) return "fr-FR"; + if (voiceId.startsWith("German_")) return "de-DE"; + if (voiceId.startsWith("Russian_")) return "ru-RU"; + if (voiceId.startsWith("Italian_")) return "it-IT"; + if (voiceId.startsWith("Dutch_")) return "nl-NL"; + if (voiceId.startsWith("Arabic_")) return "ar"; + if (voiceId.startsWith("Turkish_")) return "tr-TR"; + if (voiceId.startsWith("Ukrainian_")) return "uk-UA"; + if (voiceId.startsWith("Thai_")) return "th-TH"; + if (voiceId.startsWith("Polish_")) return "pl-PL"; + if (voiceId.startsWith("Romanian_")) return "ro-RO"; + if (voiceId.startsWith("Greek_") || voiceId.startsWith("greek_")) + return "el-GR"; + if (voiceId.startsWith("Czech_") || voiceId.startsWith("czech_")) + return "cs-CZ"; + if (voiceId.startsWith("Finnish_") || voiceId.startsWith("finnish_")) + return "fi-FI"; + if (voiceId.startsWith("Hindi_") || voiceId.startsWith("hindi_")) + return "hi-IN"; + if (voiceId.startsWith("Indonesian_")) return "id-ID"; + if (voiceId.startsWith("Vietnamese_")) return "vi-VN"; + return "en-US"; }; const getGenderFromVoiceId = (voiceId, voiceName) => { - const combined = (voiceId + " " + (voiceName || "")).toLowerCase(); - const femaleKeywords = [ - "female", - "woman", - "girl", - "lady", - "queen", - "princess", - "heroine", - "sister", - "aunt", - "wife", - "girlfriend", - "hostess", - "narrator_f", - ]; - const maleKeywords = [ - "male", - "man", - "boy", - "gentleman", - "king", - "prince", - "knight", - "butler", - "uncle", - "husband", - "narrator_m", - ]; - if (femaleKeywords.some((k) => combined.includes(k))) - return "female"; - if (maleKeywords.some((k) => combined.includes(k))) - return "male"; - return "unknown"; + const combined = (voiceId + " " + (voiceName || "")).toLowerCase(); + const femaleKeywords = [ + "female", + "woman", + "girl", + "lady", + "queen", + "princess", + "heroine", + "sister", + "aunt", + "wife", + "girlfriend", + "hostess", + "narrator_f", + ]; + const maleKeywords = [ + "male", + "man", + "boy", + "gentleman", + "king", + "prince", + "knight", + "butler", + "uncle", + "husband", + "narrator_m", + ]; + if (femaleKeywords.some((k) => combined.includes(k))) return "female"; + if (maleKeywords.some((k) => combined.includes(k))) return "male"; + return "unknown"; }; const getTTSVoice = async (config) => { - let apiKey = config.apiKey || ""; - let url = config.baseUrl + "/v1/get_voice"; - return new Promise((resolve, reject) => { - axios - .post(url, { voice_type: "all" }, { - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", + let apiKey = config.apiKey || ""; + let url = config.baseUrl + "/v1/get_voice"; + return new Promise((resolve, reject) => { + axios + .post( + url, + { voice_type: "all" }, + { + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + } + ) + .then((response) => { + let systemVoices = (response.data && response.data.system_voice) || []; + let cloningVoices = + (response.data && response.data.voice_cloning) || []; + let generationVoices = + (response.data && response.data.voice_generation) || []; + let allVoices = [ + ...systemVoices, + ...cloningVoices, + ...generationVoices, + ]; + resolve( + allVoices.map((voice) => ({ + name: voice.voice_id, + gender: getGenderFromVoiceId(voice.voice_id, voice.voice_name), + locale: getLocaleFromVoiceId(voice.voice_id), + displayName: `MiniMax TTS - ${voice.voice_name || voice.voice_id}`, + plugin: "minimax-tts-voice-plugin", + config: { + ...config, + voiceName: voice.voice_id, }, - }) - .then((response) => { - let systemVoices = (response.data && response.data.system_voice) || []; - let cloningVoices = (response.data && response.data.voice_cloning) || []; - let generationVoices = (response.data && response.data.voice_generation) || []; - let allVoices = [ - ...systemVoices, - ...cloningVoices, - ...generationVoices, - ]; - resolve(allVoices.map((voice) => ({ - name: voice.voice_id, - gender: getGenderFromVoiceId(voice.voice_id, voice.voice_name), - locale: getLocaleFromVoiceId(voice.voice_id), - displayName: `MiniMax TTS - ${voice.voice_name || voice.voice_id}`, - plugin: "minimax-tts-voice-plugin", - config: { - ...config, - voiceName: voice.voice_id, - }, - }))); - }) - .catch((error) => { - reject(error); - }); - }); + })) + ); + }) + .catch((error) => { + reject(error); + }); + }); }; module.exports = { getAudioPath, getTTSVoice }; diff --git a/src/utils/plugins/main/voice/multiTTSVoice.js b/src/utils/plugins/main/voice/multiTTSVoice.js index 36877f7c..86bdae21 100644 --- a/src/utils/plugins/main/voice/multiTTSVoice.js +++ b/src/utils/plugins/main/voice/multiTTSVoice.js @@ -2,81 +2,94 @@ const path = require("path"); const fs = require("fs"); const axios = require("axios"); const getAudioPath = async (text, speed, dirPath, config) => { - let audioName = new Date().getTime() + ".wav"; - if (!fs.existsSync(path.join(dirPath, "tts"))) { - fs.mkdirSync(path.join(dirPath, "tts")); - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - else { - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - return path.join(dirPath, "tts", audioName); + let audioName = new Date().getTime() + ".wav"; + if (!fs.existsSync(path.join(dirPath, "tts"))) { + fs.mkdirSync(path.join(dirPath, "tts")); + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } else { + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } + return path.join(dirPath, "tts", audioName); }; const objectToQueryString = (obj) => { - const queryParams = []; - for (let key in obj) { - if (obj.hasOwnProperty(key)) { - const value = obj[key]; - const encodedKey = encodeURIComponent(key); - const encodedValue = encodeURIComponent(value); - queryParams.push(`${encodedKey}=${encodedValue}`); - } + const queryParams = []; + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + const value = obj[key]; + const encodedKey = encodeURIComponent(key); + const encodedValue = encodeURIComponent(value); + queryParams.push(`${encodedKey}=${encodedValue}`); } - return queryParams.join("&"); + } + return queryParams.join("&"); }; const getTTSAudio = async (text, rate, config) => { - let host = config.host || "127.0.0.1"; - let port = config.url || "8774"; - let api = "http://" + host + ":" + port + "/forward"; - let pitch = config.pitch || "50"; - let speed = rate * 50 || "50"; - let volume = config.volume || "50"; - let voice = config.voice || ""; - return new Promise((resolve, reject) => { - axios.get(api + "?" + objectToQueryString({ + let host = config.host || "127.0.0.1"; + let port = config.url || "8774"; + let api = "http://" + host + ":" + port + "/forward"; + let pitch = config.pitch || "50"; + let speed = rate * 50 || "50"; + let volume = config.volume || "50"; + let voice = config.voice || ""; + return new Promise((resolve, reject) => { + axios + .get( + api + + "?" + + objectToQueryString({ volume, pitch, speed, voice, - text - }), { responseType: "arraybuffer" }) - .then(response => { - resolve(response.data); - }) - .catch(error => { - reject(error); - }); - }); + text, + }), + { responseType: "arraybuffer" } + ) + .then((response) => { + resolve(response.data); + }) + .catch((error) => { + reject(error); + }); + }); }; const getTTSVoice = async (config) => { - let host = config.host || "127.0.0.1"; - let port = config.url || "8774"; - let api = "http://" + host + ":" + port + "/voices"; - return new Promise((resolve, reject) => { - axios.get(api) - .then(response => { - let result = response.data; - if (result.success) { - let voices = Object.values(result.data.catalog).flat(); - resolve(voices.map(voice => { - return { - "name": voice.id, - "gender": voice.gender, - "locale": voice.locale, - "displayName": voice.name, - "plugin": "multitts-voice-plugin", - "config": config - }; - })); - } - else { - resolve([]); - } - }) - .catch(error => { - reject(error); - }); - }); + let host = config.host || "127.0.0.1"; + let port = config.url || "8774"; + let api = "http://" + host + ":" + port + "/voices"; + return new Promise((resolve, reject) => { + axios + .get(api) + .then((response) => { + let result = response.data; + if (result.success) { + let voices = Object.values(result.data.catalog).flat(); + resolve( + voices.map((voice) => { + return { + name: voice.id, + gender: voice.gender, + locale: voice.locale, + displayName: voice.name, + plugin: "multitts-voice-plugin", + config: config, + }; + }) + ); + } else { + resolve([]); + } + }) + .catch((error) => { + reject(error); + }); + }); }; module.exports = { getAudioPath, getTTSVoice }; diff --git a/src/utils/plugins/main/voice/openaiTTSVoice.js b/src/utils/plugins/main/voice/openaiTTSVoice.js index 8605686c..a286478d 100644 --- a/src/utils/plugins/main/voice/openaiTTSVoice.js +++ b/src/utils/plugins/main/voice/openaiTTSVoice.js @@ -2,74 +2,85 @@ const path = require("path"); const fs = require("fs"); const axios = require("axios"); const getAudioPath = async (text, speed, dirPath, config) => { - let audioName = new Date().getTime() + ".mp3"; - if (!fs.existsSync(path.join(dirPath, "tts"))) { - fs.mkdirSync(path.join(dirPath, "tts")); - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - else { - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - return path.join(dirPath, "tts", audioName); + let audioName = new Date().getTime() + ".mp3"; + if (!fs.existsSync(path.join(dirPath, "tts"))) { + fs.mkdirSync(path.join(dirPath, "tts")); + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } else { + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } + return path.join(dirPath, "tts", audioName); }; const getTTSAudio = async (text, speed, config) => { - let apiKey = config.apiKey || ""; - let baseUrl = config.baseUrl || "https://api.openai.com"; - let model = config.model || "gpt-4o-mini-tts"; - let voiceName = config.voiceName || "coral"; - // speed range: 0.25 ~ 4.0, default 1.0 - let speedValue = speed ? Math.min(4.0, Math.max(0.25, speed)) : 1.0; - let url = `${baseUrl}/v1/audio/speech`; - return new Promise((resolve, reject) => { - axios - .post(url, { - model: model, - input: text, - voice: voiceName, - speed: speedValue, - response_format: "mp3", - }, { - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", - }, - responseType: "arraybuffer", - }) - .then((response) => { - resolve(response.data); - }) - .catch((error) => { - reject(error); - }); - }); + let apiKey = config.apiKey || ""; + let baseUrl = config.baseUrl || "https://api.openai.com"; + let model = config.model || "gpt-4o-mini-tts"; + let voiceName = config.voiceName || "coral"; + // speed range: 0.25 ~ 4.0, default 1.0 + let speedValue = speed ? Math.min(4.0, Math.max(0.25, speed)) : 1.0; + let url = `${baseUrl}/v1/audio/speech`; + return new Promise((resolve, reject) => { + axios + .post( + url, + { + model: model, + input: text, + voice: voiceName, + speed: speedValue, + response_format: "mp3", + }, + { + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + responseType: "arraybuffer", + } + ) + .then((response) => { + resolve(response.data); + }) + .catch((error) => { + reject(error); + }); + }); }; const getTTSVoice = async (config) => { - const voices = [ - { name: "alloy", gender: "female" }, - { name: "ash", gender: "male" }, - { name: "ballad", gender: "male" }, - { name: "coral", gender: "female" }, - { name: "echo", gender: "male" }, - { name: "fable", gender: "male" }, - { name: "nova", gender: "female" }, - { name: "onyx", gender: "male" }, - { name: "sage", gender: "female" }, - { name: "shimmer", gender: "female" }, - { name: "verse", gender: "male" }, - { name: "marin", gender: "female" }, - { name: "cedar", gender: "male" }, - ]; - return Promise.resolve(voices.map((voice) => ({ - name: voice.name, - gender: voice.gender, - locale: "en", - displayName: `OpenAI TTS - ${voice.name}`, - plugin: "openai-tts-voice-plugin", - config: { - ...config, - voiceName: voice.name, - }, - }))); + const voices = [ + { name: "alloy", gender: "female" }, + { name: "ash", gender: "male" }, + { name: "ballad", gender: "male" }, + { name: "coral", gender: "female" }, + { name: "echo", gender: "male" }, + { name: "fable", gender: "male" }, + { name: "nova", gender: "female" }, + { name: "onyx", gender: "male" }, + { name: "sage", gender: "female" }, + { name: "shimmer", gender: "female" }, + { name: "verse", gender: "male" }, + { name: "marin", gender: "female" }, + { name: "cedar", gender: "male" }, + ]; + return Promise.resolve( + voices.map((voice) => ({ + name: voice.name, + gender: voice.gender, + locale: "en", + displayName: `OpenAI TTS - ${voice.name}`, + plugin: "openai-tts-voice-plugin", + config: { + ...config, + voiceName: voice.name, + }, + })) + ); }; module.exports = { getAudioPath, getTTSVoice }; diff --git a/src/utils/plugins/main/voice/qwenTTSVoice.js b/src/utils/plugins/main/voice/qwenTTSVoice.js index c90b01b2..e77da757 100644 --- a/src/utils/plugins/main/voice/qwenTTSVoice.js +++ b/src/utils/plugins/main/voice/qwenTTSVoice.js @@ -2,212 +2,213 @@ const path = require("path"); const fs = require("fs"); const axios = require("axios"); const getAudioPath = async (text, speed, dirPath, config) => { - let audioName = new Date().getTime() + ".wav"; - if (!fs.existsSync(path.join(dirPath, "tts"))) { - fs.mkdirSync(path.join(dirPath, "tts")); - } - const audioData = await getTTSAudio(text, speed, config); - fs.writeFileSync(path.join(dirPath, "tts", audioName), audioData); - return path.join(dirPath, "tts", audioName); + let audioName = new Date().getTime() + ".wav"; + if (!fs.existsSync(path.join(dirPath, "tts"))) { + fs.mkdirSync(path.join(dirPath, "tts")); + } + const audioData = await getTTSAudio(text, speed, config); + fs.writeFileSync(path.join(dirPath, "tts", audioName), audioData); + return path.join(dirPath, "tts", audioName); }; const getTTSAudio = async (text, speed, config) => { - let apiKey = config.apiKey || ""; - let model = config.model || "qwen3-tts-flash"; - let voice = config.voiceName || "Cherry"; - let baseUrl = config.baseUrl || "https://dashscope.aliyuncs.com/api/v1"; - // Build request body - const body = { - model: model, - input: { - text: text, - voice: voice, + let apiKey = config.apiKey || ""; + let model = config.model || "qwen3-tts-flash"; + let voice = config.voiceName || "Cherry"; + let baseUrl = config.baseUrl || "https://dashscope.aliyuncs.com/api/v1"; + // Build request body + const body = { + model: model, + input: { + text: text, + voice: voice, + }, + parameters: { + stream: false, + speech_rate: undefined, + }, + }; + // speed: 1.0 = normal; map to speech_rate (-500 ~ 500), 0 = normal + if (speed && speed !== 1.0) { + body.parameters.speech_rate = Math.round((speed - 1) * 500); + } + const url = `${baseUrl}/services/aigc/multimodal-generation/generation`; + return new Promise((resolve, reject) => { + axios + .post(url, body, { + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + "X-DashScope-DataInspection": "enable", }, - parameters: { - stream: false, - speech_rate: undefined, - }, - }; - // speed: 1.0 = normal; map to speech_rate (-500 ~ 500), 0 = normal - if (speed && speed !== 1.0) { - body.parameters.speech_rate = Math.round((speed - 1) * 500); - } - const url = `${baseUrl}/services/aigc/multimodal-generation/generation`; - return new Promise((resolve, reject) => { - axios - .post(url, body, { - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", - "X-DashScope-DataInspection": "enable", - }, - }) - .then(async (response) => { - const audioUrl = response.data && - response.data.output && - response.data.output.audio && - response.data.output.audio.url; - if (!audioUrl) { - reject("No audio URL in response"); - return; - } - // Download the audio file from the returned URL - const audioResponse = await axios.get(audioUrl, { - responseType: "arraybuffer", - }); - resolve(Buffer.from(audioResponse.data)); - }) - .catch((error) => { - reject(error); + }) + .then(async (response) => { + const audioUrl = + response.data && + response.data.output && + response.data.output.audio && + response.data.output.audio.url; + if (!audioUrl) { + reject("No audio URL in response"); + return; + } + // Download the audio file from the returned URL + const audioResponse = await axios.get(audioUrl, { + responseType: "arraybuffer", }); - }); + resolve(Buffer.from(audioResponse.data)); + }) + .catch((error) => { + reject(error); + }); + }); }; const getTTSVoice = async (config) => { - // Qwen TTS uses a fixed set of system voices; return a predefined list - const voices = [ - { - name: "Cherry", - gender: "female", - locale: "zh-CN", - displayName: "芊悦 Cherry (zh-CN/en) - Female", - }, - { - name: "Serena", - gender: "female", - locale: "zh-CN", - displayName: "苏瑶 Serena (zh-CN/en) - Female", - }, - { - name: "Ethan", - gender: "male", - locale: "zh-CN", - displayName: "晨煦 Ethan (zh-CN/en) - Male", - }, - { - name: "Chelsie", - gender: "female", - locale: "zh-CN", - displayName: "千雪 Chelsie (zh-CN/en) - Female", - }, - { - name: "Momo", - gender: "female", - locale: "zh-CN", - displayName: "茉兔 Momo (zh-CN/en) - Female", - }, - { - name: "Vivian", - gender: "female", - locale: "zh-CN", - displayName: "十三 Vivian (zh-CN/en) - Female", - }, - { - name: "Moon", - gender: "male", - locale: "zh-CN", - displayName: "月白 Moon (zh-CN/en) - Male", - }, - { - name: "Maia", - gender: "female", - locale: "zh-CN", - displayName: "四月 Maia (zh-CN/en) - Female", - }, - { - name: "Kai", - gender: "male", - locale: "zh-CN", - displayName: "凯 Kai (zh-CN/en) - Male", - }, - { - name: "Nofish", - gender: "male", - locale: "zh-CN", - displayName: "不吃鱼 Nofish (zh-CN/en) - Male", - }, - { - name: "Bella", - gender: "female", - locale: "zh-CN", - displayName: "萌宝 Bella (zh-CN/en) - Female", - }, - { - name: "Jennifer", - gender: "female", - locale: "zh-CN", - displayName: "詹妮弗 Jennifer (zh-CN/en) - Female", - }, - { - name: "Ryan", - gender: "male", - locale: "zh-CN", - displayName: "甜茶 Ryan (zh-CN/en) - Male", - }, - { - name: "Katerina", - gender: "female", - locale: "zh-CN", - displayName: "卡捷琳娜 Katerina (zh-CN/en) - Female", - }, - { - name: "Aiden", - gender: "male", - locale: "zh-CN", - displayName: "艾登 Aiden (zh-CN/en) - Male", - }, - { - name: "Neil", - gender: "male", - locale: "zh-CN", - displayName: "阿闻 Neil (zh-CN/en) - Male", - }, - { - name: "Elias", - gender: "female", - locale: "zh-CN", - displayName: "墨讲师 Elias (zh-CN/en) - Female", - }, - { - name: "Sunny", - gender: "female", - locale: "zh-SC", - displayName: "四川-晴儿 Sunny (zh-SC) - Female", - }, - { - name: "Dylan", - gender: "male", - locale: "zh-BJ", - displayName: "北京-晓东 Dylan (zh-BJ) - Male", - }, - { - name: "Jada", - gender: "female", - locale: "zh-SH", - displayName: "上海-阿珍 Jada (zh-SH) - Female", - }, - { - name: "Rocky", - gender: "male", - locale: "zh-YUE", - displayName: "粤语-阿强 Rocky (zh-YUE) - Male", - }, - { - name: "Kiki", - gender: "female", - locale: "zh-YUE", - displayName: "粤语-阿清 Kiki (zh-YUE) - Female", - }, - ]; - return voices.map((v) => ({ - name: v.name, - gender: v.gender, - locale: v.locale, - displayName: `Qwen TTS - ${v.displayName}`, - plugin: "qwen-tts-voice-plugin", - config: { - ...config, - voiceName: v.name, - }, - })); + // Qwen TTS uses a fixed set of system voices; return a predefined list + const voices = [ + { + name: "Cherry", + gender: "female", + locale: "zh-CN", + displayName: "芊悦 Cherry (zh-CN/en) - Female", + }, + { + name: "Serena", + gender: "female", + locale: "zh-CN", + displayName: "苏瑶 Serena (zh-CN/en) - Female", + }, + { + name: "Ethan", + gender: "male", + locale: "zh-CN", + displayName: "晨煦 Ethan (zh-CN/en) - Male", + }, + { + name: "Chelsie", + gender: "female", + locale: "zh-CN", + displayName: "千雪 Chelsie (zh-CN/en) - Female", + }, + { + name: "Momo", + gender: "female", + locale: "zh-CN", + displayName: "茉兔 Momo (zh-CN/en) - Female", + }, + { + name: "Vivian", + gender: "female", + locale: "zh-CN", + displayName: "十三 Vivian (zh-CN/en) - Female", + }, + { + name: "Moon", + gender: "male", + locale: "zh-CN", + displayName: "月白 Moon (zh-CN/en) - Male", + }, + { + name: "Maia", + gender: "female", + locale: "zh-CN", + displayName: "四月 Maia (zh-CN/en) - Female", + }, + { + name: "Kai", + gender: "male", + locale: "zh-CN", + displayName: "凯 Kai (zh-CN/en) - Male", + }, + { + name: "Nofish", + gender: "male", + locale: "zh-CN", + displayName: "不吃鱼 Nofish (zh-CN/en) - Male", + }, + { + name: "Bella", + gender: "female", + locale: "zh-CN", + displayName: "萌宝 Bella (zh-CN/en) - Female", + }, + { + name: "Jennifer", + gender: "female", + locale: "zh-CN", + displayName: "詹妮弗 Jennifer (zh-CN/en) - Female", + }, + { + name: "Ryan", + gender: "male", + locale: "zh-CN", + displayName: "甜茶 Ryan (zh-CN/en) - Male", + }, + { + name: "Katerina", + gender: "female", + locale: "zh-CN", + displayName: "卡捷琳娜 Katerina (zh-CN/en) - Female", + }, + { + name: "Aiden", + gender: "male", + locale: "zh-CN", + displayName: "艾登 Aiden (zh-CN/en) - Male", + }, + { + name: "Neil", + gender: "male", + locale: "zh-CN", + displayName: "阿闻 Neil (zh-CN/en) - Male", + }, + { + name: "Elias", + gender: "female", + locale: "zh-CN", + displayName: "墨讲师 Elias (zh-CN/en) - Female", + }, + { + name: "Sunny", + gender: "female", + locale: "zh-SC", + displayName: "四川-晴儿 Sunny (zh-SC) - Female", + }, + { + name: "Dylan", + gender: "male", + locale: "zh-BJ", + displayName: "北京-晓东 Dylan (zh-BJ) - Male", + }, + { + name: "Jada", + gender: "female", + locale: "zh-SH", + displayName: "上海-阿珍 Jada (zh-SH) - Female", + }, + { + name: "Rocky", + gender: "male", + locale: "zh-YUE", + displayName: "粤语-阿强 Rocky (zh-YUE) - Male", + }, + { + name: "Kiki", + gender: "female", + locale: "zh-YUE", + displayName: "粤语-阿清 Kiki (zh-YUE) - Female", + }, + ]; + return voices.map((v) => ({ + name: v.name, + gender: v.gender, + locale: v.locale, + displayName: `Qwen TTS - ${v.displayName}`, + plugin: "qwen-tts-voice-plugin", + config: { + ...config, + voiceName: v.name, + }, + })); }; module.exports = { getAudioPath, getTTSVoice }; diff --git a/src/utils/plugins/main/voice/ttsServerVoice.js b/src/utils/plugins/main/voice/ttsServerVoice.js index 76a2bfa2..d5e45dbb 100644 --- a/src/utils/plugins/main/voice/ttsServerVoice.js +++ b/src/utils/plugins/main/voice/ttsServerVoice.js @@ -2,62 +2,73 @@ const path = require("path"); const fs = require("fs"); const axios = require("axios"); const getAudioPath = async (text, speed, dirPath, config) => { - let audioName = new Date().getTime() + ".wav"; - if (!fs.existsSync(path.join(dirPath, "tts"))) { - fs.mkdirSync(path.join(dirPath, "tts")); - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - else { - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - } - return path.join(dirPath, "tts", audioName); + let audioName = new Date().getTime() + ".wav"; + if (!fs.existsSync(path.join(dirPath, "tts"))) { + fs.mkdirSync(path.join(dirPath, "tts")); + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } else { + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + } + return path.join(dirPath, "tts", audioName); }; const objectToQueryString = (obj) => { - const queryParams = []; - for (let key in obj) { - if (obj.hasOwnProperty(key)) { - const value = obj[key]; - const encodedKey = encodeURIComponent(key); - const encodedValue = encodeURIComponent(value); - queryParams.push(`${encodedKey}=${encodedValue}`); - } + const queryParams = []; + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + const value = obj[key]; + const encodedKey = encodeURIComponent(key); + const encodedValue = encodeURIComponent(value); + queryParams.push(`${encodedKey}=${encodedValue}`); } - return queryParams.join("&"); + } + return queryParams.join("&"); }; const getTTSAudio = async (text, speed, config) => { - let host = config.host || "127.0.0.1"; - let url = config.url || ""; - let params = parseUrlParams(url); - let api = params.api || "http://localhost:1221/api/tts"; - let engine = params.engine || "com.github.jing332.tts_server_android"; - let pitch = params.pitch || "100"; - let rate = params.rate || "50"; - let voice = params.voice || ""; - api = api.replace("localhost", host); - return new Promise((resolve, reject) => { - axios.get(api + "?" + objectToQueryString({ + let host = config.host || "127.0.0.1"; + let url = config.url || ""; + let params = parseUrlParams(url); + let api = params.api || "http://localhost:1221/api/tts"; + let engine = params.engine || "com.github.jing332.tts_server_android"; + let pitch = params.pitch || "100"; + let rate = params.rate || "50"; + let voice = params.voice || ""; + api = api.replace("localhost", host); + return new Promise((resolve, reject) => { + axios + .get( + api + + "?" + + objectToQueryString({ engine, pitch, rate, voice, - text - }), { responseType: "arraybuffer" }) - .then(response => { - resolve(response.data); - }) - .catch(error => { - reject(error); - }); - }); + text, + }), + { responseType: "arraybuffer" } + ) + .then((response) => { + resolve(response.data); + }) + .catch((error) => { + reject(error); + }); + }); }; function parseUrlParams(url) { - const urlObj = new URL(url); - const params = new URLSearchParams(urlObj.search); - const result = {}; - for (const [key, value] of params.entries()) { - result[key] = decodeURIComponent(value); // 解码参数值 - } - return result; + const urlObj = new URL(url); + const params = new URLSearchParams(urlObj.search); + const result = {}; + for (const [key, value] of params.entries()) { + result[key] = decodeURIComponent(value); // 解码参数值 + } + return result; } module.exports = { getAudioPath }; diff --git a/src/utils/plugins/main/voice/volcengineTTSVoice.js b/src/utils/plugins/main/voice/volcengineTTSVoice.js index 28199f65..c7610aa4 100644 --- a/src/utils/plugins/main/voice/volcengineTTSVoice.js +++ b/src/utils/plugins/main/voice/volcengineTTSVoice.js @@ -2,385 +2,1786 @@ const path = require("path"); const fs = require("fs"); const axios = require("axios"); const getAudioPath = async (text, speed, dirPath, config) => { - let audioName = new Date().getTime() + ".mp3"; - if (!fs.existsSync(path.join(dirPath, "tts"))) { - fs.mkdirSync(path.join(dirPath, "tts")); - } - fs.writeFileSync(path.join(dirPath, "tts", audioName), await getTTSAudio(text, speed, config)); - return path.join(dirPath, "tts", audioName); + let audioName = new Date().getTime() + ".mp3"; + if (!fs.existsSync(path.join(dirPath, "tts"))) { + fs.mkdirSync(path.join(dirPath, "tts")); + } + fs.writeFileSync( + path.join(dirPath, "tts", audioName), + await getTTSAudio(text, speed, config) + ); + return path.join(dirPath, "tts", audioName); }; const mapSpeedToSpeechRate = (speed) => { - if (!speed || speed === 1.0) - return 0; - return Math.min(100, Math.max(-50, Math.round((speed - 1) * 100))); + if (!speed || speed === 1.0) return 0; + return Math.min(100, Math.max(-50, Math.round((speed - 1) * 100))); }; const parseChunkedResponse = (responseText) => { - const audioChunks = []; - const lines = responseText.split("\n"); - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed) - continue; - try { - const obj = JSON.parse(trimmed); - const code = obj.code || 0; - if (code === 20000000) - continue; - if (code !== 0) { - throw new Error(obj.message || `TTS error code ${code}`); - } - if (obj.data) { - audioChunks.push(Buffer.from(obj.data, "base64")); - } - } - catch (err) { - if (err instanceof Error && err.message.startsWith("TTS error")) - throw err; - } + const audioChunks = []; + const lines = responseText.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + try { + const obj = JSON.parse(trimmed); + const code = obj.code || 0; + if (code === 20000000) continue; + if (code !== 0) { + throw new Error(obj.message || `TTS error code ${code}`); + } + if (obj.data) { + audioChunks.push(Buffer.from(obj.data, "base64")); + } + } catch (err) { + if (err instanceof Error && err.message.startsWith("TTS error")) + throw err; } - return Buffer.concat(audioChunks); + } + return Buffer.concat(audioChunks); }; const getTTSAudio = async (text, speed, config) => { - const apiKey = config.apiKey || ""; - if (!apiKey) { - return Promise.reject("Missing API Key"); - } - const voiceName = config.voiceName || "zh_female_vv_uranus_bigtts"; - const resourceId = config.resourceId || "seed-tts-2.0"; - const url = "https://openspeech.bytedance.com/api/v3/tts/unidirectional"; - const audioParams = { - format: "mp3", - sample_rate: 24000, - }; - const speechRate = mapSpeedToSpeechRate(speed); - if (speechRate !== 0) { - audioParams.speech_rate = speechRate; - } - const payload = { - req_params: { - text: text, - speaker: voiceName, - audio_params: audioParams, + const apiKey = config.apiKey || ""; + if (!apiKey) { + return Promise.reject("Missing API Key"); + } + const voiceName = config.voiceName || "zh_female_vv_uranus_bigtts"; + const resourceId = config.resourceId || "seed-tts-2.0"; + const url = "https://openspeech.bytedance.com/api/v3/tts/unidirectional"; + const audioParams = { + format: "mp3", + sample_rate: 24000, + }; + const speechRate = mapSpeedToSpeechRate(speed); + if (speechRate !== 0) { + audioParams.speech_rate = speechRate; + } + const payload = { + req_params: { + text: text, + speaker: voiceName, + audio_params: audioParams, + }, + }; + return new Promise((resolve, reject) => { + axios + .post(url, payload, { + headers: { + "X-Api-Key": apiKey, + "Content-Type": "application/json", + "X-Api-Resource-Id": resourceId, }, - }; - return new Promise((resolve, reject) => { - axios - .post(url, payload, { - headers: { - "X-Api-Key": apiKey, - "Content-Type": "application/json", - "X-Api-Resource-Id": resourceId, - }, - responseType: "text", - timeout: 60000, - }) - .then((response) => { - const audioBuffer = parseChunkedResponse(response.data); - if (!audioBuffer.length) { - reject("No audio data in response"); - return; - } - resolve(audioBuffer); - }) - .catch((error) => { - reject(error); - }); - }); + responseType: "text", + timeout: 60000, + }) + .then((response) => { + const audioBuffer = parseChunkedResponse(response.data); + if (!audioBuffer.length) { + reject("No audio data in response"); + return; + } + resolve(audioBuffer); + }) + .catch((error) => { + reject(error); + }); + }); }; const VOICES_2_0 = [ - { name: "zh_female_vv_uranus_bigtts", displayName: "Vivi 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_xiaohe_uranus_bigtts", displayName: "小何 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_m191_uranus_bigtts", displayName: "云舟 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_taocheng_uranus_bigtts", displayName: "小天 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_liufei_uranus_bigtts", displayName: "刘飞 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_sophie_uranus_bigtts", displayName: "魅力苏菲 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_qingxinnvsheng_uranus_bigtts", displayName: "清新女声 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_cancan_uranus_bigtts", displayName: "知性灿灿 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_sajiaoxuemei_uranus_bigtts", displayName: "撒娇学妹 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_tianmeixiaoyuan_uranus_bigtts", displayName: "甜美小源 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_tianmeitaozi_uranus_bigtts", displayName: "甜美桃子 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_shuangkuaisisi_uranus_bigtts", displayName: "爽快思思 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_peiqi_uranus_bigtts", displayName: "佩奇猪 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_linjianvhai_uranus_bigtts", displayName: "邻家女孩 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_shaonianzixin_uranus_bigtts", displayName: "少年梓辛/ Brayan 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_sunwukong_uranus_bigtts", displayName: "猴哥 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_yingyujiaoxue_uranus_bigtts", displayName: "Tina老师 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_kefunvsheng_uranus_bigtts", displayName: "暖阳女声 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_xiaoxue_uranus_bigtts", displayName: "儿童绘本 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_dayi_uranus_bigtts", displayName: "大壹 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_mizai_uranus_bigtts", displayName: "黑猫侦探社 咪仔 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_jitangnv_uranus_bigtts", displayName: "鸡汤女 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_meilinvyou_uranus_bigtts", displayName: "魅力女友 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_liuchangnv_uranus_bigtts", displayName: "流畅女声 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_ruyayichen_uranus_bigtts", displayName: "儒雅逸辰 2.0", gender: "male", locale: "zh-CN" }, - { name: "en_male_tim_uranus_bigtts", displayName: "Tim", gender: "male", locale: "en-US" }, - { name: "en_female_dacey_uranus_bigtts", displayName: "Dacey", gender: "female", locale: "en-US" }, - { name: "en_female_stokie_uranus_bigtts", displayName: "Stokie", gender: "female", locale: "en-US" }, - { name: "zh_female_wenroumama_uranus_bigtts", displayName: "温柔妈妈 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_jieshuoxiaoming_uranus_bigtts", displayName: "解说小明 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_tvbnv_uranus_bigtts", displayName: "TVB女声 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_yizhipiannan_uranus_bigtts", displayName: "译制片男 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_qiaopinv_uranus_bigtts", displayName: "俏皮女声 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_zhishuaiyingzi_uranus_bigtts", displayName: "直率英子 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_linjiananhai_uranus_bigtts", displayName: "邻家男孩 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_silang_uranus_bigtts", displayName: "四郎 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_ruyaqingnian_uranus_bigtts", displayName: "儒雅青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_qingcang_uranus_bigtts", displayName: "擎苍 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_xionger_uranus_bigtts", displayName: "熊二 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_yingtaowanzi_uranus_bigtts", displayName: "樱桃丸子 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_wennuanahu_uranus_bigtts", displayName: "温暖阿虎/ Alvin 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_naiqimengwa_uranus_bigtts", displayName: "奶气萌娃 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_popo_uranus_bigtts", displayName: "婆婆 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_gaolengyujie_uranus_bigtts", displayName: "高冷御姐 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_aojiaobazong_uranus_bigtts", displayName: "傲娇霸总 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_lanyinmianbao_uranus_bigtts", displayName: "懒音绵宝 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_wenroushunv_uranus_bigtts", displayName: "温柔淑女 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_gufengshaoyu_uranus_bigtts", displayName: "古风少御 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_huolixiaoge_uranus_bigtts", displayName: "活力小哥 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_baqiqingshu_uranus_bigtts", displayName: "霸气青叔 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_xuanyijieshuo_uranus_bigtts", displayName: "悬疑解说 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_mengyatou_uranus_bigtts", displayName: "萌丫头/ Cutey 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_tiexinnvsheng_uranus_bigtts", displayName: "贴心女声/ Candy 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_jitangmei_uranus_bigtts", displayName: "鸡汤妹妹/", gender: "female", locale: "zh-CN" }, - { name: "zh_male_cixingjieshuonan_uranus_bigtts", displayName: "磁性解说男 声/Morgan 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_liangsangmengzai_uranus_bigtts", displayName: "亮嗓萌仔 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_kailangjiejie_uranus_bigtts", displayName: "开朗姐姐 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_gaolengchenwen_uranus_bigtts", displayName: "高冷沉稳 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_shenyeboke_uranus_bigtts", displayName: "深夜播客 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_lubanqihao_uranus_bigtts", displayName: "鲁班七号 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_jiaochuannv_uranus_bigtts", displayName: "娇喘女声 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_linxiao_uranus_bigtts", displayName: "邻萧 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_lingling_uranus_bigtts", displayName: "玲玲姐姐 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_chunribu_uranus_bigtts", displayName: "春日部姐姐 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_tangseng_uranus_bigtts", displayName: "唐僧 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_zhuangzhou_uranus_bigtts", displayName: "庄周 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_kailangdidi_uranus_bigtts", displayName: "开朗弟弟 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_zhubajie_uranus_bigtts", displayName: "猪八戒 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_ganmaodianyin_uranus_bigtts", displayName: "感冒电音姐 姐 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_chanmeinv_uranus_bigtts", displayName: "谄媚女声 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_nvleishen_uranus_bigtts", displayName: "女雷神 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_qinqienv_uranus_bigtts", displayName: "亲切女声 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_kuailexiaodong_uranus_bigtts", displayName: "快乐小东 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_kailangxuezhang_uranus_bigtts", displayName: "开朗学长 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_youyoujunzi_uranus_bigtts", displayName: "悠悠君子 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_wenjingmaomao_uranus_bigtts", displayName: "文静毛毛 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_zhixingnv_uranus_bigtts", displayName: "知性女声 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_qingshuangnanda_uranus_bigtts", displayName: "清爽男大 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_yuanboxiaoshu_uranus_bigtts", displayName: "渊博小叔 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_male_yangguangqingnian_uranus_bigtts", displayName: "阳光青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_qingchezizi_uranus_bigtts", displayName: "清澈梓梓 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_tianmeiyueyue_uranus_bigtts", displayName: "甜美悦悦 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_xinlingjitang_uranus_bigtts", displayName: "心灵鸡汤 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_wenrouxiaoge_uranus_bigtts", displayName: "温柔小哥 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_roumeinvyou_uranus_bigtts", displayName: "柔美女友 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_dongfanghaoran_uranus_bigtts", displayName: "东方浩然 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_wenrouxiaoya_uranus_bigtts", displayName: "温柔小雅 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_tiancaitongsheng_uranus_bigtts", displayName: "天才童声 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_wuzetian_uranus_bigtts", displayName: "武则天 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_female_gujie_uranus_bigtts", displayName: "顾姐 2.0", gender: "female", locale: "zh-CN" }, - { name: "zh_male_guanggaojieshuo_uranus_bigtts", displayName: "广告解说 2.0", gender: "male", locale: "zh-CN" }, - { name: "zh_female_shaoergushi_uranus_bigtts", displayName: "少儿故事 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_en_female_charlie_tob", displayName: "Charlie 2.0", gender: "female", locale: "en-US" }, - { name: "ICL_uranus_en_male_ethan_tob", displayName: "Ethan 2.0", gender: "male", locale: "en-US" }, - { name: "ICL_uranus_en_male_alastor_tob", displayName: "Alastor 2.0", gender: "male", locale: "en-US" }, - { name: "ICL_uranus_en_male_chucky_tob", displayName: "Chucky 2.0", gender: "male", locale: "en-US" }, - { name: "ICL_uranus_en_male_noah_tob", displayName: "Noah 2.0", gender: "male", locale: "en-US" }, - { name: "ICL_uranus_en_male_jigsaw_tob", displayName: "Jigsaw 2.0", gender: "male", locale: "en-US" }, - { name: "ICL_uranus_en_male_clown_man_tob", displayName: "Clown Man 2.0", gender: "male", locale: "en-US" }, - { name: "ICL_uranus_en_male_cartoon_chef_tob", displayName: "Cartoon Chef 2.0", gender: "male", locale: "en-US" }, - { name: "ICL_uranus_en_male_frosty_man_tob", displayName: "Frosty Man 2.0", gender: "male", locale: "en-US" }, - { name: "ICL_uranus_en_male_the_grinch_tob", displayName: "The Grinch 2.0", gender: "male", locale: "en-US" }, - { name: "ICL_uranus_en_male_kevin_mccallister_tob", displayName: "Kevin McCallister 2.0", gender: "male", locale: "en-US" }, - { name: "ICL_uranus_en_male_michael_tob", displayName: "Michael 2.0", gender: "male", locale: "en-US" }, - { name: "ICL_uranus_en_male_big_boogie_tob", displayName: "Big Boogie 2.0", gender: "male", locale: "en-US" }, - { name: "ICL_uranus_en_male_xavier_tob", displayName: "Xavier 2.0", gender: "male", locale: "en-US" }, - { name: "ICL_uranus_en_male_zayne_tob", displayName: "Zayne 2.0", gender: "male", locale: "en-US" }, - { name: "ICL_uranus_zh_female_aojiaonvyou_tob", displayName: "傲娇女友 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_aomanjiaosheng_tob", displayName: "傲慢娇声 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_xiemeinvwang_tob", displayName: "邪魅女王 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_bingjiaojiejie_tob", displayName: "病娇姐姐 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_bingjiaomengmei_tob", displayName: "病娇萌妹 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_bingruoshaonv_tob", displayName: "病弱少女 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_chengshuwenrou_tob", displayName: "成熟温柔 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_chengshujiejie_tob", displayName: "成熟姐姐 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_chunzhenshaonv_tob", displayName: "纯真少女 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_chunchenvsheng_tob", displayName: "纯澈女生 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_wumeikeren_tob", displayName: "妩媚可人 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_heainainai_tob", displayName: "乖巧可儿 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_huopodiaoman_tob", displayName: "活泼刁蛮 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_huoponvhai_tob", displayName: "活泼女孩 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_jiaohannvwang_tob", displayName: "娇憨女王 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_jiaoruoluoli_tob", displayName: "娇弱萝莉 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_jiaxiaozi_tob", displayName: "假小子 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_jinglingxiangdao_tob", displayName: "精灵向导 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_kaixinxiaohong_tob", displayName: "开朗婷婷 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_keainvsheng_tob", displayName: "可爱女生 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_lingdongxinxin_tob", displayName: "灵动欣欣 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_linjuayi_tob", displayName: "邻居阿姨 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_tianmeijiaoqiao_tob", displayName: "甜美娇俏 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_qinglenggaoya_tob", displayName: "清冷高雅 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_lixingyuanzi_tob", displayName: "理性圆子 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_xingganmeihuo_tob", displayName: "性感魅惑 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_nuanxinqianqian_tob", displayName: "暖心茜茜 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_nuanxinxuejie_tob", displayName: "暖心学姐 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_qingtianmeimei_tob", displayName: "清甜莓莓 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_qingtiantaotao_tob", displayName: "清甜桃桃 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_qingxixiaoxue_tob", displayName: "清晰小雪 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_qingxinshaonv_tob", displayName: "倾心少女 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_ruanmengtangtang_tob", displayName: "柔骨魂师 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_ruanmengtuanzi_tob", displayName: "软萌团子 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_tianmeihuopo_tob", displayName: "甜美活泼 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_tianmeixiaoju_tob", displayName: "甜美小橘 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_tianmeixiaoyu_tob", displayName: "甜美小雨 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_tiaopigongzhu_tob", displayName: "调皮公主 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_tiexinnvyou_tob", displayName: "贴心女友 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_wenrounvshen_tob", displayName: "温柔女神 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_wenrouwenya_tob", displayName: "温柔文雅 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_zhixinjiejie_tob", displayName: "知心姐姐 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_wumeiyujie_tob", displayName: "妩媚御姐 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_yuanqitianmei_tob", displayName: "元气甜妹 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_xiemeiyujie_tob", displayName: "邪魅御姐 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_xingganyujie_tob", displayName: "性感御姐 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_xiuliqianqian_tob", displayName: "秀丽倩倩 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_tiexinguimi_tob", displayName: "贴心闺蜜 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_tiexinmeimei_tob", displayName: "贴心妹妹 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_wenroubaiyueguang_tob", displayName: "温柔白月光 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_chuliannvyou_tob", displayName: "初恋女友 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_female_zhixingwenwan_tob", displayName: "知性温婉 2.0", gender: "female", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_aoqilingren_tob", displayName: "傲气凌人 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_anrenqinzhu_tob", displayName: "黯刃秦主 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_aojiaogongzi_tob", displayName: "傲娇公子 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_aojiaojingying_tob", displayName: "傲娇精英 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_aomanqingnian_tob", displayName: "傲慢青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_aomanshaoye_tob", displayName: "傲慢少爷 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_zhenbiandiyu_tob", displayName: "枕边低语 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_badaoshaoye_tob", displayName: "霸道少爷 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_badaozongcai_tob", displayName: "霸道总裁 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_bingjiaobailian_tob", displayName: "病娇白莲 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_bingjiaodidi_tob", displayName: "病娇弟弟 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_bingjiaogege_tob", displayName: "病娇哥哥 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_bingjiaonanyou_tob", displayName: "病娇男友 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_bingjiaoshaonian_tob", displayName: "病娇少年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_bingruogongzi_tob", displayName: "病弱公子 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_bingruoshaonian_tob", displayName: "病弱少年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_bujiqingnian_tob", displayName: "不羁青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_chunhoudiyin_tob", displayName: "醇厚低音 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_paoxiaoxiaoge_tob", displayName: "咆哮小哥 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_yangyang_tob", displayName: "炀炀 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_chanruoshaoye_tob", displayName: "孱弱少爷 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_chengshuzongcai_tob", displayName: "成熟总裁 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_chenwenmingzai_tob", displayName: "沉稳明仔 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_qingyisugan_tob", displayName: "清逸苏感 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_chunzhenxuedi_tob", displayName: "纯真学弟 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_cixingnansang_tob", displayName: "磁性男嗓 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_cujingnansheng_tob", displayName: "醋精男生 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_cujingnanyou_tob", displayName: "醋精男友 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_diyinchenyu_tob", displayName: "低音沉郁 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_fengfashaonian_tob", displayName: "风发少年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_ruyagongzi_tob", displayName: "儒雅公子 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_fuheigongzi_tob", displayName: "腹黑公子 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_ganjingshaonian_tob", displayName: "干净少年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_gaolengzongcai_tob", displayName: "高冷总裁 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_guaogongzi_tob", displayName: "孤傲公子 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_guiyishenmi_tob", displayName: "aogongzi_to b 诡异神秘 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_guzhibingjiao_tob", displayName: "固执病娇 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_hanhoudunshi_tob", displayName: "憨厚敦实 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_huoliqingnian_tob", displayName: "活力青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_huoponanyou_tob", displayName: "活泼男友 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_huoposhuanglang_tob", displayName: "活泼爽朗 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_huzishushu_tob", displayName: "胡子叔叔 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_jijiazhineng_tob", displayName: "机甲智能 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_jingyingqingnian_tob", displayName: "精英青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_junyigongzi_tob", displayName: "俊逸公子 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_kailangqingkuai_tob", displayName: "开朗轻快 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_kailangqingnian_tob", displayName: "开朗青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_lanyincaohunshi_tob", displayName: "蓝银草魂师 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_lengaozongcai_tob", displayName: "冷傲总裁 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_lengdanshuli_tob", displayName: "冷淡疏离 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_lengjungaozhi_tob", displayName: "冷峻高智 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_lengjunshangsi_tob", displayName: "冷峻上司 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_lengkugege_tob", displayName: "冷酷哥哥 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_lenglianxiongzhang_tob", displayName: "冷脸兄长 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_lenglianxueba_tob", displayName: "冷脸学霸 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_lengmonanyou_tob", displayName: "冷漠男友 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_lengmoxiongzhang_tob", displayName: "冷漠兄长 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_lingyunqingnian_tob", displayName: "凌云青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_qinglengjingui_tob", displayName: "清冷矜贵 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_lvchaxiaoge_tob", displayName: "绿茶小哥 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_mengdongqingnian_tob", displayName: "懵懂青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_menyoupingxiaoge_tob", displayName: "闷油瓶小哥 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_xiaozhangxiaoge_tob", displayName: "嚣张小哥 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_nianrennanyou_tob", displayName: "粘人男友 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_neiliancaijun_tob", displayName: "内敛才俊 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_nuanxintitie_tob", displayName: "暖心体贴 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_pianpiangongzi_tob", displayName: "翩翩公子 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_chenwenyouya_tob", displayName: "沉稳优雅 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_qingsexiaosheng_tob", displayName: "青涩小生 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_qingseqingnian_tob", displayName: "青涩青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_qingshuangshaonian_tob", displayName: "清爽少年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_qingxinbobo_tob", displayName: "清新波波 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_qinqieqingnian_tob", displayName: "亲切青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_qinqiexiaozhuo_tob", displayName: "亲切小卓 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_qinglangwenrun_tob", displayName: "清朗温润 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_rexueshaonian_tob", displayName: "热血少年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_ruyacaijun_tob", displayName: "儒雅才俊 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_ruyajunzi_tob", displayName: "儒雅君子 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_ruyazongcai_tob", displayName: "儒雅总裁 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_sajiaonansheng_tob", displayName: "撒娇男生 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_sajiaonanyou_tob", displayName: "撒娇男友 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_sajiaonianren_tob", displayName: "撒娇粘人 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_satuoqingnian_tob", displayName: "洒脱青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_shaonianjiangjun_tob", displayName: "少年将军 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_shenchenzongcai_tob", displayName: "深沉总裁 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_jilingxiaohuo_tob", displayName: "机灵小伙 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_shenmifashi_tob", displayName: "神秘法师 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_shuaizhenxiaohuo_tob", displayName: "率真小伙 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_shuanglangxiaoyang_tob", displayName: "爽朗小阳 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_dichenqianquan_tob", displayName: "低沉缱绻 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_siwenqingnian_tob", displayName: "斯文青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_tianxinanyou_tob", displayName: "甜系男友 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_tiexinnanyou_tob", displayName: "贴心男友 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_wenrounantongzhuo_tob", displayName: "温柔男同桌 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_wenrounanyou_tob", displayName: "温柔男友 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_wenrouxuezhang_tob", displayName: "温柔学长 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_wenrunxuezhe_tob", displayName: "温润学者 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_wenshunshaonian_tob", displayName: "温顺少年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_guayanxiaoge_tob", displayName: "寡言小哥 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_xiaohouye_tob", displayName: "小侯爷 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_naiqixiaosheng_tob", displayName: "奶气小生 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_xiaosasuixing_tob", displayName: "潇洒随性 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_wenrouneilian_tob", displayName: "温柔内敛 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_xuebanantongzhuo_tob", displayName: "学霸男同桌 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_xuebatongzhuo_tob", displayName: "学霸同桌 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_yangguangyangyang_tob", displayName: "阳光洋洋 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_wennuanshaonian_tob", displayName: "温暖少年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_yiqishaonian_tob", displayName: "意气少年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_younidashu_tob", displayName: "油腻大叔 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_youmodaye_tob", displayName: "幽默大爷 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_youmoshushu_tob", displayName: "幽默叔叔 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_youroubangzhu_tob", displayName: "优柔帮主 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_yourougongzi_tob", displayName: "优柔公子 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_yuanqishaonian_tob", displayName: "元气少年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_zhangjianjunzi_tob", displayName: "仗剑君子 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_zhangjianxiake_tob", displayName: "仗剑侠客 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_zhengzhiqingnian_tob", displayName: "正直青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_zhishuaiqingnian_tob", displayName: "直率青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_zhongerqingnian_tob", displayName: "中二青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_zifuqingnian_tob", displayName: "自负青年 2.0", gender: "male", locale: "zh-CN" }, - { name: "ICL_uranus_zh_male_zixinqingnian_tob", displayName: "自信青年 2.0", gender: "male", locale: "zh-CN" }, + { + name: "zh_female_vv_uranus_bigtts", + displayName: "Vivi 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_xiaohe_uranus_bigtts", + displayName: "小何 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_m191_uranus_bigtts", + displayName: "云舟 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_taocheng_uranus_bigtts", + displayName: "小天 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_liufei_uranus_bigtts", + displayName: "刘飞 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_sophie_uranus_bigtts", + displayName: "魅力苏菲 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_qingxinnvsheng_uranus_bigtts", + displayName: "清新女声 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_cancan_uranus_bigtts", + displayName: "知性灿灿 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_sajiaoxuemei_uranus_bigtts", + displayName: "撒娇学妹 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_tianmeixiaoyuan_uranus_bigtts", + displayName: "甜美小源 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_tianmeitaozi_uranus_bigtts", + displayName: "甜美桃子 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_shuangkuaisisi_uranus_bigtts", + displayName: "爽快思思 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_peiqi_uranus_bigtts", + displayName: "佩奇猪 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_linjianvhai_uranus_bigtts", + displayName: "邻家女孩 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_shaonianzixin_uranus_bigtts", + displayName: "少年梓辛/ Brayan 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_sunwukong_uranus_bigtts", + displayName: "猴哥 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_yingyujiaoxue_uranus_bigtts", + displayName: "Tina老师 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_kefunvsheng_uranus_bigtts", + displayName: "暖阳女声 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_xiaoxue_uranus_bigtts", + displayName: "儿童绘本 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_dayi_uranus_bigtts", + displayName: "大壹 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_mizai_uranus_bigtts", + displayName: "黑猫侦探社 咪仔 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_jitangnv_uranus_bigtts", + displayName: "鸡汤女 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_meilinvyou_uranus_bigtts", + displayName: "魅力女友 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_liuchangnv_uranus_bigtts", + displayName: "流畅女声 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_ruyayichen_uranus_bigtts", + displayName: "儒雅逸辰 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "en_male_tim_uranus_bigtts", + displayName: "Tim", + gender: "male", + locale: "en-US", + }, + { + name: "en_female_dacey_uranus_bigtts", + displayName: "Dacey", + gender: "female", + locale: "en-US", + }, + { + name: "en_female_stokie_uranus_bigtts", + displayName: "Stokie", + gender: "female", + locale: "en-US", + }, + { + name: "zh_female_wenroumama_uranus_bigtts", + displayName: "温柔妈妈 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_jieshuoxiaoming_uranus_bigtts", + displayName: "解说小明 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_tvbnv_uranus_bigtts", + displayName: "TVB女声 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_yizhipiannan_uranus_bigtts", + displayName: "译制片男 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_qiaopinv_uranus_bigtts", + displayName: "俏皮女声 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_zhishuaiyingzi_uranus_bigtts", + displayName: "直率英子 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_linjiananhai_uranus_bigtts", + displayName: "邻家男孩 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_silang_uranus_bigtts", + displayName: "四郎 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_ruyaqingnian_uranus_bigtts", + displayName: "儒雅青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_qingcang_uranus_bigtts", + displayName: "擎苍 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_xionger_uranus_bigtts", + displayName: "熊二 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_yingtaowanzi_uranus_bigtts", + displayName: "樱桃丸子 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_wennuanahu_uranus_bigtts", + displayName: "温暖阿虎/ Alvin 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_naiqimengwa_uranus_bigtts", + displayName: "奶气萌娃 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_popo_uranus_bigtts", + displayName: "婆婆 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_gaolengyujie_uranus_bigtts", + displayName: "高冷御姐 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_aojiaobazong_uranus_bigtts", + displayName: "傲娇霸总 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_lanyinmianbao_uranus_bigtts", + displayName: "懒音绵宝 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_wenroushunv_uranus_bigtts", + displayName: "温柔淑女 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_gufengshaoyu_uranus_bigtts", + displayName: "古风少御 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_huolixiaoge_uranus_bigtts", + displayName: "活力小哥 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_baqiqingshu_uranus_bigtts", + displayName: "霸气青叔 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_xuanyijieshuo_uranus_bigtts", + displayName: "悬疑解说 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_mengyatou_uranus_bigtts", + displayName: "萌丫头/ Cutey 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_tiexinnvsheng_uranus_bigtts", + displayName: "贴心女声/ Candy 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_jitangmei_uranus_bigtts", + displayName: "鸡汤妹妹/", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_cixingjieshuonan_uranus_bigtts", + displayName: "磁性解说男 声/Morgan 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_liangsangmengzai_uranus_bigtts", + displayName: "亮嗓萌仔 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_kailangjiejie_uranus_bigtts", + displayName: "开朗姐姐 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_gaolengchenwen_uranus_bigtts", + displayName: "高冷沉稳 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_shenyeboke_uranus_bigtts", + displayName: "深夜播客 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_lubanqihao_uranus_bigtts", + displayName: "鲁班七号 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_jiaochuannv_uranus_bigtts", + displayName: "娇喘女声 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_linxiao_uranus_bigtts", + displayName: "邻萧 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_lingling_uranus_bigtts", + displayName: "玲玲姐姐 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_chunribu_uranus_bigtts", + displayName: "春日部姐姐 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_tangseng_uranus_bigtts", + displayName: "唐僧 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_zhuangzhou_uranus_bigtts", + displayName: "庄周 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_kailangdidi_uranus_bigtts", + displayName: "开朗弟弟 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_zhubajie_uranus_bigtts", + displayName: "猪八戒 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_ganmaodianyin_uranus_bigtts", + displayName: "感冒电音姐 姐 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_chanmeinv_uranus_bigtts", + displayName: "谄媚女声 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_nvleishen_uranus_bigtts", + displayName: "女雷神 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_qinqienv_uranus_bigtts", + displayName: "亲切女声 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_kuailexiaodong_uranus_bigtts", + displayName: "快乐小东 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_kailangxuezhang_uranus_bigtts", + displayName: "开朗学长 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_youyoujunzi_uranus_bigtts", + displayName: "悠悠君子 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_wenjingmaomao_uranus_bigtts", + displayName: "文静毛毛 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_zhixingnv_uranus_bigtts", + displayName: "知性女声 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_qingshuangnanda_uranus_bigtts", + displayName: "清爽男大 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_yuanboxiaoshu_uranus_bigtts", + displayName: "渊博小叔 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_male_yangguangqingnian_uranus_bigtts", + displayName: "阳光青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_qingchezizi_uranus_bigtts", + displayName: "清澈梓梓 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_tianmeiyueyue_uranus_bigtts", + displayName: "甜美悦悦 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_xinlingjitang_uranus_bigtts", + displayName: "心灵鸡汤 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_wenrouxiaoge_uranus_bigtts", + displayName: "温柔小哥 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_roumeinvyou_uranus_bigtts", + displayName: "柔美女友 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_dongfanghaoran_uranus_bigtts", + displayName: "东方浩然 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_wenrouxiaoya_uranus_bigtts", + displayName: "温柔小雅 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_tiancaitongsheng_uranus_bigtts", + displayName: "天才童声 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_wuzetian_uranus_bigtts", + displayName: "武则天 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_female_gujie_uranus_bigtts", + displayName: "顾姐 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "zh_male_guanggaojieshuo_uranus_bigtts", + displayName: "广告解说 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "zh_female_shaoergushi_uranus_bigtts", + displayName: "少儿故事 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_en_female_charlie_tob", + displayName: "Charlie 2.0", + gender: "female", + locale: "en-US", + }, + { + name: "ICL_uranus_en_male_ethan_tob", + displayName: "Ethan 2.0", + gender: "male", + locale: "en-US", + }, + { + name: "ICL_uranus_en_male_alastor_tob", + displayName: "Alastor 2.0", + gender: "male", + locale: "en-US", + }, + { + name: "ICL_uranus_en_male_chucky_tob", + displayName: "Chucky 2.0", + gender: "male", + locale: "en-US", + }, + { + name: "ICL_uranus_en_male_noah_tob", + displayName: "Noah 2.0", + gender: "male", + locale: "en-US", + }, + { + name: "ICL_uranus_en_male_jigsaw_tob", + displayName: "Jigsaw 2.0", + gender: "male", + locale: "en-US", + }, + { + name: "ICL_uranus_en_male_clown_man_tob", + displayName: "Clown Man 2.0", + gender: "male", + locale: "en-US", + }, + { + name: "ICL_uranus_en_male_cartoon_chef_tob", + displayName: "Cartoon Chef 2.0", + gender: "male", + locale: "en-US", + }, + { + name: "ICL_uranus_en_male_frosty_man_tob", + displayName: "Frosty Man 2.0", + gender: "male", + locale: "en-US", + }, + { + name: "ICL_uranus_en_male_the_grinch_tob", + displayName: "The Grinch 2.0", + gender: "male", + locale: "en-US", + }, + { + name: "ICL_uranus_en_male_kevin_mccallister_tob", + displayName: "Kevin McCallister 2.0", + gender: "male", + locale: "en-US", + }, + { + name: "ICL_uranus_en_male_michael_tob", + displayName: "Michael 2.0", + gender: "male", + locale: "en-US", + }, + { + name: "ICL_uranus_en_male_big_boogie_tob", + displayName: "Big Boogie 2.0", + gender: "male", + locale: "en-US", + }, + { + name: "ICL_uranus_en_male_xavier_tob", + displayName: "Xavier 2.0", + gender: "male", + locale: "en-US", + }, + { + name: "ICL_uranus_en_male_zayne_tob", + displayName: "Zayne 2.0", + gender: "male", + locale: "en-US", + }, + { + name: "ICL_uranus_zh_female_aojiaonvyou_tob", + displayName: "傲娇女友 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_aomanjiaosheng_tob", + displayName: "傲慢娇声 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_xiemeinvwang_tob", + displayName: "邪魅女王 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_bingjiaojiejie_tob", + displayName: "病娇姐姐 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_bingjiaomengmei_tob", + displayName: "病娇萌妹 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_bingruoshaonv_tob", + displayName: "病弱少女 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_chengshuwenrou_tob", + displayName: "成熟温柔 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_chengshujiejie_tob", + displayName: "成熟姐姐 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_chunzhenshaonv_tob", + displayName: "纯真少女 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_chunchenvsheng_tob", + displayName: "纯澈女生 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_wumeikeren_tob", + displayName: "妩媚可人 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_heainainai_tob", + displayName: "乖巧可儿 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_huopodiaoman_tob", + displayName: "活泼刁蛮 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_huoponvhai_tob", + displayName: "活泼女孩 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_jiaohannvwang_tob", + displayName: "娇憨女王 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_jiaoruoluoli_tob", + displayName: "娇弱萝莉 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_jiaxiaozi_tob", + displayName: "假小子 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_jinglingxiangdao_tob", + displayName: "精灵向导 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_kaixinxiaohong_tob", + displayName: "开朗婷婷 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_keainvsheng_tob", + displayName: "可爱女生 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_lingdongxinxin_tob", + displayName: "灵动欣欣 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_linjuayi_tob", + displayName: "邻居阿姨 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_tianmeijiaoqiao_tob", + displayName: "甜美娇俏 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_qinglenggaoya_tob", + displayName: "清冷高雅 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_lixingyuanzi_tob", + displayName: "理性圆子 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_xingganmeihuo_tob", + displayName: "性感魅惑 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_nuanxinqianqian_tob", + displayName: "暖心茜茜 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_nuanxinxuejie_tob", + displayName: "暖心学姐 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_qingtianmeimei_tob", + displayName: "清甜莓莓 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_qingtiantaotao_tob", + displayName: "清甜桃桃 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_qingxixiaoxue_tob", + displayName: "清晰小雪 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_qingxinshaonv_tob", + displayName: "倾心少女 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_ruanmengtangtang_tob", + displayName: "柔骨魂师 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_ruanmengtuanzi_tob", + displayName: "软萌团子 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_tianmeihuopo_tob", + displayName: "甜美活泼 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_tianmeixiaoju_tob", + displayName: "甜美小橘 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_tianmeixiaoyu_tob", + displayName: "甜美小雨 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_tiaopigongzhu_tob", + displayName: "调皮公主 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_tiexinnvyou_tob", + displayName: "贴心女友 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_wenrounvshen_tob", + displayName: "温柔女神 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_wenrouwenya_tob", + displayName: "温柔文雅 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_zhixinjiejie_tob", + displayName: "知心姐姐 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_wumeiyujie_tob", + displayName: "妩媚御姐 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_yuanqitianmei_tob", + displayName: "元气甜妹 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_xiemeiyujie_tob", + displayName: "邪魅御姐 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_xingganyujie_tob", + displayName: "性感御姐 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_xiuliqianqian_tob", + displayName: "秀丽倩倩 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_tiexinguimi_tob", + displayName: "贴心闺蜜 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_tiexinmeimei_tob", + displayName: "贴心妹妹 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_wenroubaiyueguang_tob", + displayName: "温柔白月光 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_chuliannvyou_tob", + displayName: "初恋女友 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_female_zhixingwenwan_tob", + displayName: "知性温婉 2.0", + gender: "female", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_aoqilingren_tob", + displayName: "傲气凌人 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_anrenqinzhu_tob", + displayName: "黯刃秦主 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_aojiaogongzi_tob", + displayName: "傲娇公子 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_aojiaojingying_tob", + displayName: "傲娇精英 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_aomanqingnian_tob", + displayName: "傲慢青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_aomanshaoye_tob", + displayName: "傲慢少爷 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_zhenbiandiyu_tob", + displayName: "枕边低语 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_badaoshaoye_tob", + displayName: "霸道少爷 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_badaozongcai_tob", + displayName: "霸道总裁 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_bingjiaobailian_tob", + displayName: "病娇白莲 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_bingjiaodidi_tob", + displayName: "病娇弟弟 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_bingjiaogege_tob", + displayName: "病娇哥哥 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_bingjiaonanyou_tob", + displayName: "病娇男友 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_bingjiaoshaonian_tob", + displayName: "病娇少年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_bingruogongzi_tob", + displayName: "病弱公子 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_bingruoshaonian_tob", + displayName: "病弱少年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_bujiqingnian_tob", + displayName: "不羁青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_chunhoudiyin_tob", + displayName: "醇厚低音 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_paoxiaoxiaoge_tob", + displayName: "咆哮小哥 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_yangyang_tob", + displayName: "炀炀 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_chanruoshaoye_tob", + displayName: "孱弱少爷 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_chengshuzongcai_tob", + displayName: "成熟总裁 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_chenwenmingzai_tob", + displayName: "沉稳明仔 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_qingyisugan_tob", + displayName: "清逸苏感 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_chunzhenxuedi_tob", + displayName: "纯真学弟 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_cixingnansang_tob", + displayName: "磁性男嗓 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_cujingnansheng_tob", + displayName: "醋精男生 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_cujingnanyou_tob", + displayName: "醋精男友 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_diyinchenyu_tob", + displayName: "低音沉郁 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_fengfashaonian_tob", + displayName: "风发少年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_ruyagongzi_tob", + displayName: "儒雅公子 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_fuheigongzi_tob", + displayName: "腹黑公子 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_ganjingshaonian_tob", + displayName: "干净少年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_gaolengzongcai_tob", + displayName: "高冷总裁 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_guaogongzi_tob", + displayName: "孤傲公子 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_guiyishenmi_tob", + displayName: "aogongzi_to b 诡异神秘 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_guzhibingjiao_tob", + displayName: "固执病娇 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_hanhoudunshi_tob", + displayName: "憨厚敦实 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_huoliqingnian_tob", + displayName: "活力青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_huoponanyou_tob", + displayName: "活泼男友 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_huoposhuanglang_tob", + displayName: "活泼爽朗 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_huzishushu_tob", + displayName: "胡子叔叔 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_jijiazhineng_tob", + displayName: "机甲智能 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_jingyingqingnian_tob", + displayName: "精英青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_junyigongzi_tob", + displayName: "俊逸公子 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_kailangqingkuai_tob", + displayName: "开朗轻快 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_kailangqingnian_tob", + displayName: "开朗青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_lanyincaohunshi_tob", + displayName: "蓝银草魂师 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_lengaozongcai_tob", + displayName: "冷傲总裁 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_lengdanshuli_tob", + displayName: "冷淡疏离 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_lengjungaozhi_tob", + displayName: "冷峻高智 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_lengjunshangsi_tob", + displayName: "冷峻上司 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_lengkugege_tob", + displayName: "冷酷哥哥 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_lenglianxiongzhang_tob", + displayName: "冷脸兄长 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_lenglianxueba_tob", + displayName: "冷脸学霸 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_lengmonanyou_tob", + displayName: "冷漠男友 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_lengmoxiongzhang_tob", + displayName: "冷漠兄长 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_lingyunqingnian_tob", + displayName: "凌云青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_qinglengjingui_tob", + displayName: "清冷矜贵 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_lvchaxiaoge_tob", + displayName: "绿茶小哥 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_mengdongqingnian_tob", + displayName: "懵懂青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_menyoupingxiaoge_tob", + displayName: "闷油瓶小哥 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_xiaozhangxiaoge_tob", + displayName: "嚣张小哥 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_nianrennanyou_tob", + displayName: "粘人男友 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_neiliancaijun_tob", + displayName: "内敛才俊 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_nuanxintitie_tob", + displayName: "暖心体贴 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_pianpiangongzi_tob", + displayName: "翩翩公子 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_chenwenyouya_tob", + displayName: "沉稳优雅 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_qingsexiaosheng_tob", + displayName: "青涩小生 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_qingseqingnian_tob", + displayName: "青涩青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_qingshuangshaonian_tob", + displayName: "清爽少年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_qingxinbobo_tob", + displayName: "清新波波 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_qinqieqingnian_tob", + displayName: "亲切青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_qinqiexiaozhuo_tob", + displayName: "亲切小卓 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_qinglangwenrun_tob", + displayName: "清朗温润 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_rexueshaonian_tob", + displayName: "热血少年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_ruyacaijun_tob", + displayName: "儒雅才俊 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_ruyajunzi_tob", + displayName: "儒雅君子 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_ruyazongcai_tob", + displayName: "儒雅总裁 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_sajiaonansheng_tob", + displayName: "撒娇男生 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_sajiaonanyou_tob", + displayName: "撒娇男友 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_sajiaonianren_tob", + displayName: "撒娇粘人 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_satuoqingnian_tob", + displayName: "洒脱青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_shaonianjiangjun_tob", + displayName: "少年将军 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_shenchenzongcai_tob", + displayName: "深沉总裁 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_jilingxiaohuo_tob", + displayName: "机灵小伙 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_shenmifashi_tob", + displayName: "神秘法师 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_shuaizhenxiaohuo_tob", + displayName: "率真小伙 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_shuanglangxiaoyang_tob", + displayName: "爽朗小阳 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_dichenqianquan_tob", + displayName: "低沉缱绻 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_siwenqingnian_tob", + displayName: "斯文青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_tianxinanyou_tob", + displayName: "甜系男友 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_tiexinnanyou_tob", + displayName: "贴心男友 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_wenrounantongzhuo_tob", + displayName: "温柔男同桌 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_wenrounanyou_tob", + displayName: "温柔男友 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_wenrouxuezhang_tob", + displayName: "温柔学长 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_wenrunxuezhe_tob", + displayName: "温润学者 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_wenshunshaonian_tob", + displayName: "温顺少年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_guayanxiaoge_tob", + displayName: "寡言小哥 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_xiaohouye_tob", + displayName: "小侯爷 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_naiqixiaosheng_tob", + displayName: "奶气小生 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_xiaosasuixing_tob", + displayName: "潇洒随性 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_wenrouneilian_tob", + displayName: "温柔内敛 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_xuebanantongzhuo_tob", + displayName: "学霸男同桌 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_xuebatongzhuo_tob", + displayName: "学霸同桌 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_yangguangyangyang_tob", + displayName: "阳光洋洋 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_wennuanshaonian_tob", + displayName: "温暖少年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_yiqishaonian_tob", + displayName: "意气少年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_younidashu_tob", + displayName: "油腻大叔 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_youmodaye_tob", + displayName: "幽默大爷 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_youmoshushu_tob", + displayName: "幽默叔叔 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_youroubangzhu_tob", + displayName: "优柔帮主 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_yourougongzi_tob", + displayName: "优柔公子 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_yuanqishaonian_tob", + displayName: "元气少年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_zhangjianjunzi_tob", + displayName: "仗剑君子 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_zhangjianxiake_tob", + displayName: "仗剑侠客 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_zhengzhiqingnian_tob", + displayName: "正直青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_zhishuaiqingnian_tob", + displayName: "直率青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_zhongerqingnian_tob", + displayName: "中二青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_zifuqingnian_tob", + displayName: "自负青年 2.0", + gender: "male", + locale: "zh-CN", + }, + { + name: "ICL_uranus_zh_male_zixinqingnian_tob", + displayName: "自信青年 2.0", + gender: "male", + locale: "zh-CN", + }, ]; const getTTSVoice = async (config) => { - return Promise.resolve(VOICES_2_0.map((voice) => ({ - name: voice.name, - gender: voice.gender, - locale: voice.locale, - displayName: `Volcengine TTS - ${voice.displayName} (${voice.locale})`, - plugin: "volcengine-tts-voice-plugin", - config: { - ...config, - voiceName: voice.name, - }, - }))); + return Promise.resolve( + VOICES_2_0.map((voice) => ({ + name: voice.name, + gender: voice.gender, + locale: voice.locale, + displayName: `Volcengine TTS - ${voice.displayName} (${voice.locale})`, + plugin: "volcengine-tts-voice-plugin", + config: { + ...config, + voiceName: voice.name, + }, + })) + ); }; module.exports = { getAudioPath, getTTSVoice }; diff --git a/src/utils/plugins/main/voice/zhipuTTSVoice.js b/src/utils/plugins/main/voice/zhipuTTSVoice.js index cb2d6046..3c56a614 100644 --- a/src/utils/plugins/main/voice/zhipuTTSVoice.js +++ b/src/utils/plugins/main/voice/zhipuTTSVoice.js @@ -2,98 +2,98 @@ const path = require("path"); const fs = require("fs"); const axios = require("axios"); const getAudioPath = async (text, speed, dirPath, config) => { - let audioName = new Date().getTime() + ".wav"; - if (!fs.existsSync(path.join(dirPath, "tts"))) { - fs.mkdirSync(path.join(dirPath, "tts")); - } - const audioData = await getTTSAudio(text, speed, config); - fs.writeFileSync(path.join(dirPath, "tts", audioName), audioData); - return path.join(dirPath, "tts", audioName); + let audioName = new Date().getTime() + ".wav"; + if (!fs.existsSync(path.join(dirPath, "tts"))) { + fs.mkdirSync(path.join(dirPath, "tts")); + } + const audioData = await getTTSAudio(text, speed, config); + fs.writeFileSync(path.join(dirPath, "tts", audioName), audioData); + return path.join(dirPath, "tts", audioName); }; const getTTSAudio = async (text, speed, config) => { - let apiKey = config.apiKey || ""; - let voiceName = config.voiceName || "tongtong"; - const body = { - model: "glm-tts", - input: text, - voice: voiceName, - response_format: "wav", - speed: speed || 1.0, - }; - const url = "https://open.bigmodel.cn/api/paas/v4/audio/speech"; - return new Promise((resolve, reject) => { - axios - .post(url, body, { - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", - }, - responseType: "arraybuffer", - }) - .then((response) => { - resolve(Buffer.from(response.data)); - }) - .catch((error) => { - reject(error); - }); - }); + let apiKey = config.apiKey || ""; + let voiceName = config.voiceName || "tongtong"; + const body = { + model: "glm-tts", + input: text, + voice: voiceName, + response_format: "wav", + speed: speed || 1.0, + }; + const url = "https://open.bigmodel.cn/api/paas/v4/audio/speech"; + return new Promise((resolve, reject) => { + axios + .post(url, body, { + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + responseType: "arraybuffer", + }) + .then((response) => { + resolve(Buffer.from(response.data)); + }) + .catch((error) => { + reject(error); + }); + }); }; const getTTSVoice = async (config) => { - const voices = [ - { - name: "tongtong", - gender: "female", - locale: "zh-CN", - displayName: "彤彤 tongtong (zh-CN) - Female", - }, - { - name: "chuichui", - gender: "male", - locale: "zh-CN", - displayName: "锤锤 chuichui (zh-CN) - Male", - }, - { - name: "xiaochen", - gender: "male", - locale: "zh-CN", - displayName: "小陈 xiaochen (zh-CN) - Male", - }, - { - name: "jam", - gender: "male", - locale: "zh-CN", - displayName: "jam (zh-CN) - Male", - }, - { - name: "kazi", - gender: "female", - locale: "zh-CN", - displayName: "kazi (zh-CN) - Female", - }, - { - name: "douji", - gender: "male", - locale: "zh-CN", - displayName: "douji (zh-CN) - Male", - }, - { - name: "luodo", - gender: "male", - locale: "zh-CN", - displayName: "luodo (zh-CN) - Male", - }, - ]; - return voices.map((voice) => ({ - name: voice.name, - gender: voice.gender, - locale: voice.locale, - displayName: `Zhipu TTS - ${voice.displayName}`, - plugin: "zhipu-tts-voice-plugin", - config: { - ...config, - voiceName: voice.name, - }, - })); + const voices = [ + { + name: "tongtong", + gender: "female", + locale: "zh-CN", + displayName: "彤彤 tongtong (zh-CN) - Female", + }, + { + name: "chuichui", + gender: "male", + locale: "zh-CN", + displayName: "锤锤 chuichui (zh-CN) - Male", + }, + { + name: "xiaochen", + gender: "male", + locale: "zh-CN", + displayName: "小陈 xiaochen (zh-CN) - Male", + }, + { + name: "jam", + gender: "male", + locale: "zh-CN", + displayName: "jam (zh-CN) - Male", + }, + { + name: "kazi", + gender: "female", + locale: "zh-CN", + displayName: "kazi (zh-CN) - Female", + }, + { + name: "douji", + gender: "male", + locale: "zh-CN", + displayName: "douji (zh-CN) - Male", + }, + { + name: "luodo", + gender: "male", + locale: "zh-CN", + displayName: "luodo (zh-CN) - Male", + }, + ]; + return voices.map((voice) => ({ + name: voice.name, + gender: voice.gender, + locale: voice.locale, + displayName: `Zhipu TTS - ${voice.displayName}`, + plugin: "zhipu-tts-voice-plugin", + config: { + ...config, + voiceName: voice.name, + }, + })); }; module.exports = { getAudioPath, getTTSVoice };