Compare commits

..

53 Commits

Author SHA1 Message Date
Talon 6a024f8cb6 Change GPT 4 to GPT 4O in chat module 2024-12-26 20:31:04 +01:00
Talon 591eb227b3 Fix bug 2024-04-25 01:08:18 +02:00
Talon 17ba5e2048 Flush on channel leave 2024-04-25 01:08:11 +02:00
Talon 4a371cf4d6 Add OpenAI TTS 2023-11-16 23:28:30 +01:00
Talon 118a1a72e3 Set chatgpt model to gpt4 2023-11-13 00:07:15 +01:00
Talon f709282ca1 Add unreal tts 2023-11-12 23:59:03 +01:00
guilevi 0ed2b4d091 Change to multilingual v2 for eleven 2023-08-22 00:10:36 +02:00
Talon a0c4630bed Gods this thing is so rickety 2023-03-21 19:54:59 +01:00
Talon b964f4acfd Add summon command 2023-03-21 19:42:51 +01:00
Talon c92eacb7fd Fix off by one error 2023-03-21 13:40:22 +01:00
Talon b6008e0ad3 Update bot to new discord API and add simple chatgpt module 2023-03-21 13:35:28 +01:00
guilevi 136173f4de Add Eleven Labs TTS 2023-03-08 13:14:48 +00:00
Talon 9d25ed6e7b Basic dockerfile 2022-11-30 22:39:08 +00:00
Talon 1f06ae0301 Load db from env var 2022-11-30 22:38:55 +00:00
Talon e1f5f81338 Quote db stuff 2022-01-16 23:52:41 +01:00
guilevi 6f9113a463 New shit for wordbyword 2021-11-06 01:42:07 +01:00
guilevi 604fe3fd10 FFS I cannot code for shit 2021-11-05 02:36:07 +01:00
guilevi 9ba6185335 split up things 2021-11-05 02:32:33 +01:00
guilevi b8b2906fe3 I'm fucking dumb 2021-11-05 02:24:57 +01:00
guilevi dd618bf785 Add word by word 2021-11-05 02:15:50 +01:00
guilevi 78e2c77bc7 maybe now 2021-09-15 02:13:50 +02:00
guilevi cd0ae8aadf Merge branch 'master' of https://code.iamtalon.me/talon/tardis-bot 2021-09-15 02:09:55 +02:00
guilevi ba76ae1023 Possibly added SSML to Azure 2021-09-15 02:07:23 +02:00
Talon 9b83cdd21a Fix google audio profile 2021-07-12 20:20:51 +02:00
Talon ca8f3a4be2 Add startup sound 2021-07-12 20:00:22 +02:00
Talon fc43c43d59 Fixed sound volume and sysmsg path 2021-07-12 19:57:53 +02:00
Talon af02d2d279 New sysmsg sound 2021-07-12 19:54:57 +02:00
Talon c33d35689b Add random last say command 2021-07-12 18:03:45 +02:00
guilevi 82d5cbc758 Possibly added phonetic input to sam 2021-07-04 01:06:42 +02:00
Talon ee88a48fa5 Fixed crash with announcer 2021-07-03 22:51:19 +02:00
Talon 57d37a7b0b Fix typo in sam require 2021-07-03 17:11:28 +02:00
Talon 43d7f60a7a Add sam as speech synth 2021-07-03 17:09:19 +02:00
guilevi a27d68ec92 Add mangle 2021-05-15 22:43:09 +02:00
guilevi 5882be1e43 I'm a dumbass, fixed for real ffs 2021-05-02 19:01:14 +02:00
guilevi a74cf74574 Merge branch 'master' of https://code.iamtalon.me/talon/tardis-bot 2021-05-02 18:57:43 +02:00
guilevi 83624684e6 Fix string issue 2021-05-02 18:57:34 +02:00
Talon b47437907b To lower case watson 2021-05-02 18:57:16 +02:00
guilevi cd51b092fc Add string customization and example.env 2021-05-02 18:26:18 +02:00
Talon cbb4a6898b Fix non neuralness 2021-04-11 00:55:42 +02:00
guilevi 6b43761ca7 Remove logging and fix case stuff 2021-04-11 00:26:03 +02:00
guilevi f0c71d75dc Added Google TTS, lowercase voices so that people don't have to remember ridiculous case 2021-04-10 23:45:02 +02:00
Talon 40e50b6546 Move azure back where it belongs 2021-04-09 18:15:33 +02:00
guilevi c6c370d22b Implement queue flush 2021-04-09 16:21:31 +02:00
Talon 4a37af0795 Add azure tts 2021-04-09 15:56:53 +02:00
Talon 9ec6ee3922 Add sysmsg sound 2021-04-09 13:33:14 +02:00
Talon fdfc431107 Add binary data to a textual repository. Wooo I'm a dumbass 2021-04-09 13:30:16 +02:00
guilevi 75dc0c2da2 Add audio queue 2021-04-09 13:23:13 +02:00
guilevi 1fe978eec6 Add canttalk module, few changes here and there to accomodate for it 2021-04-09 03:00:55 +02:00
guilevi 3b1a419db4 I'm a dumbass, actually fixed conflict 2021-04-07 11:27:22 +02:00
guilevi 70686c2ce0 Changes made to Watson to populate voices, then used VSCode to reformat, so that should resolve the conflict 2021-04-07 11:14:31 +02:00
guilevi 0b2b443efe Fix formatting 2021-04-07 11:13:21 +02:00
Talon 12a9f8fc53 Fix formatting 2021-04-07 01:36:35 +02:00
guilevi 158ed0372f Implement api.output, announcevoice command, a bunch of other tts shit, etc etc etc buh 2021-04-07 01:33:47 +02:00
35 changed files with 6886 additions and 2413 deletions

