Bot TTS per Discord: aggiungine uno o crea il tuo

Rene, Kabir Goel 
Bot TTS per Discord: aggiungine uno o crea il tuo

Un bot TTS per Discord entra in un canale vocale e legge il testo ad alta voce, così chi non può parlare al microfono, o semplicemente scrive più velocemente di quanto parli, si fa comunque sentire. Ci sono due modi per averne uno: invitare un bot ospitato che qualcun altro mantiene, oppure creare il tuo. Questa guida ne crea uno con discord.js e Cartesia in circa 80 righe di JavaScript, e poi esamina cosa controllare se preferisci invitare quello di qualcun altro.

Se ti serve solo la sintesi vocale integrata di Discord, quella è un’altra funzione: legge i messaggi nei canali di testo con la voce di sistema e non coinvolge nessun bot. Come usare la sintesi vocale su Discord la spiega. Il comando integrato non può parlare nei canali vocali né cambiare voce, ed è proprio il vuoto che un bot riempie.

Cosa costruirai

Un bot con due comandi slash: /tts entra nel tuo canale vocale e legge il tuo testo con una voce di Cartesia, e /stop lo disconnette. Ogni file qui sotto è stato eseguito con gli SDK attuali a ottobre 2026: la richiesta all’API di Cartesia, la conversione audio, la codifica Opus e la registrazione dei comandi sono stati eseguiti e verificati su Node 24 con discord.js 14.27, @discordjs/voice 0.19.2 e @cartesia/cartesia-js 4.2.0.

Prerequisiti

  1. Node.js 20 o successivo.
  2. Un account Discord e un server in cui hai il permesso Manage Server per aggiungere un bot.
  3. Una chiave API di Cartesia dalla dashboard. Il piano gratuito va bene.
  4. Un ID voce dalla Voice Library. Apri una voce che ti piace e copia il suo ID.

1. Crea l’applicazione Discord

  1. Apri il Discord Developer Portal e scegli New Application. Il nome che inserisci è quello che le persone vedranno quando il bot entra.
  2. Nella pagina Bot, scegli Reset Token e copia il token. Il token è la password del bot: tienilo in una variabile d’ambiente, fuori dal codice e fuori da git.
  3. Questo bot non ha bisogno di intent privilegiati. I comandi slash e lo stato vocale funzionano senza gli interruttori Presence, Server Members o Message Content, quindi lasciali spenti.
  4. Invita il bot nel tuo server. Sotto OAuth2, apri l’URL Generator, seleziona gli scope bot e applications.commands, poi i permessi Connect e Speak. Apri l’URL generato e scegli il tuo server.

2. Installa il progetto

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

Cosa fa ogni pacchetto: discord.js parla con l’API di Discord. @discordjs/voice gestisce la connessione vocale. @discordjs/opus trasforma l’audio grezzo nei frame Opus che Discord trasmette. libsodium-wrappers cifra i pacchetti vocali. @cartesia/cartesia-js è l’SDK di Cartesia.

Nella lista non c’è ffmpeg. Il bot produce esattamente il formato PCM che Discord si aspetta, quindi @discordjs/opus gestisce la codifica da solo. ffmpeg entra in gioco solo quando dai a @discordjs/voice un formato che deve transcodificare, come un file MP3.

3. Registra i comandi slash

