Files
ss14-wega/Content.Server/Corvax/TTS/TTSSystem.cs
T
2026-05-25 06:05:16 +07:00

153 lines
6.7 KiB
C#

using System.Threading.Tasks;
using Content.Shared.Chat;
using Content.Server.Chat.Systems;
using Content.Shared.Corvax.CCCVars;
using Content.Shared.Corvax.TTS;
using Content.Shared.GameTicking;
using Content.Shared.Players.RateLimiting;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Corvax.TTS;
// ReSharper disable once InconsistentNaming
public sealed partial class TTSSystem : EntitySystem
{
[Dependency] private IConfigurationManager _cfg = default!;
[Dependency] private IPrototypeManager _prototypeManager = default!;
[Dependency] private TTSManager _ttsManager = default!;
[Dependency] private SharedTransformSystem _xforms = default!;
[Dependency] private IRobustRandom _rng = default!;
private readonly List<string> _sampleText =
new()
{
"Съешь же ещё этих мягких французских булок, да выпей чаю.",
"Клоун, прекрати разбрасывать банановые кожурки офицерам под ноги!",
"Капитан, вы уверены что хотите назначить клоуна на должность главы персонала?",
"Эс Бэ! Тут человек в сером костюме, с тулбоксом и в маске! Помогите!!",
"Учёные, тут странная аномалия в баре! Она уже съела мима!",
"Я надеюсь что инженеры внимательно следят за сингулярностью...",
"Вы слышали эти странные крики в техах? Мне кажется туда ходить небезопасно.",
"Вы не видели Гамлета? Мне кажется он забегал к вам на кухню.",
"Здесь есть доктор? Человек умирает от отравленного пончика! Нужна помощь!",
"Вам нужно согласие и печать квартирмейстера, если вы хотите сделать заказ на партию дробовиков.",
"Возле эвакуационного шаттла разгерметизация! Инженеры, нам срочно нужна ваша помощь!",
"Бармен, налей мне самого крепкого вина, которое есть в твоих запасах!"
};
private const int MaxMessageChars = 100 * 2; // same as SingleBubbleCharLimit * 2
private bool _isEnabled = false;
public override void Initialize()
{
_cfg.OnValueChanged(CCCVars.TTSEnabled, v => _isEnabled = v, true);
SubscribeLocalEvent<TransformSpeechEvent>(OnTransformSpeech);
SubscribeLocalEvent<TTSComponent, EntitySpokeEvent>(OnEntitySpoke);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
SubscribeNetworkEvent<RequestPreviewTTSEvent>(OnRequestPreviewTTS);
RegisterRateLimits();
}
private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
{
_ttsManager.ResetCache();
}
private async void OnRequestPreviewTTS(RequestPreviewTTSEvent ev, EntitySessionEventArgs args)
{
if (!_isEnabled ||
!_prototypeManager.TryIndex<TTSVoicePrototype>(ev.VoiceId, out var protoVoice))
return;
if (HandleRateLimit(args.SenderSession) != RateLimitStatus.Allowed)
return;
var previewText = _rng.Pick(_sampleText);
var soundData = await GenerateTTS(previewText, protoVoice.Speaker);
if (soundData is null)
return;
RaiseNetworkEvent(new PlayTTSEvent(soundData), Filter.SinglePlayer(args.SenderSession));
}
private async void OnEntitySpoke(EntityUid uid, TTSComponent component, EntitySpokeEvent args)
{
var voiceId = component.VoicePrototypeId;
if (!_isEnabled ||
args.Message.Length > MaxMessageChars ||
string.IsNullOrEmpty(voiceId))
return;
var voiceEv = new TransformSpeakerVoiceEvent(uid, voiceId);
RaiseLocalEvent(uid, voiceEv);
voiceId = voiceEv.VoiceId;
if (!_prototypeManager.TryIndex<TTSVoicePrototype>(voiceId, out var protoVoice))
return;
if (args.ObfuscatedMessage != null)
{
HandleWhisper(uid, args.Message, args.ObfuscatedMessage, protoVoice.Speaker);
return;
}
HandleSay(uid, args.Message, protoVoice.Speaker);
}
private async void HandleSay(EntityUid uid, string message, string speaker)
{
var soundData = await GenerateTTS(message, speaker);
if (soundData is null) return;
RaiseNetworkEvent(new PlayTTSEvent(soundData, GetNetEntity(uid)), Filter.Pvs(uid));
}
private async void HandleWhisper(EntityUid uid, string message, string obfMessage, string speaker)
{
var fullSoundData = await GenerateTTS(message, speaker, true);
if (fullSoundData is null) return;
var obfSoundData = await GenerateTTS(obfMessage, speaker, true);
if (obfSoundData is null) return;
var fullTtsEvent = new PlayTTSEvent(fullSoundData, GetNetEntity(uid), true);
var obfTtsEvent = new PlayTTSEvent(obfSoundData, GetNetEntity(uid), true);
// TODO: Check obstacles
var xformQuery = GetEntityQuery<TransformComponent>();
var sourcePos = _xforms.GetWorldPosition(xformQuery.GetComponent(uid), xformQuery);
var receptions = Filter.Pvs(uid).Recipients;
foreach (var session in receptions)
{
if (!session.AttachedEntity.HasValue) continue;
var xform = xformQuery.GetComponent(session.AttachedEntity.Value);
var distance = (sourcePos - _xforms.GetWorldPosition(xform, xformQuery)).Length();
if (distance > ChatSystem.VoiceRange)
continue;
RaiseNetworkEvent(distance > ChatSystem.WhisperClearRange ? obfTtsEvent : fullTtsEvent, session);
}
}
// ReSharper disable once InconsistentNaming
private async Task<byte[]?> GenerateTTS(string text, string speaker, bool isWhisper = false)
{
var textSanitized = Sanitize(text);
if (textSanitized == "") return null;
if (char.IsLetter(textSanitized[^1]))
textSanitized += ".";
var ssmlTraits = SoundTraits.RateFast;
if (isWhisper)
ssmlTraits = SoundTraits.PitchVerylow;
var textSsml = ToSsmlText(textSanitized, ssmlTraits);
return await _ttsManager.ConvertTextToSpeech(speaker, textSsml);
}
}