浏览代码

Raccourcir le vocal sans reflexion avec cinq messages et ajouter status

Andy Esnard 20 小时之前
父节点
当前提交
deaf9dc84f
共有 3 个文件被更改,包括 78 次插入 和 2 次删除
  1. 25 2
      chatbot.py
  2. 44 0
      test_voice_stt.py
  3. 9 0
      voice_stt.py

+ 25 - 2
chatbot.py

@@ -417,7 +417,7 @@ async def call_openai_api(user_text, user_name, voice=False):
     # Assembler les messages avec le prompt de personnalité en premier
     messages = [
         {"role": "system", "content": PERSONALITY_PROMPT}
-    ] + conversation_history + [message_to_send]
+    ] + (conversation_history[-5:] if voice else conversation_history) + [message_to_send]
 
     # Le contexte web reste limité à cet appel, sans remplir l'historique.
     web_results = await search_web(user_text)
@@ -445,15 +445,19 @@ async def call_openai_api(user_text, user_name, voice=False):
             "Il peut contenir des erreurs de reconnaissance, notamment sur les noms propres. "
             "Interprète les petites erreurs à l'aide du contexte sans inventer les propos. "
             "Si une ambiguïté change le sens, demande une précision. "
-            "Réponds naturellement à l'ensemble des interventions, en distinguant les personnes."
+            "Réponds naturellement à l'ensemble des interventions, en distinguant les personnes. "
+            "Réponds en une à trois phrases courtes, sauf demande explicite de détails."
         )
 
+    voice_options = {"extra_body": {"options": {"think": False}}} if voice else {}
+
     try:
         response = await openai_client.chat.completions.create(
             model=MODEL,
             stream=False,
             messages=messages,
             temperature=TEMPERATURE,
+            **voice_options,
             **search_options
         )
 
@@ -480,6 +484,7 @@ async def call_openai_api(user_text, user_name, voice=False):
             # Permettre de préciser la recherche, puis imposer une réponse à la limite.
             response = await openai_client.chat.completions.create(
                 model=MODEL, stream=False, messages=messages, temperature=TEMPERATURE,
+                **voice_options,
                 **(search_options if search_count < 3 and _ < 2 else {})
             )
 
@@ -544,6 +549,21 @@ async def on_ready():
 
 voice_command_lock = asyncio.Lock()
 
+async def handle_status(message):
+    voice = message.guild.voice_client if message.guild else None
+    sink = getattr(voice, 'sink', None) if voice else None
+    lines = [f"Satoko {BOT_VERSION} — modèle : {MODEL}",
+             f"Vocal : {'connecté' if voice and voice.is_connected() else 'déconnecté'}"]
+    if sink:
+        def duration(value):
+            return f"{value:.1f} s" if value is not None else "pas encore mesuré"
+        lines += [f"Réponse en cours : {'oui' if sink.responding else 'non'}",
+                  f"Audio en attente STT : {sink.queue.qsize()} — transcriptions en attente : {len(sink.pending)}",
+                  f"Dernier STT réussi : {duration(sink.last_stt_seconds)}",
+                  f"Dernier traitement de réponse : {duration(sink.last_reply_seconds)}"]
+    await message.channel.send("\n".join(lines), allowed_mentions=discord.AllowedMentions.none())
+
+
 async def handle_voice_command(message):
     if not VOICE_ENABLED:
         await message.channel.send("Le vocal est désactivé (VOICE_ENABLED=false).")
@@ -639,6 +659,9 @@ async def on_message(message):
         return
 
     user_text = message.content.strip()
+    if user_text.lower() == '!status':
+        await handle_status(message)
+        return
     if user_text.lower() in ('!join', '!quit'):
         await handle_voice_command(message)
         return

+ 44 - 0
test_voice_stt.py

@@ -158,6 +158,50 @@ class VoiceTests(unittest.IsolatedAsyncioTestCase):
 
 
 class CommandTests(unittest.IsolatedAsyncioTestCase):
