Переработка вкладки TTS (#3381)

This commit is contained in:
Zekins 2025-10-02 10:50:50 +03:00 • committed by GitHub
parent 7fd7fb4f0d
commit 5ac7223980
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 290 additions and 108 deletions

View file

@ -1,78 +0,0 @@
using System.Linq;
using Content.Client.Corvax.TTS;
using Content.Client.Lobby;
using Content.Corvax.Interfaces.Shared;
using Content.Shared.Corvax.TTS;
using Content.Shared.Preferences;
namespace Content.Client.Lobby.UI;
public sealed partial class HumanoidProfileEditor
{
private ISharedSponsorsManager? _sponsorsMgr;
private List<TTSVoicePrototype> _voiceList = new();
private void InitializeVoice()
{
_voiceList = _prototypeManager
.EnumeratePrototypes<TTSVoicePrototype>()
.Where(o => o.RoundStart)
.OrderBy(o => Loc.GetString(o.Name))
.ToList();
VoiceButton.OnItemSelected += args =>
{
VoiceButton.SelectId(args.Id);
SetVoice(_voiceList[args.Id].ID);
};
VoicePlayButton.OnPressed += _ => PlayPreviewTTS();
IoCManager.Instance!.TryResolveType(out _sponsorsMgr);
}
private void UpdateTTSVoicesControls()
{
if (Profile is null)
return;
VoiceButton.Clear();
var firstVoiceChoiceId = 1;
for (var i = 0; i < _voiceList.Count; i++)
{
var voice = _voiceList[i];
if (!HumanoidCharacterProfile.CanHaveVoice(voice, Profile.Sex))
continue;
var name = Loc.GetString(voice.Name);
VoiceButton.AddItem(name, i);
if (firstVoiceChoiceId == 1)
firstVoiceChoiceId = i;
if (_sponsorsMgr is null)
continue;
if (voice.SponsorOnly && _sponsorsMgr != null &&
!_sponsorsMgr.GetClientPrototypes().Contains(voice.ID))
{
VoiceButton.SetItemDisabled(VoiceButton.GetIdx(i), true);
}
}
var voiceChoiceId = _voiceList.FindIndex(x => x.ID == Profile.Voice);
if (!VoiceButton.TrySelectId(voiceChoiceId) &&
VoiceButton.TrySelectId(firstVoiceChoiceId))
{
SetVoice(_voiceList[firstVoiceChoiceId].ID);
}
}
private void PlayPreviewTTS()
{
if (Profile is null)
return;
_entManager.System<TTSSystem>().RequestPreviewTTS(Profile.Voice);
}
}

View file

@ -0,0 +1,24 @@
<Control xmlns="https://spacestation14.io" xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="5">
<LineEdit Name="SearchEdit" MinWidth="600" PlaceHolder="{Loc 'humanoid-profile-editor-voice-placeholder'}"/>
<Label Name="ResultsLabel" MinWidth="100" HorizontalExpand="True" HorizontalAlignment="Right"/>
</BoxContainer>
<PanelContainer HorizontalExpand="True" VerticalExpand="False" MinSize="0 200" Margin="0 5 0 20">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1A1A1A" />
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="5">
<Label Text="{Loc 'humanoid-profile-editor-voice-categories'}" StyleClasses="LabelHeading" HorizontalAlignment="Center" Margin="0 0 0 5"/>
<ScrollContainer VerticalExpand="True" HorizontalExpand="True">
<GridContainer Name="CategoriesContainer" Columns="4" HorizontalExpand="True"/>
</ScrollContainer>
</BoxContainer>
</PanelContainer>
<ScrollContainer VerticalExpand="True" Margin="5">
<GridContainer Name="VoicesGrid" Columns="3" HorizontalExpand="True"/>
</ScrollContainer>
</BoxContainer>
</Control>

View file

@ -0,0 +1,194 @@
using System.Linq;
using Content.Corvax.Interfaces.Shared;
using Content.Shared.Corvax.TTS;
using Content.Shared.Preferences;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
using Content.Client.Stylesheets;
using Content.Shared.Humanoid;
using System.Text.RegularExpressions;
namespace Content.Client.Corvax.TTS;
[GenerateTypedNameReferences]
public sealed partial class TTSTab : Control
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public event Action<string>? OnVoiceSelected;
public event Action<string>? OnPreviewRequested;
private List<TTSVoicePrototype> _allVoices = new();
private List<TTSVoicePrototype> _filteredVoices = new();
private Dictionary<string, List<TTSVoicePrototype>> _categorizedVoices = new();
private string? _selectedVoiceId;
private static readonly Regex CategoryRegex = new Regex(@"^(.*?)\s*\(([^)]+)\)\s*$", RegexOptions.Compiled);
public TTSTab()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
LoadVoices();
SearchEdit.OnTextChanged += OnSearchChanged;
}
private void LoadVoices()
{
foreach (var voice in _allVoices)
{
var name = Loc.GetString(voice.Name);
var category = Loc.GetString("humanoid-profile-editor-voice-other");
var match = CategoryRegex.Match(name);
if (match.Success)
{
category = match.Groups[2].Value.Trim();
}
if (!_categorizedVoices.ContainsKey(category))
_categorizedVoices[category] = new List<TTSVoicePrototype>();
_categorizedVoices[category].Add(voice);
}
CategoriesContainer.RemoveAllChildren();
foreach (var category in _categorizedVoices.Keys.OrderBy(k => k))
{
var button = new Button
{
Text = category,
ToolTip = Loc.GetString("humanoid-profile-editor-voice-category-tooltip", ("category", category)),
HorizontalExpand = true,
};
button.OnPressed += _ =>
{
SearchEdit.Text = category;
UpdateResults();
};
CategoriesContainer.AddChild(button);
}
UpdateResults();
}
private void OnSearchChanged(LineEdit.LineEditEventArgs args)
{
UpdateResults();
}
private void UpdateResults()
{
VoicesGrid.RemoveAllChildren();
_filteredVoices.Clear();
var searchText = SearchEdit.Text.ToLowerInvariant();
foreach (var voice in _allVoices)
{
var name = Loc.GetString(voice.Name).ToLowerInvariant();
if (string.IsNullOrEmpty(searchText) ||
name.Contains(searchText) ||
voice.ID.ToLowerInvariant().Contains(searchText))
{
_filteredVoices.Add(voice);
}
}
foreach (var voice in _filteredVoices)
{
var displayName = Loc.GetString(voice.Name);
var canSelectVoice = CanUseVoice(voice);
var voiceContainer = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center
};
var selectButton = new Button
{
Text = displayName,
ToolTip = canSelectVoice ? voice.ID : Loc.GetString("humanoid-profile-editor-voice-tooltip-sponsoronly"),
HorizontalExpand = true,
Disabled = !canSelectVoice,
StyleClasses = { StyleNano.ButtonOpenRight }
};
if (voice.ID == _selectedVoiceId)
{
selectButton.AddStyleClass(StyleBase.ButtonCaution);
}
selectButton.OnPressed += _ =>
{
if (canSelectVoice)
{
OnVoiceSelected?.Invoke(voice.ID);
}
};
var previewButton = new Button
{
Text = Loc.GetString("humanoid-profile-editor-voice-play"),
MinWidth = 30,
ToolTip = Loc.GetString("humanoid-profile-editor-voice-tooltip-play"),
StyleClasses = { StyleNano.ButtonOpenLeft }
};
previewButton.OnPressed += _ =>
{
OnPreviewRequested?.Invoke(voice.ID);
};
voiceContainer.AddChild(selectButton);
voiceContainer.AddChild(previewButton);
VoicesGrid.AddChild(voiceContainer);
}
ResultsLabel.Text = Loc.GetString("humanoid-profile-editor-voice-match",
("filtered", _filteredVoices.Count), ("all", _allVoices.Count));
}
private bool CanUseVoice(TTSVoicePrototype voice)
{
if (!voice.SponsorOnly)
return true;
var sponsorsManager = IoCManager.Resolve<ISharedSponsorsManager>();
return sponsorsManager?.GetClientPrototypes().Contains(voice.ID) == true;
}
public void UpdateControls(HumanoidCharacterProfile? profile, Sex sex)
{
if (profile == null)
return;
_selectedVoiceId = profile.Voice;
_allVoices = _prototypeManager
.EnumeratePrototypes<TTSVoicePrototype>()
.Where(o => o.RoundStart && HumanoidCharacterProfile.CanHaveVoice(o, sex))
.OrderBy(o => Loc.GetString(o.Name))
.ToList();
_categorizedVoices.Clear();
LoadVoices();
}
public void SetSelectedVoice(string voiceId)
{
_selectedVoiceId = voiceId;
UpdateResults();
}
}

