no voice
Some checks failed
Mirror to Gitea / git-sync (push) Has been cancelled

This commit is contained in:
2026-03-27 16:37:05 +02:00
parent 635dacb2ad
commit ea2d84f5cc
7 changed files with 28 additions and 306 deletions

View File

@@ -11,7 +11,7 @@ public class WeatherModule : IModule
}
public string Name => "Прогноз Погоды";
public string Name => "Модуль компиляции ардуино";
public string[] GetCommands() => new[] { "погода", "поверни на *", "верни *" };

View File

@@ -1,18 +1,14 @@
using System;
using System;
using System.IO;
using System.Reflection;
using VisionAsist.SDK;
using System.Collections.Generic;
using VisionAsist.SDK;
using System.Linq;
namespace VisionAsist.Models;
public class Core
{
public class Modules
{
public string Name { get; set; }
@@ -21,17 +17,23 @@ public class Core
}
public static List<Modules> modulelist = new();
public static TrigerCore triger = new();
public static string TextAsist;
static string Plugin = Path.Combine(AppContext.BaseDirectory, "Modules");
static Core()
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.InputEncoding = System.Text.Encoding.UTF8;
if (!Directory.Exists(Plugin))
{
Directory.CreateDirectory(Plugin);
}
string[] folderNames = new DirectoryInfo(Plugin)
.GetDirectories()
.Select(d => d.Name)
.ToArray();
foreach (string folderName in folderNames)
{
string mpn = Path.Combine(Plugin, folderName, "Module.dll");
@@ -43,37 +45,13 @@ public class Core
if (type != null)
{
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())
{
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();
}
}

View File

@@ -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();
}
}

View File

@@ -18,4 +18,4 @@ sealed class Program
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using VisionAsist.Models;
@@ -15,23 +15,21 @@ public partial class MainWindowViewModel : ViewModelBase
{
DataContext = new SettingsViewModel()
}.Show();
}
[ObservableProperty]
private bool isListening;
private string commandLog = string.Empty;
[ObservableProperty]
private string recognizedtext;
[ObservableProperty]
private string commandText;
private string commandText = string.Empty;
[RelayCommand]
private void Send()
{
if (!string.IsNullOrWhiteSpace(CommandText))
{
// Эмулируем распознанный текст для отображения в логе
Recognizedtext = CommandText;
// Отображаем введенную команду в логе
CommandLog = CommandText;
// Отправляем в селектор
Selector.selector(CommandText);
@@ -40,37 +38,4 @@ public partial class MainWindowViewModel : ViewModelBase
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;
}
}
}
}

View File

@@ -10,18 +10,12 @@
Title="VisionAsist" Height="400" Width="600">
<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/>
</Design.DataContext>
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="10">
<Button Content="Настройки" Command="{Binding SettingseCommand}"/>
<ToggleButton IsChecked="{Binding IsListening, Mode=TwoWay}"
Content="Запуск асистента"
HorizontalAlignment="Center"
Margin="10,0"
/>
</StackPanel>
<Grid ColumnDefinitions="*, Auto" Margin="10,0,10,10">
@@ -39,14 +33,11 @@
Margin="5,0,0,0"/>
</Grid>
<ScrollViewer Height="300" CornerRadius="5" Background="Black">
<TextBlock Text="{Binding Recognizedtext}"
TextWrapping="Wrap"
Padding="10"
/>
<ScrollViewer Height="300" CornerRadius="5" Background="Black">
<TextBlock Text="{Binding CommandLog}"
TextWrapping="Wrap"
Padding="10"
Foreground="White"/>
</ScrollViewer>
</StackPanel>
</Window>
</Window>

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
@@ -6,7 +6,6 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<RunCommand>LD_LIBRARY_PATH=/usr/lib $(RunCommand)</RunCommand>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
@@ -24,13 +23,9 @@
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<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>
<ProjectReference Include="..\VisionAsist.SDK\VisionAsist.SDK.csproj" />
</ItemGroup>
</Project>
</Project>