Bot de TTS no Discord: adicione ou crie o seu

Rene, Kabir Goel 
Bot de TTS no Discord: adicione ou crie o seu

Um bot de TTS para Discord entra em um canal de voz e lê texto em voz alta, para que pessoas que não podem falar no microfone, ou que simplesmente digitam mais rápido do que falam, continuem sendo ouvidas. Há duas maneiras de conseguir um: convidar um bot hospedado que outra pessoa executa, ou criar o seu. Este guia cria um com discord.js e Cartesia em cerca de 80 linhas de JavaScript e, em seguida, mostra o que verificar se você preferir convidar o de outra pessoa.

Se você só precisa do texto em fala nativo do Discord, esse é outro recurso: ele lê mensagens em canais de texto com a voz do sistema, sem nenhum bot envolvido. Como usar o texto em fala no Discord explica isso. O comando nativo não fala em canais de voz nem troca de voz, que é exatamente a lacuna que um bot preenche.

O que você vai construir

Um bot com dois comandos de barra: /tts entra no seu canal de voz e lê seu texto com uma voz da Cartesia, e /stop o desconecta. Cada arquivo abaixo foi executado com os SDKs atuais em outubro de 2026: a requisição à API da Cartesia, a conversão de áudio, a codificação Opus e o registro dos comandos foram todos executados e verificados no Node 24 com discord.js 14.27, @discordjs/voice 0.19.2 e @cartesia/cartesia-js 4.2.0.

Pré-requisitos

  1. Node.js 20 ou superior.
  2. Uma conta do Discord e um servidor em que você tenha a permissão Manage Server para adicionar um bot.
  3. Uma chave de API da Cartesia do painel. O plano gratuito serve.
  4. Um ID de voz da Voice Library. Abra uma voz de que você goste e copie o ID.

1. Crie o aplicativo do Discord

  1. Abra o Discord Developer Portal e escolha New Application. O nome digitado é o que as pessoas verão quando o bot entrar.
  2. Na página Bot, escolha Reset Token e copie o token. O token é a senha do bot: guarde-o em uma variável de ambiente, fora do código e fora do git.
  3. Este bot não precisa de intents privilegiadas. Comandos de barra e estado de voz funcionam sem as chaves Presence, Server Members ou Message Content, então deixe-as desligadas.
  4. Convide o bot para o seu servidor. Em OAuth2, abra o URL Generator, selecione os escopos bot e applications.commands e depois as permissões Connect e Speak. Abra a URL gerada e escolha seu servidor.

2. Instale o projeto

mkdir discord-tts-bot && cd discord-tts-bot
npm init -y
npm pkg set type=module
npm install discord.js @discordjs/voice @discordjs/opus libsodium-wrappers @cartesia/cartesia-js

O que cada pacote faz: discord.js fala com a API do Discord. @discordjs/voice gerencia a conexão de voz. @discordjs/opus transforma o áudio bruto nos quadros Opus que o Discord transmite. libsodium-wrappers criptografa os pacotes de voz. @cartesia/cartesia-js é o SDK da Cartesia.

Não há ffmpeg na lista. O bot produz exatamente o formato PCM que o Discord espera, então o @discordjs/opus cuida da codificação sozinho. O ffmpeg só entra em cena quando você entrega ao @discordjs/voice um formato que ele precisa transcodificar, como um arquivo MP3.

3. Registre os comandos de barra

O Discord precisa conhecer /tts e /stop antes que alguém possa digitá-los. Salve isto como deploy-commands.js:

// Registers the bot's slash commands with Discord.
// Run once after creating the bot, and again whenever the commands change:
//   node deploy-commands.js
import { REST, Routes, SlashCommandBuilder } from "discord.js";

