fix: ТТС снова выдает правильные голоса

This commit is contained in:
Remuchi
2024-04-17 11:36:02 +07:00
parent fbb957d6d5
commit 44b9c3d723
16 changed files with 147 additions and 144 deletions

View File

@@ -972,7 +972,8 @@ public sealed partial class ChangelingSystem
ClonePerson(polymorphEntity.Value, transformData.AppearanceComponent, polyAppearance);
TransferDna(polymorphEntity.Value, transformData.Dna);
_humanoidAppearance.SetTTSVoice(polymorphEntity.Value, transformData.AppearanceComponent.Voice, polyAppearance);
_humanoidAppearance.SetTTSVoice(polymorphEntity.Value, transformData.AppearanceComponent.Voice,
humanoid: polyAppearance);
if (!TryComp<MetaDataComponent>(polymorphEntity.Value, out var meta))
return null;

View File

@@ -40,15 +40,15 @@ public sealed partial class HumanoidAppearanceSystem : SharedHumanoidAppearanceS
return;
}
targetHumanoid.Species = sourceHumanoid.Species;
targetHumanoid.SkinColor = sourceHumanoid.SkinColor;
SetSpecies(target, sourceHumanoid.Species, false, targetHumanoid);
SetSkinColor(target, sourceHumanoid.SkinColor, false, true, targetHumanoid);
targetHumanoid.EyeColor = sourceHumanoid.EyeColor;
targetHumanoid.Age = sourceHumanoid.Age;
SetSex(target, sourceHumanoid.Sex, false, targetHumanoid);
targetHumanoid.CustomBaseLayers = new(sourceHumanoid.CustomBaseLayers);
targetHumanoid.MarkingSet = new(sourceHumanoid.MarkingSet);
targetHumanoid.BodyType = sourceHumanoid.BodyType;
SetTTSVoice(target, sourceHumanoid.Voice, targetHumanoid);
SetSex(target, sourceHumanoid.Sex, false, targetHumanoid);
SetBodyType(target, sourceHumanoid.BodyType, false, targetHumanoid);
SetTTSVoice(target, sourceHumanoid.Voice, false, targetHumanoid);
targetHumanoid.Gender = sourceHumanoid.Gender;
if (TryComp<GrammarComponent>(target, out var grammar))

View File

@@ -1,17 +0,0 @@
using Content.Server._White.TTS;
using Content.Shared.Humanoid;
namespace Content.Server.Humanoid;
public sealed partial class HumanoidAppearanceSystem
{
// ReSharper disable once InconsistentNaming
public void SetTTSVoice(EntityUid uid, string voiceId, HumanoidAppearanceComponent humanoid)
{
if (!TryComp<TTSComponent>(uid, out var comp))
return;
humanoid.Voice = voiceId;
comp.VoicePrototypeId = voiceId;
}
}

View File

@@ -1,17 +1,11 @@
namespace Content.Server._White.TTS;
public sealed class TTSAnnouncementEvent : EntityEventArgs
// ReSharper disable once InconsistentNaming
public sealed class TTSAnnouncementEvent(string message, string voiceId, EntityUid source, bool global)
: EntityEventArgs
{
public readonly string Message;
public readonly bool Global;
public readonly string VoiceId;
public readonly EntityUid Source;
public TTSAnnouncementEvent(string message, string voiceId, EntityUid source , bool global)
{
Message = message;
Global = global;
VoiceId = voiceId;
Source = source;
}
}
public readonly string Message = message;
public readonly bool Global = global;
public readonly string VoiceId = voiceId;
public readonly EntityUid Source = source;
}

View File

@@ -1,19 +0,0 @@
using Content.Shared._White.TTS;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server._White.TTS;
/// <summary>
/// Apply TTS for entity chat say messages
/// </summary>
[RegisterComponent]
// ReSharper disable once InconsistentNaming
public sealed partial class TTSComponent : Component
{
/// <summary>
/// Prototype of used voice for TTS.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("voice", customTypeSerializer:typeof(PrototypeIdSerializer<TTSVoicePrototype>))]
public string VoicePrototypeId { get; set; } = "Eugene";
}

