Fixes associated to voice commands
This commit is contained in:
parent
606cef3040
commit
d9dc09bca9
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -8,4 +8,6 @@ __pycache__
|
||||||
*.db
|
*.db
|
||||||
*.db-journal
|
*.db-journal
|
||||||
|
|
||||||
build/
|
build/
|
||||||
|
|
||||||
|
audio-logs/
|
||||||
18
.vscode/launch.json
vendored
18
.vscode/launch.json
vendored
|
|
@ -22,10 +22,24 @@
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"python": "${workspaceFolder}/env/Scripts/python.exe",
|
"python": "${workspaceFolder}/env/Scripts/python.exe",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"program": "${workspaceFolder}/run-transcribe.py",
|
"program": "${workspaceFolder}/run-transcribe-parsing.py",
|
||||||
"justMyCode": true,
|
"justMyCode": true,
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"args": []
|
"args": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Python: Vosk Transcribe",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"python": "${workspaceFolder}/env/Scripts/python.exe",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"program": "${workspaceFolder}/run-transcribe-vosk.py",
|
||||||
|
"justMyCode": false,
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"env": {
|
||||||
|
"PYTHONUNBUFFERED": "1"
|
||||||
|
},
|
||||||
|
"args": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
42
app/api.py
42
app/api.py
|
|
@ -367,3 +367,45 @@ def regenerate_message():
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка при регенерации сообщения: {e}")
|
print(f"Ошибка при регенерации сообщения: {e}")
|
||||||
return jsonify({"error": str(e)}, 500)
|
return jsonify({"error": str(e)}, 500)
|
||||||
|
|
||||||
|
|
||||||
|
from app.voice_service import VoiceService
|
||||||
|
|
||||||
|
# 1. Создаем экземпляр (обязательно укажите путь к вашей модели Vosk)
|
||||||
|
voice_inst = VoiceService(model_path=r"D:\\Work\\Software_Development\\Projects\\vosk\\vosk-model-small-ru-0.22")
|
||||||
|
|
||||||
|
current_settings = {
|
||||||
|
'interval': 5,
|
||||||
|
'context_limit': 3000,
|
||||||
|
'voice_model': 'gemini-2.0-flash',
|
||||||
|
'marker_start': 'старт',
|
||||||
|
'marker_stop': 'стоп',
|
||||||
|
'voice_context_limit': 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.route('/api/voice/status', methods=['GET'])
|
||||||
|
def get_voice_status():
|
||||||
|
# 2. Вызываем метод у экземпляра voice_inst, а не у модуля
|
||||||
|
return jsonify({
|
||||||
|
"transcription": voice_inst.get_last_chars(3000),
|
||||||
|
"response1": voice_inst.responses["regular"],
|
||||||
|
"response2": voice_inst.responses["commands"][-5:],
|
||||||
|
"is_running": voice_inst.is_running
|
||||||
|
})
|
||||||
|
|
||||||
|
@api.route('/api/voice/control', methods=['POST'])
|
||||||
|
def control_voice():
|
||||||
|
action = request.json.get("action")
|
||||||
|
if action == "start": voice_inst.start_session(False)
|
||||||
|
elif action == "continue": voice_inst.start_session(True)
|
||||||
|
elif action == "stop": voice_inst.stop_session()
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
|
||||||
|
@api.route('/api/settings/sync', methods=['POST'])
|
||||||
|
def sync_settings():
|
||||||
|
global current_settings
|
||||||
|
data = request.json
|
||||||
|
if data:
|
||||||
|
current_settings.update(data)
|
||||||
|
print(f"⚙️ Settings synced: {current_settings}")
|
||||||
|
return jsonify({"status": "synced"})
|
||||||
|
|
@ -12,10 +12,12 @@ logger.setLevel(logging.INFO)
|
||||||
# Добавляем обработчик, если его еще нет (чтобы логи выводились в консоль)
|
# Добавляем обработчик, если его еще нет (чтобы логи выводились в консоль)
|
||||||
if not logger.handlers:
|
if not logger.handlers:
|
||||||
handler = logging.StreamHandler()
|
handler = logging.StreamHandler()
|
||||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
formatter = logging.Formatter(
|
||||||
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
|
||||||
class HeartbeatMonitor:
|
class HeartbeatMonitor:
|
||||||
"""
|
"""
|
||||||
Мониторит доступность заданных внешних эндпоинтов путем периодических HTTP-запросов.
|
Мониторит доступность заданных внешних эндпоинтов путем периодических HTTP-запросов.
|
||||||
|
|
@ -31,7 +33,9 @@ class HeartbeatMonitor:
|
||||||
self._running = False
|
self._running = False
|
||||||
self._thread = None
|
self._thread = None
|
||||||
self._last_check_results = {}
|
self._last_check_results = {}
|
||||||
logger.info(f"HeartbeatMonitor инициализирован с интервалом {interval_minutes} минут.")
|
logger.info(
|
||||||
|
f"HeartbeatMonitor инициализирован с интервалом {interval_minutes} минут."
|
||||||
|
)
|
||||||
logger.info(f"Мониторинг URL: {', '.join(self._target_urls)}")
|
logger.info(f"Мониторинг URL: {', '.join(self._target_urls)}")
|
||||||
|
|
||||||
# Вывод call stack
|
# Вывод call stack
|
||||||
|
|
@ -50,27 +54,50 @@ class HeartbeatMonitor:
|
||||||
for url in self._target_urls:
|
for url in self._target_urls:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, timeout=10) # Таймаут 10 секунд
|
response = requests.get(url,
|
||||||
|
timeout=10) # Таймаут 10 секунд
|
||||||
if 200 <= response.status_code < 300:
|
if 200 <= response.status_code < 300:
|
||||||
status = {"healthy": True, "status_code": response.status_code, "message": "OK"}
|
status = {
|
||||||
logger.info(f"✅ Heartbeat {url}: OK (HTTP {response.status_code})")
|
"healthy": True,
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"message": "OK"
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
f"✅ Heartbeat {url}: OK (HTTP {response.status_code})"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
status = {"healthy": False, "status_code": response.status_code, "message": f"Non-2xx status code"}
|
status = {
|
||||||
logger.warning(f"⚠️ Heartbeat {url}: Ошибка (HTTP {response.status_code})")
|
"healthy": False,
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"message": f"Non-2xx status code"
|
||||||
|
}
|
||||||
|
logger.warning(
|
||||||
|
f"⚠️ Heartbeat {url}: Ошибка (HTTP {response.status_code})"
|
||||||
|
)
|
||||||
overall_healthy = False
|
overall_healthy = False
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
status = {"healthy": False, "status_code": None, "message": f"Request failed: {e}"}
|
status = {
|
||||||
|
"healthy": False,
|
||||||
|
"status_code": None,
|
||||||
|
"message": f"Request failed: {e}"
|
||||||
|
}
|
||||||
logger.error(f"❌ Heartbeat {url}: Ошибка запроса: {e}")
|
logger.error(f"❌ Heartbeat {url}: Ошибка запроса: {e}")
|
||||||
overall_healthy = False
|
overall_healthy = False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
status = {"healthy": False, "status_code": None, "message": f"Unexpected error: {e}"}
|
status = {
|
||||||
logger.critical(f"🔥 Heartbeat {url}: Неожиданная ошибка: {e}")
|
"healthy": False,
|
||||||
|
"status_code": None,
|
||||||
|
"message": f"Unexpected error: {e}"
|
||||||
|
}
|
||||||
|
logger.critical(
|
||||||
|
f"🔥 Heartbeat {url}: Неожиданная ошибка: {e}")
|
||||||
overall_healthy = False
|
overall_healthy = False
|
||||||
finally:
|
finally:
|
||||||
status["timestamp"] = datetime.datetime.now().isoformat()
|
status["timestamp"] = datetime.datetime.now().isoformat()
|
||||||
status["response_time_ms"] = int((time.time() - start_time) * 1000)
|
status["response_time_ms"] = int(
|
||||||
|
(time.time() - start_time) * 1000)
|
||||||
current_results[url] = status
|
current_results[url] = status
|
||||||
|
|
||||||
self._last_check_results = current_results
|
self._last_check_results = current_results
|
||||||
if overall_healthy:
|
if overall_healthy:
|
||||||
logger.info("✔ Все Heartbeat-проверки успешно пройдены.")
|
logger.info("✔ Все Heartbeat-проверки успешно пройдены.")
|
||||||
|
|
@ -87,7 +114,8 @@ class HeartbeatMonitor:
|
||||||
self._running = True
|
self._running = True
|
||||||
# daemon=True позволяет приложению завершиться, даже если этот поток еще работает.
|
# daemon=True позволяет приложению завершиться, даже если этот поток еще работает.
|
||||||
# Python автоматически завершит daemon-потоки при выходе из основной программы.
|
# Python автоматически завершит daemon-потоки при выходе из основной программы.
|
||||||
self._thread = threading.Thread(target=self._run_heartbeat_loop, daemon=True)
|
self._thread = threading.Thread(target=self._run_heartbeat_loop,
|
||||||
|
daemon=True)
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
logger.info("HeartbeatMonitor запущен.")
|
logger.info("HeartbeatMonitor запущен.")
|
||||||
else:
|
else:
|
||||||
|
|
@ -101,9 +129,12 @@ class HeartbeatMonitor:
|
||||||
logger.info("Остановка HeartbeatMonitor...")
|
logger.info("Остановка HeartbeatMonitor...")
|
||||||
self._running = False
|
self._running = False
|
||||||
if self._thread and self._thread.is_alive():
|
if self._thread and self._thread.is_alive():
|
||||||
self._thread.join(timeout=5) # Даем потоку 5 секунд на завершение
|
self._thread.join(
|
||||||
|
timeout=5) # Даем потоку 5 секунд на завершение
|
||||||
if self._thread.is_alive():
|
if self._thread.is_alive():
|
||||||
logger.warning("HeartbeatMonitor поток не завершился в течение таймаута.")
|
logger.warning(
|
||||||
|
"HeartbeatMonitor поток не завершился в течение таймаута."
|
||||||
|
)
|
||||||
logger.info("HeartbeatMonitor остановлен.")
|
logger.info("HeartbeatMonitor остановлен.")
|
||||||
else:
|
else:
|
||||||
logger.warning("HeartbeatMonitor не был запущен.")
|
logger.warning("HeartbeatMonitor не был запущен.")
|
||||||
|
|
@ -113,3 +144,65 @@ class HeartbeatMonitor:
|
||||||
Возвращает результаты последней проверки (для возможного использования внутри приложения).
|
Возвращает результаты последней проверки (для возможного использования внутри приложения).
|
||||||
"""
|
"""
|
||||||
return self._last_check_results
|
return self._last_check_results
|
||||||
|
|
||||||
|
|
||||||
|
class VoiceHeartbeatMonitor(threading.Thread):
|
||||||
|
|
||||||
|
def __init__(self, voice_instance, generator, settings_fn):
|
||||||
|
super().__init__(daemon=True)
|
||||||
|
self.vs = voice_instance # Тот самый экземпляр из api.py
|
||||||
|
self.generator = generator
|
||||||
|
self.get_settings = settings_fn # Функция, возвращающая текущий конфиг
|
||||||
|
self.last_regular_run = time.time()
|
||||||
|
self.last_command_time = ""
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while True:
|
||||||
|
if self.vs.is_running:
|
||||||
|
s = self.get_settings()
|
||||||
|
# 1. Проверка команд <start>...<stop>
|
||||||
|
self._check_commands(s)
|
||||||
|
|
||||||
|
# 2. Регулярный запрос
|
||||||
|
if time.time() - self.last_regular_run > s['interval'] * 60:
|
||||||
|
self._run_regular_analysis(s)
|
||||||
|
self.last_regular_run = time.time()
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
def _check_commands(self, s):
|
||||||
|
# Получаем данные за последние секунды
|
||||||
|
raw_text = self.vs.get_last_chars(s['context_limit'])
|
||||||
|
|
||||||
|
import re
|
||||||
|
# Чистим таймстемпы Vosk для LLM (удаляем паттерны [00:00:00])
|
||||||
|
clean_text = re.sub(r'\[.*?\]', '', raw_text)
|
||||||
|
|
||||||
|
pattern = f"{re.escape(s['marker_start'])}(.*?){re.escape(s['marker_stop'])}"
|
||||||
|
matches = re.findall(pattern, clean_text, re.DOTALL)
|
||||||
|
|
||||||
|
for match in matches:
|
||||||
|
cmd = match.strip()
|
||||||
|
# Проверяем, не обрабатывали ли мы ЭТУ конкретную строку только что
|
||||||
|
# Используем множество для хранения хешей обработанных команд
|
||||||
|
if not hasattr(self, '_processed_hashes'): self._processed_hashes = set()
|
||||||
|
|
||||||
|
cmd_hash = hash(cmd)
|
||||||
|
if cmd_hash not in self._processed_hashes:
|
||||||
|
print(f"🎤 New Voice Command detected: {cmd}")
|
||||||
|
|
||||||
|
# ОТПРАВЛЯЕМ В LLM через TitleGenerator
|
||||||
|
self.generator.add_voice_task_to_queue(
|
||||||
|
cmd,
|
||||||
|
s['prompt_commands'] # Это путь к .md файлу из настроек
|
||||||
|
)
|
||||||
|
|
||||||
|
self._processed_hashes.add(cmd_hash)
|
||||||
|
# Ограничиваем размер множества, чтобы не росло бесконечно
|
||||||
|
if len(self._processed_hashes) > 100: self._processed_hashes.clear()
|
||||||
|
|
||||||
|
def _run_regular_analysis(self, s):
|
||||||
|
raw_text = self.vs.get_last_chars(s['voice_context_limit'])
|
||||||
|
if not raw_text.strip(): return
|
||||||
|
|
||||||
|
# Добавим новый тип 'voice_regular' вTitleGenerator аналогично 'voice_command'
|
||||||
|
self.generator.add_voice_regular_task(raw_text, s['prompt_regular'])
|
||||||
|
|
@ -88,6 +88,19 @@ MODELS: Dict[str, Dict[str, Any]] = {
|
||||||
"stream": True,
|
"stream": True,
|
||||||
"capabilities": ["vision"],
|
"capabilities": ["vision"],
|
||||||
},
|
},
|
||||||
|
"gemini-3.0-flash-openrouter": {
|
||||||
|
"name": "google/gemini-3-flash-preview",
|
||||||
|
"provider": "openai",
|
||||||
|
"model_name": "google/gemini-3-flash-preview",
|
||||||
|
"apiBase": "https://openrouter.ai/api/v1", # Добавлено /api/v1
|
||||||
|
"apiKey":
|
||||||
|
"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",
|
||||||
|
"stream": True,
|
||||||
|
"capabilities": ["vision", "reasoning"],
|
||||||
|
"model_kwargs": {
|
||||||
|
"include_reasoning": True
|
||||||
|
}
|
||||||
|
},
|
||||||
"gemini-3.1-pro-preview-bh": {
|
"gemini-3.1-pro-preview-bh": {
|
||||||
"name": "gemini-3.1-pro-preview",
|
"name": "gemini-3.1-pro-preview",
|
||||||
"provider": "openai", # Изменено на "openai"
|
"provider": "openai", # Изменено на "openai"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from typing import Any, Dict, Optional
|
||||||
from nodes import DEFAULT_SUMMARIZATION_LLM_NAME
|
from nodes import DEFAULT_SUMMARIZATION_LLM_NAME
|
||||||
from llm_client import get_llm
|
from llm_client import get_llm
|
||||||
from langchain_core.messages import SystemMessage, HumanMessage
|
from langchain_core.messages import SystemMessage, HumanMessage
|
||||||
|
import os
|
||||||
|
|
||||||
MAX_TITLE_GENERATION_CONTENT_LENGTH = 5000 # Максимальное количество символов для генерации заголовков
|
MAX_TITLE_GENERATION_CONTENT_LENGTH = 5000 # Максимальное количество символов для генерации заголовков
|
||||||
|
|
||||||
|
|
@ -128,6 +129,11 @@ class TitleGenerator:
|
||||||
self._generate_graph_title(item["graph_id"])
|
self._generate_graph_title(item["graph_id"])
|
||||||
elif item["item_type"] == "node":
|
elif item["item_type"] == "node":
|
||||||
self._generate_node_title(item["graph_id"], item["node_id"])
|
self._generate_node_title(item["graph_id"], item["node_id"])
|
||||||
|
# ОБРАБОТКА ГОЛОСА
|
||||||
|
elif item["item_type"] == "voice_command":
|
||||||
|
self._process_voice_command(item["node_id"], item["graph_id"])
|
||||||
|
elif item["item_type"] == "voice_regular":
|
||||||
|
self._process_voice_regular(item["node_id"], item["graph_id"])
|
||||||
else:
|
else:
|
||||||
# Если очередь пуста, ждем немного
|
# Если очередь пуста, ждем немного
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
@ -337,4 +343,66 @@ class TitleGenerator:
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
if not ack_received.is_set():
|
if not ack_received.is_set():
|
||||||
print(f"❌ Не удалось доставить {event_name} после {max_retries} попыток: {data}")
|
print(f"❌ Не удалось доставить {event_name} после {max_retries} попыток: {data}")
|
||||||
|
|
||||||
|
|
||||||
|
# VOICE
|
||||||
|
|
||||||
|
def add_voice_task_to_queue(self, command_text, prompt, priority=20):
|
||||||
|
"""Добавляет задачу на обработку голосовой команды в очередь."""
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
# Используем node_id для хранения текста команды,
|
||||||
|
# а item_type 'voice_command' для идентификации
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO title_generation_queue (item_type, graph_id, node_id, priority) VALUES (?, ?, ?, ?)",
|
||||||
|
("voice_command", prompt, command_text, priority))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# 2. Добавьте метод постановки регулярной задачи
|
||||||
|
def add_voice_regular_task(self, transcription_text, prompt, priority=15):
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO title_generation_queue (item_type, graph_id, node_id, priority) VALUES (?, ?, ?, ?)",
|
||||||
|
("voice_regular", prompt, transcription_text, priority))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# 3. Добавьте метод обработки регулярной задачи
|
||||||
|
def _process_voice_regular(self, text, prompt):
|
||||||
|
try:
|
||||||
|
print(f"📡 Анализ транскрибации (Prompt 1)...")
|
||||||
|
llm_messages = [
|
||||||
|
SystemMessage(content=prompt),
|
||||||
|
HumanMessage(content=f"Последний фрагмент диалога для анализа: {text}")
|
||||||
|
]
|
||||||
|
response = self.llm.invoke(llm_messages)
|
||||||
|
|
||||||
|
import app.api as api
|
||||||
|
api.voice_inst.responses["regular"] = response # Записываем в Response 1
|
||||||
|
print(f"✅ Регулярный анализ завершен.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка регулярного анализа: {e}")
|
||||||
|
|
||||||
|
# 4. Поправьте _process_voice_command (запись команды в Response 2)
|
||||||
|
def _process_voice_command(self, text, prompt):
|
||||||
|
try:
|
||||||
|
print(f"🤖 Обработка голосовой команды (Prompt 2)...")
|
||||||
|
llm_messages = [
|
||||||
|
SystemMessage(content=prompt),
|
||||||
|
HumanMessage(content=f"Выполни команду из транскрибации: {text}")
|
||||||
|
]
|
||||||
|
response = self.llm.invoke(llm_messages)
|
||||||
|
|
||||||
|
import app.api as api
|
||||||
|
# Форматируем для вкладки Response 2
|
||||||
|
formatted_res = f"<b>Команда:</b> {text}<br><b>Ответ:</b> {response}"
|
||||||
|
api.voice_inst.responses["commands"].append(formatted_res)
|
||||||
|
|
||||||
|
# Ограничиваем историю команд, чтобы не раздувать память
|
||||||
|
if len(api.voice_inst.responses["commands"]) > 20:
|
||||||
|
api.voice_inst.responses["commands"].pop(0)
|
||||||
|
|
||||||
|
print(f"✅ Команда обработана.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка обработки команды: {e}")
|
||||||
137
app/voice_service.py
Normal file
137
app/voice_service.py
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
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("🏁 Сессия транскрибации завершена")
|
||||||
|
|
@ -12,4 +12,8 @@ flask_socketio
|
||||||
|
|
||||||
mistralai==1.9.10
|
mistralai==1.9.10
|
||||||
|
|
||||||
selenium
|
selenium
|
||||||
|
|
||||||
|
vosk==0.3.45
|
||||||
|
sounddevice==0.5.1
|
||||||
|
numpy==1.26.4
|
||||||
14
run.py
14
run.py
|
|
@ -12,8 +12,9 @@ logger = logging.getLogger(__name__)
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Добавляем папку app в Python path
|
# Добавляем папку app в Python path
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app'))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app'))
|
||||||
from app.api import api
|
from app.api import api, voice_inst, current_settings
|
||||||
from app.heartbeat_monitor import HeartbeatMonitor
|
from app.heartbeat_monitor import HeartbeatMonitor
|
||||||
|
from app.workflows import graph_history_manager
|
||||||
|
|
||||||
logger.info("🚀 Запуск LLM Agent Backend сервера...")
|
logger.info("🚀 Запуск LLM Agent Backend сервера...")
|
||||||
logger.info("📍 API будет доступно на: http://localhost:5000/api")
|
logger.info("📍 API будет доступно на: http://localhost:5000/api")
|
||||||
|
|
@ -31,6 +32,17 @@ if __name__ == "__main__":
|
||||||
atexit.register(heartbeat_monitor.stop)
|
atexit.register(heartbeat_monitor.stop)
|
||||||
logger.info("HeartbeatMonitor запущен.")
|
logger.info("HeartbeatMonitor запущен.")
|
||||||
|
|
||||||
|
|
||||||
|
from app.heartbeat_monitor import VoiceHeartbeatMonitor
|
||||||
|
|
||||||
|
v_monitor = VoiceHeartbeatMonitor(
|
||||||
|
voice_inst,
|
||||||
|
graph_history_manager.title_generator,
|
||||||
|
lambda: current_settings # Теперь берет настройки из api.py
|
||||||
|
)
|
||||||
|
v_monitor.start()
|
||||||
|
|
||||||
|
|
||||||
api.run(
|
api.run(
|
||||||
debug=True,
|
debug=True,
|
||||||
port=5000,
|
port=5000,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user