BIN
.DS_Store vendored

Binary file not shown.

4
.gitignore vendored
View File

@ -1,4 +1,6 @@
node_modules
voice_tmp
.env
*.db
*.db
.DS_Store
gkey.json

31
AudioQueue.js 100644
View File

@ -0,0 +1,31 @@
const Voice = require("@discordjs/voice");
module.exports = class AudioQueue {
constructor(connection, api) {
this.connection = connection;
this.api = api;
this.queue = [];
this.current = undefined;
this.api.player.on(Voice.AudioPlayerStatus.Idle, this.handleStop.bind(this));
}
playNext() {
if (this.queue.length == 0) {
this.current = undefined;
return;
}
this.current = this.api.play(this.queue[0]);
}
handleStop(current) {
this.queue.shift();
this.playNext();
}
add(element) {
this.queue.push(element);
if (this.queue.length == 1) this.playNext();
}
flush() {
this.current.setVolume(0);
this.queue=[];
this.playNext();
}
}

5
Dockerfile 100644
View File

@ -0,0 +1,5 @@
FROM node:18-alpine
COPY . .
RUN apk add ffmpeg
RUN npm install
ENTRYPOINT ["node", "index.js"]

62
adapter.js 100644
View File

@ -0,0 +1,62 @@
const { Snowflake, Client, Guild, VoiceBasedChannel, Events, Status, GatewayDispatchEvents } = require("discord.js");
const adapters = new Map();
const trackedClients = new Set();
const trackedShards = new Map();
function trackClient(client) {
if (trackedClients.has(client)) return;
trackedClients.add(client);
client.ws.on(GatewayDispatchEvents.VoiceServerUpdate, (payload) => {
adapters.get(payload.guild_id)?.onVoiceServerUpdate(payload);
});
client.ws.on(GatewayDispatchEvents.VoiceStateUpdate, (payload) => {
if (payload.guild_id && payload.session_id && payload.user_id === client.user?.id) {
adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload);
}
});
client.on(Events.ShardDisconnect, (_, shardId) => {
const guilds = trackedShards.get(shardId);
if (guilds) {
for (const guildID of guilds.values()) {
adapters.get(guildID)?.destroy();
}
}
trackedShards.delete(shardId);
});
}
function trackGuild(guild) {
let guilds = trackedShards.get(guild.shardId);
if (!guilds) {
guilds = new Set();
trackedShards.set(guild.shardId, guilds);
}
guilds.add(guild.id);
}
module.exports = function (channel) {
return (methods) => {
adapters.set(channel.guild.id, methods);
trackClient(channel.client);
trackGuild(channel.guild);
return {
sendPayload(data) {
console.log(channel.guild.shard.status);
// if (channel.guild.shard.status === Status.READY) {
console.log("Sending shard data");
channel.guild.shard.send(data);
return true;
// }
console.log("Unable to send channel payload");
return false;
},
destroy() {
console.log("Destroying adapter");
return adapters.delete(channel.guild.id);
},
};
};
}

13
example.env 100644
View File

@ -0,0 +1,13 @@
TOKEN=DISCORD_BOT_TOKEN_HERE
GUILD=GUILD_ID_HERE
CHANNEL=VOICE_CHANNEL_ID_HERE
STRING_SET=en
VOICE_TMP_PATH=./voice_tmp/
DB_FILE=DATABASE_PATH_HERE
PREFIX=+
ANNOUNCEMENT_ENGINE=espeak
ANNOUNCEMENT_VOICE=en
watsonURL=WATSON_URL_HERE
watsonAPIKey=WATSON_API_KEY_HERE
TTS_CHANNEL=CANTTALK_TEXT_CHANNEL_ID_HERE
GOOGLE_APPLICATION_CREDENTIALS=GOOGLE_CLOUD_KEY_HERE

119
index.js
View File

