137 lines
6.0 KiB
Python
137 lines
6.0 KiB
Python
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("🏁 Сессия транскрибации завершена") |