GUIDE

A Discord TTS bot in Python that reads chat aloud in a voice channel

Discord’s own /tts reads a message on each listener’s device with that device’s voice, and it never speaks in a voice channel. For a server where muted friends type and everyone in the call hears them, you need a bot that joins the voice channel and plays the messages there.

This guide builds one in Python with discord.py. It reads a chosen text channel aloud with any SuperClonica voice: with the free engine on your own computer, offline and with no limits, or through the SuperClonica speech API if you would rather not run the model yourself.

What you need

Create an application in the Discord Developer Portal, add a bot, turn on the Message Content intent on the Bot page and invite the bot with the Connect and Speak permissions. Install FFmpeg so that it is on your PATH, then the libraries:

python -m pip install "discord.py[voice]" superclonica-cpu

The voice extra of discord.py 2.7 brings the encryption libraries Discord voice needs. The SuperClonica engine downloads the Supertonic 3 model, about 400 MB, on its first run and then works offline on an ordinary CPU.

The bot with the free engine

Set DISCORD_TOKEN and TTS_CHANNEL_ID, the ID of the text channel to read, and run this file. When someone who is in a voice channel writes in that text channel, the bot joins their voice channel and reads the message with the chosen voice; lines wait in a queue, so they never talk over each other.

import asyncio, os, re, tempfile
import discord
from superclonica import Superclonica

TOKEN = os.environ["DISCORD_TOKEN"]
TEXT_CHANNEL = int(os.environ["TTS_CHANNEL_ID"])  # the text channel the bot reads
VOICE = "announcer.json"  # a voice file from the catalog, or M1..M5, F1..F5
LINK = re.compile(r"https?://\S+")

intents = discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)
tts = Superclonica()
queue = asyncio.Queue()

def voice_line(text):
    audio, _ = tts.synthesize(text, voice=VOICE, lang="en")
    path = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
    tts.save(path, audio)
    return path

async def speaker():
    while True:
        voice_client, text = await queue.get()
        path = await asyncio.to_thread(voice_line, text)
        done = asyncio.Event()
        voice_client.play(discord.FFmpegPCMAudio(path), after=lambda error: client.loop.call_soon_threadsafe(done.set))
        await done.wait()
        os.remove(path)

@client.event
async def on_ready():
    client.loop.create_task(speaker())

@client.event
async def on_message(message):
    if message.author.bot or message.channel.id != TEXT_CHANNEL or not message.author.voice:
        return
    voice_client = message.guild.voice_client or await message.author.voice.channel.connect()
    text = LINK.sub("link", message.clean_content)[:300]
    await queue.put((voice_client, f"{message.author.display_name} says: {text}"))

client.run(TOKEN)

VOICE can be one of the built-in voices M1 to M5 and F1 to F5, or a voice file from the SuperClonica catalog: free voices download without an account, paid ones once you buy them. Synthesis runs in a worker thread, so the bot stays responsive while a line is voiced.

The same bot through the API

If the bot runs on a small server with no room for the model, voice the lines through the SuperClonica speech API instead: replace voice_line with this function and set SUPERCLONICA_KEY. Any catalog voice works by the ID shown on its page, without buying it.

import os, tempfile
import requests

API = "https://superclonica.com/api/v1/speech"
KEY = os.environ["SUPERCLONICA_KEY"]
VOICE_ID = "49cd4d119695528694fac0a1b9fb58a8"  # any catalog voice: the ID on its page

def voice_line(text):
    answer = requests.post(API, headers={"Authorization": "Bearer " + KEY}, timeout=120,
                           json={"text": text, "voice": VOICE_ID, "language": "en", "format": "mp3"})
    answer.raise_for_status()
    path = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False).name
    with open(path, "wb") as file:
        file.write(answer.content)
    return path

A free key gives a minute of audio a day, enough to test; Starter, $5 a month, gives up to 3 minutes of speech in every 10-minute window, plenty for a busy server.

Keep it pleasant

Read one channel only, cut long messages, say “link” instead of a web address and let moderators move or kick the bot. The engine reads common chat slang in English, such as idk or pls, as words. Character voices in the catalog are fan-made and not endorsed by the rights holders: use them for fun on your server, never to impersonate anyone.