View file

@ -91,14 +91,6 @@
<Control HorizontalExpand="True"/>
<OptionButton Name="SpawnPriorityButton" HorizontalAlignment="Right" />
</BoxContainer>
<!-- Corvax-TTS-Start -->
<BoxContainer HorizontalExpand="True" Visible="False" Name="TTSContainer">
<Label Text="{Loc 'humanoid-profile-editor-voice-label'}" />
<Control HorizontalExpand="True"/>
<OptionButton Name="VoiceButton" HorizontalAlignment="Right" />
<Button Name="VoicePlayButton" Text="{Loc 'humanoid-profile-editor-voice-play'}" MaxWidth="80" />
</BoxContainer>
<!-- Corvax-TTS-End -->
</BoxContainer>
<!-- Skin -->
<BoxContainer Margin="10" HorizontalExpand="True" Orientation="Vertical">

View file

@ -36,6 +36,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Direction = Robust.Shared.Maths.Direction;
using static Content.Client.Corvax.SponsorOnlyHelpers; // Corvax-Sponsors
using Content.Client.Corvax.TTS; // Corvax-TTS
namespace Content.Client.Lobby.UI
{
@ -65,6 +66,8 @@ namespace Content.Client.Lobby.UI
// One at a time.
private LoadoutWindow? _loadoutWindow;
private TTSTab? _ttsTab; // Corvax-TTS
private bool _exporting;
private bool _imaging;
@ -223,18 +226,6 @@ namespace Content.Client.Lobby.UI
#endregion Gender
// Corvax-TTS-Start
#region Voice
if (configurationManager.GetCVar(CCCVars.TTSEnabled))
{
TTSContainer.Visible = true;
InitializeVoice();
}
#endregion
// Corvax-TTS-End
RefreshSpecies();
SpeciesButton.OnItemSelected += args =>
@ -441,6 +432,8 @@ namespace Content.Client.Lobby.UI
RefreshFlavorText();
RefreshVoiceTab(); // Corvax-TTS
#region Dummy
SpriteRotateLeft.OnPressed += _ =>
@ -500,6 +493,56 @@ namespace Content.Client.Lobby.UI
}
}
// Corvax-TTS-Start
#region Voice
private void RefreshVoiceTab()
{
if (!_cfgManager.GetCVar(CCCVars.TTSEnabled))
return;
_ttsTab = new TTSTab();
var children = new List<Control>();
foreach (var child in TabContainer.Children)
children.Add(child);
TabContainer.RemoveAllChildren();
for (int i = 0; i < children.Count; i++)
{
if (i == 1) // Set the tab to the 2nd place.
{
TabContainer.AddChild(_ttsTab);
}
TabContainer.AddChild(children[i]);
}
TabContainer.SetTabTitle(1, Loc.GetString("humanoid-profile-editor-voice-tab"));
_ttsTab.OnVoiceSelected += voiceId =>
{
SetVoice(voiceId);
_ttsTab.SetSelectedVoice(voiceId);
};
_ttsTab.OnPreviewRequested += voiceId =>
{
_entManager.System<TTSSystem>().RequestPreviewTTS(voiceId);
};
}
private void UpdateTTSVoicesControls()
{
if (Profile is null || _ttsTab is null)
return;
_ttsTab.UpdateControls(Profile, Profile.Sex);
_ttsTab.SetSelectedVoice(Profile.Voice);
}
#endregion
// Corvax-TTS-End
/// <summary>
/// Refreshes traits selector
/// </summary>
@ -591,7 +634,7 @@ namespace Content.Client.Lobby.UI
{
TraitsList.AddChild(new Label
{
Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount) ,("max", category.MaxTraitPoints)),
Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount), ("max", category.MaxTraitPoints)),
FontColorOverride = Color.Gray
});
}

