llm-agent-backend/app/voice_service.py
2026-04-30 18:48:42 +03:00

137 lines
6.0 KiB
Python
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.

import os
import json
import threading
import datetime
import time
import numpy as np
from vosk import Model, KaldiRecognizer
import sounddevice as sd
class VoiceService:
def __init__(self, model_path, samplerate=48000):
self.model_path = model_path
self.samplerate = samplerate
self.model = Model(model_path)
self.is_running = False
self.current_file = None
self.last_processed_timestamp = 0.0
self.responses = {"regular": "", "commands": []}
self.lock = threading.Lock()
self._stop_event = threading.Event()
def get_last_chars(self, n):
"""Метод для получения последних N символов лога."""
if not self.current_file or not os.path.exists(self.current_file):
return "Лог-файл пуст или не создан."
try:
with self.lock:
with open(self.current_file, "r", encoding="utf-8") as f:
f.seek(0, os.SEEK_END)
size = f.tell()
# Читаем последние байты с запасом (UTF-8 до 4 байт на символ)
f.seek(max(0, size - n * 4))
content = f.read()
return content[-n:]
except Exception as e:
return f"Ошибка чтения: {str(e)}"
def start_session(self, continue_last=False):
if self.is_running: return
log_dir = "audio-logs"
if not os.path.exists(log_dir): os.makedirs(log_dir)
if continue_last:
files = sorted([f for f in os.listdir(log_dir) if f.endswith(".txt")])
self.current_file = os.path.join(log_dir, files[-1]) if files else self._create_new_file(log_dir)
else:
self.current_file = self._create_new_file(log_dir)
self.is_running = True
self._stop_event.clear()
threading.Thread(target=self._run_transcription, daemon=True).start()
def _create_new_file(self, log_dir):
name = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S.txt")
path = os.path.join(log_dir, name)
with open(path, "w", encoding="utf-8") as f:
f.write(f"--- Session Start: {name} ---\n\n")
return path
def stop_session(self):
self.is_running = False
self._stop_event.set()
def _run_transcription(self):
# 1. Поиск устройств (строго перед запуском потока)
id_gm, id_pcs = None, None
devices = sd.query_devices()
hostapis = sd.query_hostapis()
mme_api_index = next((i for i, h in enumerate(hostapis) if h['name'] == 'MME'), 0)
for i, d in enumerate(devices):
# Проверяем, что устройство относится к MME и содержит нужное имя
if d['hostapi'] == mme_api_index:
if "Voicemeeter Out B1" in d['name']: id_gm = i
if "Voicemeeter Out B2" in d['name']: id_pcs = i
if id_gm is None or id_pcs is None:
# Если MME не нашел, пробуем любой
for i, d in enumerate(devices):
if "Voicemeeter Out B1" in d['name']: id_gm = i
if "Voicemeeter Out B2" in d['name']: id_pcs = i
# 2. Создаем рекогнайзеры ВНУТРИ потока
rec_gm = KaldiRecognizer(self.model, self.samplerate)
rec_pcs = KaldiRecognizer(self.model, self.samplerate)
# 3. Фабрика колбэков
def create_callback(rec, role):
def callback_func(indata, frames, time_info, status):
if status:
print(f"Audio Status ({role}): {status}")
if np.max(np.abs(indata)) < 0.001:
return
# Важно: indata уже в формате bytes для RawInputStream
if rec.AcceptWaveform(bytes(indata)):
res = json.loads(rec.Result())
text = res.get('text', '')
if text:
now = datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3]
entry = f"[{now}] {role}: {text}\n"
# Пишем в файл
try:
with self.lock:
with open(self.current_file, "a", encoding="utf-8") as f:
f.write(entry)
except Exception as e:
print(f"File write error: {e}")
return callback_func
# 4. Запуск стримов
try:
# Используем те же параметры, что в рабочем демо
with sd.RawInputStream(samplerate=self.samplerate,
blocksize=8000,
device=id_gm,
dtype='int16',
channels=1,
callback=create_callback(rec_gm, "GM")), \
sd.RawInputStream(samplerate=self.samplerate,
blocksize=8000,
device=id_pcs,
dtype='int16',
channels=1,
callback=create_callback(rec_pcs, "PCs")):
print(f"✅ Транскрибация запущена (IDs: {id_gm}, {id_pcs})")
while not self._stop_event.is_set():
self._stop_event.wait(1.0) # Экономнее чем sleep
except Exception as e:
print(f"🔥 Критическая ошибка транскрибации: {e}")
finally:
self.is_running = False
print("🏁 Сессия транскрибации завершена")