using System.Text;
using System.Text.RegularExpressions;
namespace Content.Server.Corvax.TTS;
// ReSharper disable once InconsistentNaming
public sealed partial class TTSSystem
{
private static readonly Regex UpperCaseWordRegex = new(@"\b([А-ЯЁ]{2,})\b",
RegexOptions.Compiled);
private static readonly Regex YesNoRegex = new(@"\b(НЕТ|ДА|нет|да)\b",
RegexOptions.Compiled);
private static readonly Regex NotRegex = new(@"\b(НЕ|не)\b",
RegexOptions.Compiled);
private static readonly Regex ImportantWordRegex = new(
@"\b(внимание|Внимание|ВНИМАНИЕ|" +
@"опасность|Опасность|ОПАСНОСТЬ|" +
@"срочно|Срочно|СРОЧНО|" +
@"важно|Важно|ВАЖНО|" +
@"предупреждение|Предупреждение|ПРЕДУПРЕЖДЕНИЕ|" +
@"помогите|Помогите|ПОМОГИТЕ|" +
@"капитан|Капитан|КАПИТАН|" +
@"командир|Командир|КОМАНДИР)\b",
RegexOptions.Compiled);
private static readonly Regex WhitespaceRegex = new(@"\s+",
RegexOptions.Compiled);
///
/// Converts text into valid SSML with pauses and emphasis.
///
private string ToSsmlText(string text, SoundTraits traits = SoundTraits.None)
{
text = XmlEscape(text);
text = InsertPauses(text);
text = InsertEmphasis(text);
var prosodyAttrs = BuildProsodyAttributes(traits);
return !string.IsNullOrEmpty(prosodyAttrs)
? $"{text}"
: $"{text}";
}
private string XmlEscape(string text)
{
return text.Replace("&", "&")
.Replace("<", "<")
.Replace(">", ">")
.Replace("\"", """)
.Replace("'", "'");
}
private string InsertPauses(string text)
{
var sb = new StringBuilder();
int i = 0;
while (i < text.Length)
{
char c = text[i];
if (c == '!')
{
int count = 0;
while (i < text.Length && text[i] == '!')
{
count++;
i++;
}
sb.Append(new string('!', count));
if (i < text.Length && text[i] == ' ' && i + 1 < text.Length && !char.IsWhiteSpace(text[i + 1]))
sb.Append(" ");
continue;
}
if (c == '?')
{
int count = 0;
while (i < text.Length && text[i] == '?')
{
count++;
i++;
}
sb.Append(new string('?', count));
if (i < text.Length && text[i] == ' ' && i + 1 < text.Length && !char.IsWhiteSpace(text[i + 1]))
sb.Append(" ");
continue;
}
if (c == '?' && i + 1 < text.Length && text[i + 1] == '!')
{
sb.Append("?!");
i += 2;
if (i < text.Length && text[i] == ' ' && i + 1 < text.Length && !char.IsWhiteSpace(text[i + 1]))
sb.Append(" ");
continue;
}
if (c == '!' && i + 1 < text.Length && text[i + 1] == '?')
{
sb.Append("!?");
i += 2;
if (i < text.Length && text[i] == ' ' && i + 1 < text.Length && !char.IsWhiteSpace(text[i + 1]))
sb.Append(" ");
continue;
}
if (c == '.' && i + 2 < text.Length && text[i + 1] == '.' && text[i + 2] == '.')
{
sb.Append("...");
i += 3;
if (i < text.Length && text[i] == ' ' && i + 1 < text.Length && !char.IsWhiteSpace(text[i + 1]))
sb.Append(" ");
continue;
}
sb.Append(c);
if (c == '.')
{
if (i + 1 < text.Length && char.IsLetter(text[i + 1]))
{
// Nothing
}
else if (i + 1 < text.Length && text[i + 1] == ' ' && i + 2 < text.Length && !char.IsWhiteSpace(text[i + 2]))
{
sb.Append(" ");
}
}
else if (c == ',')
{
if (i + 1 < text.Length && text[i + 1] == ' ')
sb.Append(" ");
}
else if (c == ';')
{
if (i + 1 < text.Length && text[i + 1] == ' ')
sb.Append(" ");
}
else if (c == ':')
{
if (i + 1 < text.Length && text[i + 1] == ' ')
sb.Append(" ");
}
i++;
}
var result = sb.ToString();
result = WhitespaceRegex.Replace(result, " ");
return result.Trim();
}
private string InsertEmphasis(string text)
{
text = UpperCaseWordRegex.Replace(text, m =>
{
var word = m.Value.ToLowerInvariant();
return $"{word}";
});
text = YesNoRegex.Replace(text, m =>
{
var word = m.Value.ToLowerInvariant();
return $"{word}";
});
text = NotRegex.Replace(text, m =>
{
var word = m.Value.ToLowerInvariant();
return $"{word}";
});
text = ImportantWordRegex.Replace(text, m =>
{
var word = m.Value.ToLowerInvariant();
return $"{word}";
});
return text;
}
private string BuildProsodyAttributes(SoundTraits traits)
{
var attrs = new List();
if (traits.HasFlag(SoundTraits.RateXSlow))
attrs.Add("rate=\"x-slow\"");
else if (traits.HasFlag(SoundTraits.RateSlow))
attrs.Add("rate=\"slow\"");
else if (traits.HasFlag(SoundTraits.RateMedium))
attrs.Add("rate=\"medium\"");
else if (traits.HasFlag(SoundTraits.RateFast))
attrs.Add("rate=\"fast\"");
else if (traits.HasFlag(SoundTraits.RateXFast))
attrs.Add("rate=\"x-fast\"");
if (traits.HasFlag(SoundTraits.PitchVerylow))
attrs.Add("pitch=\"x-low\"");
else if (traits.HasFlag(SoundTraits.PitchLow))
attrs.Add("pitch=\"low\"");
else if (traits.HasFlag(SoundTraits.PitchMedium))
attrs.Add("pitch=\"medium\"");
else if (traits.HasFlag(SoundTraits.PitchHigh))
attrs.Add("pitch=\"high\"");
else if (traits.HasFlag(SoundTraits.PitchVeryhigh))
attrs.Add("pitch=\"x-high\"");
if (traits.HasFlag(SoundTraits.VolumeXSoft))
attrs.Add("volume=\"x-soft\"");
else if (traits.HasFlag(SoundTraits.VolumeSoft))
attrs.Add("volume=\"soft\"");
else if (traits.HasFlag(SoundTraits.VolumeMedium))
attrs.Add("volume=\"medium\"");
else if (traits.HasFlag(SoundTraits.VolumeLoud))
attrs.Add("volume=\"loud\"");
else if (traits.HasFlag(SoundTraits.VolumeXLoud))
attrs.Add("volume=\"x-loud\"");
return string.Join(" ", attrs);
}
[Flags]
private enum SoundTraits : ushort
{
None = 0,
RateXSlow = 1 << 0,
RateSlow = 1 << 1,
RateMedium = 1 << 2,
RateFast = 1 << 3,
RateXFast = 1 << 4,
PitchVerylow = 1 << 5,
PitchLow = 1 << 6,
PitchMedium = 1 << 7,
PitchHigh = 1 << 8,
PitchVeryhigh = 1 << 9,
VolumeXSoft = 1 << 10,
VolumeSoft = 1 << 11,
VolumeMedium = 1 << 12,
VolumeLoud = 1 << 13,
VolumeXLoud = 1 << 14,
}
}