Skip to content

Commit b52de41

Browse files
committed
RE1-T115 TTS changes and fixed issue with SystemAuth api breaking some api controllers
1 parent d193f87 commit b52de41

22 files changed

Lines changed: 144 additions & 759 deletions

Core/Resgrid.Config/TtsConfig.cs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ public static class TtsConfig
2424
public static int S3PresignedUrlExpiryMinutes = 60;
2525
public static string S3PublicBaseUrl = "";
2626

27-
public static string DefaultVoice = "en-us+klatt6";
28-
public static int DefaultSpeed = 175;
27+
public static string DefaultVoice = "en-us+klatt4";
28+
public static int DefaultSpeed = 165;
2929
public static int MaxConcurrentGenerations = 4;
3030
public static int MaxTextLength = 1000;
3131
public static string EspeakExecutable = "espeak-ng";
@@ -35,7 +35,42 @@ public static class TtsConfig
3535
public static int NormalizedSampleRate = 8000;
3636
public static int NormalizedChannels = 1;
3737
public static bool WarmupEnabled = true;
38-
public static string PreGeneratedPrompts = "Press 1 for yes;Press 2 for no;Invalid option;Please try again;Please stay on the line;This call has been closed. Goodbye.;You have been marked responding to the scene, goodbye.;Sorry, that was not a valid selection.;Hello, this is Resgrid calling with your verification code.;That was your Resgrid verification code. Goodbye.;Thank you for calling Resgrid, automated personnel system. The number you called is not tied to an active department or the department doesn't have this feature enabled. Goodbye.;We couldn't complete your verification call. Please request a new code and try again. Goodbye.;Please select from the following options.;To list current active calls, press 1.;To list current user statuses, press 2.;To list current unit statuses, press 3.;To list upcoming calendar events, press 4.;To list upcoming shifts, press 5.;To set your current status, press 6.;To set your current staffing level, press 7.;Press 0 to repeat. Press 1 to respond to the scene.;Press 0 to go back to the main menu.;Invalid status selection, goodbye.;No status selection made, goodbye.;Invalid staffing selection. Returning to the main menu.;No staffing selection made. Returning to the main menu.;Thank you. Your response has been recorded.";
38+
public static string PreGeneratedPrompts = string.Join(";", new[]
39+
{
40+
"Press 1 for yes",
41+
"Press 2 for no",
42+
"Invalid option",
43+
"Please try again",
44+
"Please stay on the line",
45+
"This call has been closed. Goodbye.",
46+
"You have been marked responding to the scene. Goodbye.",
47+
"Sorry, that was not a valid selection.",
48+
"Hello, this is Resgrid calling with your verification code.",
49+
"That was your Resgrid verification code. Goodbye.",
50+
"Thank you for calling the Resgrid automated personnel system. The number you called is not tied to an active department, or the department doesn't have this feature enabled. Goodbye.",
51+
"We couldn't complete your verification call. Please request a new code and try again. Goodbye.",
52+
"Please select from the following options.",
53+
"To list current active calls, press 1.",
54+
"To list current user statuses, press 2.",
55+
"To list current unit statuses, press 3.",
56+
"To list upcoming calendar events, press 4.",
57+
"To list upcoming shifts, press 5.",
58+
"To set your current status, press 6.",
59+
"To set your current staffing level, press 7.",
60+
"Press 0 to repeat. Press 1 to respond to the scene.",
61+
"To hear the dispatch again, press 1. To hear response options, press 2.",
62+
"To choose a response option, enter the option number, then press pound.",
63+
"To hear the dispatch again, enter 0 and press pound.",
64+
"Press 0 to go back to the main menu.",
65+
"To go back to the main menu, enter 0 and press pound.",
66+
"To set your current status, enter the number of your selection, then press pound.",
67+
"To set your current staffing, enter the number of your selection, then press pound.",
68+
"Invalid status selection. Returning to the main menu.",
69+
"No status selection made. Returning to the main menu.",
70+
"Invalid staffing selection. Returning to the main menu.",
71+
"No staffing selection made. Returning to the main menu.",
72+
"Thank you. Your response has been recorded."
73+
});
3974

