using System.Runtime.InteropServices;
using DotNetTtsWrapper.Events;
using DotNetTtsWrapper.Models;
using NativeVoice = RustTtsWrapper.TtsVoice;
namespace RustTtsWrapper;
public class RustTtsClient : AbstractTtsClient
{
private readonly TtsClient _inner;
public string EngineId { get; }
public string CredentialsJson { get; }
public RustTtsClient(string engineId, Dictionary<string, string>? credentials = null)
{
EngineId = engineId;
CredentialsJson = System.Text.Json.JsonSerializer.Serialize(credentials ?? new());
_inner = new TtsClient(engineId, credentials);
bool supportsSsml = engineId is "azure" or "google" or "sapi" or "avsynth"
or "watson" or "polly" or "elevenlabs" or "openai";
Capabilities = new EngineCapabilities
{
SupportsStreaming = true,
SupportsWordTimings = engineId is "azure" or "google" or "sapi" or "elevenlabs",
SupportsSsml = supportsSsml,
SupportsSpeechMarkdown = supportsSsml,
RequiresInternet = engineId is not "sherpaonnx" and not "sapi" and not "avsynth" and not "system",
IsWindowsSupported = true,
IsLinuxSupported = engineId is not "sapi" and not "avsynth",
IsMacOsSupported = engineId is not "sapi",
};
}
public override Task<List<DotNetTtsWrapper.Models.TtsVoice>> GetVoicesAsync()
{
var voices = _inner.GetVoices().Select(MapVoice).ToList();
return Task.FromResult(voices);
}
public override Task<List<DotNetTtsWrapper.Models.TtsVoice>> GetVoicesByLanguageAsync(string languageCode)
{
var all = _inner.GetVoices();
var filtered = all
.Where(v => v.Language.StartsWith(languageCode, StringComparison.OrdinalIgnoreCase))
.Select(MapVoice)
.ToList();
return Task.FromResult(filtered);
}
public override void SetVoice(string voiceId)
{
base.SetVoice(voiceId);
_inner.SetVoice(voiceId);
}
public override async Task<TtsSynthesisResult> SynthToBytesAsync(string text, TtsOptions? options = null)
{
var prepared = await PrepareTextAsync(text, options);
ApplyOptions(options);
var audio = _inner.SynthToBytes(prepared);
return new TtsSynthesisResult
{
AudioData = audio,
Format = AudioFormat.Wav,
SampleRate = SampleRate,
Channels = 1,
};
}
public override async Task<StreamingTtsResult> SynthToStreamAsync(string text, TtsOptions? options = null)
{
var bytes = await SynthToBytesAsync(text, options);
var result = new StreamingTtsResult
{
FinalAudioData = bytes.AudioData,
Format = AudioFormat.Wav,
SampleRate = SampleRate,
Channels = 1,
};
return result;
}
public override async Task SynthToFileAsync(string text, string outputPath, AudioFormat format = AudioFormat.Wav, TtsOptions? options = null)
{
var synth = await SynthToBytesAsync(text, options);
await System.IO.File.WriteAllBytesAsync(outputPath, synth.AudioData);
}
public override async Task SpeakAsync(string text, TtsOptions? options = null)
{
var prepared = await PrepareTextAsync(text, options);
ApplyOptions(options);
await Task.Run(() => _inner.SpeakSync(prepared));
}
public override async Task SpeakStreamedAsync(string text, Action<WordTimingEventArgs>? wordCallback = null, TtsOptions? options = null)
{
if (wordCallback != null)
{
_inner.SetOnBoundary((word, _offset, _len, start, end, _estimated) =>
wordCallback(new WordTimingEventArgs(word, start, end)));
}
try
{
await SpeakAsync(text, options);
}
finally
{
if (wordCallback != null) _inner.SetOnBoundary(null);
}
}
public override void Pause() => _inner.Pause();
public override void Resume() => _inner.Resume();
public override void Stop() => _inner.Stop();
public override async Task<CredentialsValidationResult> CheckCredentialsAsync()
{
try
{
var voices = await GetVoicesAsync();
return new CredentialsValidationResult
{
IsValid = true,
AvailableVoiceCount = voices.Count,
EngineName = EngineId,
};
}
catch (Exception ex)
{
return new CredentialsValidationResult
{
IsValid = false,
ErrorMessage = ex.Message,
EngineName = EngineId,
};
}
}
public override string GetSpeechMarkdownPlatform()
{
return EngineId switch
{
"azure" => global::SpeechMarkdown.Platform.MicrosoftAzure,
"google" => global::SpeechMarkdown.Platform.GoogleAssistant,
_ => global::SpeechMarkdown.Platform.AmazonAlexa,
};
}
protected override void Dispose(bool disposing)
{
if (disposing) _inner.Dispose();
base.Dispose(disposing);
}
private void ApplyOptions(TtsOptions? options)
{
if (options == null) return;
if (options.VoiceId != null) _inner.SetVoice(options.VoiceId);
if (options.Rate.HasValue) _inner.SetRate(RateToFloat(options.Rate.Value));
if (options.Pitch.HasValue) _inner.SetPitch(PitchToFloat(options.Pitch.Value));
if (options.Volume.HasValue) _inner.SetVolume(VolumeToFloat(options.Volume.Value));
}
private static float RateToFloat(SpeechRate rate) => rate switch
{
SpeechRate.XSlow => 0.5f,
SpeechRate.Slow => 0.75f,
SpeechRate.Medium => 1.0f,
SpeechRate.Fast => 1.5f,
SpeechRate.XFast => 2.0f,
_ => 1.0f,
};
private static float PitchToFloat(SpeechPitch pitch) => pitch switch
{
SpeechPitch.XLow => 0.5f,
SpeechPitch.Low => 0.85f,
SpeechPitch.Medium => 1.0f,
SpeechPitch.High => 1.4f,
SpeechPitch.XHigh => 2.0f,
_ => 1.0f,
};
private static float VolumeToFloat(int volume) => Math.Clamp(volume, 0, 100) / 50.0f;
private static DotNetTtsWrapper.Models.TtsVoice MapVoice(NativeVoice native)
{
var lang = native.Language;
return new DotNetTtsWrapper.Models.TtsVoice
{
Id = native.Id,
Name = native.Name,
Provider = native.Engine,
Gender = ParseGender(native.Gender),
LanguageCodes = new()
{
new()
{
Bcp47 = lang,
Iso639_3 = lang.Length >= 2 ? lang.Split('-')[0] : lang,
Display = lang,
},
},
};
}
private static VoiceGender ParseGender(string gender)
{
if (string.IsNullOrEmpty(gender)) return VoiceGender.Unknown;
return gender.ToLowerInvariant() switch
{
"male" => VoiceGender.Male,
"female" => VoiceGender.Female,
"nonbinary" or "non-binary" => VoiceGender.NonBinary,
_ => VoiceGender.Unknown,
};
}
}