ソースを参照

Fix: ConversationEnd nach TTS statt nach Recording

Der AudioAccumulator sendete ConversationEnd sofort nach dem
Recording — noch bevor der IntentDispatcher entscheiden konnte
ob ein Follow-Up aktiv ist. Dadurch ging der Satellite zurueck
zu LISTENING bevor das FollowUpExpected ankam.

Jetzt entscheidet der IntentDispatcher nach TTS:
- Follow-Up aktiv → FollowUpExpected (Satellite lauscht weiter)
- Kein Follow-Up → ConversationEnd (Satellite → LISTENING)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
patrick 6 ヶ月 前
親
コミット
f703a25fee
2 ファイル変更、50 行追加、24 行削除
  1. 6 4
      trixy_core/audio/accumulator.py
  2. 44 20
      trixy_core/nlp/intent_dispatcher.py

+ 6 - 4
trixy_core/audio/accumulator.py

@@ -110,10 +110,12 @@ class AudioAccumulatorService:
                 "ended_by": ended_by,
                 "ended_by": ended_by,
             })
             })
 
 
-            # ConversationEnd an Client senden — signalisiert dem Satellite,
-            # dass er zur Wakeword-Erkennung zurückkehren kann.
-            # (TTS/Responses werden über separaten Audio-Stream geliefert)
-            await self._send_conversation_end(satellite_id, conversation_id, "completed")
+            # ConversationEnd wird NICHT hier gesendet!
+            # Der IntentDispatcher entscheidet nach dem Handler-Ergebnis:
+            # - Follow-Up aktiv → FollowUpExpected (Satellite lauscht weiter)
+            # - Kein Follow-Up → ConversationEnd (Satellite → LISTENING)
+            # Siehe intent_dispatcher.py: on_tts_completed() und
+            # _send_conversation_end_if_no_followup()
         else:
         else:
             # Keine Sprache → Fehlauslösung, Conversation abbrechen
             # Keine Sprache → Fehlauslösung, Conversation abbrechen
             pinfo(f"Keine Sprache erkannt für {satellite_id} — Conversation abbrechen")
             pinfo(f"Keine Sprache erkannt für {satellite_id} — Conversation abbrechen")

+ 44 - 20
trixy_core/nlp/intent_dispatcher.py

@@ -401,42 +401,66 @@ class IntentDispatcherService(IService):
         Verarbeitet TTS-Ergebnis:
         Verarbeitet TTS-Ergebnis:
         1. Audio an Satellite senden
         1. Audio an Satellite senden
         2. Bei Follow-up: followup_expected emittieren
         2. Bei Follow-up: followup_expected emittieren
+        3. Ohne Follow-up: ConversationEnd senden
         """
         """
         # tts_completed kommt als generisches EventData (via emit() mit dict)
         # tts_completed kommt als generisches EventData (via emit() mit dict)
         satellite_id = event_data.get("satellite_id", "")
         satellite_id = event_data.get("satellite_id", "")
         audio_data_hex = event_data.get("audio_data", "")
         audio_data_hex = event_data.get("audio_data", "")
         request_id = event_data.get("request_id", "")
         request_id = event_data.get("request_id", "")
+        session_id = event_data.get("session_id", "")
 
 
         # Audio an Satellite senden (falls satellite_id vorhanden)
         # Audio an Satellite senden (falls satellite_id vorhanden)
         if satellite_id and audio_data_hex:
         if satellite_id and audio_data_hex:
             await self._send_tts_to_satellite(satellite_id, audio_data_hex)
             await self._send_tts_to_satellite(satellite_id, audio_data_hex)
 
 
         # Follow-up prüfen
         # Follow-up prüfen
-        if not request_id:
-            return
+        followup_info = self._pending_followups.pop(request_id, None) if request_id else None
+
+        if followup_info:
+            success = event_data.get("success", True)
+            if not success:
+                perror(f"TTS fehlgeschlagen für Follow-up Request: {request_id}")
+                await self._send_conversation_end(satellite_id, session_id)
+                return
+
+            # followup_expected emittieren - Client wechselt in Hör-Modus
+            followup_event = FollowUpExpected(
+                satellite_id=followup_info["satellite_id"],
+                session_id=followup_info["session_id"],
+                room_id=followup_info["room_id"],
+                timeout_seconds=30.0,
+                followup_context={
+                    "previous_intent": followup_info["intent"],
+                },
+            )
 
 
-        followup_info = self._pending_followups.pop(request_id, None)
-        if not followup_info:
-            return
+            pinfo(f"Follow-up erwartet für Satellite: {followup_info['satellite_id']}")
+            await self._application.events.trigger("followup_expected", followup_event)
+        else:
+            # Kein Follow-up → ConversationEnd senden
+            if satellite_id:
+                await self._send_conversation_end(satellite_id, session_id)
 
 
-        success = event_data.get("success", True)
-        if not success:
-            perror(f"TTS fehlgeschlagen für Follow-up Request: {request_id}")
+    async def _send_conversation_end(self, satellite_id: str, session_id: str = "") -> None:
+        """Sendet ConversationEnd an den Satellite."""
+        satellites = getattr(self._application, "satellites", None)
+        if satellites is None:
             return
             return
 
 
-        # followup_expected emittieren - Client wechselt in Hör-Modus
-        followup_event = FollowUpExpected(
-            satellite_id=followup_info["satellite_id"],
-            session_id=followup_info["session_id"],
-            room_id=followup_info["room_id"],
-            timeout_seconds=30.0,
-            followup_context={
-                "previous_intent": followup_info["intent"],
-            },
-        )
+        satellite = satellites.get(satellite_id)
+        if satellite is None or not satellite.is_connected:
+            return
 
 
-        pinfo(f"Follow-up erwartet für Satellite: {followup_info['satellite_id']}")
-        await self._application.events.trigger("followup_expected", followup_event)
+        try:
+            from trixy_core.network.cmd.wakeword import ConversationEnd
+            cmd = ConversationEnd(
+                session_id=session_id,
+                reason="completed",
+            )
+            await satellite.send_command(cmd)
+            pdebug(f"ConversationEnd gesendet an {satellite_id} (reason=completed)")
+        except Exception as e:
+            perror(f"Fehler beim Senden von ConversationEnd: {e}")
 
 
     async def _send_tts_to_satellite(self, satellite_id: str, audio_data_hex: str) -> None:
     async def _send_tts_to_satellite(self, satellite_id: str, audio_data_hex: str) -> None:
         """Sendet TTS-Audio an den Satellite."""
         """Sendet TTS-Audio an den Satellite."""