4075
public static int RateLimitPermitLimit = 60;
4176
public static int RateLimitQueueLimit = 10;

Tests/Resgrid.Tests/Services/DepartmentSettingsServiceTtsLanguageTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public void SetUp()
4141
_cacheProvider.Object);
4242

4343
global::Resgrid.Config.SystemBehaviorConfig.CacheEnabled = false;
44-
TtsConfig.DefaultVoice = "en-us+klatt6";
44+
TtsConfig.DefaultVoice = "en-us+klatt4";
4545
}
4646

4747
[TearDown]
@@ -83,7 +83,7 @@ public async Task should_fall_back_to_default_tts_language_when_setting_missing(
8383
[Test]
8484
public async Task should_fall_back_to_default_tts_language_when_setting_is_invalid()
8585
{
86-
TtsConfig.DefaultVoice = "fr+klatt6";
86+
TtsConfig.DefaultVoice = "fr+klatt4";
8787

8888
_departmentSettingsRepository
8989
.Setup(x => x.GetDepartmentSettingByIdTypeAsync(7, DepartmentSettingTypes.TtsLanguage))

Tests/Resgrid.Tests/Web/Tts/S3StorageServiceTests.cs

Lines changed: 22 additions & 216 deletions
Original file line numberDiff line numberDiff line change
@@ -56,49 +56,32 @@ public async Task upload_async_should_buffer_non_seekable_stream_for_retries()
5656
}
5757

5858
[Test]
59-
public async Task exists_async_should_fall_back_to_presigned_head_when_metadata_response_is_malformed()
59+
public async Task exists_async_should_treat_malformed_metadata_response_as_existing_object()
6060
{
6161
var s3Client = new Mock<IAmazonS3>(MockBehavior.Strict);
6262
s3Client
6363
.Setup(x => x.GetObjectMetadataAsync(It.IsAny<GetObjectMetadataRequest>(), It.IsAny<CancellationToken>()))
6464
.ThrowsAsync(new FormatException("bad metadata expiration header"));
65-
s3Client
66-
.Setup(x => x.GetPreSignedURL(It.IsAny<GetPreSignedUrlRequest>()))
67-
.Returns<GetPreSignedUrlRequest>(request =>
68-
{
69-
request.BucketName.Should().Be("tts-bucket");
70-
request.Key.Should().Be("tts/audio.wav");
71-
request.Verb.Should().Be(HttpVerb.HEAD);
72-
request.Protocol.Should().Be(Protocol.HTTP);
73-
return "http://upload.example.com/tts/audio.wav?signature=head";
74-
});
7565

76-
var handler = new RecordingHttpMessageHandler((request, _) =>
77-
{
78-
request.Method.Should().Be(HttpMethod.Head);
79-
request.RequestUri.Should().Be(new Uri("http://upload.example.com/tts/audio.wav?signature=head"));
80-
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
81-
});
66+
var handler = new RecordingHttpMessageHandler((_, _) =>
67+
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)));
8268
var service = CreateService(s3Client.Object, handler, useSsl: false);
8369

8470
var exists = await service.ExistsAsync("tts/audio.wav", CancellationToken.None);
8571

8672
exists.Should().BeTrue();
87-
handler.Requests.Should().HaveCount(1);
73+
handler.Requests.Should().BeEmpty();
8874
s3Client.Verify(x => x.GetObjectMetadataAsync(It.Is<GetObjectMetadataRequest>(request => request.BucketName == "tts-bucket" && request.Key == "tts/audio.wav"), It.IsAny<CancellationToken>()), Times.Once);
89-
s3Client.Verify(x => x.GetPreSignedURL(It.IsAny<GetPreSignedUrlRequest>()), Times.Once);
75+
s3Client.Verify(x => x.GetPreSignedURL(It.IsAny<GetPreSignedUrlRequest>()), Times.Never);
9076
}
9177

