This commit is contained in:
@@ -11,7 +11,7 @@ public class WeatherModule : IModule
|
|||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
public string Name => "Прогноз Погоды";
|
public string Name => "Модуль компиляции ардуино";
|
||||||
|
|
||||||
public string[] GetCommands() => new[] { "погода", "поверни на *", "верни *" };
|
public string[] GetCommands() => new[] { "погода", "поверни на *", "верни *" };
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using VisionAsist.SDK;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using VisionAsist.SDK;
|
using VisionAsist.SDK;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace VisionAsist.Models;
|
namespace VisionAsist.Models;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public class Core
|
public class Core
|
||||||
{
|
{
|
||||||
|
|
||||||
public class Modules
|
public class Modules
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
@@ -21,17 +17,23 @@ public class Core
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static List<Modules> modulelist = new();
|
public static List<Modules> modulelist = new();
|
||||||
public static TrigerCore triger = new();
|
|
||||||
public static string TextAsist;
|
|
||||||
static string Plugin = Path.Combine(AppContext.BaseDirectory, "Modules");
|
static string Plugin = Path.Combine(AppContext.BaseDirectory, "Modules");
|
||||||
|
|
||||||
static Core()
|
static Core()
|
||||||
{
|
{
|
||||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||||
Console.InputEncoding = System.Text.Encoding.UTF8;
|
Console.InputEncoding = System.Text.Encoding.UTF8;
|
||||||
|
|
||||||
|
if (!Directory.Exists(Plugin))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Plugin);
|
||||||
|
}
|
||||||
|
|
||||||
string[] folderNames = new DirectoryInfo(Plugin)
|
string[] folderNames = new DirectoryInfo(Plugin)
|
||||||
.GetDirectories()
|
.GetDirectories()
|
||||||
.Select(d => d.Name)
|
.Select(d => d.Name)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
foreach (string folderName in folderNames)
|
foreach (string folderName in folderNames)
|
||||||
{
|
{
|
||||||
string mpn = Path.Combine(Plugin, folderName, "Module.dll");
|
string mpn = Path.Combine(Plugin, folderName, "Module.dll");
|
||||||
@@ -43,37 +45,13 @@ public class Core
|
|||||||
if (type != null)
|
if (type != null)
|
||||||
{
|
{
|
||||||
var module = (IModule)Activator.CreateInstance(type)!;
|
var module = (IModule)Activator.CreateInstance(type)!;
|
||||||
modulelist.Add(new Modules{Name = folderName, Module = module, commands = module.GetCommands()});
|
modulelist.Add(new Modules { Name = module.Name, Module = module, commands = module.GetCommands() });
|
||||||
foreach (var cmd in module.GetCommands())
|
foreach (var cmd in module.GetCommands())
|
||||||
{
|
{
|
||||||
Console.WriteLine($"- {cmd}");
|
Console.WriteLine($"- {cmd}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
public static void StartListing()
|
|
||||||
{
|
|
||||||
// Подписываемся на событие новых слов
|
|
||||||
triger.OnRecognized += word =>
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
TextAsist = triger.RecognizedText;
|
|
||||||
Selector.selector(triger.RecognizedText);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
// Запускаем запись
|
|
||||||
triger.StartRecording();
|
|
||||||
}
|
|
||||||
|
|
||||||
static public async void StopListing ()
|
|
||||||
{
|
|
||||||
triger.StopRecording();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using Vosk;
|
|
||||||
using SoundIOSharp;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
|
|
||||||
namespace VisionAsist.Models;
|
|
||||||
|
|
||||||
public class TrigerCore : IDisposable
|
|
||||||
{
|
|
||||||
private SoundIO? _soundIO;
|
|
||||||
private SoundIODevice? _device;
|
|
||||||
private SoundIOInStream? _inStream;
|
|
||||||
|
|
||||||
private Thread? _eventThread;
|
|
||||||
private Thread? _processingThread;
|
|
||||||
private bool _isRecording;
|
|
||||||
|
|
||||||
private readonly Model _model;
|
|
||||||
private readonly VoskRecognizer _rec;
|
|
||||||
|
|
||||||
private readonly BlockingCollection<byte[]> _audioQueue = new(100);
|
|
||||||
|
|
||||||
public string RecognizedText { get; private set; } = "";
|
|
||||||
public event Action<string>? OnRecognized;
|
|
||||||
|
|
||||||
// Сохраняем ссылки в полях класса, чтобы GC не удалил их в Debug-режиме
|
|
||||||
private Action<int, int>? _readCallback;
|
|
||||||
private Action? _overflowCallback;
|
|
||||||
private Action? _errorCallback;
|
|
||||||
|
|
||||||
public TrigerCore()
|
|
||||||
{
|
|
||||||
string voskPath = Path.Combine(AppContext.BaseDirectory, "models", "Vosk");
|
|
||||||
if (!Directory.Exists(voskPath))
|
|
||||||
throw new DirectoryNotFoundException($"Модель Vosk не найдена: {voskPath}");
|
|
||||||
|
|
||||||
_model = new Model(voskPath);
|
|
||||||
_rec = new VoskRecognizer(_model, 16000.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartRecording()
|
|
||||||
{
|
|
||||||
if (_isRecording) return;
|
|
||||||
|
|
||||||
_soundIO = new SoundIO();
|
|
||||||
_soundIO.Connect();
|
|
||||||
_soundIO.FlushEvents();
|
|
||||||
|
|
||||||
int deviceIndex = _soundIO.DefaultInputDeviceIndex;
|
|
||||||
if (deviceIndex < 0) throw new Exception("Микрофон не найден.");
|
|
||||||
|
|
||||||
_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)
|
|
||||||
{
|
|
||||||
try { _soundIO.WaitEvents(); } catch { break; }
|
|
||||||
}
|
|
||||||
}) { IsBackground = true, Name = "SoundIO_Wait" };
|
|
||||||
_eventThread.Start();
|
|
||||||
|
|
||||||
// Поток обработки 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++)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using VisionAsist.Models;
|
using VisionAsist.Models;
|
||||||
@@ -15,23 +15,21 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
DataContext = new SettingsViewModel()
|
DataContext = new SettingsViewModel()
|
||||||
}.Show();
|
}.Show();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool isListening;
|
private string commandLog = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string recognizedtext;
|
private string commandText = string.Empty;
|
||||||
[ObservableProperty]
|
|
||||||
private string commandText;
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void Send()
|
private void Send()
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(CommandText))
|
if (!string.IsNullOrWhiteSpace(CommandText))
|
||||||
{
|
{
|
||||||
// Эмулируем распознанный текст для отображения в логе
|
// Отображаем введенную команду в логе
|
||||||
Recognizedtext = CommandText;
|
CommandLog = CommandText;
|
||||||
|
|
||||||
// Отправляем в селектор
|
// Отправляем в селектор
|
||||||
Selector.selector(CommandText);
|
Selector.selector(CommandText);
|
||||||
@@ -40,37 +38,4 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
CommandText = string.Empty;
|
CommandText = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Action<string>? _coreHandler;
|
|
||||||
|
|
||||||
partial void OnIsListeningChanged(bool value)
|
|
||||||
{
|
|
||||||
if (value)
|
|
||||||
{
|
|
||||||
Core.StartListing();
|
|
||||||
|
|
||||||
// Сохраняем ссылку на обработчик
|
|
||||||
_coreHandler = word =>
|
|
||||||
{
|
|
||||||
|
|
||||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
Recognizedtext = Core.TextAsist;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Core.triger.OnRecognized += _coreHandler;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Core.StopListing();
|
|
||||||
|
|
||||||
// Правильная отписка
|
|
||||||
if (_coreHandler != null)
|
|
||||||
{
|
|
||||||
Core.triger.OnRecognized -= _coreHandler;
|
|
||||||
_coreHandler = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -10,18 +10,12 @@
|
|||||||
Title="VisionAsist" Height="400" Width="600">
|
Title="VisionAsist" Height="400" Width="600">
|
||||||
|
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<!-- This only sets the DataContext for the previewer in an IDE,
|
|
||||||
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
|
|
||||||
<vm:MainWindowViewModel/>
|
<vm:MainWindowViewModel/>
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
|
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" Margin="10">
|
<StackPanel Orientation="Horizontal" Margin="10">
|
||||||
<Button Content="Настройки" Command="{Binding SettingseCommand}"/>
|
<Button Content="Настройки" Command="{Binding SettingseCommand}"/>
|
||||||
<ToggleButton IsChecked="{Binding IsListening, Mode=TwoWay}"
|
|
||||||
Content="Запуск асистента"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Margin="10,0"
|
|
||||||
/>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*, Auto" Margin="10,0,10,10">
|
<Grid ColumnDefinitions="*, Auto" Margin="10,0,10,10">
|
||||||
@@ -40,13 +34,10 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<ScrollViewer Height="300" CornerRadius="5" Background="Black">
|
<ScrollViewer Height="300" CornerRadius="5" Background="Black">
|
||||||
<TextBlock Text="{Binding Recognizedtext}"
|
<TextBlock Text="{Binding CommandLog}"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
Padding="10"
|
Padding="10"
|
||||||
/>
|
Foreground="White"/>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</Window>
|
</Window>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
<RunCommand>LD_LIBRARY_PATH=/usr/lib $(RunCommand)</RunCommand>
|
<RunCommand>LD_LIBRARY_PATH=/usr/lib $(RunCommand)</RunCommand>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -24,13 +23,9 @@
|
|||||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
<PackageReference Include="libsoundio-sharp-net" Version="1.0.0" />
|
|
||||||
<PackageReference Include="NAudio" Version="2.3.0" />
|
|
||||||
<PackageReference Include="Vosk" Version="0.3.38" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\VisionAsist.SDK\VisionAsist.SDK.csproj" />
|
<ProjectReference Include="..\VisionAsist.SDK\VisionAsist.SDK.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
Reference in New Issue
Block a user