From 1b883f532f4cf5726d7f584a4ad0712c5479dfe1 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Fri, 20 Feb 2026 20:24:50 +0100 Subject: [PATCH] Fix collision stuck state + add streaming partial text event - Skip mic buffer flush during collision avoidance (bAgentGenerating guard in StopListening) to prevent sending audio to a mid-generation server which caused both sides to stall permanently - Add OnAgentPartialResponse event: streams LLM text fragments from agent_chat_response_part in real-time (opt-in via bEnableAgentPartialResponse), separate from the existing OnAgentTextResponse (full text at end) - French agent server drop after 3 turns is a server-side issue, not client Co-Authored-By: Claude Opus 4.6 --- .../PS_AI_Agent/Content/test_AI_Actor.uasset | Bin 164995 -> 158165 bytes ...ElevenLabsConversationalAgentComponent.cpp | 55 +++++++++++++++--- .../Private/ElevenLabsWebSocketProxy.cpp | 20 ++++++- .../ElevenLabsConversationalAgentComponent.h | 29 +++++++++ .../Public/ElevenLabsWebSocketProxy.h | 11 +++- 5 files changed, 102 insertions(+), 13 deletions(-) diff --git a/Unreal/PS_AI_Agent/Content/test_AI_Actor.uasset b/Unreal/PS_AI_Agent/Content/test_AI_Actor.uasset index d0e446fc6ee8be35b63802ff11875d2c10db83e5..83bff19c72014252c3d5654ae24b101db7687b30 100644 GIT binary patch delta 19143 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 Cx}6UI delta 20254 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 LZ_j!Za1;I)UC)_& diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsConversationalAgentComponent.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsConversationalAgentComponent.cpp index 01836b7..a505247 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsConversationalAgentComponent.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsConversationalAgentComponent.cpp @@ -158,6 +158,8 @@ void UElevenLabsConversationalAgentComponent::StartConversation() &UElevenLabsConversationalAgentComponent::HandleInterrupted); WebSocketProxy->OnAgentResponseStarted.AddDynamic(this, &UElevenLabsConversationalAgentComponent::HandleAgentResponseStarted); + WebSocketProxy->OnAgentResponsePart.AddDynamic(this, + &UElevenLabsConversationalAgentComponent::HandleAgentResponsePart); } // Pass configuration to the proxy before connecting. @@ -266,7 +268,23 @@ void UElevenLabsConversationalAgentComponent::StopListening() // Flush any partially-accumulated mic audio before signalling end-of-turn. // This ensures the final words aren't discarded just because the last callback // didn't push the buffer over the MicChunkMinBytes threshold. - if (MicAccumulationBuffer.Num() > 0 && WebSocketProxy && IsConnected()) + // + // EXCEPT during collision avoidance: bAgentGenerating is already true when + // HandleAgentResponseStarted calls StopListening (it sets the flag before calling us). + // Flushing audio to a server that is mid-generation can cause it to re-enter + // "user speaking" state and stall waiting for more audio that never arrives, + // leaving both sides stuck — no audio for the collision response and no response + // for subsequent turns. + if (bAgentGenerating) + { + if (MicAccumulationBuffer.Num() > 0) + { + UE_LOG(LogElevenLabsAgent, Log, + TEXT("StopListening: discarding %d bytes of accumulated mic audio (collision — server is mid-generation)."), + MicAccumulationBuffer.Num()); + } + } + else if (MicAccumulationBuffer.Num() > 0 && WebSocketProxy && IsConnected()) { WebSocketProxy->SendAudioChunk(MicAccumulationBuffer); } @@ -423,7 +441,8 @@ void UElevenLabsConversationalAgentComponent::HandleAgentResponseStarted() { // The server has started generating a response (first agent_chat_response_part). // Set bAgentGenerating BEFORE StopListening so that any StartListening call - // triggered by the Blueprint's OnAgentStartedGenerating handler is blocked. + // triggered by the Blueprint's OnAgentStartedGenerating handler is blocked, + // and so that StopListening knows to skip the mic buffer flush (collision path). bAgentGenerating = true; bWaitingForAgentResponse = false; // Server is generating — response timeout cancelled. @@ -433,21 +452,39 @@ void UElevenLabsConversationalAgentComponent::HandleAgentResponseStarted() if (bIsListening) { // Collision: server started generating Turn N's response while Turn M (M>N) mic was open. - // Log both turn indices so the timeline is unambiguous. + // The server's VAD detected a pause in the user's speech and started generating + // prematurely — the user hasn't finished speaking yet. + // + // Stop the mic WITHOUT flushing the accumulated audio buffer (see StopListening's + // bAgentGenerating guard). Flushing would send audio to a server that is mid-generation, + // causing it to re-enter "user speaking" state and stall — both sides stuck. + // + // Do NOT send an interrupt here: the ElevenLabs server does not always send the + // interruption ack, which would leave bIgnoreIncomingContent=true and silently + // discard all subsequent content. Instead, let the server's response play out: + // - If audio arrives → EnqueueAgentAudio sets bAgentSpeaking, response plays normally. + // - If audio never arrives → generating timeout (10s) clears bAgentGenerating. + // Either way the state machine recovers and Blueprint can reopen the mic. UE_LOG(LogElevenLabsAgent, Log, TEXT("[T+%.2fs] [Turn %d → Turn %d collision] Agent generating Turn %d response — mic (Turn %d) was open, stopping. (%.2fs after turn end)"), T, LastClosedTurnIndex, TurnIndex, LastClosedTurnIndex, TurnIndex, LatencyFromTurnEnd); StopListening(); } - else - { - UE_LOG(LogElevenLabsAgent, Log, - TEXT("[T+%.2fs] [Turn %d] Agent generating. (%.2fs after turn end)"), - T, LastClosedTurnIndex, LatencyFromTurnEnd); - } + + UE_LOG(LogElevenLabsAgent, Log, + TEXT("[T+%.2fs] [Turn %d] Agent generating. (%.2fs after turn end)"), + T, LastClosedTurnIndex, LatencyFromTurnEnd); OnAgentStartedGenerating.Broadcast(); } +void UElevenLabsConversationalAgentComponent::HandleAgentResponsePart(const FString& PartialText) +{ + if (bEnableAgentPartialResponse) + { + OnAgentPartialResponse.Broadcast(PartialText); + } +} + // ───────────────────────────────────────────────────────────────────────────── // Audio playback // ───────────────────────────────────────────────────────────────────────────── diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsWebSocketProxy.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsWebSocketProxy.cpp index 23e1a8b..de6ffee 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsWebSocketProxy.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsWebSocketProxy.cpp @@ -400,7 +400,7 @@ void UElevenLabsWebSocketProxy::OnWsMessage(const FString& Message) } else if (MsgType == ElevenLabsMessageType::AgentChatResponsePart) { - HandleAgentChatResponsePart(); + HandleAgentChatResponsePart(Root); } else if (MsgType == ElevenLabsMessageType::AgentResponseCorrection) { @@ -602,7 +602,7 @@ void UElevenLabsWebSocketProxy::HandleAgentResponse(const TSharedPtr& Root) { // Ignore response parts that belong to a generation we have already interrupted. // Without this guard, old parts arriving after SendInterrupt() would re-trigger @@ -628,7 +628,21 @@ void UElevenLabsWebSocketProxy::HandleAgentChatResponsePart() T, LatencyFromTurnEnd); OnAgentResponseStarted.Broadcast(); } - // Subsequent parts logged at Verbose only (can be dozens per response). + + // Extract the streaming text fragment and broadcast it. + // API structure: + // { "type": "agent_chat_response_part", + // "agent_chat_response_part_event": { "agent_response_part": "partial text" } + // } + const TSharedPtr* PartEvent = nullptr; + if (Root->TryGetObjectField(TEXT("agent_chat_response_part_event"), PartEvent) && PartEvent) + { + FString PartText; + if ((*PartEvent)->TryGetStringField(TEXT("agent_response_part"), PartText) && !PartText.IsEmpty()) + { + OnAgentResponsePart.Broadcast(PartText); + } + } } void UElevenLabsWebSocketProxy::HandleInterruption(const TSharedPtr& Root) diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsConversationalAgentComponent.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsConversationalAgentComponent.h index 83346fd..05d0665 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsConversationalAgentComponent.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsConversationalAgentComponent.h @@ -43,6 +43,15 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAgentInterrupted); */ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAgentStartedGenerating); +/** + * Fired for every agent_chat_response_part — streams the agent's text as the LLM + * generates it, token by token. Use this for real-time subtitles / text display. + * Each call provides the text fragment from that individual part (NOT accumulated). + * The final complete text is still available via OnAgentTextResponse (agent_response). + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAgentPartialResponse, + const FString&, PartialText); + /** * Fired when the server has not started generating a response within ResponseTimeoutSeconds * after the user stopped speaking (StopListening was called). @@ -138,6 +147,15 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ElevenLabs|Events") bool bEnableAgentTextResponse = true; + /** + * Forward streaming text parts (agent_chat_response_part events) to the + * OnAgentPartialResponse delegate. Each part is a text fragment as the LLM + * generates it — use this for real-time subtitles that appear while the agent + * speaks, instead of waiting for the full text (OnAgentTextResponse). + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ElevenLabs|Events") + bool bEnableAgentPartialResponse = false; + /** * How many seconds to wait for the server to start generating a response * after the user stops speaking (StopListening) before firing OnAgentResponseTimeout. @@ -168,6 +186,14 @@ public: UPROPERTY(BlueprintAssignable, Category = "ElevenLabs|Events") FOnAgentTextResponse OnAgentTextResponse; + /** + * Streaming text fragments as the LLM generates them. + * Fires for every agent_chat_response_part — each call gives one text chunk. + * Enable with bEnableAgentPartialResponse. + */ + UPROPERTY(BlueprintAssignable, Category = "ElevenLabs|Events") + FOnAgentPartialResponse OnAgentPartialResponse; + UPROPERTY(BlueprintAssignable, Category = "ElevenLabs|Events") FOnAgentStartedSpeaking OnAgentStartedSpeaking; @@ -285,6 +311,9 @@ private: UFUNCTION() void HandleAgentResponseStarted(); + UFUNCTION() + void HandleAgentResponsePart(const FString& PartialText); + // ── Audio playback ──────────────────────────────────────────────────────── void InitAudioPlayback(); void EnqueueAgentAudio(const TArray& PCMData); diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsWebSocketProxy.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsWebSocketProxy.h index 09a4d28..b75d6e7 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsWebSocketProxy.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsWebSocketProxy.h @@ -43,6 +43,11 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnElevenLabsInterrupted); */ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnElevenLabsAgentResponseStarted); +/** Fired for every agent_chat_response_part — streams the LLM text as it is generated. + * PartialText is the text fragment from this individual part (NOT accumulated). */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnElevenLabsAgentResponsePart, + const FString&, PartialText); + // ───────────────────────────────────────────────────────────────────────────── // WebSocket Proxy @@ -94,6 +99,10 @@ public: UPROPERTY(BlueprintAssignable, Category = "ElevenLabs|Events") FOnElevenLabsAgentResponseStarted OnAgentResponseStarted; + /** Fired for every agent_chat_response_part with the streaming text fragment. */ + UPROPERTY(BlueprintAssignable, Category = "ElevenLabs|Events") + FOnElevenLabsAgentResponsePart OnAgentResponsePart; + // ── Lifecycle ───────────────────────────────────────────────────────────── /** @@ -182,7 +191,7 @@ private: void HandleAudioResponse(const TSharedPtr& Payload); void HandleTranscript(const TSharedPtr& Payload); void HandleAgentResponse(const TSharedPtr& Payload); - void HandleAgentChatResponsePart(); + void HandleAgentChatResponsePart(const TSharedPtr& Payload); void HandleInterruption(const TSharedPtr& Payload); void HandlePing(const TSharedPtr& Payload);