9278
[Test]
93-
public async Task upload_async_should_treat_format_exception_as_success_when_object_exists_after_upload()
79+
public async Task upload_async_should_treat_malformed_put_response_as_success_without_using_presigned_uploads()
9480
{
9581
var s3Client = new Mock<IAmazonS3>(MockBehavior.Strict);
9682
s3Client
9783
.Setup(x => x.PutObjectAsync(It.IsAny<PutObjectRequest>(), It.IsAny<CancellationToken>()))
9884
.ThrowsAsync(new FormatException("bad expiration header"));
99-
s3Client
100-
.Setup(x => x.GetObjectMetadataAsync(It.IsAny<GetObjectMetadataRequest>(), It.IsAny<CancellationToken>()))
101-
.ReturnsAsync(new GetObjectMetadataResponse());
10285

10386
var handler = new RecordingHttpMessageHandler((_, _) =>
10487
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)));
@@ -109,216 +92,39 @@ public async Task upload_async_should_treat_format_exception_as_success_when_obj
10992
await service.UploadAsync("tts/audio.wav", content, "audio/wav", CancellationToken.None);
11093

11194
handler.Requests.Should().BeEmpty();
112-
s3Client.Verify(x => x.GetObjectMetadataAsync(It.Is<GetObjectMetadataRequest>(request => request.BucketName == "tts-bucket" && request.Key == "tts/audio.wav"), It.IsAny<CancellationToken>()), Times.Once);
95+
s3Client.Verify(x => x.PutObjectAsync(It.Is<PutObjectRequest>(request => request.BucketName == "tts-bucket" && request.Key == "tts/audio.wav" && request.ContentType == "audio/wav"), It.IsAny<CancellationToken>()), Times.Once);
96+
s3Client.Verify(x => x.GetObjectMetadataAsync(It.IsAny<GetObjectMetadataRequest>(), It.IsAny<CancellationToken>()), Times.Never);
11397
s3Client.Verify(x => x.GetPreSignedURL(It.IsAny<GetPreSignedUrlRequest>()), Times.Never);
11498
}
11599

