Compare commits

...

21 Commits

Author SHA1 Message Date
e196bb7115 Temporary deactivate voice sending as it is not usablern
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 1m56s
2025-07-31 13:06:42 +02:00
1c4dcf427e Some fixes with audio channel
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 2s
2025-07-06 16:14:59 +02:00
e9b9e303a5 Quit if alone
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 2s
2025-07-05 20:25:40 +02:00
764aa7bced fix contains → includes
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 2s
2025-07-05 20:20:02 +02:00
de33c4f63d Fix destroy voice connection
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 2s
2025-07-05 20:17:58 +02:00
c9868d72a1 Remove trash ouput 2025-07-05 20:17:21 +02:00
15ff9c214a Remove unnecessary imports 2025-07-05 20:11:39 +02:00
050f1eb92e fix
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 2s
2025-07-05 20:11:04 +02:00
8458095f79 Send message to ninluc
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 2s
2025-07-05 20:09:53 +02:00
225e7d0bad add folder
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Failing after 2s
2025-07-05 20:00:18 +02:00
11020e0e87 Fix docker
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 2m53s
2025-07-05 19:44:07 +02:00
90da9c78c5 Voice chatting when alone in VC
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 44s
2025-07-05 18:19:45 +02:00
ec04dba632 Restart container unless stopped
Some checks failed
Dat_Boi upload to portainer / Deploy (push) Has been cancelled
2025-01-13 12:01:32 +01:00
382830340e Set timezone
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 2m32s
2025-01-07 12:18:35 +01:00
fed52827e8 Removed discord token logging
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 19s
2024-12-20 17:42:58 +01:00
89c5082f11 Added discord token env
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 8s
2024-12-20 17:42:00 +01:00
05fabcf65b Logging the token
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 8s
2024-12-20 17:39:38 +01:00
76b76738c2 Revert test + removed uncessary imports
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 9s
2024-12-20 17:38:03 +01:00
b17a8a90b3 test
All checks were successful
Dat_Boi upload to portainer / Deploy (push) Successful in 29s
2024-12-20 17:33:01 +01:00
1a9f533a12 Changed stack webhook 2024-12-20 17:25:48 +01:00
6a5cc045ee Rebuild on pull 2024-12-20 17:20:03 +01:00
14 changed files with 1105 additions and 391 deletions

View File

@ -6,4 +6,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send webhook to portainer
run: "curl -X POST -H 'Content-Type: application/json' -d '' http://172.23.0.1:20000/api/stacks/webhooks/6dbf44ed-eab0-4d52-a633-309d21ffd8c1"
run: "curl -X POST -H 'Content-Type: application/json' -d '' http://172.23.0.1:20000/api/stacks/webhooks/0012fba5-2fc7-40e7-92d8-c3d14c7deae4"

5
.gitignore vendored
View File

