diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvReplay/Source/PS_AI_ConvReplay/Private/PS_AI_ConvReplay_Subsystem.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvReplay/Source/PS_AI_ConvReplay/Private/PS_AI_ConvReplay_Subsystem.cpp index 468ebb6..a83fc27 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvReplay/Source/PS_AI_ConvReplay/Private/PS_AI_ConvReplay_Subsystem.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvReplay/Source/PS_AI_ConvReplay/Private/PS_AI_ConvReplay_Subsystem.cpp @@ -316,6 +316,12 @@ void UPS_AI_ConvReplay_Subsystem::EndRecordingSession() } RecordingMicComps.Reset(); + // Flush any agent utterance still open at stop. + for (UPS_AI_ConvReplay_AgentTag* Tag : Tags) + { + if (Tag) FlushAgentSegment(Tag); + } + AgentAccum.Reset(); PlayerAccum.Reset(); @@ -357,25 +363,54 @@ void UPS_AI_ConvReplay_Subsystem::RecordAgentStarted(UPS_AI_ConvReplay_AgentTag* if (!bRecording || !Tag) return; FAgentAccum& A = AgentAccum.FindOrAdd(Tag); A.StartTime = NowDemoSeconds(); + A.LastAudioTime = A.StartTime; A.PCM.Reset(); A.bOpen = true; if (AActor* Owner = Tag->GetOwner()) { A.Pos = Owner->GetActorLocation(); } + if (CVarConvReplayDebug.GetValueOnGameThread() > 0) + { + UE_LOG(LogPS_AI_ConvReplay, Log, TEXT("REC agent-START id=%s t=%.2f"), *Tag->GetResolvedId(), A.StartTime); + } } void UPS_AI_ConvReplay_Subsystem::RecordAgentAudio(UPS_AI_ConvReplay_AgentTag* Tag, const TArray& PCM) { if (!bRecording || !Tag || PCM.Num() == 0) return; FAgentAccum& A = AgentAccum.FindOrAdd(Tag); + const double Now = NowDemoSeconds(); + const bool bDbg = CVarConvReplayDebug.GetValueOnGameThread() > 0; if (!A.bOpen) { - // Audio before an explicit start — open a segment defensively. - A.StartTime = NowDemoSeconds(); + // Audio before an explicit OnAgentStartedSpeaking — open a segment defensively. + A.StartTime = Now; + A.LastAudioTime = Now; + A.bOpen = true; + if (AActor* Owner = Tag->GetOwner()) A.Pos = Owner->GetActorLocation(); + if (bDbg) UE_LOG(LogPS_AI_ConvReplay, Warning, TEXT("REC agent-audio-NO-START (defensive open) id=%s t=%.2f"), *Tag->GetResolvedId(), Now); + } + else if (A.PCM.Num() > 0 && (Now - A.LastAudioTime) > AgentGapSplitSeconds) + { + // Audio resumed after a long gap while the segment is still open. The earlier + // audio was a DISTINCT utterance — typically the first-message greeting, which + // never fires OnAgentStoppedSpeaking (no agent_response), so the segment stayed + // open and the real reply would otherwise be glued onto the greeting's early + // StartTime. Flush the earlier utterance at ITS time, then start a fresh segment + // here so this audio (the reply) gets its correct, later timestamp. + if (bDbg) + { + UE_LOG(LogPS_AI_ConvReplay, Log, + TEXT("REC agent-SPLIT id=%s gap=%.2fs → flush segStart=%.2f, new seg at %.2f"), + *Tag->GetResolvedId(), Now - A.LastAudioTime, A.StartTime, Now); + } + FlushAgentSegment(Tag); // writes the earlier utterance, resets A + A.StartTime = Now; A.bOpen = true; if (AActor* Owner = Tag->GetOwner()) A.Pos = Owner->GetActorLocation(); } + A.LastAudioTime = Now; A.PCM.Append(PCM); } @@ -385,32 +420,44 @@ void UPS_AI_ConvReplay_Subsystem::RecordAgentText(UPS_AI_ConvReplay_AgentTag* Ta AgentAccum.FindOrAdd(Tag).Text = Text; } -void UPS_AI_ConvReplay_Subsystem::RecordAgentStopped(UPS_AI_ConvReplay_AgentTag* Tag) +void UPS_AI_ConvReplay_Subsystem::FlushAgentSegment(UPS_AI_ConvReplay_AgentTag* Tag) { - if (!bRecording || !Tag) return; + if (!Tag) return; FAgentAccum* A = AgentAccum.Find(Tag); - if (!A || !A->bOpen || A->PCM.Num() == 0) - { - if (A) { A->bOpen = false; A->PCM.Reset(); A->Text.Reset(); } - return; - } + if (!A) return; - FPS_AI_ConvReplay_Record R; - R.Kind = static_cast(EPS_AI_ConvReplay_Kind::AgentSpeech); - R.TimeSec = A->StartTime; - R.DurationSec = static_cast(A->PCM.Num() / 2) / PS_AI_ConvReplay_Audio::SampleRate; - R.Id = Tag->GetResolvedId(); - R.Speaker = 0; - R.Text = A->Text; - R.Position = A->Pos; - EncodeUtterance(A->PCM, R.Audio); - WriteRecord(R); + if (A->bOpen && A->PCM.Num() > 0) + { + FPS_AI_ConvReplay_Record R; + R.Kind = static_cast(EPS_AI_ConvReplay_Kind::AgentSpeech); + R.TimeSec = A->StartTime; + R.DurationSec = static_cast(A->PCM.Num() / 2) / PS_AI_ConvReplay_Audio::SampleRate; + R.Id = Tag->GetResolvedId(); + R.Speaker = 0; + R.Text = A->Text; + R.Position = A->Pos; + EncodeUtterance(A->PCM, R.Audio); + WriteRecord(R); + + if (CVarConvReplayDebug.GetValueOnGameThread() > 0) + { + UE_LOG(LogPS_AI_ConvReplay, Log, + TEXT("REC agent-STOP id=%s start=%.2f now=%.2f dur=%.2f text='%.50s'"), + *R.Id, R.TimeSec, NowDemoSeconds(), R.DurationSec, *R.Text); + } + } A->bOpen = false; A->PCM.Reset(); A->Text.Reset(); } +void UPS_AI_ConvReplay_Subsystem::RecordAgentStopped(UPS_AI_ConvReplay_AgentTag* Tag) +{ + if (!bRecording || !Tag) return; + FlushAgentSegment(Tag); +} + void UPS_AI_ConvReplay_Subsystem::RecordAction(UPS_AI_ConvReplay_AgentTag* Tag, const FString& ActionName) { if (!bRecording || !Tag) return; @@ -457,6 +504,10 @@ void UPS_AI_ConvReplay_Subsystem::RecordUserTranscript(UPS_AI_ConvReplay_AgentTa } } WriteRecord(R); + if (CVarConvReplayDebug.GetValueOnGameThread() > 0) + { + UE_LOG(LogPS_AI_ConvReplay, Log, TEXT("REC user-TEXT t=%.2f text='%.50s'"), R.TimeSec, *Text); + } } void UPS_AI_ConvReplay_Subsystem::RecordPlayerAudio(APawn* SpeakerPawn, const TArray& PCM) @@ -500,6 +551,12 @@ void UPS_AI_ConvReplay_Subsystem::FlushPlayerSegment(APawn* Pawn) R.Position = A->Pos; EncodeUtterance(A->PCM, R.Audio); WriteRecord(R); + if (CVarConvReplayDebug.GetValueOnGameThread() > 0) + { + UE_LOG(LogPS_AI_ConvReplay, Log, + TEXT("REC player-SEG id=%s start=%.2f now=%.2f dur=%.2f"), + *R.Id, R.TimeSec, NowDemoSeconds(), R.DurationSec); + } } A->bActive = false; A->PCM.Reset(); diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvReplay/Source/PS_AI_ConvReplay/Public/PS_AI_ConvReplay_Subsystem.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvReplay/Source/PS_AI_ConvReplay/Public/PS_AI_ConvReplay_Subsystem.h index cbdef49..e795fdd 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvReplay/Source/PS_AI_ConvReplay/Public/PS_AI_ConvReplay_Subsystem.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvReplay/Source/PS_AI_ConvReplay/Public/PS_AI_ConvReplay_Subsystem.h @@ -104,6 +104,7 @@ private: // Helpers. double NowDemoSeconds() const; + void FlushAgentSegment(UPS_AI_ConvReplay_AgentTag* Tag); // write + reset an agent utterance void FlushPlayerSegment(APawn* Pawn); static bool IsVoiced(const TArray& PCM16); static FString ResolvePlayerId(APawn* Pawn); @@ -132,6 +133,7 @@ private: struct FAgentAccum { double StartTime = 0.0; + double LastAudioTime = 0.0; // demo time of the last appended chunk (gap detection) FVector Pos = FVector::ZeroVector; FString Text; TArray PCM; @@ -159,6 +161,7 @@ private: TArray> PlayerAudioPool; // Tunables. + static constexpr float AgentGapSplitSeconds = 1.5f; // split an agent utterance if audio resumes after this gap (greeting vs reply) static constexpr float PlayerSilenceFlushSec = 0.6f; static constexpr float VoicedRmsThreshold = 0.02f; static constexpr float ScrubBackThreshold = 0.05f; // seconds jumped back => scrub