const commands = [
	new SlashCommandBuilder()
		.setName("tts")
		.setDescription("Read text aloud in your voice channel")
		.addStringOption((option) =>
			option
				.setName("text")
				.setDescription("What the bot should say")
				.setRequired(true)
				.setMaxLength(1000),
		),
	new SlashCommandBuilder()
		.setName("stop")
		.setDescription("Stop playback and leave the voice channel"),
].map((command) => command.toJSON());

const rest = new REST().setToken(process.env.DISCORD_TOKEN);

const registration = await rest.put(
	Routes.applicationGuildCommands(
		process.env.DISCORD_CLIENT_ID,
		process.env.DISCORD_GUILD_ID,
	),
	{ body: commands },
);
console.log(`Registered ${registration.length} slash commands.`);

Execute-o uma vez com as credenciais do bot no ambiente:

export DISCORD_TOKEN="your-bot-token"
export DISCORD_CLIENT_ID="your-application-id"
export DISCORD_GUILD_ID="your-server-id"
node deploy-commands.js

O script registra comandos de guilda, que atualizam na hora, então os comandos funcionam no seu servidor assim que ele termina. DISCORD_CLIENT_ID é o Application ID na página General Information do app, e DISCORD_GUILD_ID é o ID do seu servidor; ative o Modo de desenvolvedor nas configurações do Discord e clique com o botão direito no servidor para copiá-lo.

4. Escreva o bot

Salve isto como index.js:

// A Discord TTS bot: /tts reads a message aloud in the caller's voice channel.
// Needs DISCORD_TOKEN, CARTESIA_API_KEY and CARTESIA_VOICE_ID in the environment.
import {
	AudioPlayerStatus,
	StreamType,
	createAudioPlayer,
	createAudioResource,
	getVoiceConnection,
	joinVoiceChannel,
} from "@discordjs/voice";
import Cartesia from "@cartesia/cartesia-js";
import { Client, Events, GatewayIntentBits, MessageFlags } from "discord.js";
import { Readable, Transform, pipeline } from "node:stream";
import { pathToFileURL } from "node:url";

const cartesia = new Cartesia({ apiKey: process.env.CARTESIA_API_KEY });
const VOICE_ID = process.env.CARTESIA_VOICE_ID; // pick one at play.cartesia.ai/voices

// Sonic's raw output is mono; Discord voice expects stereo, so duplicate each
// 16-bit sample into both channels.
class MonoToStereo extends Transform {
	#pending = null;

	_transform(chunk, _encoding, callback) {
		let buffer = this.#pending ? Buffer.concat([this.#pending, chunk]) : chunk;
		if (buffer.length % 2 === 1) {
			this.#pending = buffer.subarray(buffer.length - 1);
			buffer = buffer.subarray(0, buffer.length - 1);
		} else {
			this.#pending = null;
		}
		const stereo = Buffer.allocUnsafe(buffer.length * 2);
		for (let i = 0, j = 0; i < buffer.length; i += 2, j += 4) {
			stereo[j] = buffer[i];
			stereo[j + 1] = buffer[i + 1];
			stereo[j + 2] = buffer[i];
			stereo[j + 3] = buffer[i + 1];
		}
		this.push(stereo);
		callback();
	}

	_flush(callback) {
		callback();
	}
}

// Calls Sonic and returns a stream of stereo 48 kHz PCM, ready for Discord.
async function synthesize(text) {
	const response = await cartesia.tts.generate({
		model_id: "sonic-3.6",
		transcript: text,
		voice: { id: VOICE_ID },
		language: "en",
		output_format: {
			container: "raw",
			encoding: "pcm_s16le",
			sample_rate: 48000,
		},
	});
	// pipeline, not .pipe: a mid-stream Cartesia error reaches the destination
	// stream (and with it the player's "error" listener) instead of crashing.
	return pipeline(
		Readable.fromWeb(response.body),
		new MonoToStereo(),
		() => {},
	);
}

// One queue per guild, so a second /tts waits for the first to finish.
const queues = new Map();