View File

@@ -5,7 +5,6 @@ using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Content.Shared.CCVar;
using Content.Shared._White;
using Prometheus;
using Robust.Shared.Configuration;
@@ -18,9 +17,9 @@ public sealed class TTSManager
private static readonly Histogram RequestTimings = Metrics.CreateHistogram(
"tts_req_timings",
"Timings of TTS API requests",
new HistogramConfiguration()
new HistogramConfiguration
{
LabelNames = new[] {"type"},
LabelNames = new[] { "type" },
Buckets = Histogram.ExponentialBuckets(.1, 1.5, 10),
});
@@ -55,7 +54,12 @@ public sealed class TTSManager
/// <param name="text">SSML formatted text</param>
/// <returns>OGG audio bytes</returns>
/// <exception cref="Exception">Throws if url or token CCVar not set or http request failed</exception>
public async Task<byte[]?> ConvertTextToSpeech(string speaker, string text, string pitch, string rate, string? effect = null)
public async Task<byte[]?> ConvertTextToSpeech(
string speaker,
string text,
string pitch,
string rate,
string? effect = null)
{
var url = _cfg.GetCVar(WhiteCVars.TtsApiUrl);
var maxCacheSize = _cfg.GetCVar(WhiteCVars.TtsMaxCacheSize);
@@ -96,7 +100,7 @@ public sealed class TTSManager
var soundData = await response.Content.ReadAsByteArrayAsync(cts.Token);
if(_cache.Count > maxCacheSize)
if (_cache.Count > maxCacheSize)
{
_cache.Remove(_cache.Last().Key);
}
@@ -104,7 +108,9 @@ public sealed class TTSManager
_cache.Add(cacheKey, soundData);
CachedCount.Inc();
_sawmill.Debug($"Generated new sound for '{text}' speech by '{speaker}' speaker ({soundData.Length} bytes)");
_sawmill.Debug(
$"Generated new sound for '{text}' speech by '{speaker}' speaker ({soundData.Length} bytes)");
RequestTimings.WithLabels("Success").Observe((DateTime.UtcNow - reqTime).TotalSeconds);
return soundData;
@@ -149,7 +155,7 @@ public sealed class TTSManager
private string GenerateCacheKey(string speaker, string text)
{
var key = $"{speaker}/{text}";
byte[] keyData = Encoding.UTF8.GetBytes(key);
var keyData = Encoding.UTF8.GetBytes(key);
var sha256 = System.Security.Cryptography.SHA256.Create();
var bytes = sha256.ComputeHash(keyData);
return Convert.ToHexString(bytes);
@@ -187,4 +193,4 @@ public sealed class TTSManager
[JsonPropertyName("audio")]
public string Audio { get; set; }
}
}
}

View File

