Decompiled source of VoiceInputFix v1.0.6

VoiceInputFix.dll

Decompiled 2 hours ago
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using SherpaOnnx;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: IgnoresAccessChecksTo("Assembly-CSharp-firstpass")]
[assembly: IgnoresAccessChecksTo("Assembly-CSharp")]
[assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.Universal.Runtime")]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("MHZ")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyCopyright("Copyright © 2026 Masaicker")]
[assembly: AssemblyDescription("Replace YAPYAP original Vosk engine with Fun-ASR (Sherpa-ONNX)")]
[assembly: AssemblyFileVersion("1.0.6")]
[assembly: AssemblyInformationalVersion("1.0.6+18d42f2c2f8c0f3d4faefb3a5ecfd3c76e10bc68")]
[assembly: AssemblyProduct("VoiceInputFix")]
[assembly: AssemblyTitle("VoiceInputFix")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.6.0")]
[module: UnverifiableCode]
namespace VoiceInputFix
{
	[BepInPlugin("Mhz.voiceinputfix", "VoiceInputFix", "1.0.6")]
	public class Plugin : BaseUnityPlugin
	{
		public static ManualLogSource LogSource;

		private static string _initError;

		private static string _pluginFolder;

		private static string _apiFolder;

		private static string _onnxFolder;

		private static string _managedFolder;

		private static ConfigEntry<bool> _enableDebugLog;

		private static ConfigEntry<float> _speechThreshold;

		private static ConfigEntry<string> _language;

		[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
		private static extern bool SetDllDirectory(string lpPathName);

		[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
		private static extern IntPtr LoadLibrary(string lpLibFileName);

		[DllImport("user32.dll", CharSet = CharSet.Auto)]
		private static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);

		private void Awake()
		{
			//IL_00c0: Unknown result type (might be due to invalid IL or missing references)
			LogSource = ((BaseUnityPlugin)this).Logger;
			_pluginFolder = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location);
			AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
			bool flag = LocateDependencies();
			CheckAndRepairModelEnvironment();
			if (flag)
			{
				_enableDebugLog = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "EnableDebugLog", false, "Whether to show detailed recognition logs in the console. | 是否在控制台中显示详细的识别日志。");
				_speechThreshold = ((BaseUnityPlugin)this).Config.Bind<float>("General", "SpeechThreshold", 0.015f, "Minimum amplitude threshold for speech detection (VAD). | 语音检测的最低振幅阈值(VAD)。建议范围 0.01-0.05。");
				_language = ((BaseUnityPlugin)this).Config.Bind<string>("General", "Language", "auto", "Specified language for recognition. This PRIORITIZES the selected language to improve accuracy, but may still detect others if confidence is high. Options: auto, zh, en, ja, ko, yue. Invalid values will fallback to auto. | 指定识别语言。这会【优先】识别所选语言以提高准确率,但在置信度极高时仍可能识别出其他语言。可选:auto (自动), zh (中文), en (英文), ja (日文), ko (韩文), yue (粤语)。无效值将回退到自动。");
				new Harmony("Mhz.voiceinputfix").PatchAll();
				LogSource.LogInfo((object)"=== [VoiceInputFix] Initialized ===");
			}
		}

		private bool LocateDependencies()
		{
			_apiFolder = FindFileRecursively(_pluginFolder, "sherpa-onnx-c-api.dll", 4);
			_onnxFolder = FindFileRecursively(_pluginFolder, "onnxruntime.dll", 4);
			_managedFolder = FindFileRecursively(_pluginFolder, "sherpa-onnx.dll", 4);
			List<string> list = new List<string>();
			if (string.IsNullOrEmpty(_apiFolder))
			{
				list.Add(" - sherpa-onnx-c-api.dll");
			}
			if (string.IsNullOrEmpty(_onnxFolder))
			{
				list.Add(" - onnxruntime.dll");
			}
			if (string.IsNullOrEmpty(_managedFolder))
			{
				list.Add(" - sherpa-onnx.dll");
			}
			if (list.Count > 0)
			{
				_initError = "Dependency Missing";
				string text = "https://www.nexusmods.com/yapyap/mods/5?tab=files";
				string text2 = "https://thunderstore.io/c/yapyap/p/Mhz/SherpaOnnxRuntime/versions/";
				string text3 = "【语音识别运行库缺失】\n\n检测到以下核心组件缺失,插件将无法加载:\n" + string.Join("\n", list) + "\n\n点击【确定】将为您随机复制一个下载链接到剪贴板,您可以直接在浏览器地址栏粘贴(Ctrl+V)访问。\n\n--------------------------------------------------\n\n[Runtime Dependency Missing]\n\nThe following core components are missing, the plugin will not load:\n" + string.Join("\n", list) + "\n\nClick [OK] to copy a download link to your clipboard, then you can paste it into your browser.";
				int num = MessageBox(IntPtr.Zero, text3, "VoiceInputFix Diagnostic", 327697u);
				if (num == 1)
				{
					string[] array = new string[2] { text, text2 };
					GUIUtility.systemCopyBuffer = array[new Random().Next(array.Length)];
				}
				return false;
			}
			SetDllDirectory(_onnxFolder);
			LoadLibrary(Path.Combine(_onnxFolder, "onnxruntime.dll"));
			LoadLibrary(Path.Combine(_apiFolder, "sherpa-onnx-c-api.dll"));
			SetDllDirectory(null);
			LogSource.LogInfo((object)("[VoiceInputFix] Dependencies loaded: API @ " + _apiFolder + ", ONNX @ " + _onnxFolder + ", Managed @ " + _managedFolder));
			return true;
		}

		private string FindFileRecursively(string startDir, string fileName, int maxLevels)
		{
			string text = startDir;
			for (int i = 0; i < maxLevels; i++)
			{
				if (string.IsNullOrEmpty(text))
				{
					break;
				}
				if (File.Exists(Path.Combine(text, fileName)))
				{
					return text;
				}
				try
				{
					string[] files = Directory.GetFiles(text, fileName, SearchOption.AllDirectories);
					if (files.Length != 0)
					{
						return Path.GetDirectoryName(files[0]);
					}
				}
				catch
				{
				}
				text = Path.GetDirectoryName(text);
			}
			return null;
		}

		private Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
		{
			if (args.Name.Contains("sherpa-onnx") || args.Name.Contains("SherpaOnnx"))
			{
				string path = _managedFolder ?? _apiFolder ?? _pluginFolder;
				string text = Path.Combine(path, "sherpa-onnx.dll");
				if (File.Exists(text))
				{
					return Assembly.LoadFrom(text);
				}
			}
			return null;
		}

		private void CheckAndRepairModelEnvironment()
		{
			string text = Path.Combine(_pluginFolder, "models");
			if (!Directory.Exists(text))
			{
				Directory.CreateDirectory(text);
			}
			string path = Path.Combine(text, "model.onnx");
			string path2 = Path.Combine(text, "tokens.txt");
			string path3 = Path.Combine(text, "README_DOWNLOAD.md");
			if (!File.Exists(path3))
			{
				string contents = "# VoiceInputFix - Model Download Guide / 模型下载指南\n\nPlease download the following files and place them in THIS directory (the 'models' folder):\n请下载以下文件并将它们放入【当前】目录(即 models 文件夹)中:\n\n1. **model.onnx** (1.03GB)\n   URL: https://huggingface.co/csukuangfj/sherpa-onnx-sense-voice-funasr-nano-2025-12-17/resolve/main/model.onnx\n\n2. **tokens.txt** (940KB)\n   URL: https://huggingface.co/csukuangfj/sherpa-onnx-sense-voice-funasr-nano-2025-12-17/resolve/main/tokens.txt\n\n--------------------------------------------------\n\n### Optional: Runtime Dependencies / 运行库依赖 (If mod fails to load)\n\nIf the mod fails to load due to missing native DLLs, please download from:\n如果模组因缺少运行库 DLL 无法加载,请从以下地址获取:\n\n- NexusMods: https://www.nexusmods.com/yapyap/mods/5?tab=files\n- Thunderstore: https://thunderstore.io/c/yapyap/p/Mhz/SherpaOnnxRuntime/versions/\n\nPlease start or restart the game after all files are placed correctly. / 所有文件放置正确后,请重新启动游戏。";
				File.WriteAllText(path3, contents);
			}
			List<string> list = new List<string>();
			if (!File.Exists(path))
			{
				list.Add(" - model.onnx");
			}
			if (!File.Exists(path2))
			{
				list.Add(" - tokens.txt");
			}
			if (list.Count <= 0)
			{
				return;
			}
			if (string.IsNullOrEmpty(_initError))
			{
				_initError = "Models Missing";
			}
			string text2 = string.Join("\n", list);
			string text3 = "【检测到语音识别模型文件缺失】\n\n检测到以下模型权重文件缺失:\n" + text2 + "\n\n存放目录:\n" + text + "\n\n状态说明:\n由于模型文件缺失,本模组及原版语音识别功能将无法生效,但游戏仍可正常进行。\n点击确认后将为您打开目标目录,请查阅 README_DOWNLOAD.md 获取下载指引。\n\n--------------------------------------------------\n\n[Model Files Missing]\n\nMissing Files:\n" + text2 + "\n\nDirectory:\n" + text + "\n\nNotice:\nThe mod and original voice recognition will be disabled without these files, but the game will run normally. \nClick [OK] to open the folder and check README_DOWNLOAD.md.";
			int num = MessageBox(IntPtr.Zero, text3, "VoiceInputFix Diagnostic", 327729u);
			if (num == 1)
			{
				try
				{
					Process.Start(new ProcessStartInfo(text)
					{
						UseShellExecute = true
					});
				}
				catch
				{
				}
			}
			LogError("[Diagnostic] Missing required files in " + text);
		}

		private void OnDestroy()
		{
			SherpaEngine.Cleanup();
		}

		public static void Log(string m)
		{
			if (_enableDebugLog != null && _enableDebugLog.Value)
			{
				ManualLogSource logSource = LogSource;
				if (logSource != null)
				{
					logSource.LogInfo((object)m);
				}
			}
		}

		public static void LogError(string m)
		{
			ManualLogSource logSource = LogSource;
			if (logSource != null)
			{
				logSource.LogError((object)m);
			}
		}

		internal static void EnsureRecognizer()
		{
			SherpaEngine.Init(_pluginFolder, _initError, _language.Value);
		}

		public static string InternalDecode(float[] samples)
		{
			return SherpaEngine.Decode(samples);
		}

		public static async Task RecognitionLoop(VoskSpeechToText instance)
		{
			await SherpaEngine.RunLoop(instance, _speechThreshold.Value);
		}
	}
	internal static class SherpaEngine
	{
		private static OfflineRecognizer _recognizer;

		private static readonly object Lock = new object();

		private static readonly Regex TagRegex = new Regex("<\\|.*?\\|>", RegexOptions.Compiled);

		public static void Init(string folder, string initError, string language = "auto")
		{
			//IL_013b: Unknown result type (might be due to invalid IL or missing references)
			//IL_013d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0147: Expected O, but got Unknown
			if (_recognizer != null || initError != null)
			{
				return;
			}
			lock (Lock)
			{
				if (_recognizer != null)
				{
					return;
				}
				try
				{
					string path = Path.Combine(folder, "models");
					OfflineRecognizerConfig val = default(OfflineRecognizerConfig);
					((OfflineRecognizerConfig)(ref val))..ctor();
					val.ModelConfig.SenseVoice.Model = Path.Combine(path, "model.onnx");
					val.ModelConfig.SenseVoice.UseInverseTextNormalization = 1;
					string text = language?.ToLower().Trim() ?? "";
					switch (text)
					{
					default:
						text = "";
						break;
					case "zh":
					case "en":
					case "ja":
					case "ko":
					case "yue":
						break;
					}
					val.ModelConfig.SenseVoice.Language = text;
					val.ModelConfig.Tokens = Path.Combine(path, "tokens.txt");
					val.ModelConfig.NumThreads = 4;
					val.DecodingMethod = "greedy_search";
					_recognizer = new OfflineRecognizer(val);
					Plugin.Log("Fun-ASR Engine Ready. Language: " + (string.IsNullOrEmpty(text) ? "auto" : text));
				}
				catch (Exception ex)
				{
					Plugin.LogError("Init Fail: " + ex.Message);
				}
			}
		}

		public static string Decode(float[] samples)
		{
			if (samples == null || samples.Length == 0 || _recognizer == null)
			{
				return string.Empty;
			}
			lock (Lock)
			{
				OfflineStream val = _recognizer.CreateStream();
				try
				{
					val.AcceptWaveform(16000, samples);
					_recognizer.Decode(val);
					string text = val.Result.Text;
					string text2 = TagRegex.Replace(text, "").Trim();
					return text2.Replace("。", "").Replace(",", "").Replace("?", "")
						.Replace("!", "");
				}
				finally
				{
					((IDisposable)val)?.Dispose();
				}
			}
		}

		public static void Cleanup()
		{
			lock (Lock)
			{
				OfflineRecognizer recognizer = _recognizer;
				if (recognizer != null)
				{
					recognizer.Dispose();
				}
				_recognizer = null;
			}
		}

		public static async Task RunLoop(VoskSpeechToText instance, float threshold)
		{
			if (_recognizer == null)
			{
				return;
			}
			List<float> audioAccumulator = new List<float>();
			Stopwatch silenceTimer = new Stopwatch();
			Stopwatch partialTimer = Stopwatch.StartNew();
			string lastSentPartial = string.Empty;
			try
			{
				ConcurrentQueue<short[]> bufferQueue = instance._threadedBufferQueue;
				ConcurrentQueue<string> resultQueue = instance._threadedResultQueue;
				while (instance._running)
				{
					bool isFrameLoud = false;
					bool hasData = false;
					short[] pcmData;
					while (bufferQueue.TryDequeue(out pcmData))
					{
						hasData = true;
						float[] frameSamples = new float[pcmData.Length];
						float frameMax = 0f;
						for (int i = 0; i < pcmData.Length; i++)
						{
							frameSamples[i] = (float)pcmData[i] / 32768f;
							float abs = Math.Abs(frameSamples[i]);
							if (abs > frameMax)
							{
								frameMax = abs;
							}
						}
						if (frameMax > threshold)
						{
							isFrameLoud = true;
							audioAccumulator.AddRange(frameSamples);
						}
						else if (audioAccumulator.Count > 0)
						{
							audioAccumulator.AddRange(frameSamples);
						}
					}
					if (isFrameLoud)
					{
						silenceTimer.Restart();
					}
					if (hasData && audioAccumulator.Count > 0 && partialTimer.ElapsedMilliseconds > 120 && audioAccumulator.Count > 1600)
					{
						string text2 = Decode(audioAccumulator.ToArray());
						if (!string.IsNullOrEmpty(text2) && text2 != lastSentPartial)
						{
							resultQueue.Enqueue("{\"partial\":\"" + text2.Replace("\"", "\\\"") + "\"}");
							lastSentPartial = text2;
						}
						partialTimer.Restart();
					}
					if (audioAccumulator.Count > 0 && (silenceTimer.ElapsedMilliseconds > 350 || audioAccumulator.Count > 160000))
					{
						string text = Decode(audioAccumulator.ToArray());
						if (!string.IsNullOrEmpty(text))
						{
							Plugin.Log("Final: " + text);
							resultQueue.Enqueue("{\"alternatives\":[{\"conf\":1.0,\"text\":\"" + text.Replace("\"", "\\\"") + "\"}],\"partial\":false}");
						}
						resultQueue.Enqueue("{\"partial\":\"[unk]\"}");
						audioAccumulator.Clear();
						lastSentPartial = string.Empty;
						silenceTimer.Reset();
						await Task.Delay(100);
					}
					await Task.Delay(15);
				}
			}
			catch (Exception ex)
			{
				Plugin.LogError("Loop Error: " + ex.Message);
			}
		}
	}
	[HarmonyPatch(typeof(VoskSpeechToText), "StartupRoutine")]
	internal class PatchStartupRoutine
	{
		[CompilerGenerated]
		private sealed class <EmptyEnumerator>d__1 : IEnumerator<object>, IEnumerator, IDisposable
		{
			private int <>1__state;

			private object <>2__current;

			object IEnumerator<object>.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			object IEnumerator.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			[DebuggerHidden]
			public <EmptyEnumerator>d__1(int <>1__state)
			{
				this.<>1__state = <>1__state;
			}

			[DebuggerHidden]
			void IDisposable.Dispose()
			{
				<>1__state = -2;
			}

			private bool MoveNext()
			{
				if (<>1__state != 0)
				{
					return false;
				}
				<>1__state = -1;
				return false;
			}

			bool IEnumerator.MoveNext()
			{
				//ILSpy generated this explicit interface implementation from .override directive in MoveNext
				return this.MoveNext();
			}

			[DebuggerHidden]
			void IEnumerator.Reset()
			{
				throw new NotSupportedException();
			}
		}

		private static bool Prefix(VoskSpeechToText __instance, ref IEnumerator __result, ref bool ____didInit)
		{
			____didInit = true;
			if ((Object)(object)__instance.VoiceProcessor != (Object)null)
			{
				__instance.VoiceProcessor.OnFrameCaptured -= __instance.VoiceProcessorOnOnFrameCaptured;
				__instance.VoiceProcessor.OnFrameCaptured += __instance.VoiceProcessorOnOnFrameCaptured;
			}
			__result = EmptyEnumerator();
			return false;
		}

		[IteratorStateMachine(typeof(<EmptyEnumerator>d__1))]
		private static IEnumerator EmptyEnumerator()
		{
			//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
			return new <EmptyEnumerator>d__1(0);
		}
	}
	[HarmonyPatch(typeof(VoskSpeechToText), "StartRecording")]
	internal class PatchStartRecording
	{
		private static bool Prefix(VoskSpeechToText __instance)
		{
			if (__instance._running)
			{
				return false;
			}
			Plugin.EnsureRecognizer();
			__instance._running = true;
			__instance._sampleRate = __instance.VoiceProcessor.StartRecording(__instance._sampleRate, 512, (bool?)null);
			__instance._recognizerReady = true;
			Traverse.Create((object)__instance).Field("_threadedWorkTask").SetValue((object)Task.Run(() => Plugin.RecognitionLoop(__instance)));
			return false;
		}
	}
	[HarmonyPatch(typeof(VoskSpeechToText), "StartVosk")]
	internal class PatchStartVosk
	{
		private static bool Prefix(ref bool ____didInit)
		{
			if (____didInit)
			{
				return false;
			}
			return true;
		}
	}
}
namespace System.Runtime.CompilerServices
{
	[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
	internal sealed class IgnoresAccessChecksToAttribute : Attribute
	{
		public IgnoresAccessChecksToAttribute(string assemblyName)
		{
		}
	}
}