diff --git a/Core/Resgrid.Config/TtsConfig.cs b/Core/Resgrid.Config/TtsConfig.cs
index 139966e2..91a72096 100644
--- a/Core/Resgrid.Config/TtsConfig.cs
+++ b/Core/Resgrid.Config/TtsConfig.cs
@@ -25,7 +25,7 @@ public static class TtsConfig
public static string S3PublicBaseUrl = "";
public static string DefaultVoice = "en-us+klatt4";
- public static int DefaultSpeed = 165;
+ public static int DefaultSpeed = 150;
public static int MaxConcurrentGenerations = 4;
public static int MaxTextLength = 1000;
public static string PiperExecutable = "piper";
diff --git a/Tests/Resgrid.Tests/Web/Tts/TtsServiceTests.cs b/Tests/Resgrid.Tests/Web/Tts/TtsServiceTests.cs
index 569bd813..4e6c2470 100644
--- a/Tests/Resgrid.Tests/Web/Tts/TtsServiceTests.cs
+++ b/Tests/Resgrid.Tests/Web/Tts/TtsServiceTests.cs
@@ -253,7 +253,9 @@ public void create_piper_start_info_should_use_english_model_for_english_voices(
"--output_file",
"/tmp/raw.wav",
"--length-scale",
- "1.06");
+ "1.06",
+ "--sentence-silence",
+ "0.0");
}
[Test]
@@ -272,7 +274,9 @@ public void create_piper_start_info_should_fallback_to_default_model_for_unmappe
"--output_file",
"/tmp/raw.wav",
"--length-scale",
- "1.06");
+ "1.06",
+ "--sentence-silence",
+ "0.0");
}
[Test]
@@ -290,7 +294,9 @@ public void create_piper_start_info_should_adjust_length_scale_for_speed()
"--output_file",
"/tmp/raw.wav",
"--length-scale",
- "0.50");
+ "0.50",
+ "--sentence-silence",
+ "0.0");
}
[Test]
diff --git a/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml b/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml
index 15601e50..526ac72e 100644
--- a/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml
+++ b/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml
@@ -3573,6 +3573,52 @@
Is the user a group admin
+
+
+ UserId (GUID/UUID) of the User to set. This field will be ignored if the input is used on a
+ function that is setting status for the current user.
+
+
+
+
+ The state/staffing level of the user to set for the user.
+
+
+
+
+ Note for the staffing level
+
+
+
+
+ The result object for a state/staffing level request.
+
+
+
+
+ The UserId GUID/UUID for the user state/staffing level being return
+
+
+
+
+ The full name of the user for the state/staffing level being returned
+
+
+
+
+ The current staffing level (state) type for the user
+
+
+
+
+ The timestamp of the last state/staffing level. This is converted UTC to the departments, or users, TimeZone.
+
+
+
+
+ Staffing note for the User's staffing
+
+
Input data to add a staffing schedule in the Resgrid system
@@ -3678,52 +3724,6 @@
Note for this staffing schedule
-
-
- UserId (GUID/UUID) of the User to set. This field will be ignored if the input is used on a
- function that is setting status for the current user.
-
-
-
-
- The state/staffing level of the user to set for the user.
-
-
-
-
- Note for the staffing level
-
-
-
-
- The result object for a state/staffing level request.
-
-
-
-
- The UserId GUID/UUID for the user state/staffing level being return
-
-
-
-
- The full name of the user for the state/staffing level being returned
-
-
-
-
- The current staffing level (state) type for the user
-
-
-
-
- The timestamp of the last state/staffing level. This is converted UTC to the departments, or users, TimeZone.
-
-
-
-
- Staffing note for the User's staffing
-
-
A resrouce in the system this could be a user or unit
@@ -7508,209 +7508,379 @@
Identifier of the new npte
-
+
- A GPS location for a point in time of a specificed person
+ The result of getting all personnel filters for the system
-
+
- PersonId of the person that the location is for
+ The Id value of the filter
-
+
- The timestamp of the location in UTC
+ The type of the filter
-
+
- GPS Latitude of the Person
+ The filters name
-
+
- GPS Longitude of the Person
+ Result containing all the data required to populate the New Call form
-
+
- GPS Latitude\Longitude Accuracy of the Person
+ Response Data
-
+
- GPS Altitude of the Person
+ Result that contains all the options available to filter personnel against compatible Resgrid APIs
-
+
- GPS Altitude Accuracy of the Person
+ Response Data
-
+
- GPS Speed of the Person
+ Result containing all the data required to populate the New Call form
-
+
- GPS Heading of the Person
+ Response Data
-
+
- A unit location in the Resgrid system
+ Information about a User
-
+
- Response Data
+ The UserId GUID/UUID for the user
-
+
- The information about a specific unit's location
+ DepartmentId of the deparment the user belongs to
-
+
- Id of the Person
+ Department specificed ID number for this user
-
+
- The Timestamp for the location in UTC
+ The Users First Name
-
+
- GPS Latitude of the Person
+ The Users Last Name
-
+
- GPS Longitude of the Person
+ The Users Email Address
-
+
- GPS Latitude\Longitude Accuracy of the Person
+ The Users Mobile Telephone Number
-
+
- GPS Altitude of the Person
+ GroupId the user is assigned to (0 for no group)
-
+
- GPS Altitude Accuracy of the Person
+ Name of the group the user is assigned to
-
+
- GPS Speed of the Person
+ Enumeration/List of roles the user currently holds
-
+
- GPS Heading of the Person
+ The current action/status type for the user
-
+
- The result of getting the current staffing for a user
+ The current action/status string for the user
-
+
- Response Data
+ The current action/status color hex string for the user
-
+
- Information about a User staffing
+ The timestamp of the last action. This is converted UTC to the departments, or users, TimeZone.
-
+
- The UserId GUID/UUID for the user status being return
+ The current action/status destination id for the user
-
+
- DepartmentId of the deparment the user belongs to
+ The current action/status destination name for the user
-
+
- The current staffing type for the user
+ The current staffing level (state) type for the user
-
+
- The timestamp of the last staffing. This is converted UTC version of the timestamp.
+ The current staffing level (state) string for the user
-
+
- The timestamp of the last staffing. This is converted UTC to the departments, or users, TimeZone.
+ The current staffing level (state) color hex string for the user
-
+
- Note for this staffing
+ The timestamp of the last state/staffing level. This is converted UTC to the departments, or users, TimeZone.
-
+
- Saves (sets) and Personnel Staffing in the system, for a single user
+ Users last known location
-
+
- UnitId of the apparatus that the state is being set for
+ Sorting weight for the user
-
+
- The UnitStateType of the Unit
+ User Defined Field values for this personnel record
-
+
- The timestamp of the status event in UTC
+ A GPS location for a point in time of a specificed person
-
+
- The timestamp of the status event in the local time of the device
+ PersonId of the person that the location is for
-
+
- User provided note for this event
+ The timestamp of the location in UTC
-
+
- The event id used for queuing on mobile applications
+ GPS Latitude of the Person
-
+
- Depicts a result after saving a person status
+ GPS Longitude of the Person
-
+
- Response Data
+ GPS Latitude\Longitude Accuracy of the Person
-
+
- Saves (sets) and Personnel Status in the system, for a single user
+ GPS Altitude of the Person
+
+
+
+
+ GPS Altitude Accuracy of the Person
+
+
+
+
+ GPS Speed of the Person
+
+
+
+
+ GPS Heading of the Person
+
+
+
+
+ A unit location in the Resgrid system
+
+
+
+
+ Response Data
+
+
+
+
+ The information about a specific unit's location
+
+
+
+
+ Id of the Person
+
+
+
+
+ The Timestamp for the location in UTC
+
+
+
+
+ GPS Latitude of the Person
+
+
+
+
+ GPS Longitude of the Person
+
+
+
+
+ GPS Latitude\Longitude Accuracy of the Person
+
+
+
+
+ GPS Altitude of the Person
+
+
+
+
+ GPS Altitude Accuracy of the Person
+
+
+
+
+ GPS Speed of the Person
+
+
+
+
+ GPS Heading of the Person
+
+
+
+
+ The result of getting the current staffing for a user
+
+
+
+
+ Response Data
+
+
+
+
+ Information about a User staffing
+
+
+
+
+ The UserId GUID/UUID for the user status being return
+
+
+
+
+ DepartmentId of the deparment the user belongs to
+
+
+
+
+ The current staffing type for the user
+
+
+
+
+ The timestamp of the last staffing. This is converted UTC version of the timestamp.
+
+
+
+
+ The timestamp of the last staffing. This is converted UTC to the departments, or users, TimeZone.
+
+
+
+
+ Note for this staffing
+
+
+
+
+ Saves (sets) and Personnel Staffing in the system, for a single user
+
+
+
+
+ UnitId of the apparatus that the state is being set for
+
+
+
+
+ The UnitStateType of the Unit
+
+
+
+
+ The timestamp of the status event in UTC
+
+
+
+
+ The timestamp of the status event in the local time of the device
+
+
+
+
+ User provided note for this event
+
+
+
+
+ The event id used for queuing on mobile applications
+
+
+
+
+ Depicts a result after saving a person status
+
+
+
+
+ Response Data
+
+
+
+
+ Saves (sets) and Personnel Status in the system, for a single user
@@ -8011,282 +8181,112 @@
Response Data
-
+
- The result of getting all personnel filters for the system
+ Result containing all the data required to populate the New Call form
-
+
- The Id value of the filter
+ Response Data
-
+
- The type of the filter
+ Details of a protocol
-
+
- The filters name
+ Protocol id
-
+
- Result containing all the data required to populate the New Call form
+ Department id
-
+
- Response Data
+ Name of the Protocol
-
+
- Result that contains all the options available to filter personnel against compatible Resgrid APIs
+ Protocol code
-
+
- Response Data
+ This this protocol disabled
-
+
- Result containing all the data required to populate the New Call form
+ Protocol description
-
+
- Response Data
+ Text of the protocol
-
+
- Information about a User
+ UTC date and time when the Protocol was created
-
+
- The UserId GUID/UUID for the user
+ UserId of the user who created the protocol
-
+
- DepartmentId of the deparment the user belongs to
+ UTC timestamp of when the Protocol was updated
-
+
- Department specificed ID number for this user
+ Minimum triggering Weight of the Protocol
-
+
- The Users First Name
+ UserId that last updated the Protocol
-
+
- The Users Last Name
+ Triggers used to activate this Protocol
-
+
- The Users Email Address
+ Attachments for this Protocol
-
+
- The Users Mobile Telephone Number
+ Questions used to determine if this Protocol needs to be used or not
-
+
- GroupId the user is assigned to (0 for no group)
+ State type
-
+
- Name of the group the user is assigned to
+ Result containing all the data required to populate the New Call form
-
+
- Enumeration/List of roles the user currently holds
+ Response Data
-
-
- The current action/status type for the user
-
-
-
-
- The current action/status string for the user
-
-
-
-
- The current action/status color hex string for the user
-
-
-
-
- The timestamp of the last action. This is converted UTC to the departments, or users, TimeZone.
-
-
-
-
- The current action/status destination id for the user
-
-
-
-
- The current action/status destination name for the user
-
-
-
-
- The current staffing level (state) type for the user
-
-
-
-
- The current staffing level (state) string for the user
-
-
-
-
- The current staffing level (state) color hex string for the user
-
-
-
-
- The timestamp of the last state/staffing level. This is converted UTC to the departments, or users, TimeZone.
-
-
-
-
- Users last known location
-
-
-
-
- Sorting weight for the user
-
-
-
-
- User Defined Field values for this personnel record
-
-
-
-
- Result containing all the data required to populate the New Call form
-
-
-
-
- Response Data
-
-
-
-
- Details of a protocol
-
-
-
-
- Protocol id
-
-
-
-
- Department id
-
-
-
-
- Name of the Protocol
-
-
-
-
- Protocol code
-
-
-
-
- This this protocol disabled
-
-
-
-
- Protocol description
-
-
-
-
- Text of the protocol
-
-
-
-
- UTC date and time when the Protocol was created
-
-
-
-
- UserId of the user who created the protocol
-
-
-
-
- UTC timestamp of when the Protocol was updated
-
-
-
-
- Minimum triggering Weight of the Protocol
-
-
-
-
- UserId that last updated the Protocol
-
-
-
-
- Triggers used to activate this Protocol
-
-
-
-
- Attachments for this Protocol
-
-
-
-
- Questions used to determine if this Protocol needs to be used or not
-
-
-
-
- State type
-
-
-
-
- Result containing all the data required to populate the New Call form
-
-
-
-
- Response Data
-
-
-
+
A role in the Resgrid system
@@ -9480,545 +9480,545 @@
Default constructor
-
+
- Depicts a result after saving a unit status
+ Result that contains all the options available to filter units against compatible Resgrid APIs
-
+
Response Data
-
+
- Object inputs for setting a users Status/Action. If this object is used in an operation that sets
- a status for the current user the UserId value in this object will be ignored.
+ A unit in the Resgrid system
-
+
- UnitId of the apparatus that the state is being set for
+ Response Data
-
+
- The UnitStateType of the Unit
+ The information about a specific unit
-
+
- The Call/Station the unit is responding to
+ Id of the Unit
-
+
- Destination type for RespondingTo (Station = 1, Call = 2, POI = 3).
+ The Id of the department the unit is under
-
+
- The timestamp of the status event in UTC
+ Name of the Unit
-
+
- The timestamp of the status event in the local time of the device
+ Department assigned type for the unit
-
+
- User provided note for this event
+ Department assigned type id for the unit
-
+
- GPS Latitude of the Unit
+ Custom Statuses Set Id
-
+
- GPS Longitude of the Unit
+ Station Id of the station housing the unit (0 means no station)
-
+
- GPS Latitude\Longitude Accuracy of the Unit
+ Name of the station the unit is under
-
+
- GPS Altitude of the Unit
+ Vehicle Identification Number for the unit
-
+
- GPS Altitude Accuracy of the Unit
+ Plate Number for the Unit
-
+
- GPS Speed of the Unit
+ Is the unit 4-Wheel drive
-
+
- GPS Heading of the Unit
+ Does the unit require a special permit to drive
-
+
- The event id used for queuing on mobile applications
+ Id number of the units current destionation (0 means no destination)
-
+
- The accountability roles filed for this event
+ The current status/state of the Unit
-
+
- Role filled by a User on a Unit for an event
+ The Timestamp of the status
-
+
- Id of the locally stored event
+ The units current Latitude
-
+
- Local Event Id
+ The units current Longitude
-
+
- UserId of the user filling the role
+ Current user provide status note
-
+
- RoleId of the role being filled
+ User Defined Field values for this unit
-
+
- The name of the Role
+ Unit role information for roles on a unit
-
+
- Depicts a unit status in the Resgrid system.
+ Unit Role Id
-
+
- Response Data
+ User Id of the user in the role (could be null)
-
+
- Depicts a unit's status
+ Name of the Role
-
+
- Unit Id
+ Name of the user in the role (could be null)
-
+
- Units Name
+ Multiple Unit infos Result
-
+
- The Type of the Unit
+ Response Data
-
+
- Units current Status (State)
+ Default constructor
-
+
- CSS for status (for display)
+ The information about a specific unit
-
+
- CSS Style for status (for display)
+ Id of the Unit
-
+
- Timestamp of this Unit State
+ The Id of the department the unit is under
-
+
- Timestamp in Utc of this Unit State
+ Name of the Unit
-
+
- Destination Id (Station or Call)
+ Department assigned type for the unit
-
+
- Destination type (Station, Call, or POI).
+ Department assigned type id for the unit
-
+
- Name of the Desination (Call or Station)
+ Custom Statuses Set Id
-
+
- Destination address.
+ Station Id of the station housing the unit (0 means no station)
-
+
- Localized display label for the destination type (e.g. "Station", "Call", "POI"). Not
- suitable for programmatic branching; use as the
- machine-readable discriminator instead.
+ Name of the station the unit is under
-
+
- Note for the State
+ Vehicle Identification Number for the unit
-
+
- Latitude
+ Plate Number for the Unit
-
+
- Longitude
+ Is the unit 4-Wheel drive
-
+
- Name of the Group the Unit is in
+ Does the unit require a special permit to drive
-
+
- Id of the Group the Unit is in
+ Id number of the units current destination (0 means no destination)
-
+
- Unit statuses (states)
+ Name of the units current destination (0 means no destination)
-
+
- Response Data
+ The current status/state of the Unit
-
+
- Default constructor
+ The current status/state of the Unit as a name
-
+
- Result that contains all the options available to filter units against compatible Resgrid APIs
+ The current status/state of the Unit color
-
+
- Response Data
+ The Timestamp of the status
-
+
- A unit in the Resgrid system
+ The Timestamp of the status in UTC/GMT
-
+
- Response Data
+ The units current Latitude
-
+
- The information about a specific unit
+ The units current Longitude
-
+
- Id of the Unit
+ Current user provide status note
-
+
- The Id of the department the unit is under
+ Units Roles
-
+
- Name of the Unit
+ Multiple Units Result
-
+
- Department assigned type for the unit
+ Response Data
-
+
- Department assigned type id for the unit
+ Default constructor
-
+
- Custom Statuses Set Id
+ Depicts a result after saving a unit status
-
+
- Station Id of the station housing the unit (0 means no station)
+ Response Data
-
+
- Name of the station the unit is under
+ Object inputs for setting a users Status/Action. If this object is used in an operation that sets
+ a status for the current user the UserId value in this object will be ignored.
-
+
- Vehicle Identification Number for the unit
+ UnitId of the apparatus that the state is being set for
-
+
- Plate Number for the Unit
+ The UnitStateType of the Unit
-
+
- Is the unit 4-Wheel drive
+ The Call/Station the unit is responding to
-
+
- Does the unit require a special permit to drive
+ Destination type for RespondingTo (Station = 1, Call = 2, POI = 3).
-
+
- Id number of the units current destionation (0 means no destination)
+ The timestamp of the status event in UTC
-
+
- The current status/state of the Unit
+ The timestamp of the status event in the local time of the device
-
+
- The Timestamp of the status
+ User provided note for this event
-
+
- The units current Latitude
+ GPS Latitude of the Unit
-
+
- The units current Longitude
+ GPS Longitude of the Unit
-
+
- Current user provide status note
+ GPS Latitude\Longitude Accuracy of the Unit
-
+
- User Defined Field values for this unit
+ GPS Altitude of the Unit
-
+
- Unit role information for roles on a unit
+ GPS Altitude Accuracy of the Unit
-
+
- Unit Role Id
+ GPS Speed of the Unit
-
+
- User Id of the user in the role (could be null)
+ GPS Heading of the Unit
-
+
- Name of the Role
+ The event id used for queuing on mobile applications
-
+
- Name of the user in the role (could be null)
+ The accountability roles filed for this event
-
+
- Multiple Unit infos Result
+ Role filled by a User on a Unit for an event
-
+
- Response Data
+ Id of the locally stored event
-
+
- Default constructor
+ Local Event Id
-
+
- The information about a specific unit
+ UserId of the user filling the role
-
+
- Id of the Unit
+ RoleId of the role being filled
-
+
- The Id of the department the unit is under
+ The name of the Role
-
+
- Name of the Unit
+ Depicts a unit status in the Resgrid system.
-
+
- Department assigned type for the unit
+ Response Data
-
+
- Department assigned type id for the unit
+ Depicts a unit's status
-
+
- Custom Statuses Set Id
+ Unit Id
-
+
- Station Id of the station housing the unit (0 means no station)
+ Units Name
-
+
- Name of the station the unit is under
+ The Type of the Unit
-
+
- Vehicle Identification Number for the unit
+ Units current Status (State)
-
+
- Plate Number for the Unit
+ CSS for status (for display)
-
+
- Is the unit 4-Wheel drive
+ CSS Style for status (for display)
-
+
- Does the unit require a special permit to drive
+ Timestamp of this Unit State
-
+
- Id number of the units current destination (0 means no destination)
+ Timestamp in Utc of this Unit State
-
+
- Name of the units current destination (0 means no destination)
+ Destination Id (Station or Call)
-
+
- The current status/state of the Unit
+ Destination type (Station, Call, or POI).
-
+
- The current status/state of the Unit as a name
+ Name of the Desination (Call or Station)
-
+
- The current status/state of the Unit color
+ Destination address.
-
+
- The Timestamp of the status
+ Localized display label for the destination type (e.g. "Station", "Call", "POI"). Not
+ suitable for programmatic branching; use as the
+ machine-readable discriminator instead.
-
+
- The Timestamp of the status in UTC/GMT
+ Note for the State
-
+
- The units current Latitude
+ Latitude
-
+
- The units current Longitude
+ Longitude
-
+
- Current user provide status note
+ Name of the Group the Unit is in
-
+
- Units Roles
+ Id of the Group the Unit is in
-
+
- Multiple Units Result
+ Unit statuses (states)
-
+
Response Data
-
+
Default constructor
diff --git a/Web/Resgrid.Web.Tts/Configuration/TtsOptions.cs b/Web/Resgrid.Web.Tts/Configuration/TtsOptions.cs
index 48dbafc3..896765ea 100644
--- a/Web/Resgrid.Web.Tts/Configuration/TtsOptions.cs
+++ b/Web/Resgrid.Web.Tts/Configuration/TtsOptions.cs
@@ -8,7 +8,7 @@ public sealed class TtsOptions
public string DefaultVoice { get; set; } = "en-us+klatt4";
[Range(80, 450)]
- public int DefaultSpeed { get; set; } = 165;
+ public int DefaultSpeed { get; set; } = 150;
[Range(1, 64)]
public int MaxConcurrentGenerations { get; set; } = 4;
diff --git a/Web/Resgrid.Web.Tts/Services/AudioProcessingService.cs b/Web/Resgrid.Web.Tts/Services/AudioProcessingService.cs
index 9e972b58..f02a2f63 100644
--- a/Web/Resgrid.Web.Tts/Services/AudioProcessingService.cs
+++ b/Web/Resgrid.Web.Tts/Services/AudioProcessingService.cs
@@ -163,6 +163,8 @@ private ProcessStartInfo CreatePiperStartInfo(string voice, int speed, string ou
startInfo.ArgumentList.Add(outputFilePath);
startInfo.ArgumentList.Add("--length-scale");
startInfo.ArgumentList.Add(invocation.LengthScale.ToString("0.00", CultureInfo.InvariantCulture));
+ startInfo.ArgumentList.Add("--sentence-silence");
+ startInfo.ArgumentList.Add("0.0");
return startInfo;
}
diff --git a/Web/Resgrid.Web.Tts/Services/TextPreprocessor.cs b/Web/Resgrid.Web.Tts/Services/TextPreprocessor.cs
index a1baf20b..344c3f8b 100644
--- a/Web/Resgrid.Web.Tts/Services/TextPreprocessor.cs
+++ b/Web/Resgrid.Web.Tts/Services/TextPreprocessor.cs
@@ -179,10 +179,10 @@ public sealed partial class TextPreprocessor : ITextPreprocessor
// ...
// };
+ private static readonly Regex LongNumberRegex = LongNumberExpandoRegex();
private static readonly Regex WhitespaceRegex = WhitespaceExpandoRegex();
private static readonly Regex UnitIdentifierRegex = UnitIdentifierExpandoRegex();
private static readonly Regex NumberToWordRegexField = NumberToWordRegex();
-
private readonly ILogger _logger;
public TextPreprocessor(ILogger logger)
@@ -199,33 +199,40 @@ public string Preprocess(string text, string voice)
var result = text.Trim();
- // Only preprocess English voices — other languages need their
- // own abbreviation dictionaries.
- if (!IsEnglishVoice(voice))
+ // Only preprocess English voices — they need their own abbreviation
+ // dictionaries. Other languages pass through to Piper directly.
+ if (IsEnglishVoice(voice))
{
- return result;
+ var original = result;
+
+ // Order matters: expand abbreviations first so downstream
+ // passes operate on natural-language words rather than codes.
+ result = ExpandAbbreviations(result);
+ result = ExpandDispatchShorthand(result);
+ result = ExpandSlashNotation(result);
+ result = ExpandAddressAbbreviations(result);
+ result = ExpandUnitIdentifiers(result);
+ result = ExpandLongNumbers(result);
+ result = NormalizeSmallNumbers(result);
+ // Collapse any whitespace artefacts introduced by expansion.
+ result = WhitespaceRegex.Replace(result, " ").Trim();
+
+ if (!string.Equals(original, result, StringComparison.Ordinal))
+ {
+ _logger.LogDebug(
+ "TextPreprocessor normalised \"{OriginalText}\" to \"{NormalisedText}\"",
+ original,
+ result);
+ }
}
- var original = result;
-
- // Order matters: expand abbreviations first so downstream
- // passes operate on natural-language words rather than codes.
- result = ExpandAbbreviations(result);
- result = ExpandDispatchShorthand(result);
- result = ExpandSlashNotation(result);
- result = ExpandAddressAbbreviations(result);
- result = ExpandUnitIdentifiers(result);
- result = NormalizeSmallNumbers(result);
-
- // Collapse any whitespace artefacts introduced by expansion.
- result = WhitespaceRegex.Replace(result, " ").Trim();
-
- if (!string.Equals(original, result, StringComparison.Ordinal))
+ // Ensure the text ends with sentence-ending punctuation so that
+ // Piper does not hallucinate extra speech past the intended end.
+ // Without a clear sentence boundary, neural TTS models may
+ // continue generating audio that sounds like additional words.
+ if (result.Length > 0 && result[^1] is not '.' and not '!' and not '?')
{
- _logger.LogDebug(
- "TextPreprocessor normalised \"{OriginalText}\" to \"{NormalisedText}\"",
- original,
- result);
+ result += ".";
}
return result;
@@ -375,6 +382,27 @@ private static bool IsEnglishVoice(string voice)
|| string.Equals(baseVoice, "mb-us1", StringComparison.OrdinalIgnoreCase);
}
+ // ---------------------------------------------------------------
+ // Long number expansion for clarity
+ // ---------------------------------------------------------------
+
+ ///
+ /// Converts digit sequences of 4 or more consecutive digits into
+ /// individual space-separated digits so that Piper reads them as
+ /// digit-by-digit (e.g. "12345" → "1 2 3 4 5") rather than as
+ /// a large composite number ("twelve thousand three hundred forty-five").
+ /// This is critical for address numbers, call IDs, and other dispatch
+ /// identifiers where digit-by-digit reading is significantly clearer.
+ ///
+ private static string ExpandLongNumbers(string text)
+ {
+ return LongNumberRegex.Replace(text, match =>
+ {
+ var digits = match.Groups[1].Value;
+ return match.Value.Replace(digits, string.Join(" ", digits.ToCharArray()));
+ });
+ }
+
// ---------------------------------------------------------------
// Source-generated regex helpers
// ---------------------------------------------------------------
@@ -387,6 +415,10 @@ private static bool IsEnglishVoice(string voice)
[GeneratedRegex(@"\b(?(?:[1-9]|1[0-9]|20))\s(?[A-Za-z])", RegexOptions.CultureInvariant)]
private static partial Regex NumberToWordRegex();
+ /// Matches a run of 4+ consecutive digits not surrounded by other digits.
+ [GeneratedRegex(@"(?\d{4,})(?!\d)", RegexOptions.CultureInvariant)]
+ private static partial Regex LongNumberExpandoRegex();
+
/// Collapses multiple whitespace characters into a single space.
[GeneratedRegex(@"\s+")]
private static partial Regex WhitespaceExpandoRegex();