가이드

Python으로 만드는 Discord TTS 봇: 음성 채널에서 채팅을 읽어 주기

Discord에 내장된 /tts는 듣는 사람 각자의 기기에서 시스템 목소리로 메시지를 읽고, 음성 채널에서는 절대 말하지 않습니다. 마이크 없는 친구가 글을 쓰면 통화 중인 모두가 듣게 하려면, 음성 채널에 들어가 메시지를 재생하는 봇이 필요합니다.

이 가이드는 discord.py로 Python 봇을 만듭니다. 고른 텍스트 채널을 SuperClonica의 어떤 목소리로든 읽어 줍니다. 내 컴퓨터에서 무료 엔진을 쓰면 오프라인에 제한이 없고, 모델을 직접 돌리기 싫다면 SuperClonica 음성 합성 API를 쓸 수 있습니다.

준비물

Discord Developer Portal에서 애플리케이션을 만들고 봇을 추가한 뒤, Bot 페이지에서 Message Content intent를 켜고 연결·말하기 권한으로 봇을 초대하세요. FFmpeg를 PATH에 있도록 설치하고 라이브러리를 설치합니다:

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

discord.py 2.7의 voice 옵션에는 Discord 음성에 필요한 암호화 라이브러리가 들어 있습니다. SuperClonica 엔진은 처음 실행할 때 Supertonic 3 모델(약 400 MB)을 내려받고, 그다음부터는 일반 CPU에서 오프라인으로 돌아갑니다.

무료 엔진을 쓰는 봇

DISCORD_TOKEN과 읽을 텍스트 채널의 ID인 TTS_CHANNEL_ID를 설정하고 파일을 실행하세요. 음성 채널에 있는 사람이 그 텍스트 채널에 글을 쓰면, 봇이 그 사람의 음성 채널에 들어가 고른 목소리로 메시지를 읽습니다. 대사는 대기열에서 기다리므로 겹치지 않습니다.

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에는 내장 목소리 M1~M5, F1~F5 중 하나나 SuperClonica 카탈로그의 목소리 파일을 넣습니다. 무료 목소리는 계정 없이, 유료 목소리는 구매 후 내려받습니다. 합성은 별도 스레드에서 돌기 때문에 읽는 동안에도 봇이 계속 반응합니다.

API로 같은 봇 만들기

모델을 둘 공간이 없는 작은 서버에서 봇을 돌린다면 SuperClonica 음성 합성 API로 대사를 변환하세요. voice_line을 이 함수로 바꾸고 SUPERCLONICA_KEY를 설정합니다. 카탈로그의 모든 목소리는 페이지에 표시된 ID로 구매 없이 쓸 수 있습니다.

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

무료 키는 하루 1분으로 시험해 보기에 충분합니다. 월 5달러 스타트는 10분 창마다 최대 3분의 음성을 주므로 붐비는 서버에도 넉넉합니다.

쾌적하게 쓰려면

한 채널만 읽고, 긴 메시지는 자르고, 웹 주소 대신 "링크"라고 읽게 하고, 모더레이터가 봇을 옮기거나 내보낼 수 있게 하세요. 엔진은 영어의 idk, pls처럼 몇몇 언어의 흔한 채팅 은어를 단어로 읽습니다. 카탈로그의 캐릭터 목소리는 팬이 만든 것이며 권리자가 승인하지 않았습니다. 서버에서 재미로 쓰고, 다른 사람을 사칭하는 데에는 절대 쓰지 마세요.