+    async def test_voice_limits_history_and_disables_thinking_only_for_voice(self):
+        tree = ast.parse(Path(__file__).with_name('chatbot.py').read_text(encoding='utf-8'))
+        function = next(n for n in tree.body if isinstance(n, ast.AsyncFunctionDef)
+                        and n.name == 'call_openai_api')
+        response = SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(
+            tool_calls=None, content='Bonjour'))])
+        create = AsyncMock(return_value=response)
+        history = [{'role': 'user', 'content': str(i)} for i in range(10)]
+        namespace = dict(transform_emote=lambda text, output: text, PERSONALITY_PROMPT='Personnalité',
+                         conversation_history=history, search_web=AsyncMock(return_value=None),
+                         WEB_SEARCH_ENABLED=True, MODEL='test', TEMPERATURE=1, logger=Mock(),
+                         openai_client=SimpleNamespace(chat=SimpleNamespace(
+                             completions=SimpleNamespace(create=create))),
+                         add_to_conversation_history=AsyncMock())
+        exec(compile(ast.Module(body=[function], type_ignores=[]), '<api>', 'exec'), namespace)
+        await namespace['call_openai_api']('Bonjour', 'Alice', voice=True)
+        args = create.call_args.kwargs
+        self.assertEqual(args['messages'][1:-1], history[-5:])
+        self.assertEqual(args['extra_body']['options']['think'], False)
+        self.assertIn('une à trois phrases', args['messages'][0]['content'])
+        self.assertIn('search_web', args['messages'][0]['content'])
+        await namespace['call_openai_api']('Bonjour', 'Alice')
+        args = create.call_args.kwargs
+        self.assertEqual(args['messages'][1:-1], history)
+        self.assertNotIn('extra_body', args)
+
+    async def test_status_connected_and_disconnected(self):
+        tree = ast.parse(Path(__file__).with_name('chatbot.py').read_text(encoding='utf-8'))
+        function = next(n for n in tree.body if isinstance(n, ast.AsyncFunctionDef)
+                        and n.name == 'handle_status')
+        namespace = dict(BOT_VERSION='test', MODEL='gemma', discord=discord)
+        exec(compile(ast.Module(body=[function], type_ignores=[]), '<status>', 'exec'), namespace)
+        message = SimpleNamespace(guild=None, channel=SimpleNamespace(send=AsyncMock()))
+        await namespace['handle_status'](message)
+        self.assertIn('déconnecté', message.channel.send.call_args.args[0])
+        sink = SimpleNamespace(responding=True, queue=asyncio.Queue(), pending=[1, 2],
+                               last_stt_seconds=0.5, last_reply_seconds=None)
+        message.guild = SimpleNamespace(voice_client=SimpleNamespace(sink=sink, is_connected=lambda: True))
+        await namespace['handle_status'](message)
+        text = message.channel.send.call_args.args[0]
+        self.assertIn('transcriptions en attente : 2', text)
+        self.assertIn('0.5 s', text)
+        self.assertIn('pas encore mesuré', text)
+
     async def test_join_replies_in_command_channel_and_quit_closes_sink(self):
         # Charger uniquement la fonction : aucun token ni connexion Discord.
         tree = ast.parse(Path(__file__).with_name('chatbot.py').read_text(encoding='utf-8'))

+ 9 - 0
voice_stt.py

@@ -29,6 +29,9 @@ class STTSink(voice_recv.AudioSink):
         self.buffers = {}
         self.decoders = {}
         self.closed = False
+        self.responding = False
+        self.last_stt_seconds = None
+        self.last_reply_seconds = None
         self.last_error = 0
         self.stats_lock = threading.Lock()
         self.stats = Counter(audio=0, rtcp=0, pcm=0, audible=0, phrases=0, stt=0)
@@ -181,6 +184,7 @@ class STTSink(voice_recv.AudioSink):
                     )
                     response.raise_for_status()
                     text = response.json()['text'].strip()
+                    self.last_stt_seconds = time.monotonic() - started
                     logger.info("Retour STT : HTTP=%d durée=%.2fs caractères=%d",
                                 response.status_code, time.monotonic() - started, len(text))
                     if not text:
@@ -211,12 +215,17 @@ class STTSink(voice_recv.AudioSink):
             batch, self.pending = self.pending, []
             if not batch:
                 continue
+            started = time.monotonic()
+            self.responding = True
             try:
                 await self.on_phrase(batch)
             except asyncio.CancelledError:
                 raise
             except Exception as exc:
                 logger.warning("Réponse au lot vocal impossible : %s", type(exc).__name__)
+            finally:
+                self.last_reply_seconds = time.monotonic() - started
+                self.responding = False
 
     @voice_recv.AudioSink.listener()
     def on_voice_member_disconnect(self, member, ssrc):