@ -1,43 +1,87 @@
const Discord = require('discord.js');
const Voice = require("@discordjs/voice");
const adapterCreator = require("./adapter");
require('dotenv').config();
const fetch = require('node-fetch');
const fs = require('fs');
const sha1 = require('sha1');
const sqlite = require('sqlite3');
const sqlite3 = require('sqlite3');
const { open } = require('sqlite')
let joinedVoiceChannels = [];
let joinedVoiceChannelConnections = new Map();
let modules = [];
let commandHandlers = new Map();
const player = Voice.createAudioPlayer();
const bot = new Discord.Client();
const rest = new Discord.REST({ version: '10' }).setToken(process.env["TOKEN"]);
const bot = new Discord.Client({
intents: [
Discord.GatewayIntentBits.GuildMembers,
Discord.GatewayIntentBits.GuildMessageReactions,
Discord.GatewayIntentBits.GuildMessages,
Discord.GatewayIntentBits.GuildPresences,
Discord.GatewayIntentBits.GuildVoiceStates,
Discord.GatewayIntentBits.Guilds,
Discord.GatewayIntentBits.MessageContent
]
});
const db = new sqlite.Database(process.env.DB_FILE);
async function initDB() {
console.log(__dirname);
api.db = await open({
filename: process.env["DB_FILE"],
driver: sqlite3.Database
});
}
const api = {
db: db,
player: player,
db: undefined,
queue: undefined,
strings: require('./strings/' + process.env.STRING_SET + '.json'),
ttsEngines: (() => {
let engines={};
console.log(`Registering TTS engines...`);
const engineDirectories = fs.readdirSync('./tts');
engineDirectories.forEach((dir) => {
if(dir.startsWith('.')) return;
eng=require(`./tts/${dir}/index.js`);
engines[dir]=new eng;
console.log(`Loading ./tts/${dir}/index.js`)
})
return engines;
let engines = {};
console.log(`Registering TTS engines...`);
const engineDirectories = fs.readdirSync('./tts');
engineDirectories.forEach((dir) => {
if (dir.startsWith('.')) return;
eng = require(`./tts/${dir}/index.js`);
engines[dir] = new eng;
console.log(`Loading ./tts/${dir}/index.js`)
})
return engines;
})(),
announcementVoice: process.env.ANNOUNCEMENT_VOICE,
announcementEngine: undefined,
play: (file) => {
return player.play(Voice.createAudioResource(file));
},
respond: (message, text, voiceText) => {
let toSend = message.member.displayName + ", " + (voiceText ? voiceText : text);
if (message.member.voice.channel) {
api.queue.add(__dirname + "/sysmsg.wav");
api.speak(message.member.voice.channel, toSend);
} else {
message.reply(text);
}
},
getActiveVoiceChannel: () => joinedVoiceChannels[0],
isInVoiceChannel: (channel) => {
return joinedVoiceChannels.includes(channel);
},
getConnectionForVoiceChannel: (channel) => {
return bot.voice.connections.find((conn) => conn.channel === channel);
return joinedVoiceChannelConnections.get(channel);
},
generateVoice: async (string, engine, voice, params) => {
const hash = sha1(voice+string);
const hash = sha1(voice + string);
const filepath = process.env.VOICE_TMP_PATH + hash + '.' + engine.fileExtension;
if (!fs.existsSync(filepath)) {
await engine.getSpeechFile(string, filepath, voice, params);
@ -47,22 +91,30 @@ const api = {
joinChannel: async (channel) => {
if (!api.isInVoiceChannel(channel)) {
const res = await channel.join();
const res = Voice.joinVoiceChannel({
channelId: channel.id,
guildId: channel.guild.id,
adapterCreator: adapterCreator(channel)
});
res.subscribe(player);
joinedVoiceChannels.push(channel);
joinedVoiceChannelConnections.set(channel, res);
}
},
leaveChannel: async (channel) => {
if (joinedVoiceChannels.includes(channel)) {
let con = joinedVoiceChannelConnections.get(channel);
joinedVoiceChannels = joinedVoiceChannels.filter((chan) => chan !== channel);
await channel.leave();
con.disconnect();
joinedVoiceChannelConnections.delete(channel);
}
},
speak: async (channel, message, engine=api.ttsEngines.gtranslate, voice='en-us', params={}) => {
const conn = api.getConnectionForVoiceChannel(channel);
speak: async (channel, message, engine = api.announcementEngine, voice = api.announcementVoice, params = {}) => {
const filepath = await api.generateVoice(message, engine, voice, params);
if (conn) conn.play(filepath);
api.queue.add(filepath);
},
registerCommand: async (commandString, commandFunc) => {
@ -71,27 +123,36 @@ const api = {
}
function registerModules() {
console.log(`Registering modules...`);
console.log(`Registering modules...`);
const moduleDirectories = fs.readdirSync('./modules');
moduleDirectories.forEach((dir) => {
if(dir.startsWith('.')) return;
modules.push(require(`./modules/${dir}/index.js`));
if (dir.startsWith('.')) return;
modules.push(require(`./modules/${dir}`));
console.log(`Loading ./modules/${dir}/index.js`)
})
modules.forEach((mod) => mod(bot, api));
}
function handleMessage(message) {
console.log(`I got message`);
if (message.content.startsWith(process.env.PREFIX)) {
const args = message.contents.split(" ");
const args = message.content.split(" ");
const command = args[0].substr(1, args[0].length);
const execution = commandHandlers.get(command);
if (command) {
command(args, message);
if (execution) execution(args, message);
}
}
}
registerModules();
api.announcementEngine = api.ttsEngines[process.env.ANNOUNCEMENT_ENGINE];
async function start() {
await initDB();
registerModules();
}
bot.login(process.env.TOKEN);
bot.on('message', handleMessage);
bot.on('messageCreate', handleMessage);
start();

BIN
modules/.DS_Store vendored

Binary file not shown.

View File

@ -0,0 +1,49 @@
const printf=require('printf');
const fs = require('fs');
const path = require('path');
module.exports = async (bot, api) => {
bot.on('messageCreate', async (message) => {
if (!message.content.startsWith(process.env.PREFIX)) {
if (message.channel.id == process.env.TTS_CHANNEL) {
let chan=message.member.voice.channel;
let userRow = await api.db.get('select * from TTSPreferences where user_id=?', message.author.id);
if (!userRow) {
await api.db.run('insert into TTSPreferences (user_id,engine,voice) values (?,?,?)', [message.author.id, api.announcementEngine.shortName, api.announcementVoice]);
userRow = await api.db.get('select * from TTSPreferences where user_id=?', message.author.id);
}
if (api.ttsEngines[userRow.engine]) {
api.speak(chan,message.content, api.ttsEngines[userRow.engine], userRow.voice)
} else {
}
}
}
});
api.registerCommand('myvoice', async (args, message) => {
let userEngine, userVoice;
if (args.length > 3) {
return api.respond(message, printf(api.strings.TOO_MANY_ARGUMENTS));
}
if (api.ttsEngines[args[1]]) {
userEngine = args[1];
if (api.ttsEngines[userEngine].validateVoice(args[2].toLowerCase())) {
userVoice = args[2].toLowerCase();
api.respond(message, printf(api.strings.USER_VOICE_CHANGED, userVoice, api.ttsEngines[userEngine].longName));
} else {
userVoice = api.ttsEngines[userEngine].getDefaultVoice();
api.respond(message, printf(api.strings.INVALID_VOICE, userVoice, api.ttsEngines[userEngine].longName));
}
await api.db.run('update TTSPreferences set engine=?, voice=? where user_id=?', userEngine, userVoice, message.author.id);
} else {
api.respond(message, printf(api.strings.INVALID_ENGINE, args[1]));
}
});
api.registerCommand('random', async (args, message) => {
const files = fs.readdirSync(process.env["VOICE_TMP_PATH"]);
const rnd = files[Math.floor(Math.random()*files.length)];
console.log(rnd);
api.queue.add(__dirname + "/../../sysmsg.wav");
api.queue.add(process.env["VOICE_TMP_PATH"] + "/" + rnd);
});
}

View File

@ -1,25 +1,34 @@
module.exports = function(bot, api) {
bot.on('voiceStateUpdate', async (oldState, newState) => {
if (newState.member.user.bot) return;
if (oldState.channel && newState.channel) return;
const channel = oldState.channel || newState.channel;
if (!channel) return;
if (channel.members.array().length < 2) {
return await api.leaveChannel(channel);
}
await api.joinChannel(channel);
let joined = false;
if (!oldState.channel) {
joined = true;
}
let username = newState.member.displayName;
let str = "";
if (!joined) {
str = username + " left the channel";
} else {
str = username + " joined the channel";
}
api.speak(channel, str);
})
}
const printf = require('printf');
const AudioQueue = require('../../AudioQueue.js')
module.exports = function (bot, api) {
bot.on('voiceStateUpdate', async (oldState, newState) => {
if (newState.member.user.bot) return;
if (oldState.channel && newState.channel) return;
const channel = oldState.channel || newState.channel;
if (!channel) return;
if (channel.members.size < 2) {
api.AudioQueue.flush();
await api.leaveChannel(channel);
}
await api.joinChannel(channel);
let joined = false;
if (!oldState.channel) {
joined = true;
let conn = api.getConnectionForVoiceChannel(channel);
if (!api.queue) api.queue = new AudioQueue(conn, api);
}
let username = newState.member.displayName;
let str = "";
if (!joined) {
str = printf(api.strings.USER_LEFT, username);
} else {
str = printf(api.strings.USER_JOINED, username);
}
const filepath = await api.generateVoice(str, api.announcementEngine, api.announcementVoice);
api.queue.add(__dirname + "/sysmsg.wav");
api.queue.add(filepath);
})
}

View File

@ -0,0 +1,23 @@
let ChatGPTAPI = null;
module.exports = function (bot, api) {
import("chatgpt").then((mod) => {
ChatGPTAPI = mod.ChatGPTAPI;
});
api.registerCommand('chat', async (args, message) => {
const response = await getChatGPTResponse(message.content.slice(6).trim());
api.respond(message, response);
});
}
async function getChatGPTResponse(prompt) {
const api = new ChatGPTAPI({
apiKey: process.env.OPENAI_API_KEY,
completionParams: {
model: 'gpt-4o'
}
})
const res = await api.sendMessage(prompt);
return res.text;
}

View File

@ -0,0 +1,29 @@
const gtranslate = require('node-google-translate-skidz');
module.exports = function (bot, api) {
api.registerCommand('mangle', async (args, message) => {
let str = message.content.slice(8).trim();
let langs = process.env.MANGLE_LANGS.split(',');
let lang;
let i = 0;
for (let lang of langs) {
if (i >= langs.length - 1) break;
let translationResult = await translate(str, lang, langs[i + 1]);
str = translationResult.translation;
i++;
}
api.respond(message, str);
});
}
async function translate(text, fromLang, toLang) {
return new Promise((resolve, reject) => {
gtranslate({
text: text,
source: fromLang,
target: toLang
}, (res) => {
resolve(res);
})
})
}

View File

@ -0,0 +1,17 @@
const fetch = require('node-fetch');
module.exports = function (bot, api) {
api.registerCommand('randomquote', async (args, message) => {
const data = await fetch(process.env["QDB_URL"], {
headers: {
Authorization: 'Basic ' + Buffer.from(`${process.env["QDB_USER"]}:${process.env["QDB_PASS"]}`).toString('base64')
}
});
const quotes = await data.json();
const quote = quotes[Math.floor(Math.random()*quotes.length)];
let chan=message.member.voice.channel;
// api.queue.add(__dirname + "/sysmsg.wav");
// api.speak(chan, `${quote.author}, on ${quote.medium}: ${quote.quote}`);
api.respond(message, `Here's your quote: ${quote.author}, on ${author.medium}: ${quote.quote}`);
})
}

View File

@ -0,0 +1,6 @@
module.exports = function (bot, api) {
api.registerCommand("summon", async (args, message) => {
await api.joinChannel(message.member.voice.channel);
api.respond(message, `Hi!`);
})
}

View File

@ -0,0 +1,23 @@
const printf=require('printf');
module.exports = function (bot, api) {
api.registerCommand('announcevoice', (args, message) => {
let channel = api.getActiveVoiceChannel();
if (args.length > 3) {
return api.respond(message, printf(api.strings.TOO_MANY_ARGUMENTS));
}
if (api.ttsEngines[args[1]]) {
api.announcementEngine = api.ttsEngines[args[1]];
if (api.announcementEngine.validateVoice(args[2])) {
api.announcementVoice = args[2];
api.respond(message, printf(api.strings.SYSTEM_VOICE_CHANGED, api.announcementVoice, api.announcementEngine.longName));
} else {
api.announcementVoice = api.announcementEngine.getDefaultVoice();
api.respond(message, printf(api.strings.INVALID_VOICE, api.announcementVoice, api.announcementEngine.longName));
}
} else {
api.respond(message, printf(api.strings.INVALID_ENGINE, args[1]));
}
});
api.registerCommand('flush',()=>api.queue.flush());
}

View File

@ -1,10 +1,14 @@
module.exports = function(bot, api) {
bot.on('ready', async () => {
console.log("Bot initialized and listening");
const guild = await bot.guilds.fetch(process.env.GUILD);
const channel = await bot.channels.fetch(process.env.CHANNEL);
await api.joinChannel(channel);
api.speak(channel, `Hi! I'm alive. It is now ${new Date().toLocaleTimeString()} on ${new Date().toLocaleDateString()}`,api.ttsEngines.espeak, "en");
})
const AudioQueue=require('../../AudioQueue.js')
module.exports = function (bot, api) {
bot.on('ready', async () => {
console.log("Bot initialized and listening");
const guild = await bot.guilds.fetch(process.env.GUILD);
const channel = await bot.channels.fetch(process.env.CHANNEL);
await api.joinChannel(channel);
let conn=api.getConnectionForVoiceChannel(channel);
if (!api.queue) api.queue=new AudioQueue(conn, api);
api.queue.add(__dirname + "/../../sysstart.wav");
api.speak(channel, api.strings.WELCOME);
})
}

View File

@ -0,0 +1,47 @@
const printf = require('printf')
const isStringInt = require('is-string-int');
const levenshtein = require('fast-levenshtein')
module.exports = function (bot, api) {
bot.currentWBW = "";
api.registerCommand('wbw', async (args, message) => {
if (args.length == 1) {
return api.respond(message, bot.currentWBW ? printf(api.strings.CURRENT_STORY, bot.currentWBW) : printf(api.strings.NO_STORY));
}
if (args.length > 2) {
return api.respond(message, printf(api.strings.TOO_MANY_ARGUMENTS));
} else {
if (isStringInt(args[1])) {
let story = await api.db.get('select * from WBWStories where story_id=?', parseInt(args[1]))
if (!story) {
return api.respond(message, api.strings.WBW_INVALID_ID)
} else {
return api.respond(message, story.story_text)
}
} else {
let lastUser = await api.db.get('select value from BotState where key="last_wbw"');
if (message.author.id == lastUser.value && bot.currentWBW != "") {
let lastWord = (bot.currentWBW.indexOf(" ") == bot.currentWBW.lastIndexOf(" ")) ? bot.currentWBW : bot.currentWBW.slice(bot.currentWBW.slice(0,-1).lastIndexOf(' ') + 1);
console.log(args[1], lastWord, levenshtein.get(args[1], lastWord))
if (levenshtein.get(args[1], lastWord) <= 3) {
bot.currentWBW = bot.currentWBW.replace(new RegExp(lastWord + "([^" + lastWord + "]*)$"), args[1] + "$1 ");
api.respond(message, printf(api.strings.WBW_REPLACED, lastWord, args[1]))
} else {
return api.respond(message, printf(api.strings.WBW_TOO_DIFFERENT))
}
} else {
bot.currentWBW += args[1] + ' ';
api.respond(message, printf(api.strings.WBW_NEW_WORD))
let toSay = bot.currentWBW.indexOf(".") == -1 ? bot.currentWBW : bot.currentWBW.slice(bot.currentWBW.lastIndexOf('.') + 2);
api.speak(message.member.voice.channel, toSay)
await api.db.run('update BotState set value=? where key="last_wbw"', message.author.id);
}
}
}
})
api.registerCommand('newwbw', async (args, message) => {
await api.db.run('insert into WBWStories (story_text) values(?)', bot.currentWBW);
bot.currentWBW = '';
api.respond(message, printf(api.strings.WBW_RESET))
})
}

8354
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,34 @@
{
"name": "tardis-bot",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"discord.js": "^12.5.3",
"dotenv": "^8.2.0",
"google-tts-api": "^2.0.2",
"node-fetch": "^2.6.1",
"opusscript": "^0.0.8",
"sha1": "^1.1.1",
"sqlite3": "^5.0.2"
}
}
{
"name": "tardis-bot",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@discordjs/voice": "^0.15.0",
"@google-cloud/text-to-speech": "^3.1.3",
"chatgpt": "^5.1.2",
"discord.js": "^14.8.0",
"dotenv": "^8.2.0",
"fast-levenshtein": "^3.0.0",
"google-tts-api": "^2.0.2",
"is-string-int": "^1.0.1",
"libsodium-wrappers": "^0.7.11",
"microsoft-cognitiveservices-speech-sdk": "^1.16.0",
"node-fetch": "^2.6.1",
"node-google-translate-skidz": "^1.1.2",
"opusscript": "^0.0.8",
"printf": "^0.6.1",
"sam-js": "^0.1.2",
"sha1": "^1.1.1",
"sqlite": "^4.0.21",
"sqlite3": "^5.0.2",
"wavefile": "^11.0.0"
}
}

17
strings/en.json 100644
View File

@ -0,0 +1,17 @@
{
"WELCOME": "Beep boop. I'm a bot. Hi.",
"USER_JOINED": "%s joined the channel.",
"USER_LEFT": "%s left the channel.",
"SYSTEM_VOICE_CHANGED": "My new voice is %s from %s",
"USER_VOICE_CHANGED": "Your new voice is %s from %s",
"INVALID_ENGINE": "%s is not a valid engine name.",
"INVALID_VOICE": "invalid voice name. Using default voice %s for %s instead.",
"TOO_MANY_ARGUMENTS": "too many arguments for command.",
"CURRENT_STORY": "Here's the current story: %s",
"NO_STORY": "No story in progress at the moment.",
"WBW_REPLACED": "Replaced %s with %s",
"WBW_TOO_DIFFERENT": "This word is too different from the last word.",
"WBW_NEW_WORD": "Added to story.",
"WBW_RESET": "The story has been reset.",
"WBW_INVALID_ID": "No story with that ID."
}

10
strings/es.json 100644
View File

@ -0,0 +1,10 @@
{
"WELCOME": "Hola hola, soy un bot.",
"USER_JOINED": "%s se ha unido al canal.",
"USER_LEFT": "%s ha salido del canal.",
"SYSTEM_VOICE_CHANGED": "Mi nueva voz es %s de %s",
"USER_VOICE_CHANGED": "Tu nueva voz es %s de %s",
"INVALID_ENGINE": "%s no es un nombre de motor válido.",
"INVALID_VOICE": "Nombre de voz no válido. Usando voz por defecto %s para %s.",
"TOO_MANY_ARGUMENTS": "Demasiados argumentos para el comando."
}

BIN
sysmsg.wav 100644

Binary file not shown.

BIN
sysstart.wav 100644

Binary file not shown.

9
t.js 100644
View File

@ -0,0 +1,9 @@
const t=require('node-google-translate-skidz')
t({
text: 'this is a test of the meow',
'source': 'en',
target: 'ta'
}, (res) => {
console.log(res);
});

BIN
tts/.DS_Store vendored

Binary file not shown.

View File

@ -1,16 +1,22 @@
const fs=require('fs');
const fs = require('fs');
module.exports=class {
constructor(longName, fileExtension, supportedParameters=[]) {
this.longName=longName;
this.fileExtension=fileExtension;
module.exports = class {
constructor(shortName, longName, fileExtension, supportedParameters = []) {
this.shortName = shortName;
this.longName = longName;
this.fileExtension = fileExtension;
}
async getSpeech(text, voice, params) {}
getInternalVoiceName(str) {
return this.voices ? this.voices[str] : str;
}
getDefaultVoice() { }
validateVoice(voice) { return this.voices ? this.voices[voice] : true; }
async getSpeech(text, voice = this.getDefaultVoice(), params) { }
async getSpeechFile(text, filepath, voice, params) {
const data = await this.getSpeech(text, voice, params);
const contents = await data.arrayBuffer();
const buf = Buffer.from(contents);
fs.writeFileSync(filepath, buf);
const contents = await data.arrayBuffer();
const buf = Buffer.from(contents);
fs.writeFileSync(filepath, buf);
return filepath;
}
}

57
tts/azure/index.js 100644
View File

@ -0,0 +1,57 @@
const BaseEngine = require('../BaseEngine');
const sdk = require("microsoft-cognitiveservices-speech-sdk");
const fetch = require('node-fetch');
module.exports = class AzureTTS extends BaseEngine {
constructor() {
super("azure", "Microsoft Azure TTS", "wav");
this.voices = {};
this.populateVoiceList();
}
getDefaultVoice() {
return "Aria";
}
getSpeechFile(text, filepath, voice = this.getDefaultVoice(), params = {}) {
return new Promise((resolve, reject) => {
const speechConfig = sdk.SpeechConfig.fromSubscription(process.env.AZURE_API_KEY, process.env.AZURE_REGION);
speechConfig.speechSynthesisOutputFormat = sdk.SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm;
speechConfig.speechSynthesisVoiceName = this.voices[voice];
const audioConfig = sdk.AudioConfig.fromAudioFileOutput(filepath);
const synthesizer = new sdk.SpeechSynthesizer(speechConfig, audioConfig);
synthesizer.speakTextAsync(text,
result => {
synthesizer.close();
if (result) {
// return result as stream
resolve(filepath);
}
},
error => {
console.log(error);
synthesizer.close();
reject(error);
});
})
}
async populateVoiceList() {
const opts = {
headers: {
'Ocp-Apim-Subscription-Key': process.env.AZURE_API_KEY
}
}
const res = await fetch(process.env.AZURE_LIST_ENDPOINT, opts);
const json = await res.json();
json.forEach((voice) => {
if (this.voices[voice.DisplayName.toLowerCase()]) {
if (voice.Name.includes('Neural')) {
this.voices[voice.DisplayName.toLowerCase()] = voice.ShortName;
}
} else {
this.voices[voice.DisplayName.toLowerCase()] = voice.ShortName;
}
});
}
}

View File

@ -0,0 +1,47 @@
const BaseEngine = require('../BaseEngine');
const fetch = require('node-fetch');
const querystring = require('querystring');
module.exports = class extends BaseEngine {
constructor() {
super('eleven',"Eleven Labs TTS", "mp3");
this.voices = {};
this.populateVoiceList();
}
async populateVoiceList() {
const url = "https://api.elevenlabs.io/v1/voices";
const authorization = process.env.XI_API_KEY;
const opts = {
method: "get",
headers: {
'xi-api-key': authorization
},
}
const res = await fetch(url, opts);
const voices = await res.json();
voices.voices.forEach((i) => {
let voiceName = i.name.toLowerCase();
this.voices[voiceName] = i.voice_id;
});
}
getDefaultVoice() {
return 'Guillem';
}
async getSpeech(text, voice = this.getSpeechVoice(), params = {}) {
const url = "https://api.elevenlabs.io/v1/text-to-speech/" + this.getInternalVoiceName(voice);
const authorization = process.env.XI_API_KEY;
const opts = {
method: "post",
headers: {
'Content-Type': 'application/json',
'voice_id': this.getInternalVoiceName(voice),
'xi-api-key': authorization
},
body: JSON.stringify({
model_id: 'eleven_multilingual_v2',
text: text
})
};
return fetch(url, opts);
}
};

View File

@ -1,12 +1,15 @@
const BaseEngine=require('../BaseEngine')
const {spawn} = require('child_process')
const BaseEngine = require('../BaseEngine')
const { spawn } = require('child_process')
module.exports=class extends BaseEngine {
module.exports = class extends BaseEngine {
constructor() {
super('ESpeak','wav')
super('espeak','ESpeak', 'wav')
}
async getSpeechFile(text, filepath, voice='en', params={}) {
let proc=await spawn('espeak', ['-v', voice, '-w',filepath, '--stdin']);
getDefaultVoice() {
return 'en';
}
async getSpeechFile(text, filepath, voice = this.getDefaultVoice(), params = {}) {
let proc = await spawn('espeak', ['-v', voice, '-w', filepath, '--stdin']);
proc.stdin.end(text);
}
}

View File

@ -0,0 +1,36 @@
const BaseEngine = require('../BaseEngine')
const sdk = require('@google-cloud/text-to-speech');
const fs = require('fs');
const util = require('util');
module.exports = class GoogleCloudTTS extends BaseEngine {
constructor() {
super('google', 'Google Cloud TTS', 'wav');
this.client = new sdk.TextToSpeechClient();
this.client.initialize();
this.voices = {};
this.populateVoiceList();
}
async populateVoiceList() {
const [result] = await this.client.listVoices({});
const voiceList = result.voices;
voiceList.forEach((voice) => {
this.voices[voice.name.toLowerCase()] = { name: voice.name, lang: voice.languageCodes[0] };
});
}
getDefaultVoice() {
return 'en-US-Wavenet-A';
}
async getSpeechFile(text, filepath, voice = this.getDefaultVoice(), params = {}) {
const request = {
input: { text: text },
voice: { name: this.voices[voice].name, languageCode: this.voices[voice].lang },
audioConfig: { audioEncoding: 'LINEAR16' },
};
let [response] = await this.client.synthesizeSpeech(request);
const writeFile = util.promisify(fs.writeFile);
await writeFile(filepath, response.audioContent, 'binary');
return filepath;
}
}

View File

@ -1,13 +1,16 @@
const BaseEngine=require('../BaseEngine');
const BaseEngine = require('../BaseEngine');
const fetch = require('node-fetch');
const tts = require('google-tts-api');
module.exports= class extends BaseEngine {
module.exports = class extends BaseEngine {
constructor() {
super("Google Translate TTS","mp3");
super('gtranslate', "Google Translate TTS", "mp3");
}
async getSpeech(text, voice='en-us', params={}) {
const url = tts.getAudioUrl(text, {lang: voice});
getDefaultVoice() {
return 'en-us';
}
async getSpeech(text, voice = this.getDefaultVoice(), params = {}) {
const url = tts.getAudioUrl(text, { lang: voice });
return fetch(url);
}
};

View File

@ -0,0 +1,37 @@
const BaseEngine = require('../BaseEngine');
const fetch = require('node-fetch');
const tts = require('google-tts-api');
module.exports = class extends BaseEngine {
constructor() {
super('openai', "OpenAI TTS", "mp3");
this.voices = {
alloy: "alloy",
echo: "echo",
fable: "fable",
onyx: "onyx",
nova: "nova",
shimmer: "shimmer"
}
}
getDefaultVoice() {
return 'Alloy';
}
async getSpeech(text, voice = this.getDefaultVoice(), params = {}) {
const url = `https://api.openai.com/v1/audio/speech`;
const opts = {
method: "post",
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env["OPENAI_API_KEY"]}`
},
body: JSON.stringify({
model: "tts-1-hd",
input: text,
voice: voice,
})
}
console.log(opts);
return fetch(url, opts);
}
};

25
tts/sam/index.js 100644
View File

@ -0,0 +1,25 @@
const BaseEngine = require('../BaseEngine')
const Sam = require('sam-js');
const wavefile = require('wavefile');
const fs = require('fs');
module.exports = class extends BaseEngine {
constructor() {
super('sam', 'Software Automatic Mouth', 'wav')
}
getDefaultVoice() {
return 'sam';
}
async getSpeechFile(text, filepath, voice = this.getDefaultVoice(), params = {}) {
let sam = new Sam();
let phonetic = false;
if (text[0] == "$") {
text = text.slice(1);
phonetic = true;
}
const buf = sam.buf8(text, phonetic);
const file = new wavefile.WaveFile();
file.fromScratch(1, 22050, 8, buf);
fs.writeFileSync(filepath, file.toBuffer());
}
}

View File

@ -0,0 +1,47 @@
const BaseEngine = require('../BaseEngine');
const fetch = require('node-fetch');
const querystring = require('querystring');
const fs = require("fs");
module.exports = class extends BaseEngine {
constructor() {
super('unreal', "Unreal Speech TTS", "mp3");
this.voices = {
scarlett: 'Scarlett',
liv: 'Liv',
dan: 'Dan',
will: 'Will',
amy: 'Amy'
};
}
getDefaultVoice() {
return 'Liv';
}
async getSpeechFile(text, filepath, voice = this.getDefaultVoice(), params = {}) {
const url = "https://api.v6.unrealspeech.com/speech";
const authorization = process.env.UNREAL_API_KEY;
const opts = {
method: "post",
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authorization}`
},
body: JSON.stringify({
Bitrate: "320k",
Temperature: 0.1,
VoiceId: this.getInternalVoiceName(voice),
Text: text,
AudioFormat: "mp3"
})
};
const res = await fetch(url, opts);
const json = await res.json();
const data = await fetch(json.OutputUri);
const contents = await data.arrayBuffer();
const buf = Buffer.from(contents);
fs.writeFileSync(filepath, buf);
return filepath;
}
};

View File

@ -1,17 +1,41 @@
const BaseEngine=require('../BaseEngine');
const BaseEngine = require('../BaseEngine');
const fetch = require('node-fetch');
const querystring = require('querystring');
module.exports= class extends BaseEngine {
module.exports = class extends BaseEngine {
constructor() {
super("IBM Watson TTS","ogg");
super('watson',"IBM Watson TTS", "ogg");
this.voices = {};
this.populateVoiceList();
}
async getSpeech(text, voice='en-us', params={}) {
const url = process.env.watsonURL+"/v1/synthesize";
let buff=new Buffer('apikey:'+process.env.watsonAPIKey);
let b64auth=buff.toString('base64');
const authorization='Basic '+b64auth;
const opts={
async populateVoiceList() {
const url = process.env.watsonURL + "/v1/voices";
const authorization = this.IBMAuthString();
const opts = {
method: "get",
headers: {
'Authorization': authorization
},
}
const res = await fetch(url, opts);
const voices = await res.json();
voices.voices.forEach((i) => {
let voiceName = i.description.substring(0, i.description.indexOf(':')).toLowerCase();
this.voices[voiceName] = i.name;
});
}
getDefaultVoice() {
return 'Michael';
}
IBMAuthString() {
let buff = new Buffer('apikey:' + process.env.watsonAPIKey);
let b64auth = buff.toString('base64');
return 'Basic ' + b64auth;
}
async getSpeech(text, voice = this.getSpeechVoice(), params = {}) {
const url = process.env.watsonURL + "/v1/synthesize?voice=" + this.getInternalVoiceName(voice);
const authorization = this.IBMAuthString();
const opts = {
method: "post",
headers: {
'Content-Type': 'application/json',
@ -21,6 +45,6 @@ module.exports= class extends BaseEngine {
text: text
})
};
return fetch(url,opts);
return fetch(url, opts);
}
};