Bot de TTS para Discord: añade uno o crea el tuyo

Rene, Kabir Goel 
Bot de TTS para Discord: añade uno o crea el tuyo

Un bot de TTS para Discord entra en un canal de voz y lee texto en voz alta, para que las personas que no pueden hablar por micro, o que simplemente escriben más rápido de lo que hablan, sigan haciéndose oír. Hay dos formas de conseguir uno: invitar a un bot alojado que ejecuta otra persona, o crear el tuyo propio. Esta guía crea uno con discord.js y Cartesia en unas 80 líneas de JavaScript, y después repasa qué comprobar si prefieres invitar al de otra persona.

Si solo necesitas el texto a voz integrado de Discord, esa es otra función: lee mensajes en canales de texto con la voz del sistema y no interviene ningún bot. Cómo usar el texto a voz en Discord lo explica. El comando integrado no puede hablar en canales de voz ni cambiar de voz, que es justo el hueco que cubre un bot.

Qué vas a construir

Un bot con dos comandos de barra: /tts entra en tu canal de voz y lee tu texto con una voz de Cartesia, y /stop lo desconecta. Cada archivo de abajo se ejecutó con los SDK actuales en octubre de 2026: la petición a la API de Cartesia, la conversión de audio, la codificación a Opus y el registro de comandos se ejecutaron y comprobaron en Node 24 con discord.js 14.27, @discordjs/voice 0.19.2 y @cartesia/cartesia-js 4.2.0.

Requisitos previos

  1. Node.js 20 o posterior.
  2. Una cuenta de Discord y un servidor donde tengas el permiso Manage Server para añadir un bot.
  3. Una clave de API de Cartesia del dashboard. El plan gratuito sirve.
  4. Un ID de voz de la Biblioteca de voces. Abre una voz que te guste y copia su ID.

1. Crea la aplicación de Discord

  1. Abre el Discord Developer Portal y elige New Application. El nombre que pongas es el que verá la gente cuando el bot entre.
  2. En la página Bot, elige Reset Token y copia el token. El token es la contraseña del bot: guárdalo en una variable de entorno, fuera del código y fuera de git.
  3. Este bot no necesita intents privilegiados. Los comandos de barra y el estado de voz funcionan sin los conmutadores Presence, Server Members o Message Content, así que déjalos apagados.
  4. Invita al bot a tu servidor. En OAuth2, abre el URL Generator, selecciona los alcances bot y applications.commands y después los permisos Connect y Speak. Abre la URL generada y elige tu servidor.

2. Instala el proyecto

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

Qué hace cada paquete: discord.js habla con la API de Discord. @discordjs/voice gestiona la conexión de voz. @discordjs/opus convierte el audio en bruto en las tramas Opus que transmite Discord. libsodium-wrappers cifra los paquetes de voz. @cartesia/cartesia-js es el SDK de Cartesia.

No hay ffmpeg en la lista. El bot produce exactamente el formato PCM que Discord espera, así que @discordjs/opus se encarga de la codificación por sí solo. ffmpeg solo aparece cuando le das a @discordjs/voice un formato que tiene que transcodificar, como un archivo MP3.

3. Registra los comandos de barra