Discord deve conoscere /tts e /stop prima che qualcuno possa digitarli. Salva questo come 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.`);

Eseguilo una volta con le credenziali del bot nell’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

Lo script registra comandi di gilda, che si aggiornano all’istante, quindi i comandi funzionano nel tuo server appena termina. DISCORD_CLIENT_ID è l’Application ID nella pagina General Information dell’app, e DISCORD_GUILD_ID è l’ID del tuo server; attiva la Modalità sviluppatore nelle impostazioni di Discord, poi fai clic destro sul server per copiarlo.

4. Scrivi il bot

Salva questo come 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);
}

Tre cose in quel file meritano uno sguardo più attento:

Il formato audio. La voce di Discord è Opus stereo a 48 kHz. La chiamata tts.generate chiede a Sonic container: "raw", encoding: "pcm_s16le" e sample_rate: 48000, che è il formato PCM nativo di Discord, meno un canale. MonoToStereo duplica ogni campione a 16 bit in entrambi i canali, e @discordjs/voice codifica il risultato in Opus con @discordjs/opus. Chiedere 48 kHz fin dall’inizio evita qualsiasi ricampionamento lungo il percorso.

La coda. Due persone che eseguono /tts contemporaneamente sovrapporrebbero l’audio, quindi ogni server ha una coda. playNext estrae una riga, la sintetizza e la riproduce; AudioPlayerStatus.Idle scatta quando una clip finisce e prende la riga successiva. /stop funziona solo da dentro il canale del bot: ferma il player e distrugge la connessione. Una riga che fallisce a metà dello stream raggiunge il listener di errore del player e viene saltata, e una coda sostituita mentre Sonic sta ancora lavorando viene scartata invece di essere riprodotta sopra la successiva.

Dove va il tempo. Il time to first audio di Sonic è sotto i 90 ms, e synthesize restituisce uno stream, quindi la riproduzione parte dai primi chunk invece di aspettare l’intera clip. Per una frase o due, il ritardo tra il comando e la voce è per lo più il round trip di rete.

5. Eseguilo

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 in un canale vocale del tuo server e digita /tts seguito dal tuo testo. Il bot entra, legge e aspetta il comando successivo. Fermalo con Ctrl+C. Per un bot che resta online quando chiudi il portatile, eseguilo su un piccolo host sempre attivo; va bene qualsiasi cosa mantenga vivo un processo Node.js.

Se preferisci invitare un bot ospitato

Esistono bot TTS pubblici che puoi invitare senza eseguire nulla. Giudicali su tre cose: come suonano le voci, quali permessi chiede l’invito (a un bot TTS servono Connect e Speak, e poco altro) e quanto costano oltre il livello gratuito. Il rovescio della medaglia è il controllo. Un bot ospitato legge il testo del tuo server sull’infrastruttura di qualcun altro, e i suoi comandi e le sue voci sono quelli scelti dal suo autore. Se ti sta stretto, le 80 righe qui sopra sono la via d’uscita.

Portalo oltre

  • Una voce per persona: mantieni una mappa da ID utente a ID voce, così i messaggi di ogni membro vengono letti con la sua voce.
  • Altre lingue: Sonic 3.6 supporta 44 lingue con un solo modello. Passa un codice language e una voce corrispondenti.
  • Leggi un canale automaticamente: ascolta messageCreate in un canale designato invece di un comando slash. Serve l’intent privilegiato Message Content, attivato nella pagina Bot del Developer Portal.
  • Un bot che risponde: questo legge ciò che digiti, non conversa. Per un agente vocale che ascolta, pensa e risponde, vedi Cartesia Managed Agents.

Documentazione correlata

Domande frequenti

Che cos'è un bot TTS per Discord?

Un bot TTS per Discord è un account bot che entra in un canale vocale e legge ad alta voce il testo della chat. Il comando /tts integrato di Discord legge i messaggi nei canali di testo con la voce di sistema; un bot può parlare nei canali vocali e usare qualsiasi voce supportata dal suo codice, comprese voci clonate personalizzate.

Come aggiungo un bot TTS al mio server Discord?

Invitando un bot ospitato tramite il suo link di invito, oppure creando e ospitando il tuo. Per un bot ospitato, controlla che chieda solo i permessi Connect e Speak. Per il tuo bot, i passaggi sono creare un'applicazione nel Discord Developer Portal, invitarla con gli scope bot e applications.commands, ed eseguire il codice di questa guida.

Devo ospitare il bot io stesso?

Se lo costruisci tu, sì. Discord consegna i comandi al tuo codice e riproduce l'audio che il tuo codice restituisce, ma il codice deve girare da qualche parte. Un portatile va bene per i test. Per un bot sempre online, eseguilo su un piccolo host sempre attivo; va bene qualsiasi servizio che mantenga vivo un processo Node.js.

Il bot può leggere automaticamente tutti i messaggi di un canale?

Sì. Ascolta l'evento messageCreate in un canale designato invece del, o insieme al, comando /tts. Leggere il contenuto dei messaggi richiede l'intent privilegiato Message Content, che si attiva nella pagina Bot del Discord Developer Portal.

Quali voci può usare un bot TTS per Discord?

Qualsiasi voce della Voice Library di Cartesia, inclusa una voce clonata da un breve campione. Copia l'ID della voce nella variabile d'ambiente CARTESIA_VOICE_ID e riavvia il bot.

Un bot TTS per Discord è gratuito?

Discord non addebita nulla per i bot. Il piano gratuito di Cartesia include 20.000 crediti al mese, che coprono l'uso normale di un server tipico. Un uso più intenso viene fatturato a credito nei piani a pagamento.