using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using PySpeech.Patches;
using PySpeech.Util;
using UnityEngine;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("PySpeech")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("PySpeech")]
[assembly: AssemblyTitle("PySpeech")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Microsoft.CodeAnalysis.Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Microsoft.CodeAnalysis.Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
internal sealed class NullableAttribute : Attribute
{
public readonly byte[] NullableFlags;
public NullableAttribute(byte P_0)
{
NullableFlags = new byte[1] { P_0 };
}
public NullableAttribute(byte[] P_0)
{
NullableFlags = P_0;
}
}
[CompilerGenerated]
[Microsoft.CodeAnalysis.Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
internal sealed class NullableContextAttribute : Attribute
{
public readonly byte Flag;
public NullableContextAttribute(byte P_0)
{
Flag = P_0;
}
}
}
namespace PySpeech
{
internal class Engine
{
private static string[] models = new string[3] { "tiny", "base", "small" };
public static event EventHandler<SpeechEventArgs> SpeechRecognized;
internal static async Task Start()
{
GameObject dispatcher = new GameObject("PySpeech Dispatcher");
dispatcher.AddComponent<UnityMainThreadDispatcher>();
Object.DontDestroyOnLoad((Object)(object)dispatcher);
Plugin.mls.LogInfo((object)"Starting Speech Recognition engine");
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = ((BaseUnityPlugin)Plugin.Instance).Info.Location.TrimEnd("PySpeech.dll".ToCharArray()) + "pyexec/pyspeech.exe",
Arguments = "\"" + Speech.languages[(int)Plugin.language.Value] + "\" \"" + models[(int)Plugin.model.Value] + "\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
Process pyProcess = new Process
{
StartInfo = psi,
EnableRaisingEvents = true
};
try
{
pyProcess.OutputDataReceived += delegate(object sender, DataReceivedEventArgs args)
{
try
{
if (!string.IsNullOrEmpty(args.Data))
{
string recognized = args.Data.TrimStart(' ');
Speech.GetBestMatch(recognized);
UnityMainThreadDispatcher.Enqueue(delegate
{
Engine.SpeechRecognized?.Invoke(Plugin.Instance, new SpeechEventArgs(recognized));
});
}
}
catch (Exception ex)
{
Plugin.mls.LogError((object)(ex.Message + ex.StackTrace));
}
};
pyProcess.ErrorDataReceived += delegate(object sender, DataReceivedEventArgs args)
{
if (!string.IsNullOrEmpty(args.Data))
{
Plugin.mls.LogError((object)("Python Error: " + args.Data));
}
};
pyProcess.Start();
pyProcess.BeginOutputReadLine();
pyProcess.BeginErrorReadLine();
await Task.Run(delegate
{
pyProcess.WaitForExit();
});
}
finally
{
if (pyProcess != null)
{
((IDisposable)pyProcess).Dispose();
}
}
}
internal static async Task Restart()
{
Process[] processes = Process.GetProcessesByName("pyspeech");
Process[] array = processes;
foreach (Process process in array)
{
process.Kill();
}
await Start();
}
}
public enum Languages
{
Multilingual,
English,
Chinese,
German,
Spanish,
Russian,
Korean,
French,
Japanese,
Portuguese,
Turkish,
Polish,
Catalan,
Dutch,
Arabic,
Swedish,
Italian,
Indonesian,
Hindi,
Finnish,
Vietnamese,
Hebrew,
Ukrainian,
Greek,
Malay,
Czech,
Romanian,
Danish,
Hungarian,
Tamil,
Norwegian,
Thai,
Urdu,
Croatian,
Bulgarian,
Lithuanian,
Latin,
Maori,
Malayalam,
Welsh,
Slovak,
Telugu,
Persian,
Latvian,
Bengali,
Serbian,
Azerbaijani,
Slovenian,
Kannada,
Estonian,
Macedonian,
Breton,
Basque,
Icelandic,
Armenian,
Nepali,
Mongolian,
Bosnian,
Kazakh,
Albanian,
Swahili,
Galician,
Marathi,
Punjabi,
Sinhala,
Khmer,
Shona,
Yoruba,
Somali,
Afrikaans,
Occitan,
Georgian,
Belarusian,
Tajik,
Sindhi,
Gujarati,
Amharic,
Yiddish,
Lao,
Uzbek,
Faroese,
HaitianCreole,
Pashto,
Turkmen,
Nynorsk,
Maltese,
Sanskrit,
Luxembourgish,
Myanmar,
Tibetan,
Tagalog,
Malagasy,
Assamese,
Tatar,
Hawaiian,
Lingala,
Hausa,
Bashkir,
Javanese,
Sundanese,
Cantonese
}
public enum Models
{
Tiny,
Base,
Small
}
[BepInPlugin("JS03.PySpeech", "PySpeech", "1.0.0")]
public class Plugin : BaseUnityPlugin
{
private const string modGUID = "JS03.PySpeech";
private const string modName = "PySpeech";
private const string modVersion = "1.0.0";
private readonly Harmony harmony = new Harmony("JS03.PySpeech");
public static Plugin Instance;
internal static ManualLogSource mls;
public static ConfigEntry<bool> logging;
public static ConfigEntry<Languages> language;
public static ConfigEntry<Models> model;
private void Awake()
{
if ((Object)(object)Instance == (Object)null)
{
Instance = this;
}
mls = Logger.CreateLogSource("JS03.PySpeech");
logging = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Show Python logs", true, "Shows the speech recognition output");
model = ((BaseUnityPlugin)this).Config.Bind<Models>("General", "Model", Models.Tiny, "Whisper model to be used for speech recognition.\n\nTiny: low delay, lower accuracy\nBase: medium delay, higher accuracy\nSmall: high delay, very high accuracy");
model.SettingChanged += async delegate
{
await Engine.Restart();
};
language = ((BaseUnityPlugin)this).Config.Bind<Languages>("General", "Language", Languages.English, "Language to be used for speech recognition");
language.SettingChanged += async delegate
{
await Engine.Restart();
};
Speech.phrases = new List<string>();
harmony.PatchAll(typeof(GameNetworkManagerPatch));
}
}
public class Speech
{
internal static List<string> phrases;
private static string bestMatch;
private static double bestScore;
internal static readonly string[] languages = new string[101]
{
"", "en", "zh", "de", "es", "ru", "ko", "fr", "ja", "pt",
"tr", "pl", "ca", "nl", "ar", "sv", "it", "id", "hi", "fi",
"vi", "he", "uk", "el", "ms", "cs", "ro", "da", "hu", "ta",
"no", "th", "ur", "hr", "bg", "lt", "la", "mi", "ml", "cy",
"sk", "te", "fa", "lv", "bn", "sr", "az", "sl", "kn", "et",
"mk", "br", "eu", "is", "hy", "ne", "mn", "bs", "kk", "sq",
"sw", "gl", "mr", "pa", "si", "km", "sn", "yo", "so", "af",
"oc", "ka", "be", "tg", "sd", "gu", "am", "yi", "lo", "uz",
"fo", "ht", "ps", "tk", "nn", "mt", "sa", "lb", "my", "bo",
"tl", "mg", "as", "tt", "haw", "ln", "ha", "ba", "jw", "su",
"yue"
};
internal static float GetSimilarity(string phrase, string recognized)
{
if (string.IsNullOrEmpty(phrase) || string.IsNullOrEmpty(recognized))
{
return 0f;
}
int num = Math.Max(phrase.Length, recognized.Length);
if (num == 0)
{
return 1f;
}
int num2 = LevenshteinDistance(phrase, recognized);
return (float)Math.Round(1.0 - (double)num2 / (double)num, 2);
}
internal static void GetBestMatch(string recognized)
{
float num = float.MinValue;
foreach (string phrase in phrases)
{
float similarity = GetSimilarity(phrase, recognized);
if (similarity > num)
{
num = similarity;
bestMatch = phrase;
}
}
bestScore = num;
if (Plugin.logging.Value)
{
Plugin.mls.LogDebug((object)("Recognized: " + recognized));
Plugin.mls.LogDebug((object)("Best match: " + bestMatch));
Plugin.mls.LogDebug((object)$"Best similarity score: {bestScore}");
}
}
public static bool IsAboveThreshold(string[] phrases, double similarityThreshold)
{
return phrases.Contains(bestMatch) && bestScore >= similarityThreshold;
}
public static void RegisterPhrases(string[] phrases)
{
Speech.phrases.AddRange(phrases);
}
public static EventHandler<SpeechEventArgs> RegisterCustomHandler(EventHandler<SpeechEventArgs> callback)
{
Engine.SpeechRecognized += callback;
return callback;
}
private static int LevenshteinDistance(string s1, string s2)
{
s1 = s1.ToLower();
s2 = s2.ToLower();
int[,] array = new int[s1.Length + 1, s2.Length + 1];
for (int i = 0; i <= s1.Length; i++)
{
array[i, 0] = i;
}
for (int j = 0; j <= s2.Length; j++)
{
array[0, j] = j;
}
for (int k = 1; k <= s1.Length; k++)
{
for (int l = 1; l <= s2.Length; l++)
{
int num = ((s1[k - 1] != s2[l - 1]) ? 1 : 0);
array[k, l] = Math.Min(Math.Min(array[k - 1, l] + 1, array[k, l - 1] + 1), array[k - 1, l - 1] + num);
}
}
return array[s1.Length, s2.Length];
}
}
public class SpeechEventArgs : EventArgs
{
public string Text { get; }
public SpeechEventArgs(string text)
{
Text = text;
}
}
}
namespace PySpeech.Util
{
internal class UnityMainThreadDispatcher : MonoBehaviour
{
private static readonly Queue<Action> _executionQueue = new Queue<Action>();
private void Start()
{
Plugin.mls.LogInfo((object)"PySpeech Dispatcher created");
}
public void Update()
{
lock (_executionQueue)
{
while (_executionQueue.Count > 0)
{
Action action = _executionQueue.Dequeue();
if (Plugin.logging.Value)
{
Plugin.mls.LogDebug((object)"[Dispatcher] Invoking action.");
}
action?.Invoke();
}
}
}
internal static void Enqueue(Action action)
{
if (action == null)
{
return;
}
lock (_executionQueue)
{
_executionQueue.Enqueue(action);
}
}
}
}
namespace PySpeech.Patches
{
[HarmonyPatch(typeof(GameNetworkManager))]
internal class GameNetworkManagerPatch
{
[HarmonyPostfix]
[HarmonyPatch("Start")]
private static async void SetupRecognitionEngine()
{
await Engine.Start();
}
[HarmonyPostfix]
[HarmonyPatch("OnApplicationQuit")]
private static void KillPythonProcess()
{
Process[] processesByName = Process.GetProcessesByName("pyspeech");
Process[] array = processesByName;
foreach (Process process in array)
{
process.Kill();
}
}
}
}