Fix settings sync

This commit is contained in:
dimitrievgs 2026-05-01 20:29:50 +03:00
parent 19b034e4ea
commit a737c3125a
6 changed files with 66 additions and 42 deletions

View File

@ -369,12 +369,12 @@ def regenerate_message():
return jsonify({"error": str(e)}, 500)
from app.voice_service import VoiceService
from app.voice_service import VoiceService, VOICE_COMMANDS_RESPONSE_TO_STORE
# 1. Создаем экземпляр (обязательно укажите путь к вашей модели Vosk)
voice_inst = VoiceService(model_path=r"D:\\Work\\Software_Development\\Projects\\vosk\\vosk-model-small-ru-0.22")
current_settings = {
current_obsidian_settings = {
'interval': 5,
'context_limit': 3000,
'voice_model': 'gemini-2.0-flash',
@ -383,14 +383,17 @@ current_settings = {
'voice_context_limit': 3000
}
obsidian_settings_fetched = False
@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
"response2": voice_inst.responses["commands"][:VOICE_COMMANDS_RESPONSE_TO_STORE],
"is_running": voice_inst.is_running,
"obsidian_settings_fetched": obsidian_settings_fetched
})
@api.route('/api/voice/control', methods=['POST'])
@ -403,9 +406,10 @@ def control_voice():
@api.route('/api/settings/sync', methods=['POST'])
def sync_settings():
global current_settings
global current_obsidian_settings, obsidian_settings_fetched
obsidian_settings_fetched = True
data = request.json
if data:
current_settings.update(data)
print(f"⚙️ Settings synced: {current_settings}")
current_obsidian_settings.update(data)
print(f"⚙️ Settings synced: {current_obsidian_settings}")
return jsonify({"status": "synced"})

View File