View file

@ -1,5 +1,12 @@
ui-options-tts-volume = Громкость TTS:
credits-window-tts-title = Функция TTS (Text-To-Speech)
humanoid-profile-editor-voice-label = Голос:
humanoid-profile-editor-voice-tab = Голос
humanoid-profile-editor-voice-placeholder = Поиск по голосам...
humanoid-profile-editor-voice-categories = Категории
humanoid-profile-editor-voice-other = Прочие
humanoid-profile-editor-voice-play = ▶
humanoid-profile-editor-voice-tooltip-play = Прослушать
humanoid-profile-editor-voice-tooltip-sponsoronly = Доступно только спонсорам
humanoid-profile-editor-voice-category-tooltip = Показать голоса из {$category}
humanoid-profile-editor-voice-match = Найдено: {$filtered}/{$all}
tts-rate-limited = Вы генерируете TTS слишком быстро!

View file

@ -221,7 +221,7 @@ tts-voice-name-barbas = Барбас (Skyrim)
tts-voice-name-hermaeus = Хермеус (Skyrim)
tts-voice-name-hakon = Хакон (Skyrim)
tts-voice-name-rita = Рита (Рита)
tts-voice-name-barman = Бармен (н\д)
tts-voice-name-barman = Бармен (Прочие)
tts-voice-name-bridger2 = Мостовой 2 (Metro)
tts-voice-name-bridger3 = Мостовой 3 (Metro)
tts-voice-name-cannibal3 = Людоед 3 (Metro)
@ -562,7 +562,7 @@ tts-voice-name-vesemir = Весемир (Ведьмак)
tts-voice-name-kate = Катя (Зайчик)
tts-voice-name-polina = Полина (Зайчик)
tts-voice-name-crach = Крах (Ведьмак)
tts-voice-name-gryphon = Грифон (WarCraft 3)
tts-voice-name-gryphon = Грифон (Warcraft 3)
tts-voice-name-zeus = Zeus (Dota 2)
tts-voice-name-iz = Из (Проклятые земли)
tts-voice-name-geralt = Геральт (Ведьмак)
@ -721,7 +721,7 @@ tts-voice-name-stalker_metro_saharov = Сахаров (Metro)
tts-voice-name-stalker_metro_soldier = Солдат (Metro)
tts-voice-name-stalker_metro_stalker01 = Сталкер01 (Metro)
tts-voice-name-stalker_metro_newbie01 = Новичок01 (Metro)
tts-voice-name-warcraft_garrosh = Гаррош (WarCraft 3)
tts-voice-name-warcraft_garrosh = Гаррош (Warcraft 3)
tts-voice-name-male_v = Мужской V
tts-voice-name-johny = Джонни
tts-voice-name-takemura = Такемура
@ -742,10 +742,10 @@ tts-voice-name-joel = Джоэл
tts-voice-name-tess = Тесс
tts-voice-name-mission_control = Миссия Контроль
tts-voice-name-dwarves = Гномы
tts-voice-name-sunboy_inner = Санбой (внутренний)
tts-voice-name-sunboy_sad = Санбой (грустный)
tts-voice-name-sunboy_mannered = Санбой (вежливый)
tts-voice-name-sunboy_kalm = Санбой (спокойный)
tts-voice-name-sunboy_inner = Санбой (внутренний) (Мемы)
tts-voice-name-sunboy_sad = Санбой (грустный) (Мемы)
tts-voice-name-sunboy_mannered = Санбой (вежливый) (Мемы)
tts-voice-name-sunboy_kalm = Санбой (спокойный) (Мемы)
tts-voice-name-last_years_snow_narrator = Рассказчик прошлогоднего снега
tts-voice-name-last_years_snow_man = Мужчина прошлогоднего снега
tts-voice-name-titus = Титус
@ -811,7 +811,7 @@ tts-voice-name-azmodan = Азмодан (Diablo)
tts-voice-name-wizard = Волшебница (Прочие)
tts-voice-name-guldan = Гул'дан (Warcraft)
tts-voice-name-malganis = Мал'Ганис (Warcraft)
tts-voice-name-zagara = Zagara (Starcraft)
tts-voice-name-zagara = Zagara (StarCraft)
tts-voice-name-cho = Чо (Warcraft)
tts-voice-name-meiow = Мяу (Прочие)
tts-voice-name-stitches = Мясник (Warcraft)