function getQueue(guildId, channel) {
	let queue = queues.get(guildId);
	if (!queue) {
		const connection = joinVoiceChannel({
			channelId: channel.id,
			guildId,
			adapterCreator: channel.guild.voiceAdapterCreator,
			selfDeaf: false,
		});
		const player = createAudioPlayer();
		connection.subscribe(player);
		queue = {
			connection,
			player,
			lines: [],
			busy: false,
			channelId: channel.id,
		};
		queues.set(guildId, queue);
		player.on(AudioPlayerStatus.Idle, () => {
			void playNext(guildId);
		});
		player.on("error", (error) => {
			console.error(error);
			void playNext(guildId);
		});
	}
	return queue;
}

async function playNext(guildId) {
	const queue = queues.get(guildId);
	if (!queue) return;
	const text = queue.lines.shift();
	if (text === undefined) {
		queue.busy = false;
		return;
	}
	queue.busy = true;
	try {
		const audio = await synthesize(text);
		// /stop may have torn down this queue while Sonic was working.
		if (queues.get(guildId) !== queue) return;
		queue.player.play(
			createAudioResource(audio, { inputType: StreamType.Raw }),
		);
	} catch (error) {
		console.error("Cartesia TTS failed:", error);
		void playNext(guildId);
	}
}

const client = new Client({
	intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
});

client.once(Events.ClientReady, (readyClient) => {
	console.log(`Logged in as ${readyClient.user.tag}`);
});

client.on(Events.InteractionCreate, async (interaction) => {
	if (!interaction.isChatInputCommand()) return;
	if (interaction.commandName === "tts") {
		const channel = interaction.member?.voice?.channel;
		if (!channel) {
			await interaction.reply({
				content: "Join a voice channel first, then run /tts.",
				flags: MessageFlags.Ephemeral,
			});
			return;
		}
		const queue = getQueue(interaction.guildId, channel);
		queue.lines.push(interaction.options.getString("text", true));
		await interaction.reply("On it.");
		if (!queue.busy) void playNext(interaction.guildId);
	}
	if (interaction.commandName === "stop") {
		const queue = queues.get(interaction.guildId);
		if (queue && interaction.member?.voice?.channelId !== queue.channelId) {
			await interaction.reply({
				content: "Join the bot's voice channel to stop it.",
				flags: MessageFlags.Ephemeral,
			});
			return;
		}
		queue?.player.stop();
		getVoiceConnection(interaction.guildId)?.destroy();
		queues.delete(interaction.guildId);
		await interaction.reply("Stopped and left the voice channel.");
	}
});

// Exported for tests; the bot only logs in when run directly.
export { MonoToStereo, synthesize };

if (
	process.argv[1] &&
	import.meta.url === pathToFileURL(process.argv[1]).href
) {
	client.login(process.env.DISCORD_TOKEN);
}

Três pontos desse arquivo merecem um olhar mais atento:

O formato de áudio. A voz do Discord é Opus estéreo a 48 kHz. A chamada tts.generate pede ao Sonic container: "raw", encoding: "pcm_s16le" e sample_rate: 48000, que é o formato PCM nativo do Discord, com um canal a menos. O MonoToStereo duplica cada amostra de 16 bits nos dois canais, e o @discordjs/voice codifica o resultado em Opus com o @discordjs/opus. Pedir 48 kHz desde o início evita qualquer reamostragem no caminho.

A fila. Duas pessoas executando /tts ao mesmo tempo sobreporiam o áudio, então cada servidor tem uma fila. O playNext tira uma linha, a sintetiza e a reproduz; o AudioPlayerStatus.Idle dispara quando um clipe termina e puxa a linha seguinte. O /stop só funciona de dentro do canal do bot: ele para o player e destrói a conexão. Uma linha que falha no meio do stream chega ao listener de erro do player e é pulada, e uma fila substituída enquanto o Sonic ainda trabalha é descartada em vez de tocar por cima da próxima.