@ -148,36 +148,39 @@ class HeartbeatMonitor:
class VoiceHeartbeatMonitor(threading.Thread):
def __init__(self, voice_instance, generator, settings_fn):
def __init__(self, voice_instance, generator, get_obsidian_settings_fn):
super().__init__(daemon=True)
self.vs = voice_instance # Тот самый экземпляр из api.py
self.generator = generator
self.get_settings = settings_fn # Функция, возвращающая текущий конфиг
self.get_obsidian_settings = get_obsidian_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)
try:
if self.vs.is_running:
obsidian_settings = self.get_obsidian_settings()
# 1. Проверка команд <start>...<stop>
self._check_commands(obsidian_settings)
# 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)
# 2. Регулярный запрос
if time.time() - self.last_regular_run > obsidian_settings['interval'] * 60:
self._run_regular_analysis(obsidian_settings)
self.last_regular_run = time.time()
time.sleep(3) # каждые 3 секунды проверяем
except Exception as e:
logger.critical(f"❌🎤 Voice Heartbeat: Неожиданная ошибка: {e}")
def _check_commands(self, s):
def _check_commands(self, obsidian_settings):
# Получаем данные за последние секунды
raw_text = self.vs.get_last_chars(s['context_limit'])
raw_text = self.vs.get_last_chars(obsidian_settings['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'])}"
pattern = f"{re.escape(obsidian_settings['marker_start'])}(.*?){re.escape(obsidian_settings['marker_stop'])}"
matches = re.findall(pattern, clean_text, re.DOTALL)
for match in matches:
@ -191,7 +194,7 @@ class VoiceHeartbeatMonitor(threading.Thread):
print(f"🎤 New Voice Command detected: {cmd}")
# ОТПРАВЛЯЕМ В LLM через TitleGenerator
prompt = f"{(s.get('systemPrompt') or '')}\n\n{(s.get('prompt_commands') or '')}"
prompt = f"{(obsidian_settings.get('systemPrompt') or '')}\n\n{(obsidian_settings.get('prompt_commands') or '')}"
self.generator.add_voice_task_to_queue(
cmd,
prompt
@ -199,13 +202,13 @@ class VoiceHeartbeatMonitor(threading.Thread):
self._processed_hashes.add(cmd_hash)
# Ограничиваем размер множества, чтобы не росло бесконечно
if len(self._processed_hashes) > 100: self._processed_hashes.clear()
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'])
def _run_regular_analysis(self, obsidian_settings):
raw_text = self.vs.get_last_chars(obsidian_settings['voice_context_limit'])
if not raw_text.strip(): return
prompt = f"{(s.get('systemPrompt') or '')}\n\n{(s.get('prompt_regular') or '')}"
prompt = f"{(obsidian_settings.get('systemPrompt') or '')}\n\n{(obsidian_settings.get('prompt_regular') or '')}"
# Добавим новый тип 'voice_regular' вTitleGenerator аналогично 'voice_command'
self.generator.add_voice_regular_task(raw_text, prompt)

View File

@ -44,6 +44,19 @@ MODELS: Dict[str, Dict[str, Any]] = {
"stream": True,
"capabilities": ["vision"],
},
"gemini-2.0-flash-lite": {
"name": "google/gemini-2.0-flash-lite-001",
"provider": "openai",
"model_name": "google/gemini-2.0-flash-lite-001",
"apiBase": "https://openrouter.ai/api/v1", # Добавлено /api/v1
"apiKey":
"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",
"stream": True,
"capabilities": ["vision", "reasoning"],
"model_kwargs": {
"include_reasoning": True
}
},
"gemini-2.5-flash-r": {
"name": "gemini-2.5-flash-r",
"provider": "openai", # Изменено на "openai"

View File

@ -11,11 +11,13 @@ from llm_client import get_llm
from langchain_core.messages import SystemMessage, HumanMessage
import os
DEFAULT_SUMMARIZATION_LLM_NAME = "mistral-small-latest" # "mistral-small-latest" #"gemini-2.0-flash-r"
DEFAULT_VOICE_LLM_NAME = "gemini-3.0-flash-openrouter"
DEFAULT_SUMMARIZATION_LLM_NAME = "gemini-2.0-flash-lite" # "mistral-small-latest" # "mistral-small-latest" #"gemini-2.0-flash-r"
DEFAULT_VOICE_LLM_NAME = "gemini-2.0-flash-lite" # "gemini-3.0-flash-openrouter"
MAX_TITLE_GENERATION_CONTENT_LENGTH = 5000 # Максимальное количество символов для генерации заголовков
from app.voice_service import VOICE_COMMANDS_RESPONSE_TO_STORE
class TitleGenerator:
"""Сервис для асинхронной генерации заголовков."""
@ -374,7 +376,7 @@ class TitleGenerator:
# 3. Добавьте метод обработки регулярной задачи
def _process_voice_regular(self, text, prompt):
try:
print(f"📡 Анализ транскрибации (Prompt 1)...")
print(f"🎤 Анализ транскрибации (Prompt 1)...")
llm_messages = [
SystemMessage(content=prompt),
HumanMessage(content=f"Последний фрагмент диалога для анализа: {text}")
@ -383,14 +385,14 @@ class TitleGenerator:
import app.api as api
api.voice_inst.responses["regular"] = response # Записываем в Response 1
print(f" Регулярный анализ завершен.")
print(f"🎤 Регулярный анализ голосового ввода завершен.")
except Exception as e:
print(f"Ошибка регулярного анализа: {e}")
print(f"❌🎤 Ошибка регулярного анализа: {e}")
# 4. Поправьте _process_voice_command (запись команды в Response 2)
def _process_voice_command(self, text, prompt):
try:
print(f"🤖 Обработка голосовой команды (Prompt 2)...")
print(f"🎤 Обработка голосовой команды (Prompt 2)...")
llm_messages = [
SystemMessage(content=prompt),
HumanMessage(content=f"Выполни команду из транскрибации: {text}")
@ -400,12 +402,12 @@ class TitleGenerator:
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)
api.voice_inst.responses["commands"].insert(0, formatted_res)
# Ограничиваем историю команд, чтобы не раздувать память
if len(api.voice_inst.responses["commands"]) > 20:
if len(api.voice_inst.responses["commands"]) > VOICE_COMMANDS_RESPONSE_TO_STORE:
api.voice_inst.responses["commands"].pop(0)
print(f" Команда обработана.")
print(f"🎤 Голосовая команда обработана.")
except Exception as e:
print(f"Ошибка обработки команды: {e}")
print(f"❌🎤 Ошибка обработки команды: {e}")

View File

@ -7,6 +7,8 @@ import numpy as np
from vosk import Model, KaldiRecognizer
import sounddevice as sd
VOICE_COMMANDS_RESPONSE_TO_STORE = 15
class VoiceService:
def __init__(self, model_path, samplerate=48000):
self.model_path = model_path
@ -126,12 +128,12 @@ class VoiceService:
channels=1,
callback=create_callback(rec_pcs, "PCs")):
print(f" Транскрибация запущена (IDs: {id_gm}, {id_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}")
print(f"❌🎤 Критическая ошибка транскрибации: {e}")
finally:
self.is_running = False
print("🏁 Сессия транскрибации завершена")
print("🏁🎤 Сессия транскрибации завершена")

6
run.py
View File

@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
if __name__ == "__main__":
# Добавляем папку app в Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app'))
from app.api import api, voice_inst, current_settings
from app.api import api, voice_inst, current_obsidian_settings
from app.heartbeat_monitor import HeartbeatMonitor
from app.workflows import graph_history_manager
@ -38,13 +38,13 @@ if __name__ == "__main__":
v_monitor = VoiceHeartbeatMonitor(
voice_inst,
graph_history_manager.title_generator,
lambda: current_settings # Теперь берет настройки из api.py
lambda: current_obsidian_settings # Теперь берет настройки из api.py
)
v_monitor.start()
api.run(
debug=True,
debug=False, # True - тогда всё по 2 раза запускает
port=5000,
host='localhost',
threaded=True