Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -132,5 +132,63 @@ await FluentActions
x => x.GenerateSpeechUrlAsync("Hello from Resgrid", null, null, It.IsAny<CancellationToken>()),
Times.Once);
}

[Test]
public async Task pre_warm_prompt_async_should_not_throw_for_multi_chunk_text()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules low

.NET naming convention violation occurs because the test methods pre_warm_prompt_async_should_not_throw_for_multi_chunk_text and append_prompt_async_should_emit_a_play_per_chunk_for_multi_chunk_text use snake_case. Rename these methods to use PascalCase to comply with C# standards.

Kody rule violation: Use proper naming conventions

[Test]
public async Task PreWarmPromptAsyncShouldNotThrowForMultiChunkText()

[Test]
public async Task AppendPromptAsyncShouldEmitAPlayPerChunkForMultiChunkText()
Prompt for LLM

File Tests/Resgrid.Tests/Web/Services/TwilioVoiceResponseServiceTests.cs:

Line 137:

Violates rule 'Use proper naming conventions': the two new test methods use snake_case names (pre_warm_prompt_async_should_not_throw_for_multi_chunk_text and append_prompt_async_should_emit_a_play_per_chunk_for_multi_chunk_text) instead of PascalCase as required by the .NET naming convention for methods.

Suggested Code:

[Test]
public async Task PreWarmPromptAsyncShouldNotThrowForMultiChunkText()

[Test]
public async Task AppendPromptAsyncShouldEmitAPlayPerChunkForMultiChunkText()

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

{
// Regression (Sentry RESGRID-API-78): long dispatch text spans multiple TTS chunks.
// PreWarmPromptAsync previously threw ArgumentException for multi-chunk input, faulting
// the voice-dispatch pre-warm/redirect path. It must now warm every chunk without throwing.
var originalMaxLength = Resgrid.Config.TtsConfig.MaxTextLength;
Resgrid.Config.TtsConfig.MaxTextLength = 30;
try
{
var ttsAudioService = new Mock<ITtsAudioService>();
ttsAudioService
.Setup(x => x.GenerateSpeechUrlAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Uri("https://tts.example.com/tts/audio/chunk.wav"));

var service = new TwilioVoiceResponseService(ttsAudioService.Object);
var text = "Engine one respond to the structure fire. Ladder two stage at the corner. Battalion three assume command. All units use caution.";

Func<Task> act = () => service.PreWarmPromptAsync(text);

await act.Should().NotThrowAsync();
}
finally
{
Resgrid.Config.TtsConfig.MaxTextLength = originalMaxLength;
}
}

[Test]
public async Task append_prompt_async_should_emit_a_play_per_chunk_for_multi_chunk_text()
{
// Regression (Sentry RESGRID-API-78): the dispatch playback path now routes long text
// through AppendPromptAsync, which must fan multi-chunk text out to one <Play> per chunk
// rather than throwing like the single-chunk GetPromptUrlAsync did.
var originalMaxLength = Resgrid.Config.TtsConfig.MaxTextLength;
Resgrid.Config.TtsConfig.MaxTextLength = 30;
try
{
var ttsAudioService = new Mock<ITtsAudioService>();
ttsAudioService
.Setup(x => x.GenerateSpeechUrlAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Uri("https://tts.example.com/tts/audio/chunk.wav"));

var service = new TwilioVoiceResponseService(ttsAudioService.Object);
var response = new VoiceResponse();
var text = "Engine one respond to the structure fire. Ladder two stage at the corner. Battalion three assume command. All units use caution.";

await service.AppendPromptAsync(response, text, CancellationToken.None);

var playCount = response.ToString().Split("<Play>").Length - 1;
playCount.Should().BeGreaterThan(1);
}
finally
{
Resgrid.Config.TtsConfig.MaxTextLength = originalMaxLength;
}
}
}
}
6 changes: 4 additions & 2 deletions Web/Resgrid.Web.Services/Controllers/TwilioController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1246,8 +1246,10 @@ private async System.Threading.Tasks.Task<bool> TryAppendDispatchPlaybackAsync(V

try
{
var url = await _twilioVoiceResponseService.GetPromptUrlAsync(dispatchText, ttsLanguage, linkedCts.Token);
response.Append(new Play { Url = url });
// Dispatch text can exceed the TTS chunk limit (long notes/address), so use the
// multi-chunk-aware AppendPromptAsync (one <Play> per chunk) instead of GetPromptUrlAsync,
// which only supports single-chunk text and throws ArgumentException otherwise.
await _twilioVoiceResponseService.AppendPromptAsync(response, dispatchText, linkedCts.Token, ttsLanguage);
return true;
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
Expand Down
26 changes: 13 additions & 13 deletions Web/Resgrid.Web.Services/Twilio/TwilioVoiceResponseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,19 +188,19 @@ public System.Threading.Tasks.Task PreWarmPromptAsync(string text, string voice
{
ArgumentException.ThrowIfNullOrWhiteSpace(text);

var chunks = ChunkText(text).ToList();
if (chunks.Count != 1)
throw new ArgumentException($"PreWarmPromptAsync does not support multi-chunk input (got {chunks.Count} chunks). Use AppendPromptAsync for multi-chunk text.", nameof(text));

// Start the generation task (or return the existing one) without
// necessarily awaiting it. The TTS microservice's internal cache
// persists across requests, so a subsequent call will find the URL.
GetOrCreatePromptUrlAsync(chunks[0], voice, CancellationToken.None)
.ContinueWith(t =>
{
if (t.IsFaulted && t.Exception != null)
Logging.LogException(t.Exception);
}, TaskContinuationOptions.OnlyOnFaulted);
// Start generation for each chunk (or return the existing task) without
// necessarily awaiting it. The TTS microservice's internal cache persists
// across requests, so a subsequent call (GetPromptUrlAsync/AppendPromptAsync)
// will find the URLs. Long dispatch text spans multiple chunks, so warm them all.
foreach (var chunk in ChunkText(text))
{
GetOrCreatePromptUrlAsync(chunk, voice, CancellationToken.None)
.ContinueWith(t =>
{
if (t.IsFaulted && t.Exception != null)
Logging.LogException(t.Exception);
}, TaskContinuationOptions.OnlyOnFaulted);
}
return System.Threading.Tasks.Task.CompletedTask;
}

Expand Down
Loading