feat: add tts commands

This commit is contained in:
2026-01-13 18:37:21 -03:00
parent 889b86b019
commit 07da7538ba
6 changed files with 302 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
import { CommandCategoryInfo } from '../../commands';
const info: CommandCategoryInfo = {
name: 'TTS',
description: 'Text to Speech related commands'
};
export default info;
+22
View File
@@ -0,0 +1,22 @@
import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
import { Command } from '../../commands';
const builder = new SlashCommandBuilder()
.setName('tts-clear')
.setDescription('Clears the TTS queue');
const cmd: Command = {
name: builder.name,
builder: builder,
requiresAdmin: true,
execute: async (interaction: ChatInputCommandInteraction): Promise<void> => {
if (!interaction.guildId) {
interaction.reply('This command only works on Guilds');
return;
}
interaction.reply('Queue cleared.');
}
};
export default cmd;
+97
View File
@@ -0,0 +1,97 @@
import { Message } from 'discord.js';
import { Command } from '../../commands';
import { Logger } from '../../utils/log';
import { getVoiceConnection, VoiceConnectionStatus } from '@discordjs/voice';
import { TTSManager } from '../../modules/tts';
import { AudioStreamManager } from '../../modules/audiostreams';
import { Readable } from 'stream';
import { DataTypes } from 'sequelize';
import { config } from '../../utils/config';
import { DatabaseManager } from '../../modules/db';
const URL_REGEX = /(?:https?|ftp):\/\/[\n\S]+/g;
class TTSListener implements Command {
private log: Logger;
constructor() {
this.log = new Logger('TTS Listener');
}
user_keys = {
tts_voice: {
type: DataTypes.STRING,
defaultValue: config.tts_default_voice
},
tts_mode: {
type: DataTypes.STRING,
defaultValue: config.tts_default_mode
}
};
guild_keys = {
tts_channel: {
type: DataTypes.STRING
}
};
messageListener = async (msg: Message): Promise<void> => {
if (msg.content.length === 0) return;
const guild = msg.guild;
if (!guild) return;
const me = guild.members.me;
if (!me || !me.voice) return;
const member = msg.member;
if (!member || !member.voice) return;
if (member.voice.channelId !== me.voice.channelId) return;
const voiceConnection = getVoiceConnection(guild.id);
if (
!voiceConnection ||
voiceConnection.state.status !== VoiceConnectionStatus.Ready
)
return;
const db = await DatabaseManager.get;
const guildData = await db.getGuild(guild.id);
if (msg.channelId !== guildData.get('tts_channel')) return;
const userData = await db.getUser(member.id);
const modName = userData.get('tts_mode') as string;
const voiceName = userData.get('tts_voice') as string;
const processTTS = async () => {
try {
const ttsModule = TTSManager.get.getModule(modName);
if (!ttsModule) return;
const voices = await ttsModule.getVoices();
if (!voices) return;
if (!voices.includes(voiceName)) return;
const msgFiltered = msg.content.replace(URL_REGEX, 'a link');
const audio = await ttsModule.generate(voiceName, msgFiltered);
if (audio?.data) {
const stream =
AudioStreamManager.get.getOrCreateStream(voiceConnection);
const queue = stream.getQueue('TTS');
queue.enqueue(Readable.from(audio.data));
}
} catch (err) {
this.log.error('error occurred while processing TTS message (%s)', err);
}
};
processTTS();
};
}
export default new TTSListener();
+34
View File
@@ -0,0 +1,34 @@
import {
ChatInputCommandInteraction,
MessageFlags,
SlashCommandBuilder
} from 'discord.js';
import { Command } from '../../commands';
import { DatabaseManager } from '../../modules/db';
const builder = new SlashCommandBuilder()
.setName('tts-channel')
.setDescription('Sets the channel where TTS messages will be read from');
const cmd: Command = {
name: builder.name,
builder: builder,
execute: async (interaction: ChatInputCommandInteraction): Promise<void> => {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!interaction.guild) {
interaction.editReply('This message can only be executed on guilds');
return;
}
const guildData = await DatabaseManager.get.getGuild(interaction.guild.id);
await guildData.set('tts_channel', interaction.channelId);
await guildData.save();
interaction.editReply('TTS channel updated.');
}
};
export default cmd;
+65
View File
@@ -0,0 +1,65 @@
import {
AutocompleteInteraction,
ChatInputCommandInteraction,
MessageFlags,
SlashCommandBuilder
} from 'discord.js';
import { Command } from '../../commands';
import { DatabaseManager } from '../../modules/db';
import { TTSManager } from '../../modules/tts';
const builder = new SlashCommandBuilder()
.setName('tts-mode')
.setDescription('Selects a mode to use for TTS')
.addStringOption((opt) =>
opt
.setName('mode')
.setDescription('Which TTS provider to use')
.setAutocomplete(true)
.setRequired(true)
);
const cmd: Command = {
name: builder.name,
builder: builder,
execute: async (interaction: ChatInputCommandInteraction): Promise<void> => {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const userData = await DatabaseManager.get.getUser(interaction.user.id);
const modeName = interaction.options.getString('mode', true);
const selectedMode = TTSManager.get
.getModules()
.find((mode) => mode.name === modeName);
if (!selectedMode) {
await interaction.editReply(`Unknown mode (${modeName})`);
return;
}
await userData.set('tts_mode', modeName);
await userData.save();
interaction.editReply(`TTS mode has been set to: ${modeName}.`);
},
autocomplete: async (interaction: AutocompleteInteraction): Promise<void> => {
const focused = interaction.options.getFocused(true);
if (focused.name != 'mode') return;
const modes = TTSManager.get.getModules();
const filtered: string[] = modes
.filter((mode) =>
mode.name.toLowerCase().startsWith(focused.value.toLowerCase())
)
.map((mode) => mode.name)
.slice(0, 25);
await interaction.respond(
filtered.map((choice) => ({ name: choice, value: choice }))
);
}
};
export default cmd;
+76
View File
@@ -0,0 +1,76 @@
import {
AutocompleteInteraction,
ChatInputCommandInteraction,
MessageFlags,
SlashCommandBuilder
} from 'discord.js';
import { Command } from '../../commands';
import { DatabaseManager } from '../../modules/db';
import { TTSManager } from '../../modules/tts';
const builder = new SlashCommandBuilder()
.setName('tts-voice')
.setDescription('Selects a voice to use for TTS')
.addStringOption((opt) =>
opt
.setName('voice')
.setDescription('Which voice to use')
.setAutocomplete(true)
.setRequired(true)
);
const cmd: Command = {
name: builder.name,
builder: builder,
execute: async (interaction: ChatInputCommandInteraction): Promise<void> => {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const userData = await DatabaseManager.get.getUser(interaction.user.id);
const mod = TTSManager.get.getModule(userData.get('tts_mode') as string);
if (!mod) return;
const voices = await mod.getVoices();
if (!voices) {
await interaction.editReply(
'Unknown error occured while fetching TTS voices'
);
return;
}
const voiceName = interaction.options.getString('voice', true);
if (!voices.includes(voiceName)) {
await interaction.editReply('Invalid voice');
return;
}
await userData.set('tts_voice', voiceName);
await userData.save();
interaction.editReply(`TTS voice has been set to: ${voiceName}.`);
},
autocomplete: async (interaction: AutocompleteInteraction): Promise<void> => {
const focused = interaction.options.getFocused(true);
if (focused.name != 'voice') return;
const userData = await DatabaseManager.get.getUser(interaction.user.id);
const mode = TTSManager.get.getModule(userData.get('tts_mode') as string);
if (!mode) return;
const voices: string[] | undefined = await mode.getVoices();
if (!voices) return;
const filtered: string[] = voices
.filter((voice) =>
voice.toLowerCase().startsWith(focused.value.toLowerCase())
)
.slice(0, 25);
await interaction.respond(
filtered.map((choice) => ({ name: choice, value: choice }))
);
}
};
export default cmd;