Decompiled source of PySpeech v1.0.1

BepInEx/plugins/PySpeech.dll

Decompiled 4 days ago
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();
			}
		}
	}
}