Files
OldThink/Content.Server/_White/TTS/TTSManager.cs
ThereDrD0 f56e97b122 Порт атмоса с ЕЕ + пара фиксво (#491)
* fix a lot of spaces in shuttle call reason

* fix tts fatal without api url

* Physics Based Air Throws (#342)

I've made it so that when a room is explosively depressurized(or when a
body of high pressure air flows into one of lower pressure air), that
entities inside are launched by the air pressure with effects according
to their mass. An entity's mass is now used as an innate resistance to
forced movement by airflow, and more massive entities are both less
likely to be launched, and will launch less far than others. While
lighter entities are launched far more easily, and will shoot off into
space quite quickly! Spacing departments has never been so exciting!
This can be made extraordinarily fun if more objects are given the
ability to injure people when colliding with them at high speeds.

As a note, Humans are very unlikely to be sucked into space at a typical
force generated from a 101kpa room venting into 0kpa, unless they
happened to be standing right next to the opening to space when it was
created. The same cannot be said for "Lighter-Than-Human" species such
as Felinids and Harpies. I guess that's just the price to pay for being
cute. :)

On a plus side, because the math behind this is simplified even further
than it was before, this actually runs more efficiently than the
previous system.

Nothing, this is basically done. I've spent a good 6 hours straight
finely tuning the system until I was satisfied that it reflects
something close to reality.

**Before the Space Wind Rework:**

https://github.com/Simple-Station/Einstein-Engines/assets/16548818/0bf56c50-58e6-4aef-97f8-027fbe62331e

**With this Rework:**

https://github.com/Simple-Station/Einstein-Engines/assets/16548818/6be507a9-e9de-4bb8-9d46-8b7c83ed5f1d

🆑 VMSolidus
- add: Atmospheric "Throws" are now calculated using object mass, and
behave accordingly. Tiny objects will shoot out of rooms quite fast!

---------

Signed-off-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com>

* fixes

---------

Signed-off-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com>
2024-07-24 22:40:38 +03:00

198 lines
6.1 KiB
C#

using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Content.Shared._White;
using Prometheus;
using Robust.Shared.Configuration;
namespace Content.Server._White.TTS;
// ReSharper disable once InconsistentNaming
public sealed class TTSManager
{
private static readonly Histogram RequestTimings = Metrics.CreateHistogram(
"tts_req_timings",
"Timings of TTS API requests",
new HistogramConfiguration
{
LabelNames = new[] { "type" },
Buckets = Histogram.ExponentialBuckets(.1, 1.5, 10),
});
private static readonly Counter WantedCount = Metrics.CreateCounter(
"tts_wanted_count",
"Amount of wanted TTS audio.");
private static readonly Counter ReusedCount = Metrics.CreateCounter(
"tts_reused_count",
"Amount of reused TTS audio from cache.");
private static readonly Gauge CachedCount = Metrics.CreateGauge(
"tts_cached_count",
"Amount of cached TTS audio.");
[Dependency] private readonly IConfigurationManager _cfg = default!;
private readonly HttpClient _httpClient = new();
private ISawmill _sawmill = default!;
private readonly Dictionary<string, byte[]?> _cache = new();
public void Initialize()
{
_sawmill = Logger.GetSawmill("tts");
}
/// <summary>
/// Generates audio with passed text by API
/// </summary>
/// <param name="speaker">Identifier of speaker</param>
/// <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)
{
var url = _cfg.GetCVar(WhiteCVars.TtsApiUrl);
var maxCacheSize = _cfg.GetCVar(WhiteCVars.TtsMaxCacheSize);
if (string.IsNullOrWhiteSpace(url)) // zaebal padat
{
_sawmill.Log(LogLevel.Error, nameof(TTSManager), "TTS Api url not specified");
return null;
}
WantedCount.Inc();
var cacheKey = GenerateCacheKey(speaker, text);
if (_cache.TryGetValue(cacheKey, out var data))
{
ReusedCount.Inc();
_sawmill.Debug($"Use cached sound for '{text}' speech by '{speaker}' speaker");
return data;
}
var body = new GenerateVoiceRequest
{
Text = text,
Speaker = speaker,
Pitch = pitch,
Rate = rate,
Effect = effect
};
var request = CreateRequestLink(url, body);
var reqTime = DateTime.UtcNow;
try
{
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var response = await _httpClient.GetAsync(request, cts.Token);
if (!response.IsSuccessStatusCode)
{
throw new Exception($"TTS request returned bad status code: {response.StatusCode}");
}
var soundData = await response.Content.ReadAsByteArrayAsync(cts.Token);
if (_cache.Count > maxCacheSize)
{
_cache.Remove(_cache.Last().Key);
}
_cache.Add(cacheKey, soundData);
CachedCount.Inc();
_sawmill.Debug(
$"Generated new sound for '{text}' speech by '{speaker}' speaker ({soundData.Length} bytes)");
RequestTimings.WithLabels("Success").Observe((DateTime.UtcNow - reqTime).TotalSeconds);
return soundData;
}
catch (TaskCanceledException)
{
RequestTimings.WithLabels("Timeout").Observe((DateTime.UtcNow - reqTime).TotalSeconds);
_sawmill.Warning($"Timeout of request generation new sound for '{text}' speech by '{speaker}' speaker");
return null;
}
catch (Exception e)
{
RequestTimings.WithLabels("Error").Observe((DateTime.UtcNow - reqTime).TotalSeconds);
_sawmill.Warning($"Failed of request generation new sound for '{text}' speech by '{speaker}' speaker\n{e}");
return null;
}
}
private static string CreateRequestLink(string url, GenerateVoiceRequest body)
{
var uriBuilder = new UriBuilder(url);
var query = HttpUtility.ParseQueryString(uriBuilder.Query);
query["speaker"] = body.Speaker;
query["text"] = body.Text;
query["pitch"] = body.Pitch;
query["rate"] = body.Rate;
query["file"] = "1";
query["ext"] = "ogg";
if (body.Effect != null)
query["effect"] = body.Effect;
uriBuilder.Query = query.ToString();
return uriBuilder.ToString();
}
public void ResetCache()
{
_cache.Clear();
CachedCount.Set(0);
}
private string GenerateCacheKey(string speaker, string text)
{
var key = $"{speaker}/{text}";
var keyData = Encoding.UTF8.GetBytes(key);
var sha256 = System.Security.Cryptography.SHA256.Create();
var bytes = sha256.ComputeHash(keyData);
return Convert.ToHexString(bytes);
}
private record GenerateVoiceRequest
{
[JsonPropertyName("text")]
public string Text { get; set; } = default!;
[JsonPropertyName("speaker")]
public string Speaker { get; set; } = default!;
[JsonPropertyName("pitch")]
public string Pitch { get; set; } = default!;
[JsonPropertyName("rate")]
public string Rate { get; set; } = default!;
[JsonPropertyName("effect")]
public string? Effect { get; set; }
}
private struct GenerateVoiceResponse
{
[JsonPropertyName("results")]
public List<VoiceResult> Results { get; set; }
[JsonPropertyName("original_sha1")]
public string Hash { get; set; }
}
private struct VoiceResult
{
[JsonPropertyName("audio")]
public string Audio { get; set; }
}
}