116100
[Test]
117-
public async Task upload_async_should_fall_back_to_presigned_put_when_metadata_response_is_malformed()
101+
public async Task upload_async_should_treat_malformed_put_response_as_success_even_if_sdk_disposes_input_stream()
118102
{
119103
var s3Client = new Mock<IAmazonS3>(MockBehavior.Strict);
104+
byte[] capturedPayload = null;
120105
s3Client
121106
.Setup(x => x.PutObjectAsync(It.IsAny<PutObjectRequest>(), It.IsAny<CancellationToken>()))
122-
.ThrowsAsync(new FormatException("bad expiration header"));
123-
s3Client
124-
.Setup(x => x.GetObjectMetadataAsync(It.IsAny<GetObjectMetadataRequest>(), It.IsAny<CancellationToken>()))
125-
.ThrowsAsync(new FormatException("bad metadata expiration header"));
126-
s3Client
127-
.Setup(x => x.GetPreSignedURL(It.IsAny<GetPreSignedUrlRequest>()))
128-
.Returns<GetPreSignedUrlRequest>(request =>
129-
{
130-
request.BucketName.Should().Be("tts-bucket");
131-
request.Key.Should().Be("tts/audio.wav");
132-
request.Protocol.Should().Be(Protocol.HTTP);
133-
134-
return request.Verb switch
135-
{
136-
HttpVerb.HEAD => "http://upload.example.com/tts/audio.wav?signature=metadata-head",
137-
HttpVerb.PUT => "http://upload.example.com/tts/audio.wav?signature=metadata-put",
138-
_ => throw new AssertionException($"Unexpected presigned verb {request.Verb}")
139-
};
140-
});
141-
142-
var headRequests = 0;
143-
var putRequests = 0;
144-
var handler = new RecordingHttpMessageHandler(async (request, cancellationToken) =>
145-
{
146-
request.RequestUri.Should().NotBeNull();
147-
148-
if (request.Method == HttpMethod.Head)
149-
{
150-
headRequests++;
151-
request.RequestUri.Should().Be(new Uri("http://upload.example.com/tts/audio.wav?signature=metadata-head"));
152-
return new HttpResponseMessage(HttpStatusCode.NotFound);
153-
}
154-
155-
putRequests++;
156-
request.Method.Should().Be(HttpMethod.Put);
157-
request.RequestUri.Should().Be(new Uri("http://upload.example.com/tts/audio.wav?signature=metadata-put"));
158-
159-
var body = await request.Content!.ReadAsByteArrayAsync(cancellationToken);
160-
body.Should().Equal(2, 4, 6, 8);
161-
request.Content.Headers.ContentType!.MediaType.Should().Be("audio/wav");
162-
163-
return new HttpResponseMessage(HttpStatusCode.OK);
164-
});
165-
var service = CreateService(s3Client.Object, handler, useSsl: false);
166-
167-
await using var content = new MemoryStream(new byte[] { 2, 4, 6, 8 }, writable: false);
168-
169-
await service.UploadAsync("tts/audio.wav", content, "audio/wav", CancellationToken.None);
170-
171-
headRequests.Should().Be(1);
172-
putRequests.Should().Be(1);
173-
handler.Requests.Should().HaveCount(2);
174-
s3Client.Verify(x => x.GetObjectMetadataAsync(It.IsAny<GetObjectMetadataRequest>(), It.IsAny<CancellationToken>()), Times.Once);
175-
s3Client.Verify(x => x.GetPreSignedURL(It.Is<GetPreSignedUrlRequest>(request => request.Verb == HttpVerb.HEAD)), Times.Once);
176-
s3Client.Verify(x => x.GetPreSignedURL(It.Is<GetPreSignedUrlRequest>(request => request.Verb == HttpVerb.PUT)), Times.Once);
177-
}
178-
179-
[Test]
180-
public async Task upload_async_should_fall_back_to_presigned_put_when_metadata_check_times_out()
181-
{
182-
var s3Client = new Mock<IAmazonS3>(MockBehavior.Strict);
183-
s3Client
184-
.Setup(x => x.PutObjectAsync(It.IsAny<PutObjectRequest>(), It.IsAny<CancellationToken>()))
185-
.ThrowsAsync(new FormatException("bad expiration header"));
186-
s3Client
187-
.Setup(x => x.GetObjectMetadataAsync(It.IsAny<GetObjectMetadataRequest>(), It.IsAny<CancellationToken>()))
188-
.ThrowsAsync(new TaskCanceledException("metadata timeout"));
189-
s3Client
190-
.Setup(x => x.GetPreSignedURL(It.IsAny<GetPreSignedUrlRequest>()))
191-
.Returns("https://upload.example.com/tts/audio.wav?signature=timeout");
192-
193-
var handler = new RecordingHttpMessageHandler(async (request, cancellationToken) =>
194-
{
195-
var body = await request.Content!.ReadAsByteArrayAsync(cancellationToken);
196-
body.Should().Equal(6, 7, 8, 9);
197-
request.Method.Should().Be(HttpMethod.Put);
198-
request.RequestUri.Should().Be(new Uri("https://upload.example.com/tts/audio.wav?signature=timeout"));
199-
request.Content!.Headers.ContentType!.MediaType.Should().Be("audio/wav");
200-
201-
return new HttpResponseMessage(HttpStatusCode.OK);
202-
});
203-
var service = CreateService(s3Client.Object, handler);
204-
205-
await using var content = new MemoryStream(new byte[] { 6, 7, 8, 9 }, writable: false);
206-
207-
await service.UploadAsync("tts/audio.wav", content, "audio/wav", CancellationToken.None);
208-
209-
handler.Requests.Should().HaveCount(1);
210-
s3Client.Verify(x => x.GetObjectMetadataAsync(It.IsAny<GetObjectMetadataRequest>(), It.IsAny<CancellationToken>()), Times.Once);
211-
s3Client.Verify(x => x.GetPreSignedURL(It.IsAny<GetPreSignedUrlRequest>()), Times.Once);
212-
}
213-
214-
[Test]
215-
public async Task upload_async_should_fall_back_to_presigned_put_when_put_response_is_malformed_and_object_is_missing()
216-
{
217-
var s3Client = new Mock<IAmazonS3>(MockBehavior.Strict);
218-
s3Client
219-
.Setup(x => x.PutObjectAsync(It.IsAny<PutObjectRequest>(), It.IsAny<CancellationToken>()))
220-
.ThrowsAsync(new FormatException("bad expiration header"));
221-
s3Client
222-
.Setup(x => x.GetObjectMetadataAsync(It.IsAny<GetObjectMetadataRequest>(), It.IsAny<CancellationToken>()))
223-
.ThrowsAsync(new AmazonS3Exception("missing")
224-
{
225-
StatusCode = HttpStatusCode.NotFound,
226-
ErrorCode = "NoSuchKey"
227-
});
228-
s3Client
229-
.Setup(x => x.GetPreSignedURL(It.IsAny<GetPreSignedUrlRequest>()))
230-
.Returns<GetPreSignedUrlRequest>(request =>
231-
{
232-
request.BucketName.Should().Be("tts-bucket");
233-
request.Key.Should().Be("tts/audio.wav");
234-
request.Verb.Should().Be(HttpVerb.PUT);
235-
request.ContentType.Should().Be("audio/wav");
236-
return "https://upload.example.com/tts/audio.wav?signature=123";
237-
});
238-
239-
var handler = new RecordingHttpMessageHandler(async (request, cancellationToken) =>
240-
{
241-
var body = await request.Content!.ReadAsByteArrayAsync(cancellationToken);
242-
body.Should().Equal(5, 4, 3, 2);
243-
request.Method.Should().Be(HttpMethod.Put);
244-
request.RequestUri.Should().Be(new Uri("https://upload.example.com/tts/audio.wav?signature=123"));
245-
request.Content!.Headers.ContentType!.MediaType.Should().Be("audio/wav");
246-
247-
return new HttpResponseMessage(HttpStatusCode.OK);
248-
});
249-
var service = CreateService(s3Client.Object, handler);
250-
251-
await using var content = new MemoryStream(new byte[] { 5, 4, 3, 2 }, writable: false);
252-
253-
await service.UploadAsync("tts/audio.wav", content, "audio/wav", CancellationToken.None);
254-
255-
handler.Requests.Should().HaveCount(1);
256-
s3Client.Verify(x => x.GetObjectMetadataAsync(It.IsAny<GetObjectMetadataRequest>(), It.IsAny<CancellationToken>()), Times.Once);
257-
s3Client.Verify(x => x.GetPreSignedURL(It.IsAny<GetPreSignedUrlRequest>()), Times.Once);
258-
}
259-
260-
[Test]
261-
public async Task upload_async_should_reuse_buffered_payload_when_falling_back_after_sdk_disposes_input_stream()
262-
{
263-
var s3Client = new Mock<IAmazonS3>(MockBehavior.Strict);
264-
s3Client
265-
.Setup(x => x.PutObjectAsync(It.IsAny<PutObjectRequest>(), It.IsAny<CancellationToken>()))
266-
.Returns<PutObjectRequest, CancellationToken>((request, _) =>
107+
.Returns<PutObjectRequest, CancellationToken>(async (request, _) =>
267108
{
109+
using var captureStream = new MemoryStream();
110+
await request.InputStream.CopyToAsync(captureStream);
111+
capturedPayload = captureStream.ToArray();
268112
request.InputStream.Dispose();
269-
return Task.FromException<PutObjectResponse>(new FormatException("bad expiration header"));
113+
throw new FormatException("bad expiration header");
270114
});
271-
s3Client
272-
.Setup(x => x.GetObjectMetadataAsync(It.IsAny<GetObjectMetadataRequest>(), It.IsAny<CancellationToken>()))
273-
.ThrowsAsync(new FormatException("bad metadata expiration header"));
274-
s3Client
275-
.Setup(x => x.GetPreSignedURL(It.IsAny<GetPreSignedUrlRequest>()))
276-
.Returns<GetPreSignedUrlRequest>(request =>
277-
{
278-
request.Protocol.Should().Be(Protocol.HTTP);
279115

280-
return request.Verb switch
281-
{
282-
HttpVerb.HEAD => "http://upload.example.com/tts/audio.wav?signature=disposed-head",
283-
HttpVerb.PUT => "http://upload.example.com/tts/audio.wav?signature=disposed-put",
284-
_ => throw new AssertionException($"Unexpected presigned verb {request.Verb}")
285-
};
286-
});
287-
288-
var headRequests = 0;
289-
var putRequests = 0;
290-
var handler = new RecordingHttpMessageHandler(async (request, cancellationToken) =>
291-
{
292-
if (request.Method == HttpMethod.Head)
293-
{
294-
headRequests++;
295-
request.RequestUri.Should().Be(new Uri("http://upload.example.com/tts/audio.wav?signature=disposed-head"));
296-
throw new HttpRequestException("connectivity failure");
297-
}
298-
299-
putRequests++;
300-
request.Method.Should().Be(HttpMethod.Put);
301-
request.RequestUri.Should().Be(new Uri("http://upload.example.com/tts/audio.wav?signature=disposed-put"));
302-
303-
var body = await request.Content!.ReadAsByteArrayAsync(cancellationToken);
304-
body.Should().Equal(7, 5, 3, 1);
305-
request.Content.Headers.ContentType!.MediaType.Should().Be("audio/wav");
306-
307-
return new HttpResponseMessage(HttpStatusCode.OK);
308-
});
309-
310-
var service = CreateService(s3Client.Object, handler, useSsl: false);
116+
var handler = new RecordingHttpMessageHandler((_, _) =>
117+
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)));
118+
var service = CreateService(s3Client.Object, handler);
311119

