diff --git a/.gitignore b/.gitignore index e94805f..0666b91 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ node_modules/ __pycache__ *.db -*.db-journal \ No newline at end of file +*.db-journal + +build/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index deec305..871ea5b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,8 +13,19 @@ "FLASK_ENV": "development", "FLASK_DEBUG": "1" }, - "justMyCode": true + "justMyCode": true, + "console": "integratedTerminal" + }, + { + "name": "Python: VK Transcribe", + "type": "debugpy", + "request": "launch", + "python": "${workspaceFolder}/env/Scripts/python.exe", + "cwd": "${workspaceFolder}", + "program": "${workspaceFolder}/run-transcribe.py", + "justMyCode": true, + "console": "integratedTerminal", + "args": [] } - ], - "console": "integratedTerminal" -} + ] +} \ No newline at end of file diff --git a/app/api.py b/app/api.py index 7507a39..1793369 100644 --- a/app/api.py +++ b/app/api.py @@ -19,7 +19,7 @@ CORS( # ----------------------------------------------- WebSocket Initialization --------------------------------------------------------------- socketio = SocketIO( - api, cors_allowed_origins="*") # В production настроить CORS строже + api, cors_allowed_origins="*") # В production настроить CORS строже # , async_mode='eventlet' для компиляции в exe graph_history_manager.set_socketio(socketio) diff --git a/app/graph_history_manager.py b/app/graph_history_manager.py index 3e1a1fe..8517d6b 100644 --- a/app/graph_history_manager.py +++ b/app/graph_history_manager.py @@ -1,3 +1,6 @@ +# -*- coding: utf-8 -*- + + # отключаем 40-ка строчное ограничение для этого файла # ----------------------------------------------- Imports ------------------------------------------------------------ from typing import Dict, Any, List, Optional diff --git a/app/llm_client.py b/app/llm_client.py index 0314bc0..43bc306 100644 --- a/app/llm_client.py +++ b/app/llm_client.py @@ -54,6 +54,16 @@ MODELS: Dict[str, Dict[str, Any]] = { "stream": True, "capabilities": ["vision"], }, + "gemini-2.5-flash-openrouter": { + "name": "gemini-2.5-flash-openrouter", + "provider": "openai", # Изменено на "openai" + "model_name": "gemini-2.5-flash", # Добавлено имя модели для LangChain + "apiBase": + "https://openrouter.ai", # "generativelanguage.googleapis.com", # "https://render-service-gsu7.onrender.com/g/v1beta", + "apiKey": "AIzaSyDpueKFWVqknVKlQn6TdasLmJ2lvAUiBik", + "stream": True, + "capabilities": ["vision"], + }, "gemini-2.5-flash-bh": { "name": "gemini-2.5-flash", "provider": "openai", # Изменено на "openai" @@ -64,6 +74,26 @@ MODELS: Dict[str, Dict[str, Any]] = { "stream": True, "capabilities": ["vision"], }, + "gemini-3-flash-preview-bh": { + "name": "gemini-3-flash-preview", + "provider": "openai", # Изменено на "openai" + "model_name": "gemini-3-flash-preview", # Добавлено имя модели для LangChain + "apiBase": "https://bothub.chat/api/v2/openai/v1", + "apiKey": + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImQ3MTMwYTk0LTJiNmMtNDgzZi04Y2NiLWM5MjgwNzlmNDFlZSIsImlzRGV2ZWxvcGVyIjp0cnVlLCJpYXQiOjE3NTQ0NzYwMDcsImV4cCI6MjA3MDA1MjAwN30._64_DXNZatdd4-4VbXp8dOXdDhIlNWMQt9A_Nn6mu3g", + "stream": True, + "capabilities": ["vision"], + }, + "gemini-3.1-pro-preview-bh": { + "name": "gemini-3.1-pro-preview", + "provider": "openai", # Изменено на "openai" + "model_name": "gemini-3.1-pro-preview", # Добавлено имя модели для LangChain + "apiBase": "https://bothub.chat/api/v2/openai/v1", + "apiKey": + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImQ3MTMwYTk0LTJiNmMtNDgzZi04Y2NiLWM5MjgwNzlmNDFlZSIsImlzRGV2ZWxvcGVyIjp0cnVlLCJpYXQiOjE3NTQ0NzYwMDcsImV4cCI6MjA3MDA1MjAwN30._64_DXNZatdd4-4VbXp8dOXdDhIlNWMQt9A_Nn6mu3g", + "stream": True, + "capabilities": ["vision"], + }, "claude-4.5-sonnet-bh": { "name": "claude-4.5-sonnet-bh", "provider": "openai", # Изменено на "openai" diff --git a/geckodriver.exe b/geckodriver.exe new file mode 100644 index 0000000..fbde1f7 Binary files /dev/null and b/geckodriver.exe differ diff --git a/obfuscate.txt b/obfuscate.txt new file mode 100644 index 0000000..0fac873 --- /dev/null +++ b/obfuscate.txt @@ -0,0 +1,65 @@ +Для создания исполняемого файла (.exe), который запускается без установки, с обфусцированным кодом Python лучше всего использовать связку PyArmor + PyInstaller: + +1. Обфускация с PyArmor: +- PyArmor шифрует и обфусцирует исходный Python-код, усложняя его анализ и обратную разработку. +- Используется командой: + ``` + pyarmor gen -O dist --recursive путь/к/вашему/проекту + ``` + или + ``` + pyarmor gen -O dist --recursive . + ``` +- Это создаст обфусцированные версии скриптов с сохранением структуры. + +2. Упаковка в exe с PyInstaller: +- После обфускации используйте PyInstaller, чтобы упаковать проект в единый exe-файл с параметром `--onefile`: + ``` + pyinstaller --onefile --noconsole ваш_главный_скрипт.py + ``` + +? python -m PyInstaller --onefile --noconsole run.py +? pyinstaller --onefile --noconsole --paths=./app run.py +? pyinstaller --onefile --hidden-import=eventlet.green --hidden-import=eventlet.green.thread --hidden-import=engineio.async_eventlet --hidden-import=engineio.async_drivers.eventlet --noconsole --paths=./app run.py + + +- Если нужно, добавляйте параметры `--add-data` для ресурсов. + +3. Итог: +- Итоговый .exe-файл будет содержать интерпретатор Python, все зависимости и обфусцированный код. +- Запуск exe не требует предварительной установки Python или зависимостей. +- Такой файл удобно выкладывать в релизы GitHub и распространять пользователям. + +4. Важные советы: +- Всегда тестируйте exe на чистой системе. +- PyArmor даёт хорошую защиту, но не абсолютную. +- Для повышения безопасности используйте активацию лицензий PyArmor (опционно). +- Настройте spec-файл PyInstaller, чтобы включить все необходимые модули и ресурсы. + +Пример типичного workflow: + +```bash +# Обфускация +pyarmor obfuscate --recursive your_project/ + +# Переход в обфусцированную папку +cd dist/your_project + +# Создание exe +pyinstaller --onefile --noconsole main.py +``` + +Тогда в папке `dist` появится самодостаточный исполняемый файл. + +Таким образом, PyArmor + PyInstaller позволяет создать exe с обфусцированным кодом, который не требует установки Python и готов к распространению через GitHub релизы.[1][7][9] + +[1](https://pressanybutton.ru/post/poleznye-instrumenty/python-proektav-exe-fajl-i-obfuskaciya-koda/) +[2](https://www.youtube.com/watch?v=15ho_Zg_Kq4) +[3](https://www.youtube.com/watch?v=HtiYdCk0_J8) +[4](https://www.reddit.com/r/learnpython/comments/144tdhy/obfuscating_python_script/) +[5](https://stackoverflow.com/questions/58334936/pack-a-pyarmor-obfuscated-script-to-exe) +[6](https://qna.habr.com/q/1055278) +[7](https://pyarmor.readthedocs.io/en/v5.7.0/pack-obfuscated-scripts.html) +[8](https://github.com/dashingsoft/pyarmor) +[9](https://www.cyberforum.ru/python-ide/thread3151287.html) +[10](https://www.reddit.com/r/learnpython/comments/1bdsnce/which_one_is_best_for_code_obfuscation/) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index bb9000c..84bad0e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,6 @@ flask==3.1.2 flask_cors==6.0.1 flask_socketio -mistralai==1.9.10 \ No newline at end of file +mistralai==1.9.10 + +selenium \ No newline at end of file diff --git a/run-transcribe.py b/run-transcribe.py new file mode 100644 index 0000000..d4a1b39 --- /dev/null +++ b/run-transcribe.py @@ -0,0 +1,113 @@ +import os +import time +from datetime import datetime +from selenium import webdriver +from selenium.webdriver.firefox.service import Service +from selenium.webdriver.firefox.options import Options +from selenium.webdriver.common.by import By + +# --- НАСТРОЙКИ --- +OBSIDIAN_PATH = r"D:\Work\Software_Development\Projects\ttrpg-obsidian-vault\scripts\transcrib\VK_Call_Notes.md" +CHECK_INTERVAL = 0.5 # Частота проверки текста +RECONNECT_INTERVAL = 6.0 # Частота поиска вкладки + +def append_to_obsidian(text, new_line=True): + """ + Записывает текст. + new_line=True: создает новую строку с таймстампом. + new_line=False: дописывает в конец существующей строки. + """ + try: + with open(OBSIDIAN_PATH, "a", encoding="utf-8") as f: + if new_line: + timestamp = datetime.now().strftime("\n[%H:%M:%S] ") + f.write(f"{timestamp}{text}") + else: + f.write(f"{text}") # f.write(f" {text}") + print(f"Дописано: {text}") + except Exception as e: + print(f"Ошибка записи: {e}") + +def find_vk_tab(driver): + """Пытается найти и переключиться на вкладку ВК.""" + try: + for handle in driver.window_handles: + driver.switch_to.window(handle) + if "vk.com" in driver.current_url or "Call" in driver.title: + return True + except: + pass + return False + +def run_capture(): + current_dir = os.path.dirname(os.path.abspath(__file__)) + driver_path = os.path.join(current_dir, "geckodriver.exe") + + options = Options() + options.add_argument("--debugger-address 127.0.0.1:9222") + service = Service(executable_path=driver_path) + + try: + driver = webdriver.Firefox(service=service, options=options) + print("--- Соединение с браузером установлено ---") + except Exception as e: + print(f"Критическая ошибка: {e}") + return + + last_text = "" + active_tab_found = False + + try: + while True: + # 1. Если вкладка не найдена — ищем её + if not active_tab_found: + if find_vk_tab(driver): + print(f"Нашел звонок: {driver.title}. Начинаю слежку...") + active_tab_found = True + else: + print("Жду открытия вкладки со звонком ВК...") + time.sleep(RECONNECT_INTERVAL) + continue + + # 2. Попытка прочитать текст + try: + element = driver.find_element(By.CLASS_NAME, "CallsSubtitles__content") + current_full_text = element.text.strip() + + if current_full_text: + # Случай 1: Это продолжение текущей фразы + if last_full_text and current_full_text.startswith(last_full_text): + # Берем только то, чего еще не было + added_part = current_full_text[len(last_full_text):] #.strip() + if added_part: + append_to_obsidian(added_part, new_line=False) + last_full_text = current_full_text + + # Случай 2: Это абсолютно новая фраза (предыдущая исчезла) + elif current_full_text != last_full_text: + append_to_obsidian(current_full_text, new_line=True) + last_full_text = current_full_text + + else: + # В элементе пусто — сбрасываем кэш, чтобы следующая фраза шла с новой строки + last_full_text = "" + + time.sleep(CHECK_INTERVAL) + + except Exception: + # Если элемент потерян, сбрасываем состояние + last_full_text = "" + try: + _ = driver.current_url + except: + print("Вкладка потеряна. Ищу...") + active_tab_found = False + time.sleep(CHECK_INTERVAL) + + except KeyboardInterrupt: + print("\nСкрипт остановлен.") + finally: + print("Сессия завершена.") + +if __name__ == "__main__": + run_capture() diff --git a/run.py b/run.py index 20da55a..e60b203 100644 --- a/run.py +++ b/run.py @@ -10,6 +10,7 @@ logger = logging.getLogger(__name__) # ----------------------------------------------- Main ------------------------------------------------------------ if __name__ == "__main__": + # Добавляем папку app в Python path sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app')) from app.api import api from app.heartbeat_monitor import HeartbeatMonitor diff --git a/run.spec b/run.spec new file mode 100644 index 0000000..a022dc8 --- /dev/null +++ b/run.spec @@ -0,0 +1,38 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['run.py'], + pathex=['./app'], + binaries=[], + datas=[], + hiddenimports=['eventlet.green', 'eventlet.green.thread', 'engineio.async_eventlet', 'engineio.async_drivers.eventlet'], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='run', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +)