Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save ChaseIngebritson/92d8e87d4da2ada6701fc53595dd0678 to your computer and use it in GitHub Desktop.
Save ChaseIngebritson/92d8e87d4da2ada6701fc53595dd0678 to your computer and use it in GitHub Desktop.
Convert text to speech using Google's TTS library
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
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 }
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
}
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
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