Discord tiene que conocer /tts y /stop antes de que alguien pueda escribirlos. Guarda esto 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.`);

Ejecútalo una vez con las credenciales del bot en el entorno:

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

El script registra comandos de servidor (guild), que se actualizan al momento, así que los comandos funcionan en tu servidor en cuanto termina. DISCORD_CLIENT_ID es el Application ID de la página General Information de la app, y DISCORD_GUILD_ID es el ID de tu servidor; activa el Developer Mode en los ajustes de Discord y haz clic derecho sobre el servidor para copiarlo.

4. Escribe el bot

Guarda esto 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);
}

Tres cosas de ese archivo merecen una mirada más de cerca:

El formato de audio. La voz de Discord es Opus estéreo a 48 kHz. La llamada tts.generate pide a Sonic container: "raw", encoding: "pcm_s16le" y sample_rate: 48000, que es el formato PCM propio de Discord, con un canal de menos. MonoToStereo duplica cada muestra de 16 bits en ambos canales, y @discordjs/voice codifica el resultado a Opus con @discordjs/opus. Pedir 48 kHz desde el principio evita cualquier remuestreo por el camino.

La cola. Dos personas ejecutando /tts a la vez solaparían el audio, así que cada servidor tiene una cola. playNext saca una línea, la sintetiza y la reproduce; AudioPlayerStatus.Idle salta cuando un clip termina y toma la siguiente línea. /stop solo funciona desde dentro del canal del bot: detiene el reproductor y destruye la conexión. Una línea que falla a mitad del stream llega al listener de error del reproductor y se salta, y una cola reemplazada mientras Sonic aún trabaja se descarta en lugar de reproducirse sobre la siguiente.

Dónde se va el tiempo. El tiempo hasta el primer audio de Sonic es inferior a 90 ms, y synthesize devuelve un stream, así que la reproducción empieza con los primeros fragmentos en lugar de esperar al clip completo. Para una frase o dos, el retraso entre el comando y la voz es sobre todo el viaje de ida y vuelta por la red.

5. Ejecútalo

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

Entra en un canal de voz de tu servidor y escribe /tts seguido de tu texto. El bot entra, lee y espera al siguiente comando. Deténlo con Ctrl+C. Para un bot que siga en línea cuando cierres el portátil, ejecútalo en un host pequeño y siempre activo; cualquier cosa que mantenga vivo un proceso de Node.js vale.

Si prefieres invitar a un bot alojado

Existen bots de TTS públicos que puedes invitar sin ejecutar nada. Júzgalos por tres cosas: cómo suenan las voces, qué permisos pide la invitación (un bot de TTS necesita Connect y Speak, y poco más) y cuánto cuestan pasado el nivel gratuito. La contrapartida es el control. Un bot alojado lee el texto de tu servidor en la infraestructura de otra persona, y sus comandos y voces son los que eligió su autor. Si se te queda pequeño, las 80 líneas de arriba son la salida.

Llévalo más lejos

  • Una voz por persona: guarda un mapa de ID de usuario a ID de voz, para que los mensajes de cada miembro se lean con su propia voz.
  • Otros idiomas: Sonic 3.6 admite 44 idiomas desde un solo modelo. Pasa un código de language y una voz acordes.
  • Lee un canal automáticamente: escucha messageCreate en un canal designado en lugar de un comando de barra. Eso necesita el intent privilegiado Message Content, que se activa en la página Bot del Developer Portal.
  • Un bot que responde: este lee lo que escribes, no conversa. Para un agente de voz que escucha, piensa y responde, consulta Cartesia Managed Agents.

Documentación relacionada

Preguntas frecuentes

¿Qué es un bot de TTS para Discord?

Un bot de TTS para Discord es una cuenta de bot que entra en un canal de voz y lee en voz alta el texto del chat. El comando /tts integrado de Discord lee mensajes en canales de texto con la voz del sistema; un bot puede hablar en canales de voz y usar cualquier voz que admita su código, incluidas voces clonadas personalizadas.

¿Cómo añado un bot de TTS a mi servidor de Discord?

Invitando a un bot alojado mediante su enlace de invitación, o creando y alojando el tuyo. Para un bot alojado, comprueba que solo pida los permisos Connect y Speak. Para tu propio bot, los pasos son crear una aplicación en el Discord Developer Portal, invitarla con los permisos bot y applications.commands, y ejecutar el código de esta guía.

¿Tengo que alojar el bot yo mismo?

Si lo creas tú, sí. Discord entrega los comandos a tu código y reproduce el audio que tu código devuelve, pero el código tiene que ejecutarse en algún sitio. Un portátil sirve para probar. Para un bot que siga en línea, ejecútalo en un host pequeño y siempre activo; cualquier proveedor que mantenga vivo un proceso de Node.js vale.

¿Puede el bot leer automáticamente todos los mensajes de un canal?

Sí. Escucha el evento messageCreate en un canal designado en lugar de, o junto con, el comando /tts. Leer el contenido de los mensajes requiere el intent privilegiado Message Content, que se activa en la página Bot del Discord Developer Portal.

¿Qué voces puede usar un bot de TTS para Discord?

Cualquier voz de la Biblioteca de voces de Cartesia, incluida una voz que clones a partir de una muestra corta. Copia el ID de la voz en la variable de entorno CARTESIA_VOICE_ID y reinicia el bot.

¿Es gratis un bot de TTS para Discord?

Discord no cobra nada por los bots. El plan gratuito de Cartesia incluye 20.000 créditos al mes, suficiente para el uso habitual de un servidor típico. Un uso más intensivo se factura por crédito en los planes de pago.