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

View File

@ -148,36 +148,39 @@ class HeartbeatMonitor:
class VoiceHeartbeatMonitor(threading.Thread): 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) super().__init__(daemon=True)
self.vs = voice_instance # Тот самый экземпляр из api.py self.vs = voice_instance # Тот самый экземпляр из api.py
self.generator = generator self.generator = generator
self.get_settings = settings_fn # Функция, возвращающая текущий конфиг self.get_obsidian_settings = get_obsidian_settings_fn # Функция, возвращающая текущий конфиг
self.last_regular_run = time.time() self.last_regular_run = time.time()
self.last_command_time = "" self.last_command_time = ""
def run(self): def run(self):
while True: while True:
if self.vs.is_running: try:
s = self.get_settings() if self.vs.is_running:
# 1. Проверка команд <start>...<stop> obsidian_settings = self.get_obsidian_settings()
self._check_commands(s) # 1. Проверка команд <start>...<stop>
self._check_commands(obsidian_settings)
# 2. Регулярный запрос # 2. Регулярный запрос
if time.time() - self.last_regular_run > s['interval'] * 60: if time.time() - self.last_regular_run > obsidian_settings['interval'] * 60:
self._run_regular_analysis(s) self._run_regular_analysis(obsidian_settings)
self.last_regular_run = time.time() self.last_regular_run = time.time()
time.sleep(10) 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 import re
# Чистим таймстемпы Vosk для LLM (удаляем паттерны [00:00:00]) # Чистим таймстемпы Vosk для LLM (удаляем паттерны [00:00:00])
clean_text = re.sub(r'\[.*?\]', '', raw_text) 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) matches = re.findall(pattern, clean_text, re.DOTALL)
for match in matches: for match in matches:
@ -191,7 +194,7 @@ class VoiceHeartbeatMonitor(threading.Thread):
print(f"🎤 New Voice Command detected: {cmd}") print(f"🎤 New Voice Command detected: {cmd}")
# ОТПРАВЛЯЕМ В LLM через TitleGenerator # ОТПРАВЛЯЕМ В 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( self.generator.add_voice_task_to_queue(
cmd, cmd,
prompt prompt
@ -199,13 +202,13 @@ class VoiceHeartbeatMonitor(threading.Thread):
self._processed_hashes.add(cmd_hash) 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): def _run_regular_analysis(self, obsidian_settings):
raw_text = self.vs.get_last_chars(s['voice_context_limit']) raw_text = self.vs.get_last_chars(obsidian_settings['voice_context_limit'])
if not raw_text.strip(): return 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' # Добавим новый тип 'voice_regular' вTitleGenerator аналогично 'voice_command'
self.generator.add_voice_regular_task(raw_text, prompt) self.generator.add_voice_regular_task(raw_text, prompt)

View File

