282 lines
11 KiB
C#
282 lines
11 KiB
C#
using System.Runtime.InteropServices;
|
|
using System.Text.RegularExpressions;
|
|
using NAudio.Wave;
|
|
using TwitchLib.Api;
|
|
using TwitchLib.Client;
|
|
using TwitchLib.Client.Events;
|
|
using TwitchLib.Client.Models;
|
|
using TwitchLib.Communication.Clients;
|
|
using TwitchLib.Communication.Events;
|
|
using TwitchLib.PubSub;
|
|
using TwitchLib.PubSub.Events;
|
|
using NAudio.Wave.SampleProviders;
|
|
|
|
/**
|
|
Future handshake/connection procedure:
|
|
- GET all tts config data
|
|
- Continuous connection to server to receive commands from tom & send logs/errors (med priority, though tough task)
|
|
|
|
Ideas:
|
|
- Filter messages by badges, username, ..., etc.
|
|
- Filter messages by content.
|
|
- Speed up TTS based on message queue size?
|
|
- Cut TTS off shortly after raid (based on size of raid)?
|
|
- Limit duration of TTS
|
|
- Voice selection for channel and per user.
|
|
**/
|
|
|
|
// dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true
|
|
// dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true
|
|
// SE voices: https://api.streamelements.com/kappa/v2/speech?voice=brian&text=hello
|
|
|
|
// Read redeems from file.
|
|
var redeems = File.Exists(".redeems") ? await File.ReadAllLinesAsync(".redeems") : new string[0];
|
|
|
|
// Fetch id and username based on api key given.
|
|
HermesClient hermes = new HermesClient();
|
|
Console.WriteLine("Fetching Hermes account details...");
|
|
await hermes.UpdateHermesAccount();
|
|
|
|
Console.WriteLine("ID: " + hermes.Id);
|
|
Console.WriteLine("Username: " + hermes.Username);
|
|
Console.WriteLine();
|
|
|
|
Console.WriteLine("Fetching Twitch API details from Hermes...");
|
|
TwitchApiClient twitchapiclient = new TwitchApiClient(await hermes.FetchTwitchBotToken());
|
|
await twitchapiclient.Authorize();
|
|
|
|
var sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)");
|
|
var voiceRegex = new Regex(@"\b(Filiz|Astrid|Tatyana|Maxim|Carmen|Ines|Cristiano|Vitoria|Ricardo|Maja|Jan|Jacek|Ewa|Ruben|Lotte|Liv|Seoyeon|Takumi|Mizuki|Giorgio|Carla|Bianca|Karl|Dora|Mathieu|Celine|Chantal|Penelope|Miguel|Mia|Enrique|Conchita|Geraint|Salli|Matthew|Kimberly|Kendra|Justin|Joey|Joanna|Ivy|Raveena|Aditi|Emma|Brian|Amy|Russell|Nicole|Vicki|Marlene|Hans|Naja|Mads|Gwyneth|Zhiyu|Tracy|Danny|Huihui|Yaoyao|Kangkang|HanHan|Zhiwei|Asaf|An|Stefanos|Filip|Ivan|Heidi|Herena|Kalpana|Hemant|Matej|Andika|Rizwan|Lado|Valluvar|Linda|Heather|Sean|Michael|Karsten|Guillaume|Pattara|Jakub|Szabolcs|Hoda|Naayf)\:(.*?)(?=\Z|\b(?:Filiz|Astrid|Tatyana|Maxim|Carmen|Ines|Cristiano|Vitoria|Ricardo|Maja|Jan|Jacek|Ewa|Ruben|Lotte|Liv|Seoyeon|Takumi|Mizuki|Giorgio|Carla|Bianca|Karl|Dora|Mathieu|Celine|Chantal|Penelope|Miguel|Mia|Enrique|Conchita|Geraint|Salli|Matthew|Kimberly|Kendra|Justin|Joey|Joanna|Ivy|Raveena|Aditi|Emma|Brian|Amy|Russell|Nicole|Vicki|Marlene|Hans|Naja|Mads|Gwyneth|Zhiyu|Tracy|Danny|Huihui|Yaoyao|Kangkang|HanHan|Zhiwei|Asaf|An|Stefanos|Filip|Ivan|Heidi|Herena|Kalpana|Hemant|Matej|Andika|Rizwan|Lado|Valluvar|Linda|Heather|Sean|Michael|Karsten|Guillaume|Pattara|Jakub|Szabolcs|Hoda|Naayf)\:)", RegexOptions.IgnoreCase);
|
|
|
|
TTSPlayer player = new TTSPlayer();
|
|
ISampleProvider playing = null;
|
|
|
|
var channels = File.Exists(".twitchchannels") ? File.ReadAllLines(".twitchchannels") : new string[] { hermes.Username };
|
|
twitchapiclient.InitializeClient(hermes, channels);
|
|
twitchapiclient.InitializePublisher(player, redeems);
|
|
|
|
void HandleMessage(int priority, string voice, string message, OnMessageReceivedArgs e, bool bot) {
|
|
var m = e.ChatMessage;
|
|
var parts = sfxRegex.Split(message);
|
|
var sfxMatches = sfxRegex.Matches(message);
|
|
var sfxStart = sfxMatches.FirstOrDefault()?.Index ?? message.Length;
|
|
var alphanumeric = new Regex(@"[^a-zA-Z0-9!@#$%&\^*+\-_(),+':;?.,\[\]\s\\/~`]");
|
|
message = alphanumeric.Replace(message, " ");
|
|
|
|
if (string.IsNullOrWhiteSpace(message)) {
|
|
return;
|
|
}
|
|
|
|
if (parts.Length == 1) {
|
|
Console.WriteLine($"Voice: {voice}; Priority: {priority}; Message: {message}; Month: {m.SubscribedMonthCount}; {string.Join(", ", e.ChatMessage.Badges.Select(b => b.Key + " = " + b.Value))}");
|
|
player.Add(new TTSMessage() {
|
|
Voice = voice,
|
|
Bot = bot,
|
|
Message = message,
|
|
Moderator = m.IsModerator,
|
|
Timestamp = DateTime.UtcNow,
|
|
Username = m.Username,
|
|
Bits = m.Bits,
|
|
Badges = m.Badges,
|
|
Priority = priority
|
|
});
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < sfxMatches.Count; i++) {
|
|
var sfxMatch = sfxMatches[i];
|
|
var sfxName = sfxMatch.Groups[1]?.ToString()?.ToLower();
|
|
|
|
if (!File.Exists("sfx/" + sfxName + ".mp3")) {
|
|
parts[i * 2 + 2] = parts[i * 2] + " (" + parts[i * 2 + 1] + ")" + parts[i * 2 + 2];
|
|
continue;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(parts[i * 2])) {
|
|
Console.WriteLine($"Voice: {voice}; Priority: {priority}; Message: {parts[i * 2]}; Month: {m.SubscribedMonthCount}; {string.Join(", ", e.ChatMessage.Badges.Select(b => b.Key + " = " + b.Value))}");
|
|
player.Add(new TTSMessage() {
|
|
Voice = voice,
|
|
Bot = bot,
|
|
Message = parts[i * 2],
|
|
Moderator = m.IsModerator,
|
|
Timestamp = DateTime.UtcNow,
|
|
Username = m.Username,
|
|
Bits = m.Bits,
|
|
Badges = m.Badges,
|
|
Priority = priority
|
|
});
|
|
}
|
|
|
|
Console.WriteLine($"Voice: {voice}; Priority: {priority}; SFX: {sfxName}; Month: {m.SubscribedMonthCount}; {string.Join(", ", e.ChatMessage.Badges.Select(b => b.Key + " = " + b.Value))}");
|
|
player.Add(new TTSMessage() {
|
|
Voice = voice,
|
|
Bot = bot,
|
|
Message = sfxName,
|
|
File = $"sfx/{sfxName}.mp3",
|
|
Moderator = m.IsModerator,
|
|
Timestamp = DateTime.UtcNow,
|
|
Username = m.Username,
|
|
Bits = m.Bits,
|
|
Badges = m.Badges,
|
|
Priority = priority
|
|
});
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(parts.Last())) {
|
|
Console.WriteLine($"Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; Month: {m.SubscribedMonthCount}; {string.Join(", ", e.ChatMessage.Badges.Select(b => b.Key + " = " + b.Value))}");
|
|
player.Add(new TTSMessage() {
|
|
Voice = voice,
|
|
Bot = bot,
|
|
Message = parts.Last(),
|
|
Moderator = m.IsModerator,
|
|
Timestamp = DateTime.UtcNow,
|
|
Username = m.Username,
|
|
Bits = m.Bits,
|
|
Badges = m.Badges,
|
|
Priority = priority
|
|
});
|
|
}
|
|
}
|
|
|
|
twitchapiclient.AddOnNewMessageReceived(async Task (object? s, OnMessageReceivedArgs e) => {
|
|
var m = e.ChatMessage;
|
|
var msg = e.ChatMessage.Message;
|
|
if ((m.IsVip || m.IsModerator || m.IsBroadcaster) && (msg.ToLower().StartsWith("!skip ") || msg.ToLower() == "!skip")) {
|
|
AudioPlaybackEngine.Instance.RemoveMixerInput(playing);
|
|
playing = null;
|
|
return;
|
|
}
|
|
|
|
string[] bots = new string[] { "nightbot", "streamelements", "own3d", "streamlabs", "soundalerts", "pokemoncommunitygame" };
|
|
bool bot = bots.Any(b => b == m.Username);
|
|
if (bot || m.IsBroadcaster || msg.StartsWith('!')) {
|
|
return;
|
|
}
|
|
|
|
string[] bad = new string[] { "incel", "simp", "virgin", "faggot", "fagg", "fag", "nigger", "nigga", "nigg", "nig", "whore", "retard", "cock", "fuck", "bastard", "wanker", "bollocks", "motherfucker", "bitch", "bish", "bich", "asshole", "ass", "dick", "dickhead", "frigger", "shit", "slut", "turd", "twat", "nigra", "penis" };
|
|
foreach (var b in bad) {
|
|
msg = new Regex($@"\b{b}\b", RegexOptions.IgnoreCase).Replace(msg, "");
|
|
}
|
|
|
|
msg = new Regex(@"%").Replace(msg, " percent ");
|
|
msg = new Regex(@"https?\:\/\/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)").Replace(msg, "");
|
|
msg = new Regex(@"\bfreeze153").Replace(msg, "");
|
|
|
|
// Filter highly repetitive words (like emotes) from message.
|
|
var words = msg.Split(" ");
|
|
var wordCounter = new Dictionary<string, int>();
|
|
string filteredMsg = string.Empty;
|
|
foreach (var w in words) {
|
|
if (wordCounter.ContainsKey(w)) {
|
|
wordCounter[w]++;
|
|
} else {
|
|
wordCounter.Add(w, 1);
|
|
}
|
|
|
|
if (wordCounter[w] < 5) {
|
|
filteredMsg += w + " ";
|
|
}
|
|
}
|
|
msg = filteredMsg;
|
|
|
|
foreach (var w in words) {
|
|
if (wordCounter.ContainsKey(w)) {
|
|
wordCounter[w]++;
|
|
} else {
|
|
wordCounter.Add(w, 1);
|
|
}
|
|
}
|
|
|
|
int priority = 0;
|
|
if (m.IsStaff) {
|
|
priority = int.MinValue;
|
|
} else if (m.IsModerator) {
|
|
priority = -100;
|
|
} else if (m.IsVip) {
|
|
priority = -10;
|
|
} else if (m.IsPartner) {
|
|
priority = -5;
|
|
} else if (m.IsHighlighted) {
|
|
priority = -1;
|
|
}
|
|
priority = (int) Math.Round(Math.Min(priority, -m.SubscribedMonthCount * (m.Badges.Any(b => b.Key == "subscriber" && b.Value == "1") ? 1.2 : 1)));
|
|
|
|
var matches = voiceRegex.Matches(msg);
|
|
int defaultEnd = matches.FirstOrDefault()?.Index ?? msg.Length;
|
|
if (defaultEnd > 0) {
|
|
HandleMessage(priority, "Brian", msg.Substring(0, defaultEnd), e, bot);
|
|
}
|
|
|
|
foreach (Match match in matches) {
|
|
var message = match.Groups[2].ToString();
|
|
if (string.IsNullOrWhiteSpace(message)) {
|
|
continue;
|
|
}
|
|
|
|
var voice = match.Groups[1].ToString();
|
|
voice = voice[0].ToString().ToUpper() + voice.Substring(1).ToLower();
|
|
|
|
HandleMessage(priority, voice, message, e, bot);
|
|
}
|
|
});
|
|
|
|
AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) => {
|
|
if (e.SampleProvider == playing) {
|
|
playing = null;
|
|
}
|
|
});
|
|
|
|
Task.Run(async () => {
|
|
while (true) {
|
|
try {
|
|
var m = player.ReceiveBuffer();
|
|
if (m == null) {
|
|
await Task.Delay(200);
|
|
continue;
|
|
}
|
|
|
|
string url = $"https://api.streamelements.com/kappa/v2/speech?voice={m.Voice}&text={m.Message}";
|
|
var sound = new NetworkWavSound(url);
|
|
var provider = new CachedWavProvider(sound);
|
|
var data = AudioPlaybackEngine.Instance.ConvertSound(provider);
|
|
|
|
m.Audio = data;
|
|
player.Ready(m);
|
|
} catch (COMException e) {
|
|
Console.WriteLine(e.GetType().Name + ": " + e.Message + " (HResult: " + e.HResult + ")");
|
|
} catch (Exception e) {
|
|
Console.WriteLine(e.GetType().Name + ": " + e.Message);
|
|
}
|
|
}
|
|
});
|
|
|
|
Task.Run(async () => {
|
|
while (true) {
|
|
try {
|
|
while (player.IsEmpty() || playing != null) {
|
|
await Task.Delay(200);
|
|
}
|
|
var m = player.ReceiveReady();
|
|
if (m == null) {
|
|
continue;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(m.File) && File.Exists(m.File)) {
|
|
Console.WriteLine("Playing sfx: " + m.File);
|
|
AudioPlaybackEngine.Instance.PlaySound(m.File);
|
|
continue;
|
|
}
|
|
|
|
Console.WriteLine("Playing message: " + m.Message);
|
|
playing = m.Audio;
|
|
AudioPlaybackEngine.Instance.AddMixerInput(m.Audio);
|
|
} catch (Exception e) {
|
|
Console.WriteLine(e.GetType().Name + ": " + e.Message);
|
|
}
|
|
}
|
|
});
|
|
|
|
Console.WriteLine("Twitch API client connecting...");
|
|
twitchapiclient.Connect();
|
|
Console.ReadLine();
|
|
Console.ReadLine(); |