Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix] TTS #139

Merged
merged 8 commits into from
Dec 6, 2024
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
3 changes: 0 additions & 3 deletions Content.Client/Entry/EntryPoint.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Content.Client._White.TTS;
using Content.Client.Administration.Managers;
using Content.Client.Changelog;
using Content.Client.Chat.Managers;
Expand Down Expand Up @@ -73,7 +72,6 @@ public sealed class EntryPoint : GameClient
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly JoinQueueManager _joinQueue = default!;
[Dependency] private readonly DiscordAuthManager _discordAuth = default!;
[Dependency] private readonly TTSManager _ttsManager = default!; // WD EDIT

public override void Init()
{
Expand Down Expand Up @@ -168,7 +166,6 @@ public override void PostInit()
_documentParsingManager.Initialize();
_joinQueue.Initialize();
_discordAuth.Initialize();
_ttsManager.Initialize(); // WD EDIT

_baseClient.RunLevelChanged += (_, args) =>
{
Expand Down
2 changes: 0 additions & 2 deletions Content.Client/IoC/ClientContentIoC.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Content.Client._White.TTS;
using Content.Client.Administration.Managers;
using Content.Client.Changelog;
using Content.Client.Chat.Managers;
Expand Down Expand Up @@ -53,7 +52,6 @@ public static void Register()
collection.Register<ISharedPlaytimeManager, JobRequirementsManager>();
IoCManager.Register<JoinQueueManager>();
IoCManager.Register<DiscordAuthManager>();
IoCManager.Register<TTSManager>(); // WD EDIT
}
}
}
1 change: 1 addition & 0 deletions Content.Client/Options/UI/Tabs/AudioTab.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ private void UpdateChanges()
var isTtsVolumeSame =
Math.Abs(TtsVolumeSlider.Value - _cfg.GetCVar(WhiteCVars.TTSVolume) * 100f / ContentAudioSystem.TTSMultiplier) < 0.01f; // WD EDIT


var isAmbientSoundsSame = (int)AmbienceSoundsSlider.Value == _cfg.GetCVar(CCVars.MaxAmbientSources);
var isLobbySame = LobbyMusicCheckBox.Pressed == _cfg.GetCVar(CCVars.LobbyMusicEnabled);
var isRestartSoundsSame = RestartSoundsCheckBox.Pressed == _cfg.GetCVar(CCVars.RestartSoundsEnabled);
Expand Down
194 changes: 194 additions & 0 deletions Content.Client/_White/TTS/AnnounceTTSSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared._White.TTS;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Robust.Client.ResourceManagement;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Components;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Player;
using Robust.Shared.Utility;

namespace Content.Client._White.TTS;

// ReSharper disable once InconsistentNaming
public sealed class AnnounceTTSSystem : EntitySystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;

private ISawmill _sawmill = default!;
private readonly MemoryContentRoot _contentRoot = new();
private ResPath _prefix;

private float _volume = 0.0f;
private ulong _fileIdx = 0;
private static ulong _shareIdx = 0;

private TTSAudioStream? _currentlyPlaying;
private readonly Queue<TTSAudioStream> _queuedStreams = new();

/// <inheritdoc />
public override void Initialize()
{
_prefix = ResPath.Root / $"TTSAnon{_shareIdx++}";
_resourceCache.AddRoot(_prefix, _contentRoot);
_sawmill = Logger.GetSawmill("AnnounceTTSSystem");
_cfg.OnValueChanged(CCVars.AnnouncerVolume, OnTtsVolumeChanged, true);
SubscribeNetworkEvent<AnnounceTTSEvent>(OnAnnounceTTSPlay);
SubscribeNetworkEvent<RoundRestartCleanupEvent>(OnCleanup);
}

private void OnCleanup(RoundRestartCleanupEvent ev)
{
EndStreams();
_contentRoot.Clear();
}

/// <inheritdoc />
public override void FrameUpdate(float frameTime)
{
if (_queuedStreams.Count == 0)
return;

var isDoNext = true;
try
{
isDoNext = _currentlyPlaying == null ||
(_currentlyPlaying.AudioStream != null && TerminatingOrDeleted(_currentlyPlaying.AudioStream!.Value))
|| !(_currentlyPlaying.AudioStream?.Comp.Playing ?? false);
}
catch (Exception err)
{
isDoNext = true;
}

Comment on lines +65 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Добавьте логирование исключений в блоке catch

Переменная err в блоке catch (Exception err) не используется. Рекомендуется логировать перехваченные исключения для облегчения отладки и мониторинга ошибок.

if (isDoNext)
{
_currentlyPlaying?.StopAndClean(this);
ProcessEntityQueue();
}

}

