From ac183f8eb6551142a23d68ff80b1f08ae272bf25 Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 20 Mar 2026 19:26:33 +0200 Subject: [PATCH] Well Done --- ModuleWeather/Module.cs | 37 ++-- ModuleWeather/ModuleWeather.csproj | 2 +- VisionAsist.SDK/IModule.cs | 3 +- VisionAsist.sln.DotSettings.user | 4 +- VisionAsist/Models/Core.cs | 16 +- VisionAsist/Models/Selector.cs | 12 +- VisionAsist/Models/TrigerCore.cs | 215 ++++++++++++++++---- VisionAsist/ViewModels/SettingsViewModel.cs | 8 +- VisionAsist/VisionAsist.csproj | 4 + 9 files changed, 231 insertions(+), 70 deletions(-) diff --git a/ModuleWeather/Module.cs b/ModuleWeather/Module.cs index 430ebef..fd9160f 100644 --- a/ModuleWeather/Module.cs +++ b/ModuleWeather/Module.cs @@ -8,26 +8,31 @@ public class WeatherModule : IModule public string[] GetCommands() => new[] { "Погода", "Пинг" }; - public object Execute(string command, object[] args) + public void Settings(object[] args) + { + // args[0] — это родительское окно из Ядра + var parentWindow = args != null && args.Length > 0 ? args[0] as Window : null; + + var win = new Window + { + Title = "Окно Погоды", + Content = new WeatherView(), // Вставляем наш контрол + Width = 350, + Height = 250, + WindowStartupLocation = WindowStartupLocation.CenterOwner + }; + + if (parentWindow != null) win.Show(parentWindow); + else win.Show(); + + } + + public object Execute(string command) { switch (command) { case "Погода": - // args[0] — это родительское окно из Ядра - var parentWindow = args != null && args.Length > 0 ? args[0] as Window : null; - - var win = new Window - { - Title = "Окно Погоды", - Content = new WeatherView(), // Вставляем наш контрол - Width = 350, - Height = 250, - WindowStartupLocation = WindowStartupLocation.CenterOwner - }; - - if (parentWindow != null) win.Show(parentWindow); - else win.Show(); - + return "Окно открыто успешно"; case "Пинг": diff --git a/ModuleWeather/ModuleWeather.csproj b/ModuleWeather/ModuleWeather.csproj index d404b85..763c2f4 100644 --- a/ModuleWeather/ModuleWeather.csproj +++ b/ModuleWeather/ModuleWeather.csproj @@ -22,7 +22,7 @@ diff --git a/VisionAsist.SDK/IModule.cs b/VisionAsist.SDK/IModule.cs index 45d5ed0..5a61986 100644 --- a/VisionAsist.SDK/IModule.cs +++ b/VisionAsist.SDK/IModule.cs @@ -4,5 +4,6 @@ public interface IModule { string Name { get; } string[] GetCommands(); - object Execute(string command, object[] args); + void Settings(object[] args); + object Execute(string command); } \ No newline at end of file diff --git a/VisionAsist.sln.DotSettings.user b/VisionAsist.sln.DotSettings.user index c79a889..56408d8 100644 --- a/VisionAsist.sln.DotSettings.user +++ b/VisionAsist.sln.DotSettings.user @@ -1,2 +1,4 @@  - ForceIncluded \ No newline at end of file + ForceIncluded + ForceIncluded + ForceIncluded \ No newline at end of file diff --git a/VisionAsist/Models/Core.cs b/VisionAsist/Models/Core.cs index c0732ae..4e0be9c 100644 --- a/VisionAsist/Models/Core.cs +++ b/VisionAsist/Models/Core.cs @@ -12,7 +12,15 @@ namespace VisionAsist.Models; public class Core { - public static Dictionary _loadedModules = new(); + + public class Modules + { + public string Name { get; set; } + public IModule Module { get; set; } + public string[] commands { get; set; } + } + + public static List modulelist = new(); public static TrigerCore triger = new(); public static string TextAsist; static string Plugin = Path.Combine(AppContext.BaseDirectory, "Modules"); @@ -35,7 +43,7 @@ public class Core if (type != null) { var module = (IModule)Activator.CreateInstance(type)!; - _loadedModules.Add(module.Name, module); + modulelist.Add(new Modules{Name = folderName, Module = module, commands = module.GetCommands()}); foreach (var cmd in module.GetCommands()) { Console.WriteLine($"- {cmd}"); @@ -53,8 +61,10 @@ public class Core triger.OnRecognized += word => { - Console.WriteLine(word); // печатаем сразу, как распознано + TextAsist = triger.RecognizedText; + Selector.selector(triger.RecognizedText); + }; // Запускаем запись diff --git a/VisionAsist/Models/Selector.cs b/VisionAsist/Models/Selector.cs index a0c1651..2b96407 100644 --- a/VisionAsist/Models/Selector.cs +++ b/VisionAsist/Models/Selector.cs @@ -1,6 +1,14 @@ -namespace VisionAsist.Models; +using System; + +namespace VisionAsist.Models; public class Selector { - + public static void selector(string text) + { + if (text.Contains("вижен")) + { + Console.WriteLine("dddddd"); + } + } } \ No newline at end of file diff --git a/VisionAsist/Models/TrigerCore.cs b/VisionAsist/Models/TrigerCore.cs index 58da054..e6ca858 100644 --- a/VisionAsist/Models/TrigerCore.cs +++ b/VisionAsist/Models/TrigerCore.cs @@ -1,76 +1,207 @@ using System; using System.IO; -using Vosk; using System.Text.Json; -using NAudio.Wave; -using Avalonia.Controls; +using System.Threading; +using System.Collections.Concurrent; +using Vosk; +using SoundIOSharp; using Avalonia.Threading; namespace VisionAsist.Models; -public class TrigerCore +public class TrigerCore : IDisposable { + private SoundIO? _soundIO; + private SoundIODevice? _device; + private SoundIOInStream? _inStream; - private WaveInEvent? _waveIn; - private Model? _model; - private VoskRecognizer? _rec; - private readonly object _voskLock = new(); - public string RecognizedText { get; private set; } = ""; + private Thread? _eventThread; + private Thread? _processingThread; + private bool _isRecording; + + private readonly Model _model; + private readonly VoskRecognizer _rec; + + private readonly BlockingCollection _audioQueue = new(100); + + public string RecognizedText { get; private set; } = ""; + public event Action? OnRecognized; + + // Сохраняем ссылки в полях класса, чтобы GC не удалил их в Debug-режиме + private Action? _readCallback; + private Action? _overflowCallback; + private Action? _errorCallback; - public TrigerCore() { - string VoskPath = Path.Combine(AppContext.BaseDirectory, "models/Vosk/"); - if (!Directory.Exists(VoskPath)) - throw new DirectoryNotFoundException($"Модель не найдена по пути: {VoskPath}"); + string voskPath = Path.Combine(AppContext.BaseDirectory, "models", "Vosk"); + if (!Directory.Exists(voskPath)) + throw new DirectoryNotFoundException($"Модель Vosk не найдена: {voskPath}"); - _model = new Model(VoskPath); + _model = new Model(voskPath); _rec = new VoskRecognizer(_model, 16000.0f); - } public void StartRecording() { - if (_waveIn != null || _rec == null) return; + if (_isRecording) return; - _waveIn = new WaveInEvent { WaveFormat = new WaveFormat(16000, 1) }; - _waveIn.DataAvailable += OnDataAvailable; - _waveIn.StartRecording(); - } + _soundIO = new SoundIO(); + _soundIO.Connect(); + _soundIO.FlushEvents(); - - public void StopRecording() - { - _waveIn?.StopRecording(); - _waveIn?.Dispose(); - _waveIn = null; - } - public event Action? OnRecognized; + int deviceIndex = _soundIO.DefaultInputDeviceIndex; + if (deviceIndex < 0) throw new Exception("Микрофон не найден."); - private void OnDataAvailable(object? sender, WaveInEventArgs e) - { - lock (_voskLock) - { - if (_rec != null && _rec.AcceptWaveform(e.Buffer, e.BytesRecorded)) + _device = _soundIO.GetInputDevice(deviceIndex); + _inStream = _device.CreateInStream(); + + _inStream.Format = SoundIOFormat.S16LE; + _inStream.SampleRate = 16000; + _inStream.Layout = SoundIOChannelLayout.GetDefault(1); + + // Привязываем методы к полям + _readCallback = OnDataAvailable; + _overflowCallback = () => Console.WriteLine("Audio Buffer Overflow"); + _errorCallback = () => Console.WriteLine("Audio Stream Error occurred"); + + _inStream.ReadCallback = _readCallback; + _inStream.OverflowCallback = _overflowCallback; + _inStream.ErrorCallback = _errorCallback; + + _inStream.Open(); + _inStream.Start(); + + _isRecording = true; + + // Поток обработки событий SoundIO + _eventThread = new Thread(() => { + while (_isRecording && _soundIO != null) { - var json = _rec.Result(); - using var doc = JsonDocument.Parse(json); - var result = doc.RootElement.GetProperty("text").GetString(); + try { _soundIO.WaitEvents(); } catch { break; } + } + }) { IsBackground = true, Name = "SoundIO_Wait" }; + _eventThread.Start(); - if (!string.IsNullOrWhiteSpace(result)) + // Поток обработки Vosk + _processingThread = new Thread(ProcessQueue) { + IsBackground = true, + Name = "Vosk_Worker" + }; + _processingThread.Start(); + } + + private unsafe void OnDataAvailable(int frameCountMin, int frameCountMax) + { + if (!_isRecording || _inStream == null) return; + + int frameCount = frameCountMax; + // Работаем с результатом BeginRead как с объектом SoundIOChannelAreas + var areas = _inStream.BeginRead(ref frameCount); + + if (frameCount <= 0) return; + + try + { + var area = areas.GetArea(0); + if (area.Pointer == IntPtr.Zero) return; + + int bytesNeeded = frameCount * 2; // 2 байта на семпл для S16LE + byte[] data = new byte[bytesNeeded]; + + fixed (byte* pDst = data) + { + short* pSrc = (short*)area.Pointer; + short* pDstShort = (short*)pDst; + int stepInShorts = area.Step / 2; + + for (int i = 0; i < frameCount; i++) { - RecognizedText += result + " "; - OnRecognized?.Invoke(result); // уведомляем подписчиков + pDstShort[i] = pSrc[i * stepInShorts]; } } + + // Отправляем в очередь и мгновенно освобождаем аудио-поток + _audioQueue.TryAdd(data); + } + catch (Exception ex) + { + Console.WriteLine("Error copying audio data: " + ex.Message); + } + finally + { + _inStream.EndRead(); + } + } + + private void ProcessQueue() + { + // GetConsumingEnumerable будет ждать появления данных в очереди + foreach (var data in _audioQueue.GetConsumingEnumerable()) + { + if (!_isRecording) break; + + try + { + bool isFinal; + lock (_rec) + { + isFinal = _rec.AcceptWaveform(data, data.Length); + } + + if (isFinal) + { + ParseAndSend(_rec.Result()); + } + } + catch (Exception ex) + { + Console.WriteLine("Vosk processing error: " + ex.Message); + } } } - - protected void Stop() + + private void ParseAndSend(string json) + { + if (string.IsNullOrWhiteSpace(json)) return; + try + { + using var doc = JsonDocument.Parse(json); + if (doc.RootElement.TryGetProperty("text", out var el)) + { + string text = el.GetString() ?? ""; + if (!string.IsNullOrWhiteSpace(text)) + { + RecognizedText += text + " "; + // Безопасный проброс в UI поток Avalonia + Dispatcher.UIThread.Post(() => OnRecognized?.Invoke(text)); + } + } + } + catch { /* JSON Parse Error */ } + } + + public void StopRecording() + { + if (!_isRecording) return; + _isRecording = false; + + _audioQueue.CompleteAdding(); + _soundIO?.Wakeup(); + + _inStream?.Dispose(); + _device?.RemoveReference(); + _soundIO?.Dispose(); + + _inStream = null; + _device = null; + _soundIO = null; + } + + public void Dispose() { StopRecording(); _rec?.Dispose(); _model?.Dispose(); - } } \ No newline at end of file diff --git a/VisionAsist/ViewModels/SettingsViewModel.cs b/VisionAsist/ViewModels/SettingsViewModel.cs index cf18439..d88080e 100644 --- a/VisionAsist/ViewModels/SettingsViewModel.cs +++ b/VisionAsist/ViewModels/SettingsViewModel.cs @@ -23,11 +23,11 @@ public class SettingsViewModel : ViewModelBase public SettingsViewModel() { - foreach (string Name in Core._loadedModules.Keys) + foreach (var module in Core.modulelist) { - AddModule(Name); + AddModule(module.Name); } } @@ -44,10 +44,10 @@ public class SettingsViewModel : ViewModelBase private void OpenSettings(string moduleName) { - var module = Core._loadedModules.Values.FirstOrDefault(m => m.Name == moduleName); + var module = Core.modulelist.FirstOrDefault(m => m.Name == moduleName); if (module != null) { - module.Execute("ShowWeather", new object[] { this }); + module.Module.Settings(new object[] { this }); } diff --git a/VisionAsist/VisionAsist.csproj b/VisionAsist/VisionAsist.csproj index d3498af..5a0c5e9 100644 --- a/VisionAsist/VisionAsist.csproj +++ b/VisionAsist/VisionAsist.csproj @@ -5,6 +5,8 @@ enable app.manifest true + LD_LIBRARY_PATH=/usr/lib $(RunCommand) + true @@ -22,6 +24,7 @@ All + @@ -29,4 +32,5 @@ +