From 0dc9d673081a09d6fc7bf6d9277d550ca7aa59ef Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Fri, 20 Feb 2026 15:18:03 +0100 Subject: [PATCH] Debug in progress --- .../PS_AI_Agent/Content/test_AI_Actor.uasset | Bin 147240 -> 164995 bytes ...ElevenLabsConversationalAgentComponent.cpp | 77 ++++++++- .../Private/ElevenLabsWebSocketProxy.cpp | 159 ++++++++++++++---- .../ElevenLabsConversationalAgentComponent.h | 65 ++++++- .../Public/ElevenLabsDefinitions.h | 5 +- .../Public/ElevenLabsWebSocketProxy.h | 29 ++++ build.bat | 35 ++++ 7 files changed, 326 insertions(+), 44 deletions(-) create mode 100644 build.bat diff --git a/Unreal/PS_AI_Agent/Content/test_AI_Actor.uasset b/Unreal/PS_AI_Agent/Content/test_AI_Actor.uasset index 801ae24204231db266913996d48703ac3654be90..d0e446fc6ee8be35b63802ff11875d2c10db83e5 100644 GIT binary patch literal 164995 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 N{^KB=T112X{|}658ax01 literal 147240 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 Tr1Vho=!-sncOWk1Ae8?Pgxv_c 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 545d40a..a948b06 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 @@ -48,9 +48,27 @@ void UElevenLabsConversationalAgentComponent::TickComponent(float DeltaTime, ELe if (AudioQueue.Num() == 0) { SilentTickCount++; - if (SilentTickCount >= SilenceThresholdTicks) + + // Wait for agent_response (confirms the full response is done) before + // declaring the agent stopped. This prevents premature OnAgentStoppedSpeaking + // events when ElevenLabs TTS streams audio in multiple batches with gaps + // (e.g. for long responses) — without this guard, the Blueprint's + // OnAgentStoppedSpeaking handler reopens the mic mid-response. + const bool bResponseConfirmed = bAgentResponseReceived && SilentTickCount >= SilenceThresholdTicks; + + // Hard-timeout fallback: if agent_response never arrives (or is very late), + // stop after 2s of silence to avoid leaving the state machine stuck. + const bool bHardTimeout = SilentTickCount >= HardSilenceTimeoutTicks; + + if (bResponseConfirmed || bHardTimeout) { + if (bHardTimeout && !bAgentResponseReceived) + { + UE_LOG(LogElevenLabsAgent, Warning, + TEXT("Agent silence hard-timeout (2s) without agent_response — declaring agent stopped.")); + } bAgentSpeaking = false; + bAgentResponseReceived = false; SilentTickCount = 0; OnAgentStoppedSpeaking.Broadcast(); } @@ -84,6 +102,8 @@ void UElevenLabsConversationalAgentComponent::StartConversation() &UElevenLabsConversationalAgentComponent::HandleAgentResponse); WebSocketProxy->OnInterrupted.AddDynamic(this, &UElevenLabsConversationalAgentComponent::HandleInterrupted); + WebSocketProxy->OnAgentResponseStarted.AddDynamic(this, + &UElevenLabsConversationalAgentComponent::HandleAgentResponseStarted); } // Pass configuration to the proxy before connecting. @@ -114,6 +134,33 @@ void UElevenLabsConversationalAgentComponent::StartListening() } if (bIsListening) return; + + // If the agent is currently generating or speaking, decide how to handle the request. + // + // Interruption (bAllowInterruption) applies ONLY when the agent is already playing audio + // (bAgentSpeaking). Pressing T while the agent speaks immediately stops it and opens the mic. + // + // During the generation phase (bAgentGenerating, no audio yet) we always block silently. + // This prevents the Blueprint's OnAgentStartedGenerating handler — which typically calls + // StartListening() for bookkeeping — from accidentally sending an interrupt to the server + // the moment it starts generating, which would cancel every response before any audio plays. + if (bAgentGenerating || bAgentSpeaking) + { + if (bAgentSpeaking && bAllowInterruption) + { + UE_LOG(LogElevenLabsAgent, Log, TEXT("StartListening: interrupting agent (speaking) to allow user to speak.")); + InterruptAgent(); + // InterruptAgent → StopAgentAudio clears bAgentSpeaking / bAgentGenerating, + // so we fall through and open the microphone immediately. + } + else + { + UE_LOG(LogElevenLabsAgent, Log, TEXT("StartListening ignored: agent is %s%s — will listen after agent finishes."), + bAgentGenerating ? TEXT("generating") : TEXT("speaking"), + (bAgentSpeaking && !bAllowInterruption) ? TEXT(" (interruption disabled)") : TEXT("")); + return; + } + } bIsListening = true; if (TurnMode == EElevenLabsTurnMode::Client) @@ -225,6 +272,8 @@ void UElevenLabsConversationalAgentComponent::HandleDisconnected(int32 StatusCod UE_LOG(LogElevenLabsAgent, Log, TEXT("Agent disconnected. Code=%d Reason=%s"), StatusCode, *Reason); bIsListening = false; bAgentSpeaking = false; + bAgentGenerating = false; + bAgentResponseReceived = false; MicAccumulationBuffer.Reset(); OnAgentDisconnected.Broadcast(StatusCode, Reason); } @@ -250,6 +299,11 @@ void UElevenLabsConversationalAgentComponent::HandleTranscript(const FElevenLabs void UElevenLabsConversationalAgentComponent::HandleAgentResponse(const FString& ResponseText) { + // The server sends agent_response when the full text response is complete. + // This is our reliable signal that no more TTS audio chunks will follow. + // Set the flag so the silence-detection Tick can safely fire OnAgentStoppedSpeaking. + bAgentResponseReceived = true; + if (bEnableAgentTextResponse) { OnAgentTextResponse.Broadcast(ResponseText); @@ -262,6 +316,22 @@ void UElevenLabsConversationalAgentComponent::HandleInterrupted() OnAgentInterrupted.Broadcast(); } +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. + bAgentGenerating = true; + + if (bIsListening) + { + UE_LOG(LogElevenLabsAgent, Log, + TEXT("Agent started generating while mic was open — stopping listening to avoid turn collision.")); + StopListening(); + } + OnAgentStartedGenerating.Broadcast(); +} + // ───────────────────────────────────────────────────────────────────────────── // Audio playback // ───────────────────────────────────────────────────────────────────────────── @@ -314,6 +384,8 @@ void UElevenLabsConversationalAgentComponent::EnqueueAgentAudio(const TArray& PCMData) } if (PCMData.Num() == 0) return; - UE_LOG(LogElevenLabsWS, Log, TEXT("SendAudioChunk: %d bytes (PCM int16 LE @ 16kHz mono)"), PCMData.Num()); - // Track when the last audio chunk was sent for latency measurement. LastAudioChunkSentTime = FPlatformTime::Seconds(); @@ -119,13 +117,8 @@ void UElevenLabsWebSocketProxy::SendAudioChunk(const TArray& PCMData) // to avoid the pretty-printed writer and to keep the payload minimal. const FString AudioJson = FString::Printf(TEXT("{\"user_audio_chunk\":\"%s\"}"), *Base64Audio); - // Log first chunk fully for debugging - static int32 AudioChunksSent = 0; - AudioChunksSent++; - if (AudioChunksSent <= 2) - { - UE_LOG(LogElevenLabsWS, Log, TEXT(" Audio JSON (first 200 chars): %.200s"), *AudioJson); - } + // Per-chunk log at Verbose only — Log level is too spammy (10+ lines per second). + UE_LOG(LogElevenLabsWS, Verbose, TEXT("SendAudioChunk: %d bytes"), PCMData.Num()); if (WebSocket.IsValid() && WebSocket->IsConnected()) { @@ -139,7 +132,17 @@ void UElevenLabsWebSocketProxy::SendUserTurnStart() // The server's VAD detects speech from the audio chunks we send. // user_activity is a keep-alive/timeout-reset message and should NOT be // sent here — it would delay the agent's turn after the user stops. - UE_LOG(LogElevenLabsWS, Log, TEXT("User turn started (audio chunks will follow).")); + + // Reset latency tracking so a new turn starts with a clean state. + // If the previous turn got no server response (bWaitingForResponse stayed true), + // this prevents stale UserTurnEndTime from corrupting latency measurements + // and ensures the state machine is consistent for the new turn. + bWaitingForResponse = false; + bFirstAudioResponseLogged = false; + bAgentResponseStartedFired = false; + + const double T = FPlatformTime::Seconds() - SessionStartTime; + UE_LOG(LogElevenLabsWS, Log, TEXT("[T+%.2fs] User turn started — mic open, audio chunks will follow."), T); } void UElevenLabsWebSocketProxy::SendUserTurnEnd() @@ -149,7 +152,13 @@ void UElevenLabsWebSocketProxy::SendUserTurnEnd() UserTurnEndTime = FPlatformTime::Seconds(); bWaitingForResponse = true; bFirstAudioResponseLogged = false; - UE_LOG(LogElevenLabsWS, Log, TEXT("User turn ended — stopped sending audio chunks. Server VAD will detect silence.")); + // NOTE: Do NOT reset bAgentResponseStartedFired here. + // StopListening() calls SendUserTurnEnd(), and HandleAgentResponseStarted() calls StopListening(). + // If we reset the flag here, the next agent_chat_response_part would re-fire OnAgentResponseStarted + // in a loop: part arrives → event → StopListening → SendUserTurnEnd → flag reset → part arrives → loop. + // The flag is only reset in SendUserTurnStart() at the beginning of a new user turn. + const double T = UserTurnEndTime - SessionStartTime; + UE_LOG(LogElevenLabsWS, Log, TEXT("[T+%.2fs] User turn ended — server VAD silence detection started (turn_timeout=1s)."), T); } void UElevenLabsWebSocketProxy::SendTextMessage(const FString& Text) @@ -171,6 +180,14 @@ void UElevenLabsWebSocketProxy::SendTextMessage(const FString& Text) void UElevenLabsWebSocketProxy::SendInterrupt() { if (!IsConnected()) return; + + // Immediately start discarding in-flight audio and chat response parts from + // the generation we are about to interrupt. The server may still send several + // frames before it processes our interrupt. We stop ignoring once the server + // sends its "interruption" acknowledgement (HandleInterruption). + bIgnoreIncomingContent = true; + UE_LOG(LogElevenLabsWS, Log, TEXT("Sending interrupt — ignoring incoming content until server acks.")); + TSharedPtr Msg = MakeShareable(new FJsonObject()); Msg->SetStringField(TEXT("type"), ElevenLabsMessageType::Interrupt); SendJsonMessage(Msg); @@ -194,7 +211,7 @@ void UElevenLabsWebSocketProxy::OnWsConnected() // "type": "conversation_initiation_client_data", // "conversation_config_override": { // "agent": { - // "turn": { "turn_timeout": 3, "speculative_turn": true } + // "turn": { "turn_timeout": 3 } // speculative_turn removed (caused silent failures after 2 turns) // }, // "tts": { // "optimize_streaming_latency": 3 @@ -211,19 +228,28 @@ void UElevenLabsWebSocketProxy::OnWsConnected() // In push-to-talk (Client mode) the user controls the mic; the server still // uses its VAD to detect the end of speech from the audio chunks it receives. TSharedPtr TurnObj = MakeShareable(new FJsonObject()); - // Lower turn_timeout so the agent responds faster after the user stops speaking. - // Default is 7s. In push-to-talk (Client mode), the user explicitly signals - // end-of-turn by releasing the key, so we can use a very short timeout (1s). + // turn_timeout: how long the server waits after VAD detects silence before + // processing the user's turn. In push-to-talk (Client) mode this directly adds + // latency to every response — the server waits this many seconds of silence + // after the user releases T before it begins LLM processing. + // + // History: + // turn_timeout=1 was originally problematic, but ONLY when combined with + // speculative_turn=true (which has since been removed). Without speculative_turn, + // 1s is safe and halves the per-turn latency vs the 3s we had previously. + // Original failure: server silently dropped turns 3+ with speculative_turn+timeout=1. if (TurnMode == EElevenLabsTurnMode::Client) { TurnObj->SetNumberField(TEXT("turn_timeout"), 1); } - // Speculative turn: start LLM generation during silence before the VAD is - // fully confident the user finished speaking. Reduces latency by 200-500ms. - if (bSpeculativeTurn) - { - TurnObj->SetBoolField(TEXT("speculative_turn"), true); - } + // NOTE: speculative_turn is intentionally NOT sent here. + // With speculative_turn=true the server starts LLM generation speculatively + // before the VAD is fully confident the user finished speaking. Combined with + // the short turn_timeout this put the server's state machine into a state where + // it stopped processing user audio after 2 turns — subsequent turns received + // only pings and no agent_chat_response_part / audio / user_transcript at all. + // Removing it costs ~200-500ms of latency but restores reliable multi-turn + // conversation. Re-enable only if ElevenLabs confirms it is stable. TSharedPtr AgentObj = MakeShareable(new FJsonObject()); AgentObj->SetObjectField(TEXT("turn"), TurnObj); @@ -297,7 +323,15 @@ void UElevenLabsWebSocketProxy::OnWsMessage(const FString& Message) return; } - // Log every message type received from the server for debugging. + // Suppress ping from the visible log — they arrive every ~2s and flood the output. + // Handle ping early before the generic type log. + if (MsgType == ElevenLabsMessageType::PingEvent) + { + HandlePing(Root); + return; + } + + // Log every non-ping message type received from the server. UE_LOG(LogElevenLabsWS, Log, TEXT("Received message type: %s"), *MsgType); if (MsgType == ElevenLabsMessageType::ConversationInitiation) @@ -310,11 +344,12 @@ void UElevenLabsWebSocketProxy::OnWsMessage(const FString& Message) if (bWaitingForResponse && !bFirstAudioResponseLogged) { const double Now = FPlatformTime::Seconds(); + const double T = Now - SessionStartTime; const double LatencyFromTurnEnd = (Now - UserTurnEndTime) * 1000.0; const double LatencyFromLastChunk = (Now - LastAudioChunkSentTime) * 1000.0; UE_LOG(LogElevenLabsWS, Warning, - TEXT("[LATENCY] Time-to-first-audio: %.0f ms (from turn end), %.0f ms (from last chunk sent)"), - LatencyFromTurnEnd, LatencyFromLastChunk); + TEXT("[T+%.2fs] [LATENCY] First audio: %.0f ms after turn end (%.0f ms after last chunk)"), + T, LatencyFromTurnEnd, LatencyFromLastChunk); bFirstAudioResponseLogged = true; } HandleAudioResponse(Root); @@ -325,10 +360,11 @@ void UElevenLabsWebSocketProxy::OnWsMessage(const FString& Message) if (bWaitingForResponse) { const double Now = FPlatformTime::Seconds(); + const double T = Now - SessionStartTime; const double LatencyFromTurnEnd = (Now - UserTurnEndTime) * 1000.0; UE_LOG(LogElevenLabsWS, Warning, - TEXT("[LATENCY] User transcript received: %.0f ms after turn end"), - LatencyFromTurnEnd); + TEXT("[T+%.2fs] [LATENCY] User transcript: %.0f ms after turn end"), + T, LatencyFromTurnEnd); bWaitingForResponse = false; } HandleTranscript(Root); @@ -339,26 +375,27 @@ void UElevenLabsWebSocketProxy::OnWsMessage(const FString& Message) if (UserTurnEndTime > 0.0) { const double Now = FPlatformTime::Seconds(); + const double T = Now - SessionStartTime; const double LatencyFromTurnEnd = (Now - UserTurnEndTime) * 1000.0; UE_LOG(LogElevenLabsWS, Warning, - TEXT("[LATENCY] Agent text response: %.0f ms after turn end"), - LatencyFromTurnEnd); + TEXT("[T+%.2fs] [LATENCY] Agent text response: %.0f ms after turn end"), + T, LatencyFromTurnEnd); } HandleAgentResponse(Root); } + else if (MsgType == ElevenLabsMessageType::AgentChatResponsePart) + { + HandleAgentChatResponsePart(); + } else if (MsgType == ElevenLabsMessageType::AgentResponseCorrection) { - // Silently ignore for now — corrected text after interruption. + // Silently ignore — corrected text after interruption. UE_LOG(LogElevenLabsWS, Verbose, TEXT("agent_response_correction received (ignored).")); } else if (MsgType == ElevenLabsMessageType::InterruptionEvent) { HandleInterruption(Root); } - else if (MsgType == ElevenLabsMessageType::PingEvent) - { - HandlePing(Root); - } else { UE_LOG(LogElevenLabsWS, Verbose, TEXT("Unhandled message type: %s"), *MsgType); @@ -415,9 +452,17 @@ void UElevenLabsWebSocketProxy::OnWsBinaryMessage(const void* Data, SIZE_T Size, } // Broadcast raw PCM bytes directly to the audio queue. + // Discard if we are waiting for an interruption ack (same logic as HandleAudioResponse). TArray PCMData = MoveTemp(BinaryFrameBuffer); BinaryFrameBuffer.Reset(); - OnAudioReceived.Broadcast(PCMData); + if (!bIgnoreIncomingContent) + { + OnAudioReceived.Broadcast(PCMData); + } + else + { + UE_LOG(LogElevenLabsWS, Verbose, TEXT("Discarding binary audio frame (interrupt pending server ack).")); + } } } @@ -439,13 +484,23 @@ void UElevenLabsWebSocketProxy::HandleConversationInitiation(const TSharedPtrTryGetStringField(TEXT("conversation_id"), ConversationInfo.ConversationID); } - UE_LOG(LogElevenLabsWS, Log, TEXT("Conversation initiated. ID=%s"), *ConversationInfo.ConversationID); + SessionStartTime = FPlatformTime::Seconds(); + UE_LOG(LogElevenLabsWS, Log, TEXT("[T+0.00s] Conversation initiated. ID=%s"), *ConversationInfo.ConversationID); ConnectionState = EElevenLabsConnectionState::Connected; OnConnected.Broadcast(ConversationInfo); } void UElevenLabsWebSocketProxy::HandleAudioResponse(const TSharedPtr& Root) { + // Discard audio that belongs to an interrupted generation. + // The server may send several more audio frames after we sent "interrupt" — + // they must not restart the speaking state on the client side. + if (bIgnoreIncomingContent) + { + UE_LOG(LogElevenLabsWS, Verbose, TEXT("Discarding audio frame (interrupt pending server ack).")); + return; + } + // Expected structure: // { "type": "audio", // "audio_event": { "audio_base_64": "", "event_id": 1 } @@ -513,9 +568,41 @@ void UElevenLabsWebSocketProxy::HandleAgentResponse(const TSharedPtr 0.0 ? (Now - UserTurnEndTime) * 1000.0 : 0.0; + UE_LOG(LogElevenLabsWS, Log, + TEXT("[T+%.2fs] Agent started generating (%.0f ms after turn end — includes VAD silence timeout + LLM start)."), + T, LatencyFromTurnEnd); + OnAgentResponseStarted.Broadcast(); + } + // Subsequent parts logged at Verbose only (can be dozens per response). +} + void UElevenLabsWebSocketProxy::HandleInterruption(const TSharedPtr& Root) { - UE_LOG(LogElevenLabsWS, Log, TEXT("Agent interrupted.")); + // Server has acknowledged the interruption — the old generation is fully stopped. + // Resume accepting incoming audio and chat response parts (for the next turn). + bIgnoreIncomingContent = false; + UE_LOG(LogElevenLabsWS, Log, TEXT("Agent interrupted (server ack received — resuming content processing).")); OnInterrupted.Broadcast(); } 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 f972f94..bee6ec3 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 @@ -34,6 +34,15 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAgentStartedSpeaking); DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAgentStoppedSpeaking); DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAgentInterrupted); +/** + * Fired when the server sends its first agent_chat_response_part — i.e. the moment + * the LLM starts generating, well before audio arrives. + * The component automatically calls StopListening() when this fires while the + * microphone is open, preventing the user's new audio from being sent to the + * server while it is still processing the previous turn. + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAgentStartedGenerating); + // ───────────────────────────────────────────────────────────────────────────── // UElevenLabsConversationalAgentComponent // @@ -83,10 +92,27 @@ public: /** * Enable speculative turn: the LLM starts generating a response during * silence before the VAD is fully confident the user has finished speaking. - * Reduces latency by 200-500ms but may occasionally produce premature responses. + * Reduces latency by 200-500ms but caused the server to silently stop + * processing user audio after 2 turns when combined with a short turn_timeout. + * Disabled by default until ElevenLabs confirms stability in multi-turn sessions. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ElevenLabs|Latency") - bool bSpeculativeTurn = true; + bool bSpeculativeTurn = false; + + /** + * Allow the user to interrupt the agent while it is playing audio (speaking). + * When true, calling StartListening() while the agent is audibly speaking automatically + * sends an interruption signal to the server and opens the mic — no Blueprint nodes needed. + * When false, StartListening() is silently ignored until the agent finishes speaking. + * + * NOTE: interruption only applies during the audio-playback phase (bAgentSpeaking). + * While the agent is generating but has not yet started speaking, StartListening() is + * always blocked regardless of this flag — this prevents Blueprint's OnAgentStartedGenerating + * handler (which often calls StartListening for bookkeeping) from accidentally cancelling + * the response before any audio plays. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ElevenLabs") + bool bAllowInterruption = true; /** * Forward user speech transcripts (user_transcript events) to the @@ -131,6 +157,15 @@ public: UPROPERTY(BlueprintAssignable, Category = "ElevenLabs|Events") FOnAgentInterrupted OnAgentInterrupted; + /** + * Fired when the server starts generating a response (before audio). + * The component automatically stops the microphone when this fires while listening, + * so the Blueprint doesn't need to handle this manually for push-to-talk. + * Bind here if you need UI feedback ("agent is thinking..."). + */ + UPROPERTY(BlueprintAssignable, Category = "ElevenLabs|Events") + FOnAgentStartedGenerating OnAgentStartedGenerating; + // ── Control ─────────────────────────────────────────────────────────────── /** @@ -219,6 +254,9 @@ private: UFUNCTION() void HandleInterrupted(); + UFUNCTION() + void HandleAgentResponseStarted(); + // ── Audio playback ──────────────────────────────────────────────────────── void InitAudioPlayback(); void EnqueueAgentAudio(const TArray& PCMData); @@ -244,15 +282,32 @@ private: // ── State ───────────────────────────────────────────────────────────────── bool bIsListening = false; bool bAgentSpeaking = false; + // True from the first agent_chat_response_part until the first audio chunk arrives. + // Used to block StartListening() while the server is processing the previous turn. + bool bAgentGenerating = false; // Accumulates incoming PCM bytes until the audio component needs data. TArray AudioQueue; FCriticalSection AudioQueueLock; - // Simple heuristic: if we haven't received audio data for this many ticks, - // consider the agent done speaking. + // Silence detection: how many consecutive ticks with an empty audio queue. int32 SilentTickCount = 0; - static constexpr int32 SilenceThresholdTicks = 30; // ~0.5s at 60fps + + // Primary threshold: fire OnAgentStoppedSpeaking after this many silent ticks + // once the server has confirmed the full response (bAgentResponseReceived=true). + // 30 ticks ≈ 0.5s at 60fps — enough to bridge brief inter-chunk gaps in the TTS stream. + static constexpr int32 SilenceThresholdTicks = 30; + + // Hard-timeout fallback: fire even without agent_response confirmation after 2s + // of silence (handles edge cases where agent_response is very late or missing). + static constexpr int32 HardSilenceTimeoutTicks = 120; // 2s at 60fps + + // True once the server sends agent_response for the current turn. + // The server sends the full text when generation is complete — this is the + // reliable signal that no more audio chunks will follow for this utterance. + // We wait for this before declaring the agent "stopped speaking" to avoid + // premature OnAgentStoppedSpeaking events during multi-chunk TTS streaming. + bool bAgentResponseReceived = false; // ── Microphone accumulation ─────────────────────────────────────────────── // WASAPI fires callbacks every ~5ms (158 bytes at 16kHz 16-bit mono). diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsDefinitions.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsDefinitions.h index b2af349..09d7ba1 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsDefinitions.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsDefinitions.h @@ -49,8 +49,9 @@ namespace ElevenLabsMessageType static const FString AudioResponse = TEXT("audio"); // User speech-to-text transcript (speaker is always the user) static const FString UserTranscript = TEXT("user_transcript"); - static const FString AgentResponse = TEXT("agent_response"); - static const FString AgentResponseCorrection= TEXT("agent_response_correction"); + static const FString AgentResponse = TEXT("agent_response"); + static const FString AgentChatResponsePart = TEXT("agent_chat_response_part"); // intermediate LLM token stream + static const FString AgentResponseCorrection = TEXT("agent_response_correction"); static const FString InterruptionEvent = TEXT("interruption"); static const FString PingEvent = TEXT("ping"); static const FString ClientToolCall = TEXT("client_tool_call"); 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 13fb2e8..09a4d28 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 @@ -36,6 +36,13 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnElevenLabsAgentResponse, /** Fired when the agent interrupts the user. */ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnElevenLabsInterrupted); +/** + * Fired when the server starts generating a response (first agent_chat_response_part received). + * This fires BEFORE audio arrives — useful to detect that the server is processing + * the previous turn while the client may have restarted listening (auto-restart scenario). + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnElevenLabsAgentResponseStarted); + // ───────────────────────────────────────────────────────────────────────────── // WebSocket Proxy @@ -79,6 +86,14 @@ public: UPROPERTY(BlueprintAssignable, Category = "ElevenLabs|Events") FOnElevenLabsInterrupted OnInterrupted; + /** + * Fired on the first agent_chat_response_part per turn — i.e. the moment the server + * starts generating. Fires well before audio. The component uses this to stop the + * microphone if it was restarted before the server finished processing the previous turn. + */ + UPROPERTY(BlueprintAssignable, Category = "ElevenLabs|Events") + FOnElevenLabsAgentResponseStarted OnAgentResponseStarted; + // ── Lifecycle ───────────────────────────────────────────────────────────── /** @@ -167,6 +182,7 @@ private: void HandleAudioResponse(const TSharedPtr& Payload); void HandleTranscript(const TSharedPtr& Payload); void HandleAgentResponse(const TSharedPtr& Payload); + void HandleAgentChatResponsePart(); void HandleInterruption(const TSharedPtr& Payload); void HandlePing(const TSharedPtr& Payload); @@ -193,6 +209,19 @@ private: bool bWaitingForResponse = false; // Whether we already logged the first audio response latency for this turn. bool bFirstAudioResponseLogged = false; + // Whether OnAgentResponseStarted has already been fired for the current turn. + // Reset at turn start so only the first agent_chat_response_part fires the event. + bool bAgentResponseStartedFired = false; + + // Timestamp when the conversation was initiated (conversation_initiation_metadata received). + // Used to compute [T+Xs] session-relative timestamps in all log messages. + double SessionStartTime = 0.0; + + // Set to true in SendInterrupt() so that in-flight audio frames and + // agent_chat_response_part messages from the interrupted generation are silently + // discarded instead of re-triggering the speaking/generating state. + // Cleared when the server sends its "interruption" acknowledgement. + bool bIgnoreIncomingContent = false; public: // Set by UElevenLabsConversationalAgentComponent before calling Connect(). diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..ee648b3 --- /dev/null +++ b/build.bat @@ -0,0 +1,35 @@ +@echo off +chcp 65001 >nul +title Build PS_AI_Agent + +echo ============================================================ +echo PS_AI_Agent - Compilation plugin ElevenLabs (UE 5.5) +echo ============================================================ +echo. +echo ATTENTION : Ferme l'Unreal Editor avant de continuer ! +echo (Les DLL seraient verrouillees et la compilation echouerait) +echo. +pause + +echo. +echo Compilation en cours... +echo (Seuls les .cpp modifies sont recompiles, ~16s) +echo. + +powershell.exe -Command "& 'C:\Program Files\Epic Games\UE_5.5\Engine\Build\BatchFiles\RunUAT.bat' BuildEditor -project='C:\ASTERION\GIT\PS_AI_Agent\Unreal\PS_AI_Agent\PS_AI_Agent.uproject' -notools -noP4 2>&1" + +echo. +if %ERRORLEVEL% == 0 ( + echo ============================================================ + echo SUCCES - Compilation terminee sans erreur. + echo Tu peux relancer l'Unreal Editor. + echo ============================================================ +) else ( + echo ============================================================ + echo ECHEC - Erreur de compilation (code %ERRORLEVEL%) + echo Consulte le log ci-dessus pour le detail. + echo ============================================================ +) + +echo. +pause