Well Done

This commit is contained in:
2026-03-20 19:26:33 +02:00
parent f123690cb4
commit ac183f8eb6
9 changed files with 231 additions and 70 deletions

View File

@@ -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<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($"Модель не найдена по пути: {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<string>? 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();
}
}