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
2 changes: 2 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ jobs:
dockerfile: Web/Resgrid.Web.Eventing/Dockerfile
- image: resgridllc/resgridwebmcp
dockerfile: Web/Resgrid.Web.Mcp/Dockerfile
- image: resgridllc/resgridwebtts
dockerfile: Web/Resgrid.Web.Tts/Dockerfile
- image: resgridllc/resgridworkersconsole
dockerfile: Workers/Resgrid.Workers.Console/Dockerfile

Expand Down
30 changes: 15 additions & 15 deletions Core/Resgrid.Config/Resgrid.Config.csproj
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Configurations>Debug;Release;Docker</Configurations>
</PropertyGroup>
<ItemGroup>
<None Remove="_ReadMe.txt" />
</ItemGroup>
<ItemGroup>
<Content Include="_ReadMe.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<Configurations>Debug;Release;Docker</Configurations>
</PropertyGroup>
<ItemGroup>
<None Remove="_ReadMe.txt" />
</ItemGroup>
<ItemGroup>
<Content Include="_ReadMe.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
44 changes: 44 additions & 0 deletions Core/Resgrid.Config/TtsConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace Resgrid.Config
{
/// <summary>
/// Shared configuration for the Resgrid TTS microservice.
/// Values are loaded through ConfigProcessor from ResgridConfig.json or RESGRID:* environment variables.
/// </summary>
public static class TtsConfig
{
public static string ServiceBaseUrl = "";
public static string PlaybackBaseUrl = "";
public static string StaticPromptAdminKey = "";
public static int StaticPromptRefreshIntervalMinutes = 1440;
public static int PlaybackMemoryCacheMinutes = 60;
public static int PlaybackCacheControlSeconds = 86400;

public static string S3Endpoint = "";
public static string S3AccessKey = "";
public static string S3SecretKey = "";
public static string S3Bucket = "";
public static string S3Region = "us-east-1";
public static bool S3UseSsl = false;
public static bool S3ForcePathStyle = true;
public static bool S3UsePresignedUrls = true;
public static int S3PresignedUrlExpiryMinutes = 60;
public static string S3PublicBaseUrl = "";

public static string DefaultVoice = "en-us";
public static int DefaultSpeed = 175;
public static int MaxConcurrentGenerations = 4;
public static int MaxTextLength = 1000;
public static string EspeakExecutable = "espeak-ng";
public static string FfmpegExecutable = "ffmpeg";
public static string TempDirectory = "";
public static string CachePrefix = "tts";
public static int NormalizedSampleRate = 8000;
public static int NormalizedChannels = 1;
public static bool WarmupEnabled = true;
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.";

public static int RateLimitPermitLimit = 60;
public static int RateLimitQueueLimit = 10;
public static int RateLimitWindowSeconds = 60;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -645,4 +645,10 @@
<data name="CheckInTimerAllStates" xml:space="preserve">
<value>All</value>
</data>
</root>
<data name="TtsLanguage" xml:space="preserve">
<value>TTS Language</value>
</data>
<data name="TtsLanguageHelp" xml:space="preserve">
<value>Select the eSpeak-NG language or dialect to use for this department's voice prompts.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -330,4 +330,10 @@
<data name="CheckInTimerAllStates" xml:space="preserve">
<value />
</data>
</root>
<data name="TtsLanguage" xml:space="preserve">
<value>TTS Language</value>
</data>
<data name="TtsLanguageHelp" xml:space="preserve">
<value>Select the eSpeak-NG language or dialect to use for this department's voice prompts.</value>
</data>
</root>
1 change: 1 addition & 0 deletions Core/Resgrid.Model/DepartmentSettingTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@ public enum DepartmentSettingTypes
MappingUseMapboxOverride = 46,
MappingMapboxStyleUrl = 47,
MappingMapboxAccessToken = 48,
TtsLanguage = 49,
}
}
194 changes: 194 additions & 0 deletions Core/Resgrid.Model/EspeakVoiceCatalog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Resgrid.Model
{
public static class EspeakVoiceCatalog
{
private static readonly IReadOnlyList<TtsVoiceOption> VoicesInternal = new List<TtsVoiceOption>
{
new TtsVoiceOption("af", "Afrikaans"),
new TtsVoiceOption("sq", "Albanian"),
new TtsVoiceOption("am", "Amharic"),
new TtsVoiceOption("ar", "Arabic"),
new TtsVoiceOption("an", "Aragonese"),
new TtsVoiceOption("hy", "Armenian", "East Armenian"),
new TtsVoiceOption("hyw", "Armenian", "West Armenian"),
new TtsVoiceOption("as", "Assamese"),
new TtsVoiceOption("az", "Azerbaijani"),
new TtsVoiceOption("ba", "Bashkir"),
new TtsVoiceOption("cu", "Chuvash"),
new TtsVoiceOption("eu", "Basque"),
new TtsVoiceOption("be", "Belarusian"),
new TtsVoiceOption("bn", "Bengali"),
new TtsVoiceOption("bpy", "Bishnupriya Manipuri"),
new TtsVoiceOption("bs", "Bosnian"),
new TtsVoiceOption("bg", "Bulgarian"),
new TtsVoiceOption("my", "Burmese"),
new TtsVoiceOption("ca", "Catalan"),
new TtsVoiceOption("chr", "Cherokee", "Western/C.E.D."),
new TtsVoiceOption("yue", "Chinese", "Cantonese"),
new TtsVoiceOption("hak", "Chinese", "Hakka"),
new TtsVoiceOption("haw", "Hawaiian"),
new TtsVoiceOption("cmn", "Chinese", "Mandarin"),
new TtsVoiceOption("hr", "Croatian"),
new TtsVoiceOption("cs", "Czech"),
new TtsVoiceOption("da", "Danish"),
new TtsVoiceOption("nl", "Dutch"),
new TtsVoiceOption("en-us", "English", "American"),
new TtsVoiceOption("en", "English", "British"),
new TtsVoiceOption("en-029", "English", "Caribbean"),
new TtsVoiceOption("en-gb-x-gbclan", "English", "Lancastrian"),
new TtsVoiceOption("en-gb-x-rp", "English", "Received Pronunciation"),
new TtsVoiceOption("en-gb-scotland", "English", "Scottish"),
new TtsVoiceOption("en-gb-x-gbcwmd", "English", "West Midlands"),
new TtsVoiceOption("eo", "Esperanto"),
new TtsVoiceOption("et", "Estonian"),
new TtsVoiceOption("fa", "Persian"),
new TtsVoiceOption("fa-latn", "Persian"),
new TtsVoiceOption("fi", "Finnish"),
new TtsVoiceOption("fr-be", "French", "Belgium"),
new TtsVoiceOption("fr", "French", "France"),
new TtsVoiceOption("fr-ch", "French", "Switzerland"),
new TtsVoiceOption("ga", "Gaelic", "Irish"),
new TtsVoiceOption("gd", "Gaelic", "Scottish"),
new TtsVoiceOption("ka", "Georgian"),
new TtsVoiceOption("de", "German"),
new TtsVoiceOption("grc", "Greek", "Ancient"),
new TtsVoiceOption("el", "Greek", "Modern"),
new TtsVoiceOption("kl", "Greenlandic"),
new TtsVoiceOption("gn", "Guarani"),
new TtsVoiceOption("gu", "Gujarati"),
new TtsVoiceOption("ht", "Haitian Creole"),
new TtsVoiceOption("he", "Hebrew"),
new TtsVoiceOption("hi", "Hindi"),
new TtsVoiceOption("hu", "Hungarian"),
new TtsVoiceOption("is", "Icelandic"),
new TtsVoiceOption("id", "Indonesian"),
new TtsVoiceOption("ia", "Interlingua"),
new TtsVoiceOption("io", "Ido"),
new TtsVoiceOption("it", "Italian"),
new TtsVoiceOption("ja", "Japanese"),
new TtsVoiceOption("kn", "Kannada"),
new TtsVoiceOption("kok", "Konkani"),
new TtsVoiceOption("ko", "Korean"),
new TtsVoiceOption("ku", "Kurdish"),
new TtsVoiceOption("kk", "Kazakh"),
new TtsVoiceOption("ky", "Kyrgyz"),
new TtsVoiceOption("la", "Latin"),
new TtsVoiceOption("lb", "Luxembourgish"),
new TtsVoiceOption("ltg", "Latgalian"),
new TtsVoiceOption("lv", "Latvian"),
new TtsVoiceOption("lfn", "Lingua Franca Nova"),
new TtsVoiceOption("lt", "Lithuanian"),
new TtsVoiceOption("jbo", "Lojban"),
new TtsVoiceOption("mi", "Māori"),
new TtsVoiceOption("mk", "Macedonian"),
new TtsVoiceOption("ms", "Malay"),
new TtsVoiceOption("ml", "Malayalam"),
new TtsVoiceOption("mt", "Maltese"),
new TtsVoiceOption("mr", "Marathi"),
new TtsVoiceOption("nci", "Nahuatl", "Classical"),
new TtsVoiceOption("ne", "Nepali"),
new TtsVoiceOption("nb", "Norwegian Bokmål"),
new TtsVoiceOption("nog", "Nogai"),
new TtsVoiceOption("or", "Oriya"),
new TtsVoiceOption("om", "Oromo"),
new TtsVoiceOption("pap", "Papiamento"),
new TtsVoiceOption("py", "Pyash"),
new TtsVoiceOption("pl", "Polish"),
new TtsVoiceOption("qdb", "Lang Belta"),
new TtsVoiceOption("qu", "Quechua"),
new TtsVoiceOption("quc", "K'iche'"),
new TtsVoiceOption("qya", "Quenya"),
new TtsVoiceOption("pt", "Portuguese", "Portugal"),
new TtsVoiceOption("pt-br", "Portuguese", "Brazil"),
new TtsVoiceOption("pa", "Punjabi"),
new TtsVoiceOption("piqd", "Klingon"),
new TtsVoiceOption("ro", "Romanian"),
new TtsVoiceOption("ru", "Russian"),
new TtsVoiceOption("ru-lv", "Russian", "Latvia"),
new TtsVoiceOption("uk", "Ukrainian"),
new TtsVoiceOption("sjn", "Sindarin"),
new TtsVoiceOption("sr", "Serbian"),
new TtsVoiceOption("tn", "Setswana"),
new TtsVoiceOption("sd", "Sindhi"),
new TtsVoiceOption("shn", "Shan (Tai Yai)"),
new TtsVoiceOption("si", "Sinhala"),
new TtsVoiceOption("sk", "Slovak"),
new TtsVoiceOption("sl", "Slovenian"),
new TtsVoiceOption("smj", "Lule Saami"),
new TtsVoiceOption("es", "Spanish", "Spain"),
new TtsVoiceOption("es-419", "Spanish", "Latin American"),
new TtsVoiceOption("sw", "Swahili"),
new TtsVoiceOption("sv", "Swedish"),
new TtsVoiceOption("ta", "Tamil"),
new TtsVoiceOption("th", "Thai"),
new TtsVoiceOption("tk", "Turkmen"),
new TtsVoiceOption("tt", "Tatar"),
new TtsVoiceOption("te", "Telugu"),
new TtsVoiceOption("tr", "Turkish"),
new TtsVoiceOption("ug", "Uyghur"),
new TtsVoiceOption("ur", "Urdu"),
new TtsVoiceOption("uz", "Uzbek"),
new TtsVoiceOption("vi-vn-x-central", "Vietnamese", "Central Vietnam"),
new TtsVoiceOption("vi", "Vietnamese", "Northern Vietnam"),
new TtsVoiceOption("vi-vn-x-south", "Vietnamese", "Southern Vietnam"),
new TtsVoiceOption("cy", "Welsh"),
};

private static readonly Dictionary<string, TtsVoiceOption> VoiceLookup = VoicesInternal.ToDictionary(x => x.Identifier, StringComparer.OrdinalIgnoreCase);

public const string DefaultIdentifier = "en-us";

public static IReadOnlyList<TtsVoiceOption> Voices => VoicesInternal;

public static bool TryNormalizeIdentifier(string voice, out string normalizedVoice)
{
normalizedVoice = null;

if (string.IsNullOrWhiteSpace(voice))
return false;

if (!VoiceLookup.TryGetValue(voice.Trim(), out var option))
return false;

normalizedVoice = option.Identifier;
return true;
}

public static string GetDisplayName(string voice)
{
if (TryGetOption(voice, out var option))
return option.DisplayName;

return string.IsNullOrWhiteSpace(voice) ? DefaultIdentifier : voice.Trim();
}

public static bool TryGetOption(string voice, out TtsVoiceOption option)
{
option = null;

if (string.IsNullOrWhiteSpace(voice))
return false;

return VoiceLookup.TryGetValue(voice.Trim(), out option);
}
}

public sealed class TtsVoiceOption
{
public TtsVoiceOption(string identifier, string language, string accent = null)
{
Identifier = identifier;
Language = language;
Accent = accent;
}

public string Identifier { get; }
public string Language { get; }
public string Accent { get; }
public string DisplayName => string.IsNullOrWhiteSpace(Accent) ? $"{Language} ({Identifier})" : $"{Language} - {Accent} ({Identifier})";
}
}
1 change: 1 addition & 0 deletions Core/Resgrid.Model/Services/ICommunicationTestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public interface ICommunicationTestService
Task<bool> RecordEmailResponseAsync(string responseToken);
Task<bool> RecordVoiceResponseAsync(string responseToken);
Task<bool> RecordPushResponseAsync(string responseToken);
Task<int?> GetDepartmentIdByResponseTokenAsync(string responseToken);

Task ProcessScheduledTestsAsync(CancellationToken cancellationToken = default);
Task CompleteExpiredRunsAsync(CancellationToken cancellationToken = default);
Expand Down
2 changes: 2 additions & 0 deletions Core/Resgrid.Model/Services/IDepartmentSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ public interface IDepartmentSettingsService
/// <returns>Task&lt;System.String&gt;.</returns>
Task<string> GetDispatchEmailForDepartmentAsync(int departmentId);

Task<string> GetTtsLanguageForDepartmentAsync(int departmentId);

/// <summary>
/// Gets the disable automatic available for department by user identifier asynchronous.
/// </summary>
Expand Down
14 changes: 14 additions & 0 deletions Core/Resgrid.Model/Services/ITtsAudioService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Resgrid.Model.Services
{
public interface ITtsAudioService
{
Task<Uri> GenerateSpeechUrlAsync(string text, string voice = null, int? speed = null, CancellationToken cancellationToken = default);

Task RegenerateStaticPromptsAsync(IEnumerable<string> prompts, CancellationToken cancellationToken = default);
}
}
Loading
Loading