312120
await using var content = new MemoryStream(new byte[] { 7, 5, 3, 1 }, writable: false);
313121

314122
await service.UploadAsync("tts/audio.wav", content, "audio/wav", CancellationToken.None);
315123

316-
headRequests.Should().Be(3);
317-
putRequests.Should().Be(1);
318-
handler.Requests.Should().HaveCount(4);
319-
s3Client.Verify(x => x.GetObjectMetadataAsync(It.IsAny<GetObjectMetadataRequest>(), It.IsAny<CancellationToken>()), Times.Once);
320-
s3Client.Verify(x => x.GetPreSignedURL(It.Is<GetPreSignedUrlRequest>(request => request.Verb == HttpVerb.HEAD)), Times.Exactly(3));
321-
s3Client.Verify(x => x.GetPreSignedURL(It.Is<GetPreSignedUrlRequest>(request => request.Verb == HttpVerb.PUT)), Times.Once);
124+
capturedPayload.Should().Equal(7, 5, 3, 1);
125+
handler.Requests.Should().BeEmpty();
126+
s3Client.Verify(x => x.GetObjectMetadataAsync(It.IsAny<GetObjectMetadataRequest>(), It.IsAny<CancellationToken>()), Times.Never);
127+
s3Client.Verify(x => x.GetPreSignedURL(It.IsAny<GetPreSignedUrlRequest>()), Times.Never);
322128
}
323129

324130
[Test]

0 commit comments

Comments
 (0)