/// <inheritdoc />
public override void Shutdown()
{
_cfg.UnsubValueChanged(CCVars.AnnouncerVolume, OnTtsVolumeChanged);
EndStreams();
_contentRoot.Dispose();
}

private void OnAnnounceTTSPlay(AnnounceTTSEvent ev)
{
var volume = Math.Max(-5f, SharedAudioSystem.GainToVolume(_volume));


var file = new ResPath(ev.AnnouncementSound);

if (!_resourceCache.TryGetResource<AudioResource>(file, out var audio))
{
_sawmill.Error($"Server tried to play audio file {ev.AnnouncementSound} which does not exist.");
return;
}

if (TryCreateAudioSource(file, ev.AnnouncementParams.Volume, out var sourceAnnounce))
AddEntityStreamToQueue(sourceAnnounce);
if (ev.Data.Length > 0 && TryCreateAudioSource(ev.Data, volume, out var source))
{
source.DelayMs = (int) audio.AudioStream.Length.TotalMilliseconds;
AddEntityStreamToQueue(source);
}

}

private void AddEntityStreamToQueue(TTSAudioStream stream)
{
_queuedStreams.Enqueue(stream);
}

private void ProcessEntityQueue()
{
if (_queuedStreams.TryDequeue(out _currentlyPlaying))
PlayEntity(_currentlyPlaying);
}

private bool TryCreateAudioSource(byte[] data, float volume, [NotNullWhen(true)] out TTSAudioStream? source)
{
var filePath = new ResPath($"{_fileIdx++}.ogg");
_contentRoot.AddOrUpdateFile(filePath, data);

var audioParams = AudioParams.Default.WithVolume(volume).WithRolloffFactor(1f).WithMaxDistance(float.MaxValue).WithReferenceDistance(1f);
var soundPath = new SoundPathSpecifier(_prefix / filePath, audioParams);

source = new TTSAudioStream(soundPath, filePath);

return true;
}

private bool TryCreateAudioSource(ResPath audio, float volume,
[NotNullWhen(true)] out TTSAudioStream? source)
{
var audioParams = AudioParams.Default.WithVolume(volume).WithRolloffFactor(1f).WithMaxDistance(float.MaxValue).WithReferenceDistance(1f);

var soundPath = new SoundPathSpecifier(audio, audioParams);


source = new TTSAudioStream(soundPath, null);

return true;
}

private void PlayEntity(TTSAudioStream stream)
{
stream.AudioStream = _audio.PlayGlobal(stream.Source, Filter.Local(), false);
}

private void OnTtsVolumeChanged(float volume)
{
_volume = volume;
}

private void EndStreams()
{
foreach (var stream in _queuedStreams)
{
stream.StopAndClean(this);
}

_queuedStreams.Clear();
}

// ReSharper disable once InconsistentNaming
private sealed class TTSAudioStream
{
public SoundPathSpecifier Source { get; }
public ResPath? CacheFile { get; }
public Entity<AudioComponent>? AudioStream { get; set; }

public int DelayMs { get; set; }

public TTSAudioStream(SoundPathSpecifier source, ResPath? cacheFile, int delayMs = 0)
{
Source = source;
CacheFile = cacheFile;
DelayMs = delayMs;
}

public void StopAndClean(AnnounceTTSSystem sys)
{
if (AudioStream != null)
{
sys._audio.Stop(AudioStream.Value,AudioStream.Value);

}
if (CacheFile != null)
{
sys._contentRoot.RemoveFile(CacheFile.Value);
}
}
}
}
38 changes: 14 additions & 24 deletions Content.Client/_White/TTS/HumanoidProfileEditor.TTS.cs
Original file line number Diff line number Diff line change
@@ -1,43 +1,31 @@
using System.Linq;
using Content.Client._White.TTS;
using Content.Shared.Preferences;
using Content.Shared._White.TTS;
using Robust.Shared.Random;
using Content.Shared.Preferences;

// ReSharper disable InconsistentNaming
// ReSharper disable once CheckNamespace
namespace Content.Client.Lobby.UI;