@@ -8,7 +8,6 @@ using Content.Server.Station.Systems;
using Content.Shared._White.TTS;
using Content.Shared.GameTicking;
using Content.Shared._White;
using Robust.Server.Audio;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
@@ -31,7 +30,6 @@ public sealed partial class TTSSystem : EntitySystem
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly TTSPitchRateSystem _ttsPitchRateSystem = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
private const int MaxMessageChars = 100 * 2; // same as SingleBubbleCharLimit * 2
@@ -44,7 +42,7 @@ public sealed partial class TTSSystem : EntitySystem
_cfg.OnValueChanged(WhiteCVars.TtsApiUrl, url => _apiUrl = url, true);
SubscribeLocalEvent<TransformSpeechEvent>(OnTransformSpeech);
SubscribeLocalEvent<TTSComponent, EntitySpokeEvent>(OnEntitySpoke);
SubscribeLocalEvent<SharedTTSComponent, EntitySpokeEvent>(OnEntitySpoke);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
SubscribeLocalEvent<TTSAnnouncementEvent>(OnAnnounceRequest);
@@ -80,37 +78,37 @@ public sealed partial class TTSSystem : EntitySystem
foreach (var player in filter.Recipients)
{
if (player.AttachedEntity != null)
if (player.AttachedEntity == null)
continue;
// Get emergency lights in range to broadcast from
var entities = _lookup.GetEntitiesInRange(player.AttachedEntity.Value, 30f)
.Where(HasComp<EmergencyLightComponent>)
.ToList();
if (entities.Count == 0)
return;
// Get closest emergency light
var entity = entities.First();
var range = new Vector2(100f);
foreach (var item in entities)
{
// Get emergency lights in range to broadcast from
var entities = _lookup.GetEntitiesInRange(player.AttachedEntity.Value, 30f)
.Where(HasComp<EmergencyLightComponent>)
.ToList();
var itemSource = _xforms.GetWorldPosition(Transform(item));
var playerSource = _xforms.GetWorldPosition(Transform(player.AttachedEntity.Value));
if (entities.Count == 0)
return;
var distance = playerSource - itemSource;
// Get closest emergency light
var entity = entities.First();
var range = new Vector2(100f);
foreach (var item in entities)
if (range.Length() > distance.Length())
{
var itemSource = _xforms.GetWorldPosition(Transform(item));
var playerSource = _xforms.GetWorldPosition(Transform(player.AttachedEntity.Value));
var distance = playerSource - itemSource;
if (range.Length() > distance.Length())
{
range = distance;
entity = item;
}
range = distance;
entity = item;
}
RaiseNetworkEvent(new PlayTTSEvent(GetNetEntity(entity), soundData, true), Filter.SinglePlayer(player),
false);
}
RaiseNetworkEvent(new PlayTTSEvent(GetNetEntity(entity), soundData, true), Filter.SinglePlayer(player),
false);
}
}
@@ -121,7 +119,7 @@ public sealed partial class TTSSystem : EntitySystem
return;
if (!_playerManager.TryGetSessionByChannel(ev.MsgChannel, out var session) ||
!_prototypeManager.TryIndex<TTSVoicePrototype>(ev.VoiceId, out var protoVoice))
!_prototypeManager.TryIndex(ev.VoiceId, out var protoVoice))
return;
var soundData = await GenerateTTS(ev.Uid, ev.Text, protoVoice.Speaker);
@@ -129,7 +127,7 @@ public sealed partial class TTSSystem : EntitySystem
RaiseNetworkEvent(new PlayTTSEvent(GetNetEntity(ev.Uid), soundData, false), Filter.SinglePlayer(session), false);
}
private async void OnEntitySpoke(EntityUid uid, TTSComponent component, EntitySpokeEvent args)
private async void OnEntitySpoke(EntityUid uid, SharedTTSComponent component, EntitySpokeEvent args)
{
if (!_isEnabled ||
args.Message.Length > MaxMessageChars)
@@ -145,7 +143,7 @@ public sealed partial class TTSSystem : EntitySystem
RaiseLocalEvent(uid, voiceEv);
voiceId = voiceEv.VoiceId;
if (!_prototypeManager.TryIndex<TTSVoicePrototype>(voiceId, out var protoVoice))
if (!_prototypeManager.TryIndex(voiceId, out var protoVoice))
return;
var message = FormattedMessage.RemoveMarkup(args.Message);

View File

@@ -5,7 +5,6 @@ using Robust.Shared.Enums;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Utility;
namespace Content.Shared.Humanoid;
@@ -73,14 +72,17 @@ public sealed partial class HumanoidAppearanceComponent : Component
/// <summary>
/// Current body type.
/// </summary>
[DataField("bodyType"), AutoNetworkedField]
[DataField, AutoNetworkedField]
public ProtoId<BodyTypePrototype> BodyType = SharedHumanoidAppearanceSystem.DefaultBodyType;
[DataField, AutoNetworkedField]
public Color EyeColor = Color.Brown;
[DataField("voice", customTypeSerializer: typeof(PrototypeIdSerializer<TTSVoicePrototype>)), AutoNetworkedField]
public string Voice { get; set; } = SharedHumanoidAppearanceSystem.DefaultVoice;
/// <summary>
/// Current TTS voice.
/// </summary>
[DataField, AutoNetworkedField]
public ProtoId<TTSVoicePrototype> Voice { get; set; } = SharedHumanoidAppearanceSystem.DefaultVoice;
/// <summary>
/// Hair color of this humanoid. Used to avoid looping through all markings

View File

@@ -1,4 +1,5 @@
using System.Linq;
using Content.Shared._White.TTS;
using Content.Shared.Examine;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
@@ -28,10 +29,13 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
[ValidatePrototypeId<SpeciesPrototype>]
public const string DefaultSpecies = "Human";
[ValidatePrototypeId<BodyTypePrototype>]
public const string DefaultBodyType = "HumanNormal";
[ValidatePrototypeId<TTSVoicePrototype>]
public const string DefaultVoice = "Eugene";
public static readonly Dictionary<Sex, string> DefaultSexVoice = new()
public static readonly Dictionary<Sex, ProtoId<TTSVoicePrototype>> DefaultSexVoice = new()
{
{ Sex.Male, "Eugene" },
{ Sex.Female, "Kseniya" },
@@ -75,7 +79,8 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
var species = GetSpeciesRepresentation(component.Species).ToLower();
var age = GetAgeRepresentation(component.Species, component.Age);
args.PushText(Loc.GetString("humanoid-appearance-component-examine", ("user", identity), ("age", age), ("species", species)));
args.PushText(Loc.GetString("humanoid-appearance-component-examine", ("user", identity), ("age", age),
("species", species)));
}
/// <summary>
@@ -244,6 +249,36 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
}
}
/// <summary>
/// Set a humanoid mob's body yupe. This will change their base sprites.
/// </summary>
/// <param name="uid">The humanoid mob's UID.</param>
/// <param name="voiceId">The tts voice to set the mob to.</param>
/// <param name="sync">Whether to immediately synchronize this to the humanoid mob, or not.</param>
/// <param name="humanoid">Humanoid component of the entity</param>
// ReSharper disable once InconsistentNaming
public void SetTTSVoice(
EntityUid uid,
ProtoId<TTSVoicePrototype> voiceId,
bool sync = true,
HumanoidAppearanceComponent? humanoid = null)
{
if (!TryComp<SharedTTSComponent>(uid, out var comp))
return;
if (!Resolve(uid, ref humanoid))
{
return;
}
humanoid.Voice = voiceId;
comp.VoicePrototypeId = voiceId;
if (sync)
{
Dirty(uid, humanoid);
}
}
/// <summary>
/// Sets the base layer ID of this humanoid mob. A humanoid mob's 'base layer' is
/// the skin sprite that is applied to the mob's sprite upon appearance refresh.
@@ -353,7 +388,8 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
humanoid.EyeColor = profile.Appearance.EyeColor;
SetSkinColor(uid, profile.Appearance.SkinColor, false);
SetBodyType(uid, profile.BodyType, false);
SetBodyType(uid, profile.BodyType, false, humanoid);
SetTTSVoice(uid, profile.Voice, false, humanoid);
humanoid.MarkingSet.Clear();
@@ -545,4 +581,4 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
return Loc.GetString("identity-age-old");
}
}
}