@ -7,4 +7,7 @@ package-lock.json
.idea/
# Environment variables
.env
.env
recordings/*
tts.mp3

View File

@ -10,11 +10,23 @@ ARG NODE_VERSION=18.4.0
FROM node:${NODE_VERSION}-alpine
# Set the timezone
RUN apk add --no-cache tzdata
ENV TZ=Europe/Brussels
# Use production node environment by default.
ENV NODE_ENV production
WORKDIR /usr/src/app
# Install python for dependencies that require it.
RUN apk update && apk add \
python3 \
make \
g++ \
ffmpeg \
curl
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.npm to speed up subsequent builds.
# Leverage a bind mounts to package.json and package-lock.json to avoid having to copy them into
@ -23,9 +35,9 @@ RUN --mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \
--mount=type=cache,target=/root/.npm \
npm ci
# Run the application as a non-root user.
USER node
#USER node
# Copy the rest of the source files into the image.
COPY . .

View File

@ -1,5 +1,4 @@
const { MessageEmbed } = require("discord.js");
const config = require("../../botconfig/config.json");
const ee = require("../../botconfig/embed.json");
const misc = require("../../botconfig/misc.json");
const { delay } = require("../../handlers/functions");

View File

@ -13,6 +13,10 @@ services:
context: .
environment:
NODE_ENV: production
DISCORD_TOKEN: ${DISCORD_TOKEN}
OPENAI_TOKEN: ${OPENAI_TOKEN}
restart: unless-stopped
pull_policy: build
# The commented out section below is an example of how to define a PostgreSQL
# database that your application can use. `depends_on` tells Docker Compose to

View File

@ -1,5 +1,4 @@
const { MessageEmbed } = require("discord.js");
const config = require("../botconfig/config.json");
const ee = require("../botconfig/embed.json");
module.exports = {
name: "dm",

View File

@ -1,124 +1,124 @@
const fs = require("fs");
const { MessageEmbed } = require("discord.js");
const config = require("../botconfig/config.json");
const ee = require("../botconfig/embed.json");
const { choose } = require("../handlers/functions");
const { createAudioPlayer, joinVoiceChannel, createAudioResource, StreamType } = require('@discordjs/voice');
const { VoiceConnectionStatus } = require('@discordjs/voice');
// const fs = require("fs");
// const { MessageEmbed } = require("discord.js");
// const config = require("../botconfig/config.json");
// const ee = require("../botconfig/embed.json");
// const { choose } = require("../handlers/functions");
// const { createAudioPlayer, joinVoiceChannel, createAudioResource, StreamType } = require('@discordjs/voice');
// const { VoiceConnectionStatus } = require('@discordjs/voice');
const player = createAudioPlayer();
// const player = createAudioPlayer();
const ffmpeg = require("ffmpeg-static");
// const ffmpeg = require("ffmpeg-static");
module.exports = {
name: "joinvc",
isPrivate: false,
usage: "joinvc <ID_DE_LA_VOC>",
description: "Rejoins le salon vocal spécifié à l'aide de son id (activer le mode développeur → clic droit sur la voc → copier l'identifiant).",
run: async (client, message, text, args) => {
try {
if (!args[0]) {
message.channel.send({embeds: [
new MessageEmbed()
.setColor(ee.wrongcolor)
.setTitle(`❌ ERREUR | Pas assez d'arguments`)
.setDescription("`[help joinvc` pour plus d'informations")
]})
return;
}
// module.exports = {
// name: "joinvc",
// isPrivate: false,
// usage: "joinvc <ID_DE_LA_VOC>",
// description: "Rejoins le salon vocal spécifié à l'aide de son id (activer le mode développeur → clic droit sur la voc → copier l'identifiant).",
// run: async (client, message, text, args) => {
// try {
// if (!args[0]) {
// message.channel.send({embeds: [
// new MessageEmbed()
// .setColor(ee.wrongcolor)
// .setTitle(`❌ ERREUR | Pas assez d'arguments`)
// .setDescription("`[help joinvc` pour plus d'informations")
// ]})
// return;
// }
let channel = client.channels.cache.find(channel => channel.id == `${args[0]}`)
// let channel = client.channels.cache.find(channel => channel.id == `${args[0]}`)
if (
!channel || !channel.isVoice()
|| !channel.permissionsFor(channel.guild.me).has("CONNECT")
|| !channel.permissionsFor(channel.guild.me).has("SPEAK")
) {
// message.react("❌")
return message.channel.send({embeds : [
new MessageEmbed()
.setColor(ee.wrongcolor)
.setTitle(`❌ ERREUR | Pas de voc trouvée :(`)]}
);
}
// if (
// !channel || !channel.isVoice()
// || !channel.permissionsFor(channel.guild.members.me).has("CONNECT")
// || !channel.permissionsFor(channel.guild.members.me).has("SPEAK")
// ) {
// // message.react("❌")
// return message.channel.send({embeds : [
// new MessageEmbed()
// .setColor(ee.wrongcolor)
// .setTitle(`❌ ERREUR | Pas de voc trouvée :(`)]}
// );
// }
player.on('error', error => {
// subscription.unsubscribe()
// player.on('error', error => {
// // subscription.unsubscribe()
if (connection.state.status != "destroyed") {
connection.destroy();
}
});
// if (connection.state.status != "destroyed") {
// connection.destroy();
// }
// });
player.on('idle', () => {
// subscription.unsubscribe()
// player.on('idle', () => {
// // subscription.unsubscribe()
if (connection.state.status != "destroyed") {
connection.destroy();
}
console.log('Info : Ended track');
});
// if (connection.state.status != "destroyed") {
// connection.destroy();
// }
// console.log('Info : Ended track');
// });
let videos = fs.readdirSync(`./sounds/`).filter((file) => file.endsWith(".mp3"))
// let videos = fs.readdirSync(`./sounds/`).filter((file) => file.endsWith(".mp3"))
// voiceState.setSelfMute(0);
// var channel = voiceState.channel
const connection = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guild.id,
adapterCreator: channel.guild.voiceAdapterCreator,
});
// const subscription = connection.subscribe(player);
var rdVideoLink = choose(videos)
// // voiceState.setSelfMute(0);
// // var channel = voiceState.channel
// const connection = joinVoiceChannel({
// channelId: channel.id,
// guildId: channel.guild.id,
// adapterCreator: channel.guild.voiceAdapterCreator,
// });
// // const subscription = connection.subscribe(player);
// var rdVideoLink = choose(videos)
try {
connection.on(VoiceConnectionStatus.Ready, async() => {
connection;
let subscription = connection.subscribe(player);
// try {
// connection.on(VoiceConnectionStatus.Ready, async() => {
// connection;
// let subscription = connection.subscribe(player);
const resource = createAudioResource("./sounds/" + rdVideoLink, {
inputType: StreamType.Arbitrary
});
resource.playStream.on("finish", () => {
setTimeout(() => {
// subscription.unsubscribe()
if (connection.state.status != "destroyed") {
connection.destroy();
}
}, 2000)
})
// const resource = createAudioResource("./sounds/" + rdVideoLink, {
// inputType: StreamType.Arbitrary
// });
// resource.playStream.on("finish", () => {
// setTimeout(() => {
// // subscription.unsubscribe()
// if (connection.state.status != "destroyed") {
// connection.destroy();
// }
// }, 2000)
// })
// if (subscription) {
// // Unsubscribe after 5 seconds (stop playing audio on the voice connection)
// setTimeout(() => subscription.unsubscribe(), 5_000);
// }
// // if (subscription) {
// // // Unsubscribe after 5 seconds (stop playing audio on the voice connection)
// // setTimeout(() => subscription.unsubscribe(), 5_000);
// // }
player.play(resource);
})
// player.play(resource);
// })
// setTimeout(() => {
// subscription.unsubscribe()
// if (connection.state.status != "destroyed") {
// connection.destroy();
// }
// }, 60 * 1000)
// // setTimeout(() => {
// // subscription.unsubscribe()
// // if (connection.state.status != "destroyed") {
// // connection.destroy();
// // }
// // }, 60 * 1000)
message.channel.send(`Je joue *${rdVideoLink.split('.')[0]}* dans le salon ${channel} du serveur **${channel.guild.name}**`);
} catch (error) {
console.log(error.message)
}
// message.channel.send(`Je joue *${rdVideoLink.split('.')[0]}* dans le salon ${channel} du serveur **${channel.guild.name}**`);
// } catch (error) {
// console.log(error.message)
// }
} catch (e) {
console.log(String(e.stack).bgRed);
return message.channel.send({embeds : [
new MessageEmbed()
.setColor(ee.wrongcolor)
.setTitle(`❌ ERREUR | Une erreur est survenue : `)
.setDescription(`\`\`\`${e.stack}\`\`\``)]}
);
}
},
};
// } catch (e) {
// console.log(String(e.stack).bgRed);
// return message.channel.send({embeds : [
// new MessageEmbed()
// .setColor(ee.wrongcolor)
// .setTitle(`❌ ERREUR | Une erreur est survenue : `)
// .setDescription(`\`\`\`${e.stack}\`\`\``)]}
// );
// }
// },
// };

View File

@ -1,87 +0,0 @@
const fs = require("fs");
const config = require("../../botconfig/config.json"); //loading config file with token and prefix, and settings
const ee = require("../../botconfig/embed.json"); //Loading all embed settings like color footertext and icon ...
const Discord = require("discord.js"); //this is the official discord.js wrapper for the Discord Api, which we use!
const { sendNinluc } = require("../../handlers/functions.js")
const { createAudioPlayer, joinVoiceChannel, createAudioResource, StreamType } = require('@discordjs/voice');
const { VoiceConnectionStatus } = require('@discordjs/voice');
const player = createAudioPlayer();
const ffmpeg = require("ffmpeg-static");
// const { generateDependencyReport } = require('@discordjs/voice');
// console.log(generateDependencyReport().blue);
module.exports = async (client, oldState, voiceState) => {
try {
if (voiceState === null || voiceState.channel === null || !voiceState.guild || oldState.channel == voiceState.channel || voiceState.channel.full || !voiceState.channel.joinable) return;
if (!voiceState.channel.permissionsFor(voiceState.guild.me).has("CONNECT") || !voiceState.channel.permissionsFor(voiceState.guild.me).has("SPEAK")) {return;}
player.on('error', error => {
// subscription.unsubscribe()
connection.destroy();
console.error('Error:', error.message, 'with track', error.resource.metadata.title);
});
// Si c'est du bot alors se démute
if (voiceState.member.user.id === client.user.id && voiceState.mute) {
// Si il sait se demute
if (voiceState.channel.permissionsFor(voiceState.guild.me).has("MUTE_MEMBERS")) {
voiceState.setMute(false);
return;
} else {return;}
}
else if (voiceState.member.user.id === client.user.id) {
return;
}
var finish = true
var playRick = false;
if (Math.floor(Math.random() * 14) == 0) {playRick = true}
if (playRick && !voiceState.deaf && finish) {
let videos = fs.readdirSync(`./sounds/`).filter((file) => file.endsWith(".mp3"))
// voiceState.setSelfMute(0);
// var channel = voiceState.channel
const connection = joinVoiceChannel({
channelId: voiceState.channel.id,
guildId: voiceState.guild.id,
adapterCreator: voiceState.guild.voiceAdapterCreator,
});
// const subscription = connection.subscribe(player);
var rdVideoLink = videos[Math.floor(Math.random() * videos.length)]
try {
connection.on(VoiceConnectionStatus.Ready, async() => {
connection;
let subscription = connection.subscribe(player);
const resource = createAudioResource("./sounds/" + rdVideoLink, {
inputType: StreamType.Arbitrary
});
resource.playStream.on("finish", () => {
setTimeout(() => {
subscription.unsubscribe()
connection.destroy();
}, 2000)
})
player.play(resource);
})
sendNinluc(client, `Je joue *${rdVideoLink.split('.')[0]}* dans le salon ${voiceState.channel} du serveur **${voiceState.guild.name}**`);
} catch (error) {
console.log(error.message)
}
}
} catch (e) {
console.log(e.stack)
}
}

View File

@ -0,0 +1,439 @@
const fs = require("fs");
const {
joinVoiceChannel,
createAudioPlayer,
createAudioResource,
AudioPlayerStatus,
EndBehaviorType,
StreamType,
NoSubscriberBehavior,
} = require("@discordjs/voice");
const ffmpeg = require('fluent-ffmpeg');
const prism = require("prism-media");
const { getRandomInt, sendNinluc } = require("../../handlers/functions");
// const { generateDependencyReport } = require('@discordjs/voice');
// console.log(generateDependencyReport().blue);
const CHAT_MAX_LENGTH = (3 * 2) + 1; // 3 LLM reply
const SYSTEM_PROMPT =
`Tu est DatBoi, un bot discord qui fait semblant d'être une personne réelle. `
+ ` Ta personnalité est amicale mais ennuyant, tu aime faire des blagues nulles et ennuyantes de jeunes. `
+ ` Tu parle en language familier et te fais passer pour un jeune adulte. Tout le monde qui te parle est ton ami. `
+ ` Les entrées de l'utilisateur ont été transcrites par un service de transcription audio, il peut y paraitre fréquemment des erreurs, essaye d'interpreter le texte sans (par exemple Datboi est souvent transcrit "d'être beau" ou encore "Dead boy"). `
+ ` Les entrées de l'utilisateur donne le prénom de la personne qui te parle. Tu ne dois pas donner ton nom dans ta réponse.`
+ ` Ta sortie sera entièrement donnée à un service de synthèse vocale dans la variable "reply", donc tu ne dois pas dire "voici ma réponse" ou "ma réponse est", tu dois juste donner la réponse. `
+ ` Tu dois aussi essayer de donner les mots de façon qu'il puissent être bien prononcés par la synthèse vocale, donc évite les abréviations et les mots trop compliqués. `
+ ` Tu ne dois pas dire que tu es un bot, tu dois faire semblant d'être une personne réelle. `
+ ` Cela signifie aussi d'éviter les caractères spéciaux comme les émojis, tu peux cependant utiliser de la ponctuation ou des accents. `
+ ` Ta réponse doit être courte (max 3 phrases sauf si le contexte ne le permet pas comme une demande d'explication sur un gros sujet) et en français.`
+ ` Le contexte ici est que tu rejoins un salon vocal quand il n'y a qu'une seule personne, histoire de pouvoir tranquillement discuter avec elle.`
+ ` Voici un peu d'informations sur les personnes qui te parlent (tu n'est pas obligé d'utiliser) : `
+ ` - Nicolas : est très beau, tout le monde est amoureux de lui (en blague)`
+ ` - Matthias : ton maître (créateur), code beaucoup et pas très bon gamer`
+ ` - Régis : va à l'armée et est un fou du gambling`
+ ` - Hugo : Deviens fou après avoir "travailé à la poste"`
+ ` - Noa : Un beau gosse`
+ ` - Jean : un bon gamer`
+ `Voici un peu les sujets du moment dans le serveur, tu n'est pas obligé d'utiliser : `
+ ` - Le gambling`
+ ` - Le jeu vidéo "Rainbow Six Siege" renommé récemment "Rainbow Six X"`
+ `Voici un peu plus d'informations sur toi : `
+ ` - Tu es un bot discord mais tu prétends d'être une personne réelle`
+ ` - Tu fais des blagues du style "Quoi ? - Feur" ou tu lance une vidéo comme un rick roll quand quelqu'un rejoint un salon vocal`
+ `Il y a un système de fin de conversation, pour cela, tu dois mettre la variable "end_conversation" à true.`
+ `Tu peux enclencher la fin de la conversation si l'utilisateur te demande de le faire, s'il n'a pas l'air de vouloir te parler ou si tu n'as pas d'autres choses à dire.`
+ `Au bout de maximum 3 réponses à l'utilisateur, tu devra enclencher la fin de la conversation, avec optionnelement une salutation ("à plus" ou encore "à demain" par exemple).`
;
module.exports = async (client, oldState, voiceState) => {
// DOn't do anything
if (getRandomInt(5) == 1) return;
var player, isRecording = false, isThinking = false, shouldStop = false, connection;
let llmChat = [];
llmChat.push({
role: "system",
content: SYSTEM_PROMPT.replace(/'/g, ""),
});
try {
if (
voiceState === null ||
voiceState.channel === null ||
!voiceState.guild ||
oldState.channel == voiceState.channel ||
voiceState.channel.full ||
!voiceState.channel.joinable
)
return;
if (
!voiceState.channel.permissionsFor(voiceState.guild.members.me).has("CONNECT") ||
!voiceState.channel.permissionsFor(voiceState.guild.members.me).has("SPEAK")
) {
return;
}
// Si c'est du bot alors se démute
if (voiceState.member.user.id === client.user.id && voiceState.mute) {
// Si il sait se demute
if (
voiceState.channel
.permissionsFor(voiceState.guild.members.me)
.has("MUTE_MEMBERS")
) {
voiceState.setMute(false);
return;
} else {
return;
}
} else if (voiceState.member.user.id === client.user.id) {
return;
}
// Not the only reel account in vocal channel
if (voiceState.channel.members.filter(m => !m.user.bot && m.id !== client.user.id).size > 1) return;
// === Connection ===
connection = joinVoiceChannel({
channelId: voiceState.channel.id,
guildId: voiceState.channel.guild.id,
adapterCreator: voiceState.channel.guild.voiceAdapterCreator,
selfDeaf: false,
selfMute: false,
});
// Fix no sound after idle for more than 60 seconds
// https://github.com/discordjs/discord.js/issues/9185#issuecomment-1452514375
const networkStateChangeHandler = (oldNetworkState, newNetworkState) => {
const newUdp = Reflect.get(newNetworkState, 'udp');
clearInterval(newUdp?.keepAliveInterval);
}
connection.on('stateChange', (oldState, newState) => {
const oldNetworking = Reflect.get(oldState, 'networking');
const newNetworking = Reflect.get(newState, 'networking');
oldNetworking?.off('stateChange', networkStateChangeHandler);
newNetworking?.on('stateChange', networkStateChangeHandler);
// If alone in channel, quit
if (!voiceState.channel?.members || voiceState.channel.members.filter(m => !m.user.bot && m.id !== client.user.id).size < 1) {
shouldStop = true;
stop();
return;
}
});
player = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Play, // Stop the player when there are no subscribers
},
debug: true,
});
player.on("error", (error) => {
stop();
console.error(`X Error playing audio: ${error.message}`, "error", 1);
});
player.on("stateChange", (oldState, newState) => {
if (newState.status === AudioPlayerStatus.Idle) {
console.log(`> Finished playing audio`);
}
console.log(`> Player state changed from ${oldState.status} to ${newState.status}`);
});
player.on("close", () => {
console.log(`> Player closed for user ${userId}`, "info", 2);
return;
});
// connection.subscribe(player);
// while(!shouldStop) {
console.log(`> handling recording`);
handleRecording(connection, voiceState.channel);
// }
// return;
} catch (e) {
console.log(e.stack);
}
// === Functions ===
function handleRecording(connection, channel) {
if (isRecording) return;
isRecording = true;
// If alone in channel, quit
if (!channel?.members || channel.members.filter(m => !m.user.bot && m.id !== client.user.id).size < 1) {
shouldStop = true;
stop();
return;
}
const receiver = connection.receiver;
channel.members.forEach((member) => {
if (member.user.bot) return;
const filePath = `./recordings/${member.user.id}.pcm`;
const writeStream = fs.createWriteStream(filePath);
const listenStream = receiver.subscribe(member.user.id, {
end: {
behavior: EndBehaviorType.AfterSilence,
duration: 2500,
},
});
const opusDecoder = new prism.opus.Decoder({
frameSize: 960,
channels: 2,
rate: 48000,
});
listenStream.pipe(opusDecoder).pipe(writeStream);
writeStream.on("finish", () => {
console.log(`> Audio recorded for ${member.user.username}`);
if (!isThinking) {
isThinking = true;
convertAndHandleFile(filePath, member.user.id, connection, channel);
}
});
});
isRecording = false;
}
function convertAndHandleFile(filePath, userid, connection, channel) {
const mp3Path = filePath.replace(".pcm", ".mp3");
ffmpeg(filePath)
.inputFormat("s16le")
.audioChannels(1)
.format("mp3")
.on("error", (err) => {
console.log(`X Error converting file: ${err.message}`, "error", 1);
currentlythinking = false;
})
.save(mp3Path)
.on("end", () => {
console.log(`> Converted to MP3: ${mp3Path}`, "info", 2);
// Remove the pcm file
fs.unlink(filePath, (err) => {if (err != null) {console.log("error deleting cpm file : " + err);}});
answerUser(mp3Path, userid, connection, channel);
});
}
async function answerUser(fileName, userId, connection, channel) {
let transcription = await transcribeAudio(fileName);
console.log(`> Transcription: ${transcription}`);
if (!transcription || transcription.trim().length < 1) {
console.error(`X Transcription failed or is empty!`, "error", 1);
isThinking = false;
handleRecording(connection, channel);
return;
}
let llmAnswer = await getLLMAnswer(transcription, userId);
console.log(`> LLM Answer: ${llmAnswer.reply} ${llmAnswer.end_conversation ? "(end conversation)" : ""}`);
// Fin de la conversation
shouldStop = llmAnswer.end_conversation || llmChat.length >= CHAT_MAX_LENGTH || llmAnswer.reply.toLowerCase().includes("à plus");
if (!llmAnswer || llmAnswer.length < 1) {
console.error(`X LLM Answer failed or is empty!`, "error", 1);
isThinking = false;
handleRecording(connection, channel);
return;
}
await getTTS(llmAnswer.reply);
let ttsFile = `tts.mp3`;
console.log(`> TTS File: ${ttsFile}`);
if (!fs.existsSync(ttsFile)) {
console.error(`TTS file ${ttsFile} not found!`);
return;
}
// Play the tts file in the voice channel
const resource = createAudioResource(ttsFile, {
inputType: StreamType.Arbitrary,
inlineVolume: true,
});
resource.volume.setVolume(1.3);
connection;
connection.subscribe(player);
player.on("idle", () => {
isThinking = false;
// connection.destroy();
if (!shouldStop) {
handleRecording(connection, channel);
}
else {
console.log(`> Ending conversation with user ${userId}`);
sendConversationToNinluc(userId);
stop();
}
return;
});
player.play(resource);
// player.pause();
// player.unpause();
}
function transcribeAudio(filePath) {
// Make a call to the API with curl
// Example of working curl command:
// curl -s "https://speaches.matthiasg.dev/v1/audio/transcriptions" -F "file=@/pathToFile.mp3" -F "model=Infomaniak-AI/faster-whisper-large-v3-turbo"
return new Promise((resolve, reject) => {
const { exec } = require("child_process");
exec(`curl -s "https://speaches.matthiasg.dev/v1/audio/transcriptions" -F "file=@${filePath}" -F "model=Systran/faster-whisper-small" -F "language=fr"`, (error, stdout, stderr) => {
if (error) {
console.error(`X Error during transcription: ${error.message}`, "error", 1);
reject(error);
return;
}
if (stderr) {
console.error(`X Transcription stderr: ${stderr}`, "error", 1);
reject(stderr);
return;
}
try {
const response = JSON.parse(stdout);
// === FILTER OUTPUT ===
response.text = response.text.replace(/Sous-titres réalisés par la communauté d'Amara.org/g, "");
if (response.text.toLowerCase().includes("je vous remercie d'avoir regardé cette vidéo")) {
return ""; // Output is trash, ignore it
}
resolve(response.text || "");
} catch (parseError) {
console.error(`X Error parsing transcription response: ${parseError.message}`, "error", 1);
reject(parseError);
}
});
});
}
async function getLLMAnswer(transcription, userId) {
// get the name of the user
let userName = getUserById(userId);
transcription = `(${userName}) : ${transcription}`;
// Add the transcription to the llmChat
llmChat.push({
role: "user",
content: transcription.replace(/'/g, ""),
});
// Make a call to the OpenAI compatible API at https://chat.matthiasg.dev/ollama/chat/completions
// With the token in the OPENAI_TOKEN env variable
return new Promise((resolve, reject) => {
const { exec } = require("child_process");
let chatMessages = JSON.stringify(llmChat);
exec(`curl -s -X POST "https://chat.matthiasg.dev/ollama/api/chat" -H "Content-Type: application/json" -H "Authorization: Bearer ${process.env.OPENAI_TOKEN}" -d '{"model": "gemma3n:e2b", "stream": false, "messages": ${chatMessages}, "format": {"type": "object", "properties": {"reply": {"type": "string"}, "end_conversation": {"type": "boolean"}}, "required": ["reply", "end_conversation"]}}'`, (error, stdout, stderr) => {
if (error) {
console.error(`X Error during LLM request: ${error.message}`, "error", 1);
reject(error);
return;
}
if (stderr) {
console.error(`X LLM stderr: ${stderr}`, "error", 1);
reject(stderr);
return;
}
try {
const response = JSON.parse(stdout);
if (!response || !response.message || !response.message.content) {
console.error("X Invalid LLM response format", "error", 1);
reject(new Error("Invalid LLM response format"));
return;
}
let llmResponse = JSON.parse(response.message.content) || "";
// Add the LLM response to the chat history
llmChat.push({
role: "assistant",
content: llmResponse.reply.replace(/'/g, ""),
});
resolve(llmResponse);
} catch (parseError) {
console.error(`X Error parsing LLM response: ${parseError.message}`, "error", 1);
reject(parseError);
}
});
});
}
function getUserById(userId) {
return [
{ id: "613751180413108289", name: "Nicolas" },
{ id: "417731861033385985", name: "Matthias" },
{ id: "368119137957838849", name: "Régis" },
{ id: "516294391439163403", name: "Hugo" },
{ id: "344568112932192266", name: "Noa" },
{ id: "520687970273984543", name: "Jean" }
].map(user => {
if (user.id === userId) {
return user.name;
}
}).filter(name => name)[0] || null;
}
function getTTS(text) {
// Curl to the OpenAPI compatible API
/*
curl "https://speaches.matthiasg.dev/v1/audio/speech" -s -H "Content-Type: application/json" \
--output audio.mp3 \
--data @- << EOF
{
"input": "Hello World!",
"model": "speaches-ai/piper-fr_FR-upmc-medium",
"voice": "upmc"
}
EOF
*/
text = text.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"");
text = JSON.stringify(text);
return new Promise((resolve, reject) => {
const { exec } = require("child_process");
exec(`curl -s "https://speaches.matthiasg.dev/v1/audio/speech" -H "Content-Type: application/json" --output tts.mp3 --data @- << EOF\n{"input": ${text}, "model": "speaches-ai/piper-fr_FR-upmc-medium", "voice": "upmc"}\nEOF`, (error, stdout, stderr) => {
if (error) {
console.error(`X Error during TTS request: ${error.message}`, "error", 1);
reject(error);
return;
}
if (stderr) {
console.error(`X TTS stderr: ${stderr}`, "error", 1);
reject(stderr);
return;
}
resolve("tts.mp3");
});
});
}
function stop() {
player.stop();
//connection.removeAllListeners();
try {
connection.destroy();
} catch (e) {}
}
function sendConversationToNinluc(userId) {
sendNinluc(client, "Fin de la conversation avec l'utilisateur " + userId);
llmChat.forEach((message) => {
if (message.role === "user") {
sendNinluc(client, `(<@${userId}>) : ${message.content}`);
} else if (message.role === "assistant") {
sendNinluc(client, `DatBoi : ${message.content}`);
}
// ignore system message
})
}
};

View File

@ -1,5 +1,4 @@
const { MessageEmbed } = require("discord.js");
const config = require("../botconfig/config.json");
const ee = require("../botconfig/embed.json");
const { choose, isNinluc } = require("../handlers/functions");

724
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,12 +9,16 @@
"author": "Tomato#6966 (author of this template), Ninluc",
"license": "ISC",
"dependencies": {
"@discordjs/opus": "^0.10.0",
"@discordjs/voice": "^0.10.0",
"ascii-table": "0.0.9",
"colors": "^1.4.0",
"discord.js": "^13.8.1",
"ffmpeg-static": "^4.4.1",
"fluent-ffmpeg": "^2.1.3",
"libsodium-wrappers": "^0.7.10",
"moment": "^2.29.4"
"moment": "^2.29.4",
"prism": "^4.1.2",
"prism-media": "^1.3.5"
}
}

0
recordings/.gitkeep Normal file
View File

View File

@ -1,7 +1,5 @@
const { MessageEmbed, Message } = require("discord.js");
const config = require("../botconfig/config.json");
const { MessageEmbed } = require("discord.js");
const ee = require("../botconfig/embed.json");
const { sendNinluc } = require("../handlers/functions");
const RAPPEL_CHANNEL_ID = "1296051297262370866";