|
@@ -21,6 +21,8 @@ from io import BytesIO
|
|
|
from datetime import datetime, timezone, timedelta
|
|
from datetime import datetime, timezone, timedelta
|
|
|
from charset_normalizer import from_bytes
|
|
from charset_normalizer import from_bytes
|
|
|
from discord.ext import tasks
|
|
from discord.ext import tasks
|
|
|
|
|
+from discord.ext import voice_recv
|
|
|
|
|
+from voice_stt import STTSink
|
|
|
|
|
|
|
|
# Charger les variables d'environnement depuis le fichier .env
|
|
# Charger les variables d'environnement depuis le fichier .env
|
|
|
load_dotenv()
|
|
load_dotenv()
|
|
@@ -41,6 +43,8 @@ PROMPT_STATUS_CHANGE = str(os.getenv('PROMPT_STATUS_CHANGE', "Rédige un message
|
|
|
DELAY_TASK_UPDATE_STATUS = int(os.getenv('DELAY_TASK_UPDATE_STATUS', '30'))
|
|
DELAY_TASK_UPDATE_STATUS = int(os.getenv('DELAY_TASK_UPDATE_STATUS', '30'))
|
|
|
WEB_SEARCH_ENABLED = os.getenv('WEB_SEARCH_ENABLED', 'true').strip().lower() == 'true'
|
|
WEB_SEARCH_ENABLED = os.getenv('WEB_SEARCH_ENABLED', 'true').strip().lower() == 'true'
|
|
|
WEB_SEARCH_RESULT_COUNT = int(os.getenv('WEB_SEARCH_RESULT_COUNT', '3'))
|
|
WEB_SEARCH_RESULT_COUNT = int(os.getenv('WEB_SEARCH_RESULT_COUNT', '3'))
|
|
|
|
|
+VOICE_ENABLED = os.getenv('VOICE_ENABLED', 'false').strip().lower() == 'true'
|
|
|
|
|
+STT_URL = os.getenv('STT_URL', 'http://whisper-stt-chatbot:8000/transcribe')
|
|
|
if WEB_SEARCH_RESULT_COUNT < 1:
|
|
if WEB_SEARCH_RESULT_COUNT < 1:
|
|
|
raise ValueError('WEB_SEARCH_RESULT_COUNT doit être supérieur ou égal à 1')
|
|
raise ValueError('WEB_SEARCH_RESULT_COUNT doit être supérieur ou égal à 1')
|
|
|
|
|
|
|
@@ -257,6 +261,11 @@ class MyDiscordClient(discord.Client):
|
|
|
|
|
|
|
|
update_status.cancel()
|
|
update_status.cancel()
|
|
|
|
|
|
|
|
|
|
+ for voice in list(self.voice_clients):
|
|
|
|
|
+ if voice.sink:
|
|
|
|
|
+ await voice.sink.close()
|
|
|
|
|
+ await voice.disconnect(force=True)
|
|
|
|
|
+
|
|
|
if openai_client is not None:
|
|
if openai_client is not None:
|
|
|
await openai_client.close()
|
|
await openai_client.close()
|
|
|
openai_client = None
|
|
openai_client = None
|
|
@@ -524,6 +533,76 @@ async def on_ready():
|
|
|
else:
|
|
else:
|
|
|
logger.error(f"Canal avec ID {discord_channel_id} non trouvé.")
|
|
logger.error(f"Canal avec ID {discord_channel_id} non trouvé.")
|
|
|
|
|
|
|
|
|
|
+voice_command_lock = asyncio.Lock()
|
|
|
|
|
+
|
|
|
|
|
+async def handle_voice_command(message):
|
|
|
|
|
+ if not VOICE_ENABLED:
|
|
|
|
|
+ await message.channel.send("Le vocal est désactivé (VOICE_ENABLED=false).")
|
|
|
|
|
+ return
|
|
|
|
|
+ if message.guild is None or not message.author.voice:
|
|
|
|
|
+ await message.channel.send("Rejoins un salon vocal avant d'utiliser cette commande.")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ async with voice_command_lock:
|
|
|
|
|
+ voice = message.guild.voice_client
|
|
|
|
|
+ if voice and voice.channel != message.author.voice.channel:
|
|
|
|
|
+ await message.channel.send("Je suis déjà dans un autre vocal. Rejoins-moi pour utiliser !quit.")
|
|
|
|
|
+ return
|
|
|
|
|
+ if message.content.strip().lower() == '!quit':
|
|
|
|
|
+ if voice:
|
|
|
|
|
+ if voice.sink:
|
|
|
|
|
+ await voice.sink.close()
|
|
|
|
|
+ await voice.disconnect(force=True)
|
|
|
|
|
+ await message.channel.send("👋 Écoute vocale arrêtée.")
|
|
|
|
|
+ return
|
|
|
|
|
+ if voice:
|
|
|
|
|
+ await message.channel.send("Je suis déjà à l'écoute. Utilise !quit pour arrêter.")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ async def respond_to_voice(user, text):
|
|
|
|
|
+ async with message.channel.typing():
|
|
|
|
|
+ try:
|
|
|
|
|
+ result = await call_openai_api(text, user.display_name)
|
|
|
|
|
+ reply = result.choices[0].message.content if result else None
|
|
|
|
|
+ reply = transform_emote(filter_message(reply or ''), True)
|
|
|
|
|
+ for part in split_message(reply or "Désolé, le modèle ne répond pas pour le moment."):
|
|
|
|
|
+ await message.channel.send(part, allowed_mentions=discord.AllowedMentions.none())
|
|
|
|
|
+ except Exception as exc:
|
|
|
|
|
+ logger.warning("Réponse au vocal impossible : %s", type(exc).__name__)
|
|
|
|
|
+ await message.channel.send("Désolé, je n'ai pas pu répondre à cette phrase.")
|
|
|
|
|
+
|
|
|
|
|
+ sink = None
|
|
|
|
|
+ try:
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=5) as stt_client:
|
|
|
|
|
+ health = await stt_client.get(STT_URL.rsplit('/', 1)[0] + '/healthz')
|
|
|
|
|
+ health.raise_for_status()
|
|
|
|
|
+ voice = await message.author.voice.channel.connect(
|
|
|
|
|
+ cls=voice_recv.VoiceRecvClient, self_deaf=False,
|
|
|
|
|
+ )
|
|
|
|
|
+ sink = STTSink(respond_to_voice, STT_URL)
|
|
|
|
|
+ voice.listen(sink)
|
|
|
|
|
+ await message.channel.send("🎧 J'écoute le vocal et réponds ici après chaque prise de parole. !quit pour arrêter.")
|
|
|
|
|
+ except Exception as exc:
|
|
|
|
|
+ logger.warning("Connexion vocale impossible : %s", type(exc).__name__)
|
|
|
|
|
+ if sink:
|
|
|
|
|
+ await sink.close()
|
|
|
|
|
+ voice = message.guild.voice_client
|
|
|
|
|
+ if voice:
|
|
|
|
|
+ await voice.disconnect(force=True)
|
|
|
|
|
+ await message.channel.send("Impossible de démarrer le vocal. Vérifie le service STT, mes permissions et les logs du bot.")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@client_discord.event
|
|
|
|
|
+async def on_voice_state_update(member, before, after):
|
|
|
|
|
+ # Arrêter l'écoute quand la dernière personne quitte le salon.
|
|
|
|
|
+ async with voice_command_lock:
|
|
|
|
|
+ voice = member.guild.voice_client
|
|
|
|
|
+ if voice and not any(not person.bot for person in voice.channel.members):
|
|
|
|
|
+ if voice.sink:
|
|
|
|
|
+ await voice.sink.close()
|
|
|
|
|
+ await voice.disconnect(force=True)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
@client_discord.event
|
|
@client_discord.event
|
|
|
async def on_message(message):
|
|
async def on_message(message):
|
|
|
global conversation_history
|
|
global conversation_history
|
|
@@ -532,11 +611,14 @@ async def on_message(message):
|
|
|
if message.channel.id != discord_channel_id:
|
|
if message.channel.id != discord_channel_id:
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
- # Ignorer les messages du bot lui-même
|
|
|
|
|
- if message.author == client_discord.user:
|
|
|
|
|
|
|
+ # Ignorer les bots, y compris leurs commandes vocales.
|
|
|
|
|
+ if message.author.bot:
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
user_text = message.content.strip()
|
|
user_text = message.content.strip()
|
|
|
|
|
+ if user_text.lower() in ('!join', '!quit'):
|
|
|
|
|
+ await handle_voice_command(message)
|
|
|
|
|
+ return
|
|
|
file_content = None
|
|
file_content = None
|
|
|
attachment_filename = None
|
|
attachment_filename = None
|
|
|
|
|
|