View File

@@ -1,5 +1,6 @@
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared._White.TTS;
@@ -10,8 +11,8 @@ public sealed class MsgRequestTTS : NetMessage
public override MsgGroups MsgGroup => MsgGroups.Command;
public EntityUid Uid { get; set; } = EntityUid.Invalid;
public string Text { get; set; } = String.Empty;
public string VoiceId { get; set; } = String.Empty;
public string Text { get; set; } = string.Empty;
public ProtoId<TTSVoicePrototype> VoiceId { get; set; } = string.Empty;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{

View File

@@ -4,16 +4,11 @@ namespace Content.Shared._White.TTS;
[Serializable, NetSerializable]
// ReSharper disable once InconsistentNaming
public sealed class PlayTTSEvent : EntityEventArgs
public sealed class PlayTTSEvent(NetEntity uid, byte[] data, bool boostVolume) : EntityEventArgs
{
public NetEntity Uid { get; }
public byte[] Data { get; }
public bool BoostVolume { get; }
public NetEntity Uid { get; } = uid;
public PlayTTSEvent(NetEntity uid, byte[] data, bool boostVolume)
{
Uid = uid;
Data = data;
BoostVolume = boostVolume;
}
}
public byte[] Data { get; } = data;
public bool BoostVolume { get; } = boostVolume;
}

