Created
June 12, 2021 04:00
-
-
Save ChaseIngebritson/92d8e87d4da2ada6701fc53595dd0678 to your computer and use it in GitHub Desktop.
Convert text to speech using Google's TTS library
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Provide any text input and convert it to speech using various international speech patterns provided by Google | |
Dependencies: | |
* discord.js | |
* discord.js-commando | |
* lodash | |
* @google-cloud/text-to-speech |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import textToSpeech from '@google-cloud/text-to-speech' | |
async function getVoices () { | |
const client = new textToSpeech.TextToSpeechClient() | |
const response = await client.listVoices() | |
const voices = response[0].voices | |
.filter(voice => !voice.name.includes('Wavenet')) | |
return voices | |
} | |
export default { getVoices } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { countries, languages } from 'countries-list' | |
const MISSING_LANGUAGES = { | |
gu: { name: 'Gujarati' }, | |
kn: { name: 'Kannada' }, | |
ml: { name: 'Malayalam' }, | |
te: { name: 'Telugu' }, | |
fil: { name: 'Filipino' }, | |
cmn: { name: 'Mandarin Chinese' }, | |
yue: { name: 'Yue Chinese' } | |
} | |
export default function normalizeBcp (code) { | |
const [ codeLang, codeCoun ] = code.split('-') | |
let country = countries[codeCoun] | |
let language = languages[codeLang] | |
if (!language) language = MISSING_LANGUAGES[codeLang] | |
let output = '' | |
if (language || country) { | |
if (language) output += language.name | |
if (country) output += ` (${country.name})` | |
return output | |
} | |
return code | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { MessageEmbed } from 'discord.js' | |
import { Command } from 'discord.js-commando' | |
import { uniqBy } from 'lodash' | |
import tts from '../../utils/tts' | |
import GoogleTtsService from '../../services/getVoices' | |
import normalizeBcp from '../../utils/normalizeBcp' | |
class SayCommand extends Command { | |
constructor (client) { | |
super(client, { | |
name: 'say', | |
group: 'misc', | |
memberName: 'say', | |
description: 'Convert text to speech', | |
args: [{ | |
key: 'text', | |
prompt: 'The text you want to convert to speech', | |
type: 'string' | |
}] | |
}) | |
} | |
async run (message, { text }) { | |
const MAX_LENGTH = 1000 | |
const SSML_GENDERS = ['MALE', 'FEMALE', 'NEUTRAL'] | |
const BLANK_FIELD = { name: '\u200b', value: '\u200b' } | |
// Make sure that the length isn't too long | |
if (text.length > MAX_LENGTH) { | |
return message.reply(`Max length cannot surpass ${MAX_LENGTH} characters`) | |
} | |
// Get a list of all available voices | |
const voices = await GoogleTtsService.getVoices() | |
let codes = voices.map(voice => voice.languageCodes).flat() | |
let uniqueCodes = uniqBy(codes) | |
// Set the default values | |
let voice = 'en-US' | |
let gender = SSML_GENDERS[Math.floor(Math.random() * SSML_GENDERS.length)] | |
// Check to see if a list of names is being asked for | |
if (text === 'help') { | |
const embed = new MessageEmbed() | |
.setTitle('Say Help') | |
.setColor(0xda963d) | |
.setDescription('How to use the say command.') | |
embed.addFields( | |
{ name: 'Supported genders', value: SSML_GENDERS.join(', ') }, | |
{ name: 'Supported regions', value: uniqueCodes.join(', ') }, | |
BLANK_FIELD, | |
{ name: 'Default region', value: voice, inline: true }, | |
{ name: 'Default gender', value: 'Random', inline: true }, | |
BLANK_FIELD, | |
{ name: 'Example', value: `${process.env.COMMAND_PREFIX}say Hello!`, inline: true }, | |
{ name: 'Italian example', value: `${process.env.COMMAND_PREFIX}say it-IT Hello!`, inline: true }, | |
{ name: 'Female example', value: `${process.env.COMMAND_PREFIX}say female Hello!`, inline: true }, | |
{ name: 'Neutral Japanese example', value: `${process.env.COMMAND_PREFIX}say ja-JP female Hello!`, inline: true } | |
) | |
return message.channel.send(embed) | |
} | |
// Check to see if a list of BCP-47 codes is being asked for | |
if (text === 'codes') { | |
const response = uniqueCodes.map(code => { | |
return `${normalizeBcp(code)}: **${code}**` | |
}).sort() | |
message.reply(response) | |
return | |
} | |
const words = text.split(' ') | |
let textToPrint = text | |
// Determine which parameters are which | |
if (uniqueCodes.includes(words[0])) { // If the first item is a code | |
textToPrint = words.slice(1).join(' ') | |
voice = words[0] | |
if (SSML_GENDERS.includes(words[1].toUpperCase())) { // If the second item is a gender | |
gender = words[1].toUpperCase() | |
textToPrint = words.slice(2).join(' ') | |
} | |
} else if (SSML_GENDERS.includes(words[0].toUpperCase())) { // If the first item is a gender | |
gender = words[0].toUpperCase() | |
textToPrint = words.slice(1).join(' ') | |
} | |
tts(message, textToPrint, voice, gender) | |
} | |
} | |
export default SayCommand |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import fs from 'fs' | |
import util from 'util' | |
import textToSpeech from '@google-cloud/text-to-speech' | |
export default async function tts (msg, text, voice, gender) { | |
if (!msg.member.voice.channel) { | |
msg.reply('You need to join a voice channel first!') | |
return | |
} | |
// Creates a client | |
const client = new textToSpeech.TextToSpeechClient() | |
// Construct the request | |
const request = { | |
input: { text: text }, | |
// Select the language and SSML voice gender (optional) | |
voice: { languageCode: voice, ssmlGender: gender }, | |
// select the type of audio encoding | |
audioConfig: { audioEncoding: 'MP3' } | |
} | |
// Performs the text-to-speech request | |
const [response] = await client.synthesizeSpeech(request) | |
// Create the temporary directory if it doesn't exist | |
await fs.promises.mkdir('./tmp/tts', { recursive: true }) | |
// Write the binary audio content to a local file | |
const writeFile = util.promisify(fs.writeFile) | |
await writeFile('./tmp/tts/tmp.mp3', response.audioContent, 'binary') | |
const connection = await msg.member.voice.channel.join() | |
const dispatcher = connection.play('./tmp/tts/tmp.mp3') | |
dispatcher.on('finish', () => { | |
msg.member.voice.channel.leave() | |
fs.unlink('./tmp/tts/tmp.mp3', () => {}) | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment