Files
Vision/VisionAsist/Models/TrigerCore.cs
2026-03-20 19:26:33 +02:00

207 lines
6.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}