diff --git a/discord/.env.example b/discord/.env.example index 707d399..2f02ef3 100644 --- a/discord/.env.example +++ b/discord/.env.example @@ -4,8 +4,7 @@ CLIENT="123456789012345678" GUILD="123456789012345678" ADMIN="123456789012345678" -LLM_HOST="127.0.0.1" -LLM_PORT=8000 +LLM_HOST="http://127.0.0.1:8000" LLM_TOKEN="dfsl;kjsdl;kfja" REPLY_CHANCE=0.2 diff --git a/discord/bot.ts b/discord/bot.ts index cba973f..72084bb 100644 --- a/discord/bot.ts +++ b/discord/bot.ts @@ -32,6 +32,7 @@ import { openDb, reactionEmojis, recordReaction, + requestTTSResponse, sync } from './util'; import 'dotenv/config'; @@ -166,11 +167,16 @@ async function onNewMessage(message: Message) async function fetchMotd() { - const res = await fetch(process.env.MOTD_HREF); - const xml = await res.text(); - const parser = new JSDOM(xml); - const doc = parser.window.document; - return doc.querySelector(process.env.MOTD_QUERY).textContent; + try { + const res = await fetch(process.env.MOTD_HREF); + const xml = await res.text(); + const parser = new JSDOM(xml); + const doc = parser.window.document; + const el = doc.querySelector(process.env.MOTD_QUERY); + return el ? el.textContent : null; + } catch (err) { + logWarn('[bot] Failed to fetch MOTD; is the booru down?'); + } } async function requestRVCResponse(src: Attachment): Promise @@ -189,7 +195,7 @@ async function requestRVCResponse(src: Attachment): Promise const fd = new FormData(); fd.append('file', fs.readFileSync(tmpFileName), 'voice-message.ogg'); - const rvcEndpoint = `http://${process.env.LLM_HOST}:${process.env.LLM_PORT}/rvc?${queryParams.toString()}`; + const rvcEndpoint = `${process.env.LLM_HOST}/rvc?${queryParams.toString()}`; logInfo(`[bot] Requesting RVC response for ${src.id}`); const res = await fetch(rvcEndpoint, { method: 'POST', @@ -206,7 +212,7 @@ async function requestLLMResponse(messages) for (const field of Object.keys(config["llmconf"].llmSettings)) { queryParams.append(field, config["llmconf"].llmSettings[field]); } - const llmEndpoint = `http://${process.env.LLM_HOST}:${process.env.LLM_PORT}/?${queryParams.toString()}`; + const llmEndpoint = `${process.env.LLM_HOST}/?${queryParams.toString()}`; const messageList = messages.map((m: Message) => ({ role: m.author.bot ? "assistant" : "user", content: m.cleanContent, @@ -244,8 +250,18 @@ async function scheduleRandomMessage(firstTime = false) return; } const randomMessage = await fetchMotd(); - await channel.send(randomMessage); - logInfo(`[bot] Sent MOTD: ${randomMessage}`); + if (randomMessage) { + const audio = await requestTTSResponse(randomMessage); + const audioBuf = await audio.arrayBuffer(); + const audioFile = new AttachmentBuilder(Buffer.from(audioBuf)).setName('mikuified.wav'); + await channel.send({ + content: randomMessage, + files: [audioFile] + }); + logInfo(`[bot] Sent MOTD: ${randomMessage}`); + } else { + logWarn(`[bot] Could not fetch MOTD.`); + } } // wait between 2-8 hours const timeoutMins = Math.random() * 360 + 120; diff --git a/discord/commands/tts/tts.ts b/discord/commands/tts/tts.ts new file mode 100644 index 0000000..7508cfa --- /dev/null +++ b/discord/commands/tts/tts.ts @@ -0,0 +1,43 @@ +import { + AttachmentBuilder, + ChatInputCommandInteraction, + SlashCommandBuilder +} from 'discord.js'; +import 'dotenv/config'; +import { logError, logInfo, logWarn } from '../../../logging'; +import { requestTTSResponse } from '../../util'; + +const config = { + ttsSettings: { + pitch_change_oct: 1, + pitch_change_sem: 0 + } +}; + +async function ttsCommand(interaction: ChatInputCommandInteraction) +{ + const text = interaction.options.getString('text'); + await interaction.reply(`generating audio for "${text}"...`); + try { + const audio = await requestTTSResponse(text); + const audioBuf = await audio.arrayBuffer(); + const audioFile = new AttachmentBuilder(Buffer.from(audioBuf)).setName('mikuified.wav'); + await interaction.editReply({ + files: [audioFile] + }); + } catch (err) { + await interaction.editReply(`Error: ${err}`); + logError(`Error while generating TTS: ${err}`); + } +} + +export = { + data: new SlashCommandBuilder() + .setName('tts') + .setDescription('Read text in Miku\'s voice') + .addStringOption( + opt => opt.setName('text').setDescription('Text').setRequired(true) + ), + execute: ttsCommand, + config: config +}; diff --git a/discord/util.ts b/discord/util.ts index 69f4316..73cba97 100644 --- a/discord/util.ts +++ b/discord/util.ts @@ -9,6 +9,7 @@ import { get as httpGet } from 'https'; import { Database, open } from 'sqlite'; import { Database as Database3 } from 'sqlite3'; import 'dotenv/config'; +import fetch from 'node-fetch'; import { logError, logInfo, logWarn } from '../logging'; import { ScoreboardMessageRow } from '../models'; @@ -163,4 +164,19 @@ async function sync(guilds: GuildManager) { } } -export { db, clearDb, openDb, reactionEmojis, recordReaction, sync }; +async function requestTTSResponse(txt: string): Promise +{ + const queryParams = new URLSearchParams(); + queryParams.append("token", process.env.LLM_TOKEN); + queryParams.append("text", txt); + + const ttsEndpoint = `${process.env.LLM_HOST}/tts?${queryParams.toString()}`; + logInfo(`[bot] Requesting TTS response for "${txt}"`); + const res = await fetch(ttsEndpoint, { + method: 'POST' + }); + const resContents = await res.blob(); + return resContents; +} + +export { db, clearDb, openDb, reactionEmojis, recordReaction, requestTTSResponse, sync };