public sealed partial class HumanoidProfileEditor
{
private TTSSystem _ttsSystem = default!;
private TTSManager _ttsManager = default!;
private IRobustRandom _random = default!;

private List<TTSVoicePrototype> _voiceList = default!;

private readonly string[] _sampleText =
[
"Помогите, клоун насилует в технических тоннелях!",
"ХоС, ваши сотрудники украли у меня собаку и засунули ее в стиральную машину!",
"Агент синдиката украл пиво из бара и взорвался!",
"Врача! Позовите врача!"
];
private List<TTSVoicePrototype> _voiceList = new();

private void InitializeVoice()
{
_random = IoCManager.Resolve<IRobustRandom>();
_ttsManager = IoCManager.Resolve<TTSManager>();
_ttsSystem = IoCManager.Resolve<IEntityManager>().System<TTSSystem>();
_voiceList = _prototypeManager.EnumeratePrototypes<TTSVoicePrototype>().Where(o => o.RoundStart).ToList();
_voiceList = _prototypeManager
.EnumeratePrototypes<TTSVoicePrototype>()
.Where(o => o.RoundStart)
.OrderBy(o => Loc.GetString(o.Name))
.ToList();
Comment on lines +16 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Добавьте обработку случая пустого списка голосов

Необходимо добавить проверку на случай, если список голосов пуст после фильтрации.

Предлагаемые изменения:

    _voiceList = _prototypeManager
        .EnumeratePrototypes<TTSVoicePrototype>()
        .Where(o => o.RoundStart)
        .OrderBy(o => Loc.GetString(o.Name))
        .ToList();
+
+   if (!_voiceList.Any())
+   {
+       Logger.Warning($"{nameof(HumanoidProfileEditor)}: Не найдено доступных голосов TTS");
+       return;
+   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_voiceList = _prototypeManager
.EnumeratePrototypes<TTSVoicePrototype>()
.Where(o => o.RoundStart)
.OrderBy(o => Loc.GetString(o.Name))
.ToList();
_voiceList = _prototypeManager
.EnumeratePrototypes<TTSVoicePrototype>()
.Where(o => o.RoundStart)
.OrderBy(o => Loc.GetString(o.Name))
.ToList();
if (!_voiceList.Any())
{
Logger.Warning($"{nameof(HumanoidProfileEditor)}: Не найдено доступных голосов TTS");
return;
}


VoiceButton.OnItemSelected += args =>
{
VoiceButton.SelectId(args.Id);
SetVoice(_voiceList[args.Id].ID);
};

VoicePlayButton.OnPressed += _ => { PlayTTS(); };
VoicePlayButton.OnPressed += _ => PlayPreviewTTS();
}

private void UpdateTTSVoicesControls()
Expand All @@ -62,16 +50,18 @@ private void UpdateTTSVoicesControls()
}

var voiceChoiceId = _voiceList.FindIndex(x => x.ID == Profile.Voice);
if (!VoiceButton.TrySelectId(voiceChoiceId) && VoiceButton.TrySelectId(firstVoiceChoiceId))
if (!VoiceButton.TrySelectId(voiceChoiceId) &&
VoiceButton.TrySelectId(firstVoiceChoiceId))
{
SetVoice(_voiceList[firstVoiceChoiceId].ID);
}
}

private void PlayTTS()
private void PlayPreviewTTS()
{
if (Profile is null)
return;

_ttsSystem.StopCurrentTTS(PreviewDummy);
_ttsManager.RequestTTS(PreviewDummy, _random.Pick(_sampleText), Profile.Voice);
_entManager.System<TTSSystem>().RequestGlobalTTS(VoiceRequestType.Preview,Profile.Voice);
Comment on lines +60 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Добавьте обработку ошибок в PlayPreviewTTS

Метод PlayPreviewTTS должен обрабатывать случай, когда голос не установлен.

Предлагаемые изменения:

    private void PlayPreviewTTS()
    {
        if (Profile is null)
            return;

+       if (string.IsNullOrEmpty(Profile.Voice))
+       {
+           Logger.Warning($"{nameof(HumanoidProfileEditor)}: Попытка проиграть предпросмотр с неустановленным голосом");
+           return;
+       }
+
        _entManager.System<TTSSystem>().RequestGlobalTTS(VoiceRequestType.Preview, Profile.Voice);
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void PlayPreviewTTS()
{
if (Profile is null)
return;
_ttsSystem.StopCurrentTTS(PreviewDummy);
_ttsManager.RequestTTS(PreviewDummy, _random.Pick(_sampleText), Profile.Voice);
_entManager.System<TTSSystem>().RequestGlobalTTS(VoiceRequestType.Preview,Profile.Voice);
private void PlayPreviewTTS()
{
if (Profile is null)
return;
if (string.IsNullOrEmpty(Profile.Voice))
{
Logger.Warning($"{nameof(HumanoidProfileEditor)}: Попытка проиграть предпросмотр с неустановленным голосом");
return;
}
_entManager.System<TTSSystem>().RequestGlobalTTS(VoiceRequestType.Preview, Profile.Voice);
}

}
}
24 changes: 0 additions & 24 deletions Content.Client/_White/TTS/TTSManager.cs

This file was deleted.

Loading
Loading