From e57be0a1d9980e8599acc2524a3711b6d405d1e4 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Tue, 24 Feb 2026 16:46:36 +0100 Subject: [PATCH] v2.1.0: Decoupled viseme timeline with quintic crossfade and tuned dead zone Decouple viseme timing from 32ms audio chunks by introducing an independent FVisemeTimelineEntry timeline evaluated at render framerate. Playback-time envelope tracking from consumed queue frames replaces arrival-time-only updates, with fast 40ms decay when queue is empty. - Viseme subsampling caps at ~10/sec (100ms min) to prevent saccades - Full-duration quintic smootherstep crossfade (C2 continuous, no hold phase) - Dead zone lowered to 0.15 for cleaner silence transitions - TotalActiveFramesSeen cumulative counter for accurate timeline scaling - Absolute cursor preservation on timeline rebuild - Moderate Lerp smoothing (attack 0.55, release 0.40) Co-Authored-By: Claude Opus 4.6 --- .../Content/MetaHumans/Taro/BP_Taro.uasset | Bin 591078 -> 591107 bytes .../Private/ElevenLabsLipSyncComponent.cpp | 483 ++++++++++++++---- .../Public/ElevenLabsLipSyncComponent.h | 48 +- 3 files changed, 418 insertions(+), 113 deletions(-) diff --git a/Unreal/PS_AI_Agent/Content/MetaHumans/Taro/BP_Taro.uasset b/Unreal/PS_AI_Agent/Content/MetaHumans/Taro/BP_Taro.uasset index 5707f934bb40d5eef4dc94b83a3a2eeb332f7dbe..f17f98af3e4e72da6d5087c04584fc4b74018c41 100644 GIT binary patch delta 95765 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 Y&OV0mvLW$H8$&uq;vvx&ey&#k17UNxK>z>% delta 100376 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 diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsLipSyncComponent.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsLipSyncComponent.cpp index 3a5f073..a50f33d 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsLipSyncComponent.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsLipSyncComponent.cpp @@ -612,104 +612,233 @@ void UElevenLabsLipSyncComponent::TickComponent(float DeltaTime, ELevelTick Tick PlaybackTimer += DeltaTime; + bool bConsumedFrame = false; + float LastConsumedAmp = 0.0f; + while (PlaybackTimer >= WindowDuration && VisemeQueue.Num() > 0) { LastConsumedVisemes = VisemeQueue[0]; TargetVisemes = VisemeQueue[0]; + if (AmplitudeQueue.Num() > 0) + { + LastConsumedAmp = AmplitudeQueue[0]; + AmplitudeQueue.RemoveAt(0); + } VisemeQueue.RemoveAt(0); - if (AmplitudeQueue.Num() > 0) AmplitudeQueue.RemoveAt(0); PlaybackTimer -= WindowDuration; + bConsumedFrame = true; } - // ── Inter-frame interpolation ───────────────────────────────────────── - // Instead of holding the same TargetVisemes for 32ms then jumping to the - // next frame, blend smoothly between the last consumed frame and the next - // queued frame. This prevents the "frantic" look from step-wise changes - // and creates continuous, natural-looking mouth motion. - if (VisemeQueue.Num() > 0 && LastConsumedVisemes.Num() > 0) + // ── Playback-time envelope update ───────────────────────────────────── + // The AudioEnvelopeValue was originally only set in OnAudioChunkReceived + // (at chunk ARRIVAL time). In timeline mode, we need it to track the + // amplitude of audio being PLAYED (consumed frames), not received. + // Also decay towards 0 when no audio is being consumed (Q=0), + // so the timeline pauses and the mouth closes properly. { - const float T = FMath::Clamp(PlaybackTimer / WindowDuration, 0.0f, 1.0f); - for (const FName& Name : VisemeNames) + const float EnvAttackCoeff = 1.0f - FMath::Exp(-WindowDuration + / FMath::Max(0.001f, EnvelopeAttackMs * 0.001f)); + const float EnvReleaseCoeff = 1.0f - FMath::Exp(-WindowDuration + / FMath::Max(0.001f, EnvelopeReleaseMs * 0.001f)); + + if (bConsumedFrame) { - const float From = LastConsumedVisemes.FindRef(Name); - const float To = VisemeQueue[0].FindRef(Name); - TargetVisemes.FindOrAdd(Name) = FMath::Lerp(From, To, T); + // Update envelope from consumed frame amplitude (playback-synced) + const float Coeff = (LastConsumedAmp > AudioEnvelopeValue) + ? EnvAttackCoeff : EnvReleaseCoeff; + AudioEnvelopeValue += (LastConsumedAmp - AudioEnvelopeValue) * Coeff; + } + else if (VisemeQueue.Num() == 0) + { + // No audio being played — fast decay to close mouth promptly. + // Uses 40ms time constant (faster than EnvelopeReleaseMs=100ms) + // so the mouth closes in ~120ms instead of ~600ms. + const float FastDecayCoeff = 1.0f - FMath::Exp(-DeltaTime / 0.040f); + AudioEnvelopeValue *= (1.0f - FastDecayCoeff); + if (AudioEnvelopeValue < 0.001f) AudioEnvelopeValue = 0.0f; } } - // If queue runs dry, decay towards silence and reset text state - if (VisemeQueue.Num() == 0 && PlaybackTimer > WindowDuration * 3.0f) - { - for (const FName& Name : VisemeNames) - { - TargetVisemes.FindOrAdd(Name) = 0.0f; - } - TargetVisemes.FindOrAdd(FName("sil")) = 1.0f; - PlaybackTimer = 0.0f; - - // Reset text state — but ONLY after the full response (agent_response) - // has arrived AND text was applied. This prevents destroying text between - // audio chunks of the SAME utterance: partial text arrives once, but - // ElevenLabs splits the audio into 2-3 chunks with gaps. Without - // bFullTextReceived, the text is erased after chunk 1's queue empties, - // leaving chunks 2-3 without text visemes (spectral fallback only). - if (AccumulatedText.Len() > 0 && bTextVisemesApplied && bFullTextReceived) - { - AccumulatedText.Reset(); - TextVisemeSequence.Reset(); - bTextVisemesApplied = false; - bFullTextReceived = false; - } - } - - // ── Asymmetric smoothing ───────────────────────────────────────────────── - // At SmoothingSpeed=50: AttackSpeed=50 → alpha=0.83/frame, ~1-2 frames to target. - // ReleaseSpeed=32.5 → alpha=0.54/frame, ~3 frames to 70%. Mouth opens quickly, - // closes more gradually for natural-looking speech. - // - // In pose mode, use gentler smoothing: the spectral analysis oscillates - // between frames and with 50+ simultaneous curves per viseme, rapid - // tracking amplifies noise into visible vibration. Slower smoothing - // absorbs the oscillation while keeping movements deliberate and clean. - const bool bPoseSmoothing = (PoseExtractedCurveMap.Num() > 0); - const float AttackSpeed = SmoothingSpeed * (bPoseSmoothing ? 0.7f : 1.0f); - const float ReleaseSpeed = SmoothingSpeed * (bPoseSmoothing ? 0.45f : 0.65f); + const bool bPoseMode = (PoseExtractedCurveMap.Num() > 0); bool bAnyNonZero = false; - for (const FName& Name : VisemeNames) + if (bPoseMode && bVisemeTimelineActive && VisemeTimeline.Num() > 0) { - float& Current = SmoothedVisemes.FindOrAdd(Name); - const float Target = TargetVisemes.FindOrAdd(Name) * LipSyncStrength; + // ── POSE MODE: Decoupled viseme timeline ───────────────────────── + // Visemes are evaluated from an independent timeline at render + // framerate, completely decoupled from 32ms audio chunk windows. + // Audio provides only the amplitude envelope for modulation. - const float Speed = (Target > Current) ? AttackSpeed : ReleaseSpeed; - const float Alpha = FMath::Clamp(DeltaTime * Speed, 0.0f, 1.0f); + // Pause timeline during silence gaps between TTS chunks. + // This keeps the timeline in sync when ElevenLabs sends audio + // in 2-3 chunks with ~2s gaps between them. + const bool bShouldPause = (VisemeQueue.Num() == 0 && AudioEnvelopeValue < 0.05f); + if (!bShouldPause) + VisemeTimelineCursor += DeltaTime; - Current = FMath::Lerp(Current, Target, Alpha); - - // Snap to zero to avoid infinite tiny values - if (Current < 0.001f) Current = 0.0f; - if (Current > 0.001f) bAnyNonZero = true; - } - - // Periodic viseme activity log (Verbose — enable with log verbosity for debugging) - static int32 TickLogCount = 0; - if (++TickLogCount % 30 == 1) - { - FName DominantViseme = FName("sil"); - float DominantWeight = 0.0f; - for (const FName& Name : VisemeNames) + // Timeline end handling + const float TimelineEnd = VisemeTimeline.Last().StartSec + VisemeTimeline.Last().DurationSec; + if (VisemeTimelineCursor >= TimelineEnd) { - const float W = SmoothedVisemes.FindOrAdd(Name); - if (W > DominantWeight) + // Clamp at end — envelope will close the mouth naturally + VisemeTimelineCursor = TimelineEnd - 0.001f; + + // If queue is also dry, deactivate and reset text state + if (VisemeQueue.Num() == 0 && PlaybackTimer > WindowDuration * 3.0f) { - DominantWeight = W; - DominantViseme = Name; + bVisemeTimelineActive = false; + + if (AccumulatedText.Len() > 0 && bTextVisemesApplied && bFullTextReceived) + { + AccumulatedText.Reset(); + TextVisemeSequence.Reset(); + bTextVisemesApplied = false; + bFullTextReceived = false; + } } } - UE_LOG(LogElevenLabsLipSync, Verbose, - TEXT("LipSync: Queue=%d Viseme=%s(%.2f)"), - VisemeQueue.Num(), *DominantViseme.ToString(), DominantWeight); + // Dead zone: quadratic suppression of low envelope values + // to eliminate mouth trembling before silence transitions. + float EffectiveEnv = AudioEnvelopeValue; + const float DeadZone = 0.15f; + if (EffectiveEnv < DeadZone) + { + const float DzT = EffectiveEnv / DeadZone; + EffectiveEnv = DzT * DzT * DeadZone; + } + + // Find current viseme entry in timeline + int32 CurrentIdx = VisemeTimeline.Num() - 1; + for (int32 i = 0; i < VisemeTimeline.Num(); ++i) + { + if (VisemeTimelineCursor < VisemeTimeline[i].StartSec + VisemeTimeline[i].DurationSec) + { + CurrentIdx = i; + break; + } + } + + const FVisemeTimelineEntry& Entry = VisemeTimeline[CurrentIdx]; + const float LocalProgress = FMath::Clamp( + (VisemeTimelineCursor - Entry.StartSec) / FMath::Max(0.001f, Entry.DurationSec), + 0.0f, 1.0f); + + const int32 NextIdx = FMath::Min(CurrentIdx + 1, VisemeTimeline.Num() - 1); + const FName& NextViseme = VisemeTimeline[NextIdx].Viseme; + + // Full-duration crossfade with quintic smootherstep (C2 continuous). + // NO hold phase — the mouth is always in motion, transitioning from + // one viseme to the next over the entire duration. This eliminates + // the "static hold then snap" feel of partial crossfades. + // + // Quintic smootherstep: T³(6T²-15T+10) — smoother than smoothstep, + // zero 1st AND 2nd derivative at endpoints = no visible acceleration + // discontinuity at viseme boundaries. + float BlendToNext = 0.0f; + if (NextViseme != Entry.Viseme) + { + const float T = LocalProgress; // 0..1 over full duration + BlendToNext = T * T * T * (T * (T * 6.0f - 15.0f) + 10.0f); + } + + // Set TargetVisemes from timeline × amplitude envelope + for (const FName& Name : VisemeNames) + TargetVisemes.FindOrAdd(Name) = 0.0f; + + const float Amp = FMath::Max(EffectiveEnv, 0.0f); + TargetVisemes.FindOrAdd(Entry.Viseme) = Amp * (1.0f - BlendToNext); + if (BlendToNext > 0.0f) + TargetVisemes.FindOrAdd(NextViseme) = Amp * BlendToNext; + + // Moderate Lerp smoothing — smooths transitions across 50+ pose curves. + // Attack=0.55: reaches target in ~3 frames (99ms at 30fps) + // Release=0.40: fades in ~5 frames (165ms at 30fps) + // Slower than before (was 0.90/0.70) to eliminate saccades while the + // smoothstep crossfade handles the primary transition shape. + const float PoseAttackAlpha = 0.55f; + const float PoseReleaseAlpha = 0.40f; + + for (const FName& Name : VisemeNames) + { + float& Current = SmoothedVisemes.FindOrAdd(Name); + const float Target = TargetVisemes.FindOrAdd(Name) * LipSyncStrength; + const float Alpha = (Target > Current) ? PoseAttackAlpha : PoseReleaseAlpha; + Current = FMath::Lerp(Current, Target, Alpha); + if (Current < 0.005f) Current = 0.0f; + if (Current > 0.001f) bAnyNonZero = true; + } + } + else + { + // ── NON-POSE MODE (or pose mode without timeline) ──────────────── + // Queue-based system: inter-frame interpolation + asymmetric smoothing. + + // Inter-frame interpolation: blend between last consumed and next queued frame + if (VisemeQueue.Num() > 0 && LastConsumedVisemes.Num() > 0) + { + const float T = FMath::Clamp(PlaybackTimer / WindowDuration, 0.0f, 1.0f); + for (const FName& Name : VisemeNames) + { + const float From = LastConsumedVisemes.FindRef(Name); + const float To = VisemeQueue[0].FindRef(Name); + TargetVisemes.FindOrAdd(Name) = FMath::Lerp(From, To, T); + } + } + + // Queue-dry: decay to silence and reset text state + if (VisemeQueue.Num() == 0 && PlaybackTimer > WindowDuration * 3.0f) + { + for (const FName& Name : VisemeNames) + TargetVisemes.FindOrAdd(Name) = 0.0f; + TargetVisemes.FindOrAdd(FName("sil")) = 1.0f; + PlaybackTimer = 0.0f; + + if (AccumulatedText.Len() > 0 && bTextVisemesApplied && bFullTextReceived) + { + AccumulatedText.Reset(); + TextVisemeSequence.Reset(); + bTextVisemesApplied = false; + bFullTextReceived = false; + } + } + + // Asymmetric smoothing (fast attack, slow release) + const float AttackSpeed = SmoothingSpeed; + const float ReleaseSpeed = SmoothingSpeed * 0.65f; + + for (const FName& Name : VisemeNames) + { + float& Current = SmoothedVisemes.FindOrAdd(Name); + const float Target = TargetVisemes.FindOrAdd(Name) * LipSyncStrength; + const float Speed = (Target > Current) ? AttackSpeed : ReleaseSpeed; + const float Alpha = FMath::Clamp(DeltaTime * Speed, 0.0f, 1.0f); + Current = FMath::Lerp(Current, Target, Alpha); + if (Current < 0.001f) Current = 0.0f; + if (Current > 0.001f) bAnyNonZero = true; + } + } + + // Real-time viseme debug log — every 3 ticks (~100ms at 30fps). + // Shows all active smoothed visemes + envelope to diagnose trembling. + static int32 TickLogCount = 0; + if (++TickLogCount % 3 == 0 && bAnyNonZero) + { + FString ActiveVisemes; + for (const FName& Name : VisemeNames) + { + const float W = SmoothedVisemes.FindOrAdd(Name); + if (W > 0.01f) + { + if (ActiveVisemes.Len() > 0) ActiveVisemes += TEXT(" "); + ActiveVisemes += FString::Printf(TEXT("%s=%.3f"), *Name.ToString(), W); + } + } + if (ActiveVisemes.IsEmpty()) ActiveVisemes = TEXT("(none)"); + + UE_LOG(LogElevenLabsLipSync, Log, + TEXT("VISEME Q=%d Env=%.3f TL=%.0fms | %s"), + VisemeQueue.Num(), AudioEnvelopeValue, VisemeTimelineCursor * 1000.0f, *ActiveVisemes); } // Convert visemes to ARKit blendshapes @@ -794,7 +923,14 @@ void UElevenLabsLipSyncComponent::OnAgentStopped() VisemeQueue.Reset(); AmplitudeQueue.Reset(); PlaybackTimer = 0.0f; + AudioEnvelopeValue = 0.0f; bWaitingForText = false; + + // Deactivate viseme timeline (will be rebuilt on next utterance) + bVisemeTimelineActive = false; + VisemeTimeline.Reset(); + VisemeTimelineCursor = 0.0f; + TotalActiveFramesSeen = 0; } void UElevenLabsLipSyncComponent::ResetToNeutral() @@ -803,6 +939,7 @@ void UElevenLabsLipSyncComponent::ResetToNeutral() VisemeQueue.Reset(); AmplitudeQueue.Reset(); PlaybackTimer = 0.0f; + AudioEnvelopeValue = 0.0f; bWaitingForText = false; // Reset text-driven lip sync state for the interrupted utterance @@ -811,6 +948,12 @@ void UElevenLabsLipSyncComponent::ResetToNeutral() bTextVisemesApplied = false; bFullTextReceived = false; + // Reset decoupled viseme timeline + bVisemeTimelineActive = false; + VisemeTimeline.Reset(); + VisemeTimelineCursor = 0.0f; + TotalActiveFramesSeen = 0; + // Snap all visemes to silence immediately (no smoothing delay) for (const FName& Name : VisemeNames) { @@ -883,29 +1026,27 @@ void UElevenLabsLipSyncComponent::OnAudioChunkReceived(const TArray& PCMD if (bPoseMode) { - // ── Pose mode: hybrid amplitude ────────────────────────────────── - // Two amplitude levels serve different purposes: + // ── Pose mode: envelope-modulated amplitude ────────────────────── + // An envelope follower applied to per-window RMS creates a smooth + // amplitude curve that tracks speech dynamics: // - // 1. CHUNK-LEVEL RMS → shape intensity (smooth, no per-window jitter). - // All active frames use this amplitude so 50+ pose curves don't - // vibrate from 32ms-level amplitude oscillation. + // - Fast ATTACK: mouth opens quickly on speech onset / louder syllables + // - Slow RELEASE: mouth closes gradually between syllables / pauses + // - No per-window jitter: the envelope smooths 32ms-level oscillation + // that would vibrate 50+ simultaneous pose curves // - // 2. PER-WINDOW RMS → silence detection only (binary: speech or pause). - // Detects intra-chunk silence windows that chunk-level averaging - // would miss. This makes audio pauses (commas, breathing) visible - // as the mouth properly closes during gaps. + // Raw per-window RMS is still used for silence detection (binary gate). + // The envelope value drives the shape intensity (how open the mouth is). // - float ChunkSumSq = 0.0f; - for (int32 i = 0; i < NumSamples; ++i) - ChunkSumSq += FloatBuffer[i] * FloatBuffer[i]; - const float ChunkRMS = FMath::Sqrt(ChunkSumSq / FMath::Max(1.0f, static_cast(NumSamples))); - float ChunkAmplitude = FMath::Clamp(ChunkRMS * 10.0f, 0.0f, 1.5f); - ChunkAmplitude = FMath::Clamp(FMath::Pow(ChunkAmplitude, 0.4f), 0.0f, 1.0f); - ChunkAmplitude *= AmplitudeScale; + const float WindowDurationSec = static_cast(WindowSize) / 16000.0f; // ~32ms + const float AttackCoeff = 1.0f - FMath::Exp(-WindowDurationSec + / FMath::Max(0.001f, EnvelopeAttackMs * 0.001f)); + const float ReleaseCoeff = 1.0f - FMath::Exp(-WindowDurationSec + / FMath::Max(0.001f, EnvelopeReleaseMs * 0.001f)); for (int32 Offset = 0; Offset + WindowSize <= NumSamples; Offset += WindowSize) { - // Per-window amplitude for silence detection + // Per-window RMS amplitude float WindowSumSq = 0.0f; const int32 WindowEnd = FMath::Min(Offset + WindowSize, NumSamples); const int32 WindowLen = WindowEnd - Offset; @@ -916,7 +1057,24 @@ void UElevenLabsLipSyncComponent::OnAudioChunkReceived(const TArray& PCMD WindowAmp = FMath::Clamp(FMath::Pow(WindowAmp, 0.4f), 0.0f, 1.0f); WindowAmp *= AmplitudeScale; - const bool bSilentWindow = (WindowAmp < 0.08f); + // Envelope follower: fast attack, slow release + const float Coeff = (WindowAmp > AudioEnvelopeValue) ? AttackCoeff : ReleaseCoeff; + AudioEnvelopeValue += (WindowAmp - AudioEnvelopeValue) * Coeff; + + // Dead zone: quadratic suppression of low amplitudes. + // Low envelope values (0.08-0.15) produce tiny mouth movements + // that look like trembling before silence. The quadratic curve + // pushes these below the silence gate, creating a + // clean cut into silence instead of gradual fade-to-tremble. + const float DeadZone = 0.15f; + float EffectiveAmp = AudioEnvelopeValue; + if (EffectiveAmp < DeadZone) + { + const float T = EffectiveAmp / DeadZone; // 0..1 + EffectiveAmp = T * T * DeadZone; // Quadratic: low → ~0 + } + + const bool bSilentWindow = (EffectiveAmp < 0.08f); TMap Frame; for (const FName& Name : VisemeNames) @@ -925,17 +1083,18 @@ void UElevenLabsLipSyncComponent::OnAudioChunkReceived(const TArray& PCMD if (bSilentWindow) { Frame.FindOrAdd(FName("sil")) = 1.0f; - AmplitudeQueue.Add(0.0f); // Marked silent for ApplyTextVisemesToQueue + AmplitudeQueue.Add(0.0f); } else { - Frame.FindOrAdd(FName("aa")) = ChunkAmplitude; // Smooth chunk intensity - AmplitudeQueue.Add(ChunkAmplitude); + Frame.FindOrAdd(FName("aa")) = EffectiveAmp; + AmplitudeQueue.Add(EffectiveAmp); + TotalActiveFramesSeen++; } VisemeQueue.Add(Frame); - MinAmp = FMath::Min(MinAmp, bSilentWindow ? 0.0f : ChunkAmplitude); - MaxAmp = FMath::Max(MaxAmp, ChunkAmplitude); + MinAmp = FMath::Min(MinAmp, bSilentWindow ? 0.0f : EffectiveAmp); + MaxAmp = FMath::Max(MaxAmp, EffectiveAmp); WindowsQueued++; } } @@ -1156,7 +1315,10 @@ void UElevenLabsLipSyncComponent::OnAudioChunkReceived(const TArray& PCMD if (AccumulatedText.Len() > 0 && TextVisemeSequence.Num() >= 3) { // Text already available — apply and start playback immediately - ApplyTextVisemesToQueue(); + if (bPoseMode) + BuildVisemeTimeline(); + else + ApplyTextVisemesToQueue(); PlaybackTimer = 0.0f; UE_LOG(LogElevenLabsLipSync, Verbose, TEXT("Text already available (%d visemes). Starting lip sync immediately."), @@ -1176,7 +1338,10 @@ void UElevenLabsLipSyncComponent::OnAudioChunkReceived(const TArray& PCMD else if (AccumulatedText.Len() > 0 && TextVisemeSequence.Num() > 0) { // Not a new utterance but text is available — apply to new frames - ApplyTextVisemesToQueue(); + if (bPoseMode) + BuildVisemeTimeline(); + else + ApplyTextVisemesToQueue(); } UE_LOG(LogElevenLabsLipSync, Log, @@ -1216,9 +1381,13 @@ void UElevenLabsLipSyncComponent::OnPartialTextReceived(const FString& PartialTe // apply text visemes to queued frames and start consuming. if (bWaitingForText && TextVisemeSequence.Num() >= 3) { + const bool bPoseMode = (PoseExtractedCurveMap.Num() > 0); if (VisemeQueue.Num() > 0) { - ApplyTextVisemesToQueue(); + if (bPoseMode) + BuildVisemeTimeline(); + else + ApplyTextVisemesToQueue(); } bWaitingForText = false; PlaybackTimer = 0.0f; // Start consuming now @@ -1239,10 +1408,13 @@ void UElevenLabsLipSyncComponent::OnTextResponseReceived(const FString& Response UE_LOG(LogElevenLabsLipSync, Log, TEXT("Full text: \"%s\" → %d visemes"), *ResponseText, TextVisemeSequence.Num()); - // Apply to any remaining queued frames - if (VisemeQueue.Num() > 0) + // Apply to any remaining queued frames (or extend timeline in pose mode) { - ApplyTextVisemesToQueue(); + const bool bPoseMode = (PoseExtractedCurveMap.Num() > 0); + if (bPoseMode) + BuildVisemeTimeline(); + else if (VisemeQueue.Num() > 0) + ApplyTextVisemesToQueue(); } // If we were waiting for text to arrive before starting playback, start now @@ -1775,22 +1947,25 @@ void UElevenLabsLipSyncComponent::ApplyTextVisemesToQueue() Frame.FindOrAdd(Name) = 0.0f; } - // Anticipatory blending: in the last 30% of each viseme, - // gradually blend towards the next viseme shape. - const float BlendZone = 0.3f; + // Full crossfade with smoothstep in the last 40% of each viseme. + // The previous 30%/50%-max blend caused a discontinuity at viseme + // boundaries (50/50 → 0/100 jump), visible as trembling on 50+ curves. + // Now: full 0→100% crossfade with smoothstep (ease-in-out) curve + // eliminates any discontinuity at the boundary. + const float BlendZone = 0.4f; float BlendToNext = 0.0f; if (LocalProgress > (1.0f - BlendZone) && NextViseme != TextViseme) { - BlendToNext = (LocalProgress - (1.0f - BlendZone)) / BlendZone; + const float T = (LocalProgress - (1.0f - BlendZone)) / BlendZone; // 0..1 + BlendToNext = T * T * (3.0f - 2.0f * T); // Smoothstep: ease-in-out } - // Primary viseme shape × amplitude - Frame.FindOrAdd(TextViseme) += Amp * (1.0f - BlendToNext * 0.5f); + // Crossfade: current viseme fades out, next fades in + Frame.FindOrAdd(TextViseme) += Amp * (1.0f - BlendToNext); - // Blend towards next viseme if (BlendToNext > 0.0f) { - Frame.FindOrAdd(NextViseme) += Amp * BlendToNext * 0.5f; + Frame.FindOrAdd(NextViseme) += Amp * BlendToNext; } ActiveIdx++; @@ -1806,6 +1981,90 @@ void UElevenLabsLipSyncComponent::ApplyTextVisemesToQueue() FinalRatio, FinalRatio * 32.0f); } +// ───────────────────────────────────────────────────────────────────────────── +// Decoupled viseme timeline (pose mode) +// ───────────────────────────────────────────────────────────────────────────── + +void UElevenLabsLipSyncComponent::BuildVisemeTimeline() +{ + if (TextVisemeSequence.Num() == 0) return; + + // Use TOTAL active frames seen across all chunks (not just remaining queue). + // Frames already consumed by TickComponent are counted too, so the timeline + // is properly scaled to the full audio duration. + constexpr float WindowDurationSec = 512.0f / 16000.0f; // ~32ms + if (TotalActiveFramesSeen == 0) return; + + const float AudioDurationSec = TotalActiveFramesSeen * WindowDurationSec; + + // ── Subsample viseme sequence to natural speech rate ────────────────── + // Real speech has ~4-5 distinct mouth shapes per second (one per syllable + // nucleus). The text-to-viseme pipeline can produce 15-25 visemes for a + // short phrase, which at 1.2s audio = ~60ms each = saccades. + // Cap at ~10 visemes/sec (100ms minimum) — allows more phoneme detail + // while staying within natural French syllable rate (~8-10 shapes/sec). + constexpr float MinVisemeDurationSec = 0.100f; + const int32 MaxVisemes = FMath::Max(2, FMath::CeilToInt(AudioDurationSec / MinVisemeDurationSec)); + + TArray FinalSequence; + if (TextVisemeSequence.Num() > MaxVisemes) + { + // Subsample: take evenly-spaced visemes from the full sequence + for (int32 i = 0; i < MaxVisemes; ++i) + { + const int32 Idx = (i * (TextVisemeSequence.Num() - 1)) / FMath::Max(1, MaxVisemes - 1); + const FName& V = TextVisemeSequence[Idx]; + // Skip consecutive duplicates + if (FinalSequence.Num() == 0 || FinalSequence.Last() != V) + FinalSequence.Add(V); + } + } + else + { + FinalSequence = TextVisemeSequence; + } + + if (FinalSequence.Num() == 0) return; + + // Compute natural durations from phoneme weights + float NaturalTotalSec = 0.0f; + for (const FName& V : FinalSequence) + { + NaturalTotalSec += GetVisemeDurationWeight(V) * 0.120f; + } + + // Scale factor: match actual audio duration + const float Scale = (NaturalTotalSec > 0.01f) ? AudioDurationSec / NaturalTotalSec : 1.0f; + + // If timeline is already playing, preserve absolute cursor position. + const float SavedCursor = bVisemeTimelineActive ? VisemeTimelineCursor : 0.0f; + + // Build timeline entries with scaled durations + VisemeTimeline.Reset(); + float CursorSec = 0.0f; + for (const FName& V : FinalSequence) + { + FVisemeTimelineEntry Entry; + Entry.Viseme = V; + Entry.StartSec = CursorSec; + Entry.DurationSec = GetVisemeDurationWeight(V) * 0.120f * Scale; + VisemeTimeline.Add(Entry); + CursorSec += Entry.DurationSec; + } + + // Restore cursor: keep absolute position, clamped to new timeline + VisemeTimelineCursor = FMath::Min(SavedCursor, FMath::Max(0.0f, CursorSec - 0.001f)); + + bVisemeTimelineActive = true; + bTextVisemesApplied = true; + + UE_LOG(LogElevenLabsLipSync, Log, + TEXT("Built viseme timeline: %d entries (from %d, max %d), audio=%.0fms, scale=%.2f → %.0fms/viseme avg"), + FinalSequence.Num(), TextVisemeSequence.Num(), MaxVisemes, + AudioDurationSec * 1000.0f, Scale, + (FinalSequence.Num() > 0) ? (CursorSec * 1000.0f / FinalSequence.Num()) : 0.0f); +} + void UElevenLabsLipSyncComponent::AnalyzeSpectrum() { if (!SpectrumAnalyzer) return; diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsLipSyncComponent.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsLipSyncComponent.h index 8d987ac..a324697 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsLipSyncComponent.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsLipSyncComponent.h @@ -11,6 +11,15 @@ class UElevenLabsConversationalAgentComponent; class UElevenLabsLipSyncPoseMap; class USkeletalMeshComponent; +/** A single entry in the decoupled viseme timeline. + * Built from text phoneme analysis, played back independently of audio chunks. */ +struct FVisemeTimelineEntry +{ + FName Viseme; + float StartSec; // absolute start time from utterance start + float DurationSec; // how long this viseme is held +}; + // Fired every tick when viseme/blendshape data has been updated. DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnElevenLabsVisemesReady); @@ -66,6 +75,24 @@ public: ToolTip = "Smoothing speed for viseme transitions.\n35 = smooth and soft, 50 = balanced, 65 = sharp and responsive.")) float SmoothingSpeed = 50.0f; + // ── Audio Envelope ────────────────────────────────────────────────────── + + /** Envelope attack time in milliseconds. + * Controls how fast the mouth opens when speech starts or gets louder. + * Lower = snappier onset, higher = gentler opening. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ElevenLabs|LipSync", + meta = (ClampMin = "5.0", ClampMax = "100.0", + ToolTip = "Envelope attack (ms).\n10 = snappy, 15 = balanced, 30 = gentle.\nHow fast the mouth opens on speech onset.")) + float EnvelopeAttackMs = 15.0f; + + /** Envelope release time in milliseconds. + * Controls how slowly the mouth closes when speech gets quieter. + * Higher = smoother, more natural decay. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ElevenLabs|LipSync", + meta = (ClampMin = "20.0", ClampMax = "500.0", + ToolTip = "Envelope release (ms).\n60 = responsive, 100 = balanced, 200 = smooth/cinematic.\nHow slowly the mouth closes between syllables.")) + float EnvelopeReleaseMs = 100.0f; + // ── Phoneme Pose Map ───────────────────────────────────────────────────── /** Optional pose map asset mapping OVR visemes to phoneme AnimSequences. @@ -130,9 +157,14 @@ private: /** Convert text to a sequence of OVR viseme names (grapheme-to-phoneme-to-viseme). */ void ConvertTextToVisemes(const FString& Text); - /** Apply text-derived viseme shapes to the remaining queued frames. */ + /** Apply text-derived viseme shapes to the remaining queued frames (non-pose mode). */ void ApplyTextVisemesToQueue(); + /** Build a decoupled viseme timeline from text (pose mode). + * Visemes get natural phoneme durations, evaluated continuously in Tick + * instead of being quantized to 32ms audio chunks. */ + void BuildVisemeTimeline(); + /** Extract frequency band energies from the spectrum analyzer. */ void AnalyzeSpectrum(); @@ -198,6 +230,10 @@ private: // Timer for consuming queued viseme frames at the FFT window rate float PlaybackTimer = 0.0f; + // Envelope follower state: smoothed amplitude that tracks speech dynamics + // with fast attack (mouth opens quickly) and slow release (closes gradually). + float AudioEnvelopeValue = 0.0f; + // Whether we have pending analysis results to process bool bHasPendingAnalysis = false; @@ -214,6 +250,16 @@ private: // Whether text-based visemes have been applied to the current queue bool bTextVisemesApplied = false; + // ── Decoupled viseme timeline (pose mode) ──────────────────────────────── + // In pose mode, text visemes are played from an independent timeline + // evaluated each tick at render framerate, instead of being quantized + // to 32ms audio chunk windows. Audio provides only the amplitude envelope. + + TArray VisemeTimeline; + float VisemeTimelineCursor = 0.0f; // current playback position (seconds) + bool bVisemeTimelineActive = false; // true when timeline is playing + int32 TotalActiveFramesSeen = 0; // cumulative non-silent frames across all chunks + // Set when agent_response arrives (full text for this utterance). // Prevents resetting AccumulatedText between audio chunks of the // SAME utterance — only reset once the full response is confirmed.