Para onde vai o tempo. O tempo até o primeiro áudio do Sonic é inferior a 90 ms, e o synthesize devolve um stream, então a reprodução começa nos primeiros pedaços em vez de esperar o clipe inteiro. Para uma frase ou duas, o atraso entre o comando e a voz é basicamente a ida e volta na rede.

5. Execute

export DISCORD_TOKEN="your-bot-token"
export CARTESIA_API_KEY="your-cartesia-key"
export CARTESIA_VOICE_ID="the-voice-id-you-copied"
node index.js

Entre em um canal de voz do seu servidor e digite /tts seguido do seu texto. O bot entra, lê e espera o próximo comando. Pare-o com Ctrl+C. Para um bot que continue online quando você fechar o laptop, execute-o em um host pequeno e sempre ativo; qualquer coisa que mantenha um processo Node.js vivo serve.

Se você preferir convidar um bot hospedado

Existem bots de TTS públicos que você pode convidar sem executar nada. Julgue-os por três coisas: como as vozes soam, quais permissões o convite pede (um bot de TTS precisa de Connect e Speak, e pouco mais) e quanto custam além do nível gratuito. A contrapartida é o controle. Um bot hospedado lê o texto do seu servidor na infraestrutura de outra pessoa, e seus comandos e vozes são os que o autor escolheu. Se você crescer além dele, as 80 linhas acima são a saída.

Leve adiante

  • Uma voz por pessoa: mantenha um mapa de ID de usuário para ID de voz, para que as mensagens de cada membro sejam lidas com a própria voz.
  • Outros idiomas: o Sonic 3.6 suporta 44 idiomas com um único modelo. Passe um código de language e uma voz correspondentes.
  • Leia um canal automaticamente: escute messageCreate em um canal designado em vez de um comando de barra. Isso exige a intent privilegiada Message Content, ativada na página Bot do Developer Portal.
  • Um bot que responde: este lê o que você digita, não conversa. Para um agente de voz que escuta, pensa e responde, veja Cartesia Managed Agents.

Documentação relacionada

Perguntas frequentes

O que é um bot de TTS para Discord?

Um bot de TTS para Discord é uma conta de bot que entra em um canal de voz e lê o texto do chat em voz alta. O comando /tts nativo do Discord lê mensagens em canais de texto com a voz do sistema; um bot pode falar em canais de voz e usar qualquer voz que seu código suporte, incluindo vozes clonadas personalizadas.

Como adiciono um bot de TTS ao meu servidor do Discord?

Convidando um bot hospedado pelo link de convite, ou criando e hospedando o seu. Para um bot hospedado, verifique se ele pede apenas as permissões Connect e Speak. Para o seu próprio bot, os passos são criar um aplicativo no Discord Developer Portal, convidá-lo com os escopos bot e applications.commands e executar o código deste guia.

Preciso hospedar o bot eu mesmo?

Se você o criar, sim. O Discord entrega os comandos ao seu código e reproduz o áudio que seu código devolve, mas o código precisa rodar em algum lugar. Um laptop serve para testar. Para um bot sempre online, execute-o em um host pequeno e sempre ativo; qualquer provedor que mantenha um processo Node.js vivo funciona.

O bot pode ler automaticamente todas as mensagens de um canal?

Sim. Escute o evento messageCreate em um canal designado em vez de, ou junto com, o comando /tts. Ler o conteúdo das mensagens exige a intent privilegiada Message Content, que você ativa na página Bot do Discord Developer Portal.

Quais vozes um bot de TTS para Discord pode usar?

Qualquer voz da Voice Library da Cartesia, incluindo uma voz clonada a partir de uma amostra curta. Copie o ID da voz para a variável de ambiente CARTESIA_VOICE_ID e reinicie o bot.

Um bot de TTS para Discord é gratuito?

O Discord não cobra nada pelos bots. O plano gratuito da Cartesia inclui 20.000 créditos por mês, o que cobre o uso normal de um servidor típico. Uso mais intenso é cobrado por crédito nos planos pagos.