指南

把 AI 声音流式传入语音频道的 discord.js TTS 机器人

请注意:Supertonic 3 目前不支持中文,本文中的机器人可朗读其支持的 31 种语言。朗读机器人是最受欢迎的 Discord 机器人之一:它加入语音频道,朗读没开麦的成员打出的文字。自己做一个就能选择声音:旁白、播报员、游戏或动漫角色,或者服务器专属的声音。

本指南用 discord.js 和 @discordjs/voice 制作这样一个机器人。声音来自 SuperClonica 语音合成 API,按我们的测量约 300 毫秒即开始流式输出,所以消息一到,机器人几乎立刻开口。

准备工作

Node.js 18 或更新版本、在 Discord Developer Portal 中开启了 Message Content intent 的机器人应用,以及带“连接”和“说话”权限的邀请。然后安装:

npm install discord.js @discordjs/voice @discordjs/opus ffmpeg-static

根据 @discordjs/voice 的 README,它需要 @discordjs/opus 之类的 Opus 库和由 ffmpeg-static 提供的 FFmpeg;端到端加密库 DAVE 已预装。请在语音合成 API 页面创建 SuperClonica API 密钥,免费且无需绑卡。

机器人代码

设置 DISCORD_TOKEN、TTS_CHANNEL_ID(要朗读的文字频道)、SUPERCLONICA_KEY 和 VOICE_ID(所选目录声音页面上显示的 ID),然后用 node 运行文件:

import { Client, GatewayIntentBits } from "discord.js";
import { joinVoiceChannel, createAudioPlayer, createAudioResource, AudioPlayerStatus, getVoiceConnection } from "@discordjs/voice";
import { Readable } from "node:stream";

const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages,
  GatewayIntentBits.MessageContent, GatewayIntentBits.GuildVoiceStates] });
const player = createAudioPlayer();
const queue = [];

async function speak(text) {
  const answer = await fetch("https://superclonica.com/api/v1/speech", {
    method: "POST",
    headers: { Authorization: "Bearer " + process.env.SUPERCLONICA_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({ text, voice: process.env.VOICE_ID, language: "en", stream: true }),
  });
  if (!answer.ok) throw new Error(await answer.text());
  return createAudioResource(Readable.fromWeb(answer.body));
}

player.on(AudioPlayerStatus.Idle, async () => {
  const next = queue.shift();
  if (next) player.play(await next());
});

client.on("messageCreate", async (message) => {
  if (message.author.bot || message.channelId !== process.env.TTS_CHANNEL_ID) return;
  const channel = message.member?.voice.channel;
  if (!channel) return;
  const connection = getVoiceConnection(message.guildId) ?? joinVoiceChannel({
    channelId: channel.id, guildId: message.guildId, adapterCreator: message.guild.voiceAdapterCreator });
  connection.subscribe(player);
  const job = () => speak(message.member.displayName + " says: " + message.cleanContent.slice(0, 300));
  if (player.state.status === AudioPlayerStatus.Idle && queue.length === 0) player.play(await job());
  else queue.push(job);
});

client.login(process.env.DISCORD_TOKEN);

每条消息对应一次带 "stream": true 的请求。API 返回 Ogg 流,@discordjs/voice 通过 FFmpeg 转码并边收边播;机器人说话期间到达的消息会在队列中等待。

声音与套餐

SuperClonica 目录中的每个声音都能凭 ID 通过 API 使用,任何套餐都可以,无需购买;你也可以用自己的录音训练声音交给机器人。免费密钥每天 1 分钟音频。每月 5 美元的入门套餐在每个 10 分钟窗口内最多 3 分钟语音,更高套餐为 8、20 或 65 分钟。窗口用完后,API 返回 429 和 Retry-After,不会扣费。

如果完全不想用 API,可以在自己电脑上运行免费的 SuperClonica 引擎;离线版本见 Python TTS 机器人指南。