diff --git a/Core/Resgrid.Config/TtsConfig.cs b/Core/Resgrid.Config/TtsConfig.cs index 6481041b..1e912c70 100644 --- a/Core/Resgrid.Config/TtsConfig.cs +++ b/Core/Resgrid.Config/TtsConfig.cs @@ -24,7 +24,7 @@ public static class TtsConfig public static int S3PresignedUrlExpiryMinutes = 60; public static string S3PublicBaseUrl = ""; - public static string DefaultVoice = "en-us"; + public static string DefaultVoice = "en-us+f3"; public static int DefaultSpeed = 175; public static int MaxConcurrentGenerations = 4; public static int MaxTextLength = 1000; diff --git a/Core/Resgrid.Model/EspeakVoiceCatalog.cs b/Core/Resgrid.Model/EspeakVoiceCatalog.cs index 6e96d0af..e1b5369b 100644 --- a/Core/Resgrid.Model/EspeakVoiceCatalog.cs +++ b/Core/Resgrid.Model/EspeakVoiceCatalog.cs @@ -37,6 +37,7 @@ public static class EspeakVoiceCatalog new TtsVoiceOption("da", "Danish"), new TtsVoiceOption("nl", "Dutch"), new TtsVoiceOption("en-us", "English", "American"), + new TtsVoiceOption("en-us+f3", "English", "American Female 3"), new TtsVoiceOption("en", "English", "British"), new TtsVoiceOption("en-029", "English", "Caribbean"), new TtsVoiceOption("en-gb-x-gbclan", "English", "Lancastrian"), @@ -140,7 +141,7 @@ public static class EspeakVoiceCatalog private static readonly Dictionary VoiceLookup = VoicesInternal.ToDictionary(x => x.Identifier, StringComparer.OrdinalIgnoreCase); - public const string DefaultIdentifier = "en-us"; + public const string DefaultIdentifier = "en-us+f3"; public static IReadOnlyList Voices => VoicesInternal; diff --git a/Tests/Resgrid.Tests/Services/DepartmentSettingsServiceTtsLanguageTests.cs b/Tests/Resgrid.Tests/Services/DepartmentSettingsServiceTtsLanguageTests.cs index 6055e2e9..14b0c6cd 100644 --- a/Tests/Resgrid.Tests/Services/DepartmentSettingsServiceTtsLanguageTests.cs +++ b/Tests/Resgrid.Tests/Services/DepartmentSettingsServiceTtsLanguageTests.cs @@ -41,7 +41,7 @@ public void SetUp() _cacheProvider.Object); global::Resgrid.Config.SystemBehaviorConfig.CacheEnabled = false; - TtsConfig.DefaultVoice = "en-us"; + TtsConfig.DefaultVoice = "en-us+f3"; } [TearDown] @@ -77,7 +77,7 @@ public async Task should_fall_back_to_default_tts_language_when_setting_missing( var result = await _service.GetTtsLanguageForDepartmentAsync(7); - result.Should().Be("en-us"); + result.Should().Be("en-us+f3"); } [Test] diff --git a/Tests/Resgrid.Tests/Web/Tts/TtsAdminControllerTests.cs b/Tests/Resgrid.Tests/Web/Tts/TtsAdminControllerTests.cs index dffdb6de..b48b13dd 100644 --- a/Tests/Resgrid.Tests/Web/Tts/TtsAdminControllerTests.cs +++ b/Tests/Resgrid.Tests/Web/Tts/TtsAdminControllerTests.cs @@ -33,7 +33,7 @@ public void SetUp() .Returns((_, hash) => new Uri($"https://tts.example.com/tts/audio/{hash}.wav")); _options = new TtsOptions { - DefaultVoice = "en-us", + DefaultVoice = "en-us+f3", DefaultSpeed = 175, StaticPromptAdminKey = "secret-key", PreGeneratedPrompts = new List { "Alpha", "Beta" } @@ -95,8 +95,8 @@ public async Task regenerate_static_prompts_should_fall_back_to_configured_promp .Callback, CancellationToken>((requests, _) => capturedPrompts = requests.ToList()) .ReturnsAsync(new[] { - new TtsResponse { Hash = "a", ObjectKey = "tts/a.wav", Url = "https://cdn.example.com/tts/a.wav", Voice = "en-us", Speed = 175 }, - new TtsResponse { Hash = "b", ObjectKey = "tts/b.wav", Url = "https://cdn.example.com/tts/b.wav", Voice = "en-us", Speed = 175 } + new TtsResponse { Hash = "a", ObjectKey = "tts/a.wav", Url = "https://cdn.example.com/tts/a.wav", Voice = "en-us+f3", Speed = 175 }, + new TtsResponse { Hash = "b", ObjectKey = "tts/b.wav", Url = "https://cdn.example.com/tts/b.wav", Voice = "en-us+f3", Speed = 175 } }); var controller = BuildController(); @@ -106,7 +106,7 @@ public async Task regenerate_static_prompts_should_fall_back_to_configured_promp result.Result.Should().BeOfType(); capturedPrompts.Should().HaveCount(2); capturedPrompts!.Select(x => x.Text).Should().Equal("Alpha", "Beta"); - capturedPrompts.Select(x => x.Voice).Should().OnlyContain(x => x == "en-us"); + capturedPrompts.Select(x => x.Voice).Should().OnlyContain(x => x == "en-us+f3"); capturedPrompts.Select(x => x.Speed).Should().OnlyContain(x => x == 175); } diff --git a/Tests/Resgrid.Tests/Web/Tts/TtsServiceTests.cs b/Tests/Resgrid.Tests/Web/Tts/TtsServiceTests.cs index de81c5fa..aa688778 100644 --- a/Tests/Resgrid.Tests/Web/Tts/TtsServiceTests.cs +++ b/Tests/Resgrid.Tests/Web/Tts/TtsServiceTests.cs @@ -33,7 +33,7 @@ public void SetUp() _audioProcessingService.Object, Options.Create(new TtsOptions { - DefaultVoice = "en-us", + DefaultVoice = "en-us+f3", DefaultSpeed = 175, MaxConcurrentGenerations = 2, MaxTextLength = 500 @@ -47,7 +47,7 @@ public async Task generate_async_should_return_cached_response_without_generatin var cachedUri = new Uri("https://cdn.example.com/tts/abc123.wav"); _cacheService - .Setup(x => x.CreateCacheKey("Press 1 for yes", "en-us", 175)) + .Setup(x => x.CreateCacheKey("Press 1 for yes", "en-us+f3", 175)) .Returns(CacheKey); _cacheService .Setup(x => x.TryGetCachedUrlAsync(CacheKey, It.IsAny())) @@ -59,7 +59,7 @@ public async Task generate_async_should_return_cached_response_without_generatin result.Hash.Should().Be(CacheKey.Hash); result.ObjectKey.Should().Be(CacheKey.ObjectKey); result.Url.Should().Be(cachedUri.ToString()); - result.Voice.Should().Be("en-us"); + result.Voice.Should().Be("en-us+f3"); result.Speed.Should().Be(175); _audioProcessingService.Verify( @@ -77,14 +77,14 @@ public async Task generate_async_should_generate_and_store_audio_when_cache_miss var objectUri = new Uri("https://cdn.example.com/tts/abc123.wav"); _cacheService - .Setup(x => x.CreateCacheKey("Press 1 for yes", "en-us", 175)) + .Setup(x => x.CreateCacheKey("Press 1 for yes", "en-us+f3", 175)) .Returns(CacheKey); _cacheService .SetupSequence(x => x.TryGetCachedUrlAsync(CacheKey, It.IsAny())) .ReturnsAsync((Uri)null) .ReturnsAsync((Uri)null); _audioProcessingService - .Setup(x => x.GenerateNormalizedWavAsync("Press 1 for yes", "en-us", 175, It.IsAny())) + .Setup(x => x.GenerateNormalizedWavAsync("Press 1 for yes", "en-us+f3", 175, It.IsAny())) .ReturnsAsync(audioBytes); _cacheService .Setup(x => x.StoreAsync(CacheKey, It.Is(bytes => bytes.SequenceEqual(audioBytes)), It.IsAny())) @@ -94,11 +94,11 @@ public async Task generate_async_should_generate_and_store_audio_when_cache_miss result.Cached.Should().BeFalse(); result.Url.Should().Be(objectUri.ToString()); - result.Voice.Should().Be("en-us"); + result.Voice.Should().Be("en-us+f3"); result.Speed.Should().Be(175); _audioProcessingService.Verify( - x => x.GenerateNormalizedWavAsync("Press 1 for yes", "en-us", 175, It.IsAny()), + x => x.GenerateNormalizedWavAsync("Press 1 for yes", "en-us+f3", 175, It.IsAny()), Times.Once); _cacheService.Verify( x => x.StoreAsync(CacheKey, It.Is(bytes => bytes.SequenceEqual(audioBytes)), It.IsAny()), @@ -124,7 +124,7 @@ public async Task generate_async_should_deduplicate_concurrent_generation_for_th var cacheLookupCount = 0; _cacheService - .Setup(x => x.CreateCacheKey("Press 1 for yes", "en-us", 175)) + .Setup(x => x.CreateCacheKey("Press 1 for yes", "en-us+f3", 175)) .Returns(CacheKey); _cacheService .Setup(x => x.TryGetCachedUrlAsync(CacheKey, It.IsAny())) @@ -134,7 +134,7 @@ public async Task generate_async_should_deduplicate_concurrent_generation_for_th return Task.FromResult(attempt < 4 ? null : objectUri); }); _audioProcessingService - .Setup(x => x.GenerateNormalizedWavAsync("Press 1 for yes", "en-us", 175, It.IsAny())) + .Setup(x => x.GenerateNormalizedWavAsync("Press 1 for yes", "en-us+f3", 175, It.IsAny())) .Returns(async () => { generationStarted.TrySetResult(true); @@ -158,7 +158,7 @@ public async Task generate_async_should_deduplicate_concurrent_generation_for_th responses.Count(response => response.Cached).Should().Be(1); responses.Count(response => !response.Cached).Should().Be(1); _audioProcessingService.Verify( - x => x.GenerateNormalizedWavAsync("Press 1 for yes", "en-us", 175, It.IsAny()), + x => x.GenerateNormalizedWavAsync("Press 1 for yes", "en-us+f3", 175, It.IsAny()), Times.Once); _cacheService.Verify( x => x.StoreAsync(CacheKey, It.Is(bytes => bytes.SequenceEqual(audioBytes)), It.IsAny()), diff --git a/Web/Resgrid.Web.Tts/Configuration/TtsOptions.cs b/Web/Resgrid.Web.Tts/Configuration/TtsOptions.cs index 85f64ae5..8eecec6e 100644 --- a/Web/Resgrid.Web.Tts/Configuration/TtsOptions.cs +++ b/Web/Resgrid.Web.Tts/Configuration/TtsOptions.cs @@ -5,7 +5,7 @@ namespace Resgrid.Web.Tts.Configuration public sealed class TtsOptions { [Required] - public string DefaultVoice { get; set; } = "en-us"; + public string DefaultVoice { get; set; } = "en-us+f3"; [Range(80, 450)] public int DefaultSpeed { get; set; } = 175; diff --git a/Web/Resgrid.Web.Tts/k8s/deployment.yaml b/Web/Resgrid.Web.Tts/k8s/deployment.yaml index cd4d6003..d25f28f2 100644 --- a/Web/Resgrid.Web.Tts/k8s/deployment.yaml +++ b/Web/Resgrid.Web.Tts/k8s/deployment.yaml @@ -17,7 +17,7 @@ data: RESGRID__TtsConfig__S3ForcePathStyle: "true" RESGRID__TtsConfig__S3UsePresignedUrls: "true" RESGRID__TtsConfig__S3PresignedUrlExpiryMinutes: "60" - RESGRID__TtsConfig__DefaultVoice: en-us + RESGRID__TtsConfig__DefaultVoice: en-us+f3 RESGRID__TtsConfig__DefaultSpeed: "175" RESGRID__TtsConfig__MaxConcurrentGenerations: "4" RESGRID__TtsConfig__MaxTextLength: "1000"