diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs index e2d3a49..596c0c8 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/WebSocketTurnFinalizationService.cs @@ -260,6 +260,7 @@ public sealed class WebSocketTurnFinalizationService( (hotphrase.ValueKind == JsonValueKind.True || hotphrase.ValueKind == JsonValueKind.False)) { turnState.ListenHotphrase = hotphrase.GetBoolean(); + turnState.HotphraseEmptyTurnCount = 0; } if (data.TryGetProperty("intent", out var intent) && intent.ValueKind == JsonValueKind.String) @@ -310,6 +311,7 @@ public sealed class WebSocketTurnFinalizationService( turnState.SawListen = false; turnState.SawContext = false; turnState.ListenHotphrase = false; + turnState.HotphraseEmptyTurnCount = 0; turnState.ListenRules = []; turnState.ListenAsrHints = []; } @@ -364,6 +366,32 @@ public sealed class WebSocketTurnFinalizationService( return []; } + if (ShouldIgnoreInitialEmptyHotphraseTurn(finalizedTurn, turnState)) + { + turnState.HotphraseEmptyTurnCount += 1; + turnState.AwaitingTurnCompletion = true; + return + [ + new WebSocketReply + { + Text = JsonSerializer.Serialize(new + { + type = "OPENJIBO_TURN_PENDING", + data = new + { + sessionId = session.SessionId, + transID = session.LastTransId, + bufferedAudioBytes = turnState.BufferedAudioBytes, + bufferedAudioChunks = turnState.BufferedAudioChunkCount, + awaitingAudio = turnState.BufferedAudioBytes == 0, + awaitingTranscriptHint = turnState.BufferedAudioBytes > 0 && string.IsNullOrWhiteSpace(turnState.AudioTranscriptHint), + finalizeAttempts = turnState.FinalizeAttemptCount + } + }) + } + ]; + } + if (ShouldTreatEmptyHotphraseTurnAsGreeting(finalizedTurn)) { finalizedTurn = WithSyntheticTranscript(finalizedTurn, "hello"); @@ -689,6 +717,33 @@ public sealed class WebSocketTurnFinalizationService( .Any(static rule => string.Equals(rule, "launch", StringComparison.OrdinalIgnoreCase)); } + private static bool ShouldIgnoreInitialEmptyHotphraseTurn(TurnContext turn, WebSocketTurnState turnState) + { + if (!string.IsNullOrWhiteSpace(turn.NormalizedTranscript) || !string.IsNullOrWhiteSpace(turn.RawTranscript)) + { + return false; + } + + var messageType = ReadMessageType(turn); + if (messageType is not ("CLIENT_ASR" or "CLIENT_NLU")) + { + return false; + } + + if (!ReadBoolAttribute(turn, "listenHotphrase")) + { + return false; + } + + if (turnState.HotphraseEmptyTurnCount > 0) + { + return false; + } + + return ReadRules(turn, "listenRules") + .Any(static rule => string.Equals(rule, "launch", StringComparison.OrdinalIgnoreCase)); + } + private static TurnContext WithSyntheticTranscript(TurnContext turn, string transcript) { var attributes = new Dictionary(turn.Attributes, StringComparer.OrdinalIgnoreCase) diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Domain/Models/WebSocketTurnState.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Domain/Models/WebSocketTurnState.cs index 3456d00..2935c1b 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Domain/Models/WebSocketTurnState.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Domain/Models/WebSocketTurnState.cs @@ -5,6 +5,7 @@ public sealed class WebSocketTurnState public string? TransId { get; set; } public string? ContextPayload { get; set; } public bool ListenHotphrase { get; set; } + public int HotphraseEmptyTurnCount { get; set; } public string? AudioTranscriptHint { get; set; } public string? LastSttError { get; set; } public DateTimeOffset? LastSttErrorUtc { get; set; } diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs index ac77709..b79d1ff 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/JiboWebSocketServiceTests.cs @@ -573,7 +573,7 @@ public sealed class JiboWebSocketServiceTests } [Fact] - public async Task EmptyHotphraseTurn_BecomesGreetingAndKeepsFollowUpOpen() + public async Task SecondEmptyHotphraseTurn_BecomesGreetingAndKeepsFollowUpOpen() { await _service.HandleMessageAsync(new WebSocketMessageEnvelope { @@ -584,6 +584,18 @@ public sealed class JiboWebSocketServiceTests Text = """{"type":"LISTEN","transID":"trans-empty-hotphrase","data":{"hotphrase":true,"rules":["launch","globals/global_commands_launch"]}}""" }); + var firstReplies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope + { + HostName = "neo-hub.jibo.com", + Path = "/listen", + Kind = "neo-hub-listen", + Token = "hub-empty-hotphrase-token", + Text = """{"type":"CLIENT_ASR","transID":"trans-empty-hotphrase","data":{}}""" + }); + + Assert.Single(firstReplies); + Assert.Equal("OPENJIBO_TURN_PENDING", ReadReplyType(firstReplies[0])); + var replies = await _service.HandleMessageAsync(new WebSocketMessageEnvelope { HostName = "neo-hub.jibo.com",