@ -44,6 +44,19 @@ MODELS: Dict[str, Dict[str, Any]] = {
"stream": True, "stream": True,
"capabilities": ["vision"], "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": { "gemini-2.5-flash-r": {
"name": "gemini-2.5-flash-r", "name": "gemini-2.5-flash-r",
"provider": "openai", # Изменено на "openai" "provider": "openai", # Изменено на "openai"

View File

@ -11,11 +11,13 @@ from llm_client import get_llm
from langchain_core.messages import SystemMessage, HumanMessage from langchain_core.messages import SystemMessage, HumanMessage
import os import os
DEFAULT_SUMMARIZATION_LLM_NAME = "mistral-small-latest" # "mistral-small-latest" #"gemini-2.0-flash-r" 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-3.0-flash-openrouter" DEFAULT_VOICE_LLM_NAME = "gemini-2.0-flash-lite" # "gemini-3.0-flash-openrouter"
MAX_TITLE_GENERATION_CONTENT_LENGTH = 5000 # Максимальное количество символов для генерации заголовков MAX_TITLE_GENERATION_CONTENT_LENGTH = 5000 # Максимальное количество символов для генерации заголовков
from app.voice_service import VOICE_COMMANDS_RESPONSE_TO_STORE
class TitleGenerator: class TitleGenerator:
"""Сервис для асинхронной генерации заголовков.""" """Сервис для асинхронной генерации заголовков."""
@ -374,7 +376,7 @@ class TitleGenerator:
# 3. Добавьте метод обработки регулярной задачи # 3. Добавьте метод обработки регулярной задачи
def _process_voice_regular(self, text, prompt): def _process_voice_regular(self, text, prompt):
try: try:
print(f"📡 Анализ транскрибации (Prompt 1)...") print(f"🎤 Анализ транскрибации (Prompt 1)...")
llm_messages = [ llm_messages = [
SystemMessage(content=prompt), SystemMessage(content=prompt),
HumanMessage(content=f"Последний фрагмент диалога для анализа: {text}") HumanMessage(content=f"Последний фрагмент диалога для анализа: {text}")
@ -383,14 +385,14 @@ class TitleGenerator:
import app.api as api import app.api as api
api.voice_inst.responses["regular"] = response # Записываем в Response 1 api.voice_inst.responses["regular"] = response # Записываем в Response 1
print(f" Регулярный анализ завершен.") print(f"🎤 Регулярный анализ голосового ввода завершен.")
except Exception as e: except Exception as e:
print(f"Ошибка регулярного анализа: {e}") print(f"❌🎤 Ошибка регулярного анализа: {e}")
# 4. Поправьте _process_voice_command (запись команды в Response 2) # 4. Поправьте _process_voice_command (запись команды в Response 2)
def _process_voice_command(self, text, prompt): def _process_voice_command(self, text, prompt):
try: try:
print(f"🤖 Обработка голосовой команды (Prompt 2)...") print(f"🎤 Обработка голосовой команды (Prompt 2)...")
llm_messages = [ llm_messages = [
SystemMessage(content=prompt), SystemMessage(content=prompt),
HumanMessage(content=f"Выполни команду из транскрибации: {text}") HumanMessage(content=f"Выполни команду из транскрибации: {text}")
@ -400,12 +402,12 @@ class TitleGenerator:
import app.api as api import app.api as api
# Форматируем для вкладки Response 2 # Форматируем для вкладки Response 2
formatted_res = f"<b>Команда:</b> {text}<br><b>Ответ:</b> {response}" 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) api.voice_inst.responses["commands"].pop(0)
print(f" Команда обработана.") print(f"🎤 Голосовая команда обработана.")
except Exception as e: 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 from vosk import Model, KaldiRecognizer
import sounddevice as sd import sounddevice as sd
VOICE_COMMANDS_RESPONSE_TO_STORE = 15
class VoiceService: class VoiceService:
def __init__(self, model_path, samplerate=48000): def __init__(self, model_path, samplerate=48000):
self.model_path = model_path self.model_path = model_path
@ -126,12 +128,12 @@ class VoiceService:
channels=1, channels=1,
callback=create_callback(rec_pcs, "PCs")): 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(): while not self._stop_event.is_set():
self._stop_event.wait(1.0) # Экономнее чем sleep self._stop_event.wait(1.0) # Экономнее чем sleep
except Exception as e: except Exception as e:
print(f"🔥 Критическая ошибка транскрибации: {e}") print(f"❌🎤 Критическая ошибка транскрибации: {e}")
finally: finally:
self.is_running = False self.is_running = False
print("🏁 Сессия транскрибации завершена") print("🏁🎤 Сессия транскрибации завершена")

6
run.py
View File

@ -12,7 +12,7 @@ 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, voice_inst, current_settings from app.api import api, voice_inst, current_obsidian_settings
from app.heartbeat_monitor import HeartbeatMonitor from app.heartbeat_monitor import HeartbeatMonitor
from app.workflows import graph_history_manager from app.workflows import graph_history_manager
@ -38,13 +38,13 @@ if __name__ == "__main__":
v_monitor = VoiceHeartbeatMonitor( v_monitor = VoiceHeartbeatMonitor(
voice_inst, voice_inst,
graph_history_manager.title_generator, graph_history_manager.title_generator,
lambda: current_settings # Теперь берет настройки из api.py lambda: current_obsidian_settings # Теперь берет настройки из api.py
) )
v_monitor.start() v_monitor.start()
api.run( api.run(
debug=True, debug=False, # True - тогда всё по 2 раза запускает
port=5000, port=5000,
host='localhost', host='localhost',
threaded=True threaded=True