View File

@@ -4,12 +4,7 @@ namespace Content.Shared._White.TTS;
[Serializable, NetSerializable]
// ReSharper disable once InconsistentNaming
public sealed class RequestTTSEvent : EntityEventArgs
public sealed class RequestTTSEvent(string text) : EntityEventArgs
{
public string Text { get; }
public RequestTTSEvent(string text)
{
Text = text;
}
}
public string Text { get; } = text;
}

View File

@@ -0,0 +1,17 @@
using Robust.Shared.Prototypes;
namespace Content.Shared._White.TTS;
/// <summary>
/// Apply TTS for entity chat say messages
/// </summary>
[RegisterComponent, AutoGenerateComponentState]
// ReSharper disable once InconsistentNaming
public sealed partial class SharedTTSComponent : Component
{
/// <summary>
/// Prototype of used voice for TTS.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
public ProtoId<TTSVoicePrototype> VoicePrototypeId { get; set; } = "Eugene";
}

View File

@@ -3,12 +3,7 @@
namespace Content.Shared.VoiceMask;
[Serializable, NetSerializable]
public sealed class VoiceMaskChangeVoiceMessage : BoundUserInterfaceMessage
public sealed class VoiceMaskChangeVoiceMessage(string voice) : BoundUserInterfaceMessage
{
public string Voice { get; }
public VoiceMaskChangeVoiceMessage(string voice)
{
Voice = voice;
}
}
public string Voice { get; } = voice;
}

View File

@@ -1,8 +1,8 @@
using Content.Shared.Humanoid;
using Content.Shared.Preferences;
namespace Content.Shared._White.TTS;
// ReSharper disable once InconsistentNaming
public sealed class TTSPitchRateSystem : EntitySystem
{

View File

@@ -10,28 +10,27 @@ namespace Content.Shared._White.TTS;
// ReSharper disable once InconsistentNaming
public sealed class TTSVoicePrototype : IPrototype
{
[IdDataFieldAttribute]
[IdDataField]
public string ID { get; } = default!;
[DataField("name")]
[DataField]
public string Name { get; } = string.Empty;
[DataField("sex", required: true)]
public Sex Sex { get; } = default!;
[DataField(required: true)]
public Sex Sex { get; }
[ViewVariables(VVAccess.ReadWrite)]
[DataField("speaker", required: true)]
[ViewVariables(VVAccess.ReadWrite), DataField(required: true)]
public string Speaker { get; } = string.Empty;
/// <summary>
/// Whether the species is available "at round start" (In the character editor)
/// </summary>
[DataField("roundStart")]
[DataField]
public bool RoundStart { get; } = true;
[DataField("sponsorOnly")]
public bool SponsorOnly { get; } = false;
[DataField]
public bool SponsorOnly { get; }
[DataField("borgVoice")]
public bool BorgVoice { get; } = false;
[DataField]
public bool BorgVoice { get; }
}