first commit
This commit is contained in:
+220
@@ -0,0 +1,220 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can ignore the whole idea folder.
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# Data files
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# ChromaDB database
|
||||||
|
chroma_db/
|
||||||
|
|
||||||
|
# AI Models
|
||||||
|
models/
|
||||||
|
!models/.gitkeep
|
||||||
|
|
||||||
|
# Downloaded data
|
||||||
|
data/
|
||||||
|
!data/.gitkeep
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
# Application specific
|
||||||
|
# rag_corpus.json
|
||||||
|
config.json
|
||||||
|
|
||||||
|
# LLM cache and temporary files
|
||||||
|
.cache/
|
||||||
|
*.gguf.tmp
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
|
||||||
|
# Local configuration
|
||||||
|
local_config.py
|
||||||
|
settings_local.py
|
||||||
|
|
||||||
|
# Large files (добавьте сюда файлы больше 100MB если они есть)
|
||||||
|
# *.largefile
|
||||||
+429
@@ -0,0 +1,429 @@
|
|||||||
|
from PyQt6.QtCore import QThread, QMutex, QWaitCondition, pyqtSignal
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import tiktoken
|
||||||
|
from typing import List, Tuple, Optional
|
||||||
|
import chromadb
|
||||||
|
from chromadb.utils.embedding_functions import SentenceTransformerEmbeddingFunction
|
||||||
|
from llama_cpp import Llama
|
||||||
|
import torch
|
||||||
|
|
||||||
|
|
||||||
|
class MedicalRAG:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
model_path: str,
|
||||||
|
corpus_path: str = "rag_corpus.json",
|
||||||
|
db_path: str = "./chroma_db",
|
||||||
|
embedding_model_name: str = "cointegrated/rubert-tiny2",
|
||||||
|
top_k: int = 3,
|
||||||
|
n_ctx: int = 2048,
|
||||||
|
n_threads: int = None,
|
||||||
|
token_multiplier: int = 3,
|
||||||
|
n_gpu_layers: int = 0,
|
||||||
|
use_gpu_for_embeddings: bool = False,
|
||||||
|
low_memory: bool = True
|
||||||
|
):
|
||||||
|
self.corpus_path = corpus_path
|
||||||
|
self.top_k = top_k
|
||||||
|
self.token_multiplier = token_multiplier
|
||||||
|
self.low_memory = low_memory
|
||||||
|
|
||||||
|
# Автоматическое определение потоков
|
||||||
|
if n_threads is None:
|
||||||
|
import multiprocessing
|
||||||
|
n_threads = max(1, multiprocessing.cpu_count() - 1)
|
||||||
|
|
||||||
|
# Проверка доступности GPU
|
||||||
|
self.has_gpu = torch.cuda.is_available() and not low_memory
|
||||||
|
if self.has_gpu:
|
||||||
|
gpu_memory = torch.cuda.get_device_properties(0).total_memory / 1024 ** 3
|
||||||
|
print(f"✅ GPU доступен: {torch.cuda.get_device_name()} ({gpu_memory:.1f} GB)")
|
||||||
|
if gpu_memory < 4:
|
||||||
|
n_gpu_layers = min(n_gpu_layers, 10)
|
||||||
|
elif gpu_memory < 8:
|
||||||
|
n_gpu_layers = min(n_gpu_layers, 20)
|
||||||
|
else:
|
||||||
|
print("ℹ️ Используется CPU режим")
|
||||||
|
n_gpu_layers = 0
|
||||||
|
use_gpu_for_embeddings = False
|
||||||
|
|
||||||
|
# Инициализация токенизатора
|
||||||
|
print("Инициализация токенизатора...")
|
||||||
|
self.encoding = None
|
||||||
|
try:
|
||||||
|
self.encoding = tiktoken.get_encoding("cl100k_base")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Токенизатор не загружен: {e}")
|
||||||
|
|
||||||
|
# Эмбеддинги
|
||||||
|
print("Загрузка эмбеддинг-модели...")
|
||||||
|
device = "cuda" if (self.has_gpu and use_gpu_for_embeddings) else "cpu"
|
||||||
|
|
||||||
|
model_kwargs = {}
|
||||||
|
if low_memory:
|
||||||
|
model_kwargs = {
|
||||||
|
'device': device,
|
||||||
|
'model_kwargs': {'torch_dtype': torch.float16}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.embedding_function = SentenceTransformerEmbeddingFunction(
|
||||||
|
model_name=embedding_model_name,
|
||||||
|
**model_kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
# ChromaDB
|
||||||
|
print("Инициализация ChromaDB...")
|
||||||
|
self.client = chromadb.PersistentClient(path=db_path)
|
||||||
|
|
||||||
|
collection_metadata = {"hnsw:space": "cosine"}
|
||||||
|
if low_memory:
|
||||||
|
collection_metadata.update({
|
||||||
|
"hnsw:construction_ef": 100,
|
||||||
|
"hnsw:M": 16,
|
||||||
|
})
|
||||||
|
|
||||||
|
self.collection = self.client.get_or_create_collection(
|
||||||
|
name="medical_anamnesis",
|
||||||
|
embedding_function=self.embedding_function,
|
||||||
|
metadata=collection_metadata
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.collection.count() == 0:
|
||||||
|
print("Загрузка данных в коллекцию...")
|
||||||
|
self._load_corpus()
|
||||||
|
else:
|
||||||
|
print(f"Коллекция содержит {self.collection.count()} записей")
|
||||||
|
|
||||||
|
# Загрузка LLM
|
||||||
|
if not os.path.exists(model_path):
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Модель не найдена: {model_path}\n"
|
||||||
|
"Для слабых компьютеров рекомендуется использовать меньшие модели."
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Загрузка языковой модели...")
|
||||||
|
|
||||||
|
llm_params = {
|
||||||
|
"model_path": model_path,
|
||||||
|
"n_ctx": n_ctx,
|
||||||
|
"n_threads": n_threads,
|
||||||
|
"verbose": False,
|
||||||
|
"low_vram": low_memory,
|
||||||
|
"use_mlock": not low_memory,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.has_gpu and n_gpu_layers > 0:
|
||||||
|
llm_params.update({
|
||||||
|
"n_gpu_layers": n_gpu_layers,
|
||||||
|
"main_gpu": 0,
|
||||||
|
"tensor_split": None,
|
||||||
|
})
|
||||||
|
print(f"Используется GPU с {n_gpu_layers} слоями")
|
||||||
|
else:
|
||||||
|
print("Используется CPU")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.llm = Llama(**llm_params)
|
||||||
|
print("✅ Система готова к работе!")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка загрузки модели: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _load_corpus(self):
|
||||||
|
"""Загрузка корпуса с обработкой ошибок"""
|
||||||
|
try:
|
||||||
|
with open(self.corpus_path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
batch_size = 50 if self.low_memory else 100
|
||||||
|
for i in range(0, len(data), batch_size):
|
||||||
|
batch = data[i:i + batch_size]
|
||||||
|
self.collection.add(
|
||||||
|
documents=[item["full"] for item in batch],
|
||||||
|
metadatas=[{"short": item["short"]} for item in batch],
|
||||||
|
ids=[f"id_{i + j}" for j in range(len(batch))]
|
||||||
|
)
|
||||||
|
print(f"Загружено {min(i + batch_size, len(data))}/{len(data)} записей")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка загрузки корпуса: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def count_tokens(self, text: str) -> int:
|
||||||
|
"""Подсчет токенов"""
|
||||||
|
if self.encoding:
|
||||||
|
return len(self.encoding.encode(text))
|
||||||
|
else:
|
||||||
|
return len(text.split())
|
||||||
|
|
||||||
|
def build_prompt_with_token_management(self, short_note: str, processing_type: str,
|
||||||
|
max_context_tokens: int = 1500) -> Tuple[str, int]:
|
||||||
|
"""Строит промпт с оптимизированным управлением токенами"""
|
||||||
|
|
||||||
|
examples = self.retrieve(short_note)
|
||||||
|
|
||||||
|
# Разные системные сообщения для жалоб и анамнеза
|
||||||
|
if processing_type == 'complaints':
|
||||||
|
system_msg = (
|
||||||
|
"На основе примеров напиши развёрнуто жалобы пациента, грамотно с медицинской точки зрения. "
|
||||||
|
"Напиши жалобы в одно предложение, одной строкой. "
|
||||||
|
"Не пиши вводных слов и фраз. Только жалобы пациента. "
|
||||||
|
"Неуместно писать диагнозы и план лечения. "
|
||||||
|
"Расшифруй все сокращения. "
|
||||||
|
"Отвечай сразу без размышлений."
|
||||||
|
)
|
||||||
|
else: # history
|
||||||
|
system_msg = (
|
||||||
|
"На основе примеров напиши развёрнутый анамнез заболевания, грамотно с медицинской точки зрения. "
|
||||||
|
# "Опиши историю развития заболевания: когда началось, как развивалось, какое лечение получал. "
|
||||||
|
"Неуместно писать диагнозы и план лечения. "
|
||||||
|
"Используй медицинскую терминологию. "
|
||||||
|
"Расшифруй все сокращения. "
|
||||||
|
"Отвечай сразу без размышлений."
|
||||||
|
)
|
||||||
|
|
||||||
|
system_tokens = self.count_tokens(system_msg)
|
||||||
|
note_tokens = self.count_tokens(short_note)
|
||||||
|
|
||||||
|
available_tokens = max_context_tokens - system_tokens - note_tokens - 150
|
||||||
|
|
||||||
|
selected_examples = []
|
||||||
|
current_tokens = 0
|
||||||
|
|
||||||
|
for example in examples:
|
||||||
|
example_tokens = self.count_tokens(example)
|
||||||
|
if current_tokens + example_tokens <= available_tokens:
|
||||||
|
selected_examples.append(example)
|
||||||
|
current_tokens += example_tokens
|
||||||
|
else:
|
||||||
|
if self.low_memory:
|
||||||
|
break
|
||||||
|
elif len(selected_examples) > 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
context = "\n".join([f"Пример: {ex}" for ex in selected_examples])
|
||||||
|
|
||||||
|
user_msg = f"""Примеры:
|
||||||
|
{context}
|
||||||
|
|
||||||
|
{"Жалобы" if processing_type == 'complaints' else "Анамнез"}: "{short_note}"
|
||||||
|
"""
|
||||||
|
|
||||||
|
prompt = (
|
||||||
|
f"<|im_start|>system\n{system_msg}<|im_end|>\n"
|
||||||
|
f"<|im_start|>user\n{user_msg}<|im_end|>\n"
|
||||||
|
"<|im_start|>assistant\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt_tokens = self.count_tokens(prompt)
|
||||||
|
return prompt, prompt_tokens
|
||||||
|
|
||||||
|
def retrieve(self, query: str, n: int = None) -> List[str]:
|
||||||
|
"""Оптимизированный поиск"""
|
||||||
|
n = n or min(self.top_k, 3)
|
||||||
|
try:
|
||||||
|
results = self.collection.query(
|
||||||
|
query_texts=[query],
|
||||||
|
n_results=n
|
||||||
|
)
|
||||||
|
return results["documents"][0]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Ошибка поиска: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def generate(self, short_note: str, processing_type: str) -> str:
|
||||||
|
"""Генерация с оптимизацией памяти"""
|
||||||
|
prompt, prompt_tokens = self.build_prompt_with_token_management(short_note, processing_type)
|
||||||
|
|
||||||
|
available_tokens = 2048 - prompt_tokens - 30
|
||||||
|
max_tokens = min(prompt_tokens * self.token_multiplier, available_tokens, 512)
|
||||||
|
|
||||||
|
print(f"📊 Токены: промпт={prompt_tokens}, ответ={max_tokens}")
|
||||||
|
print(f"⚡️ Устройство: {'GPU' if self.has_gpu else 'CPU'}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
output = self.llm(
|
||||||
|
prompt,
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
temperature=0.1,
|
||||||
|
stop=["<|im_end|>"],
|
||||||
|
echo=False,
|
||||||
|
stream=False
|
||||||
|
)
|
||||||
|
|
||||||
|
result = output["choices"][0]["text"].strip()
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка генерации: {e}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def __call__(self, short_note: str, processing_type: str) -> str:
|
||||||
|
return self.generate(short_note, processing_type)
|
||||||
|
|
||||||
|
|
||||||
|
class AIWorker(QThread):
|
||||||
|
"""Worker для обработки данных нейросетью"""
|
||||||
|
text_processed = pyqtSignal(str, str) # text, processing_type
|
||||||
|
image_processed = pyqtSignal(str)
|
||||||
|
processing_finished = pyqtSignal(str) # processing_type
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.mutex = QMutex()
|
||||||
|
self.condition = QWaitCondition()
|
||||||
|
self.text_to_process = None
|
||||||
|
self.image_to_process = None
|
||||||
|
self.processing_type = None # 'complaints', 'history', 'image'
|
||||||
|
self._active = True
|
||||||
|
|
||||||
|
# Инициализация MedicalRAG
|
||||||
|
self.rag = None
|
||||||
|
self._init_rag()
|
||||||
|
|
||||||
|
def _init_rag(self):
|
||||||
|
"""Инициализация MedicalRAG системы"""
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
# Автоматическое определение режима низкой памяти
|
||||||
|
total_memory = psutil.virtual_memory().total / 1024 ** 3
|
||||||
|
low_memory_mode = total_memory < 8
|
||||||
|
|
||||||
|
print(f"💾 Общая память: {total_memory:.1f} GB")
|
||||||
|
print(f"🔧 Режим низкой памяти: {'Да' if low_memory_mode else 'Нет'}")
|
||||||
|
|
||||||
|
# Путь к модели - нужно будет настроить под вашу систему
|
||||||
|
model_path = "./models/YandexGPT-5-Lite-8B-instruct-Q4_K_M.gguf"
|
||||||
|
|
||||||
|
# Если модель не найдена, используем имитацию
|
||||||
|
if not os.path.exists(model_path):
|
||||||
|
print("⚠️ Модель не найдена, используется имитация LLM")
|
||||||
|
self.rag = None
|
||||||
|
return
|
||||||
|
|
||||||
|
self.rag = MedicalRAG(
|
||||||
|
model_path=model_path,
|
||||||
|
n_ctx=2048,
|
||||||
|
n_gpu_layers=10 if not low_memory_mode else 0,
|
||||||
|
use_gpu_for_embeddings=not low_memory_mode,
|
||||||
|
low_memory=low_memory_mode
|
||||||
|
)
|
||||||
|
print("✅ MedicalRAG инициализирован успешно!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка инициализации MedicalRAG: {e}")
|
||||||
|
print("⚠️ Будет использоваться имитация LLM")
|
||||||
|
self.rag = None
|
||||||
|
|
||||||
|
def process_text(self, text, processing_type):
|
||||||
|
"""Запускает обработку текста"""
|
||||||
|
self.mutex.lock()
|
||||||
|
self.text_to_process = text
|
||||||
|
self.image_to_process = None
|
||||||
|
self.processing_type = processing_type
|
||||||
|
self.condition.wakeOne()
|
||||||
|
self.mutex.unlock()
|
||||||
|
|
||||||
|
def process_image(self, image_path):
|
||||||
|
"""Запускает обработку изображения"""
|
||||||
|
self.mutex.lock()
|
||||||
|
self.image_to_process = image_path
|
||||||
|
self.text_to_process = None
|
||||||
|
self.processing_type = 'image'
|
||||||
|
self.condition.wakeOne()
|
||||||
|
self.mutex.unlock()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Останавливает worker"""
|
||||||
|
self._active = False
|
||||||
|
self.condition.wakeOne()
|
||||||
|
self.wait()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Основной цикл обработки"""
|
||||||
|
while self._active:
|
||||||
|
self.mutex.lock()
|
||||||
|
if not self.text_to_process and not self.image_to_process:
|
||||||
|
self.condition.wait(self.mutex)
|
||||||
|
|
||||||
|
if not self._active:
|
||||||
|
self.mutex.unlock()
|
||||||
|
break
|
||||||
|
|
||||||
|
if self.processing_type in ['complaints', 'history'] and self.text_to_process:
|
||||||
|
text = self.text_to_process
|
||||||
|
processing_type = self.processing_type
|
||||||
|
self.text_to_process = None
|
||||||
|
self.mutex.unlock()
|
||||||
|
|
||||||
|
# Обработка текста с использованием LLM
|
||||||
|
result = self.process_text_with_llm(text, processing_type)
|
||||||
|
self.text_processed.emit(result, processing_type)
|
||||||
|
|
||||||
|
elif self.processing_type == 'image' and self.image_to_process:
|
||||||
|
image_path = self.image_to_process
|
||||||
|
self.image_to_process = None
|
||||||
|
self.mutex.unlock()
|
||||||
|
|
||||||
|
# Обработка изображения
|
||||||
|
result = self.simulate_image_processing(image_path)
|
||||||
|
self.image_processed.emit(result)
|
||||||
|
else:
|
||||||
|
self.mutex.unlock()
|
||||||
|
|
||||||
|
self.processing_finished.emit(self.processing_type)
|
||||||
|
|
||||||
|
def process_text_with_llm(self, text, processing_type):
|
||||||
|
"""Обработка текста с использованием LLM"""
|
||||||
|
if not text.strip():
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Если LLM не инициализирован, используем имитацию
|
||||||
|
if self.rag is None:
|
||||||
|
return self.simulate_text_processing(text, processing_type)
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"🔍 Обработка {processing_type} с помощью LLM...")
|
||||||
|
print(f"📥 Входной текст: {text}")
|
||||||
|
|
||||||
|
# Обработка через LLM
|
||||||
|
result = self.rag(text, processing_type)
|
||||||
|
|
||||||
|
# Если LLM вернул пустой результат, используем исходный текст
|
||||||
|
if not result or result.strip() == "":
|
||||||
|
print("⚠️ LLM вернул пустой ответ, используется исходный текст")
|
||||||
|
result = self._format_fallback_text(text, processing_type)
|
||||||
|
else:
|
||||||
|
print(f"📤 Результат LLM: {result}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка при обработке LLM: {e}")
|
||||||
|
# В случае ошибки возвращаем форматированный исходный текст
|
||||||
|
return self._format_fallback_text(text, processing_type)
|
||||||
|
|
||||||
|
def _format_fallback_text(self, text, processing_type):
|
||||||
|
"""Форматирует исходный текст как fallback"""
|
||||||
|
if processing_type == 'complaints':
|
||||||
|
return f"🤖 Анализ жалоб ИИ:\n\n{text}\n\nВывод: Требуется дополнительное обследование."
|
||||||
|
else:
|
||||||
|
return f"🤖 Анализ анамнеза ИИ:\n\n{text}\n\nВывод: Анамнез требует уточнения."
|
||||||
|
|
||||||
|
def simulate_text_processing(self, text, processing_type):
|
||||||
|
"""Имитация обработки текста (fallback)"""
|
||||||
|
time.sleep(2) # Имитация задержки обработки
|
||||||
|
|
||||||
|
if processing_type == 'complaints':
|
||||||
|
return f"🤖 Анализ жалоб ИИ:\n\n{text}\n\nВывод: Выявлены характерные симптомы требующие дополнительного обследования."
|
||||||
|
else:
|
||||||
|
return f"🤖 Анализ анамнеза ИИ:\n\n{text}\n\nВывод: Анамнез указывает на хронический характер заболевания."
|
||||||
|
|
||||||
|
def simulate_image_processing(self, image_path):
|
||||||
|
"""Имитация обработки изображения нейросетью"""
|
||||||
|
time.sleep(3) # Имитация задержки обработки
|
||||||
|
|
||||||
|
filename = os.path.basename(image_path)
|
||||||
|
return f"🖼️ Анализ изображения '{filename}':\n\nОбнаружены патологические изменения. Рекомендуется консультация специалиста."
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
from PyQt6.QtCore import QMimeData, QUrl
|
||||||
|
from PyQt6.QtGui import QDragEnterEvent, QDropEvent
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class DragDropHandler:
|
||||||
|
"""Обработчик drag-n-drop событий"""
|
||||||
|
|
||||||
|
def __init__(self, main_window):
|
||||||
|
self.main_window = main_window
|
||||||
|
|
||||||
|
def handle_drag_enter(self, event: QDragEnterEvent):
|
||||||
|
"""Обработчик события перетаскивания в окно"""
|
||||||
|
if event.mimeData().hasUrls() or event.mimeData().hasText():
|
||||||
|
event.acceptProposedAction()
|
||||||
|
self.main_window.show_drag_drop_interface(event.mimeData())
|
||||||
|
|
||||||
|
def handle_drag_leave(self, event):
|
||||||
|
"""Обработчик события выхода перетаскивания из окна"""
|
||||||
|
self.main_window.dragLeaveEvent(event)
|
||||||
|
|
||||||
|
def save_image_by_url(self, url):
|
||||||
|
from PIL import Image
|
||||||
|
import requests
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
response = requests.get(url)
|
||||||
|
img = Image.open(BytesIO(response.content))
|
||||||
|
img.save(f"data/{os.path.basename(url)}")
|
||||||
|
|
||||||
|
return f"data/{os.path.basename(url)}"
|
||||||
|
|
||||||
|
def handle_drop(self, event: QDropEvent):
|
||||||
|
"""Обработчик события отпускания перетаскиваемого объекта"""
|
||||||
|
mime_data = event.mimeData()
|
||||||
|
|
||||||
|
if mime_data.hasUrls():
|
||||||
|
# Обработка файлов
|
||||||
|
urls = mime_data.urls()
|
||||||
|
if urls and urls[0].isLocalFile():
|
||||||
|
file_path = urls[0].toLocalFile()
|
||||||
|
if self.is_image_file(file_path):
|
||||||
|
self.main_window.handle_dropped_image(file_path)
|
||||||
|
event.acceptProposedAction()
|
||||||
|
else:
|
||||||
|
# Если это не изображение, обрабатываем как текст
|
||||||
|
self.main_window.handle_dropped_text("", 'file_drop', event.position().y())
|
||||||
|
else:
|
||||||
|
file_path = self.save_image_by_url(mime_data.text())
|
||||||
|
if self.is_image_file(file_path):
|
||||||
|
self.main_window.handle_dropped_image(file_path)
|
||||||
|
event.acceptProposedAction()
|
||||||
|
else:
|
||||||
|
self.main_window.handle_dropped_text("", 'file_drop', event.position().y())
|
||||||
|
elif mime_data.hasText():
|
||||||
|
# Обработка текста
|
||||||
|
text = mime_data.text()
|
||||||
|
self.main_window.handle_dropped_text(text, 'text_drop', event.position().y())
|
||||||
|
event.acceptProposedAction()
|
||||||
|
|
||||||
|
|
||||||
|
def is_image_file(self, file_path):
|
||||||
|
"""Проверяет, является ли файл изображением"""
|
||||||
|
image_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp'}
|
||||||
|
return os.path.splitext(file_path.lower())[1] in image_extensions
|
||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
from PyQt6.QtWidgets import QMainWindow, QVBoxLayout, QWidget, QPushButton, QGraphicsDropShadowEffect
|
||||||
|
from PyQt6.QtCore import Qt, pyqtSignal, QPoint
|
||||||
|
from PyQt6.QtGui import QMouseEvent, QColor
|
||||||
|
import time
|
||||||
|
|
||||||
|
class IconWindow(QMainWindow):
|
||||||
|
"""Окно с круглой иконкой приложения"""
|
||||||
|
icon_clicked = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.parent = parent
|
||||||
|
self.dragging = False
|
||||||
|
self.drag_offset = QPoint()
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Настраивает интерфейс иконки"""
|
||||||
|
self.setWindowFlags(Qt.WindowType.FramelessWindowHint |
|
||||||
|
Qt.WindowType.WindowStaysOnTopHint |
|
||||||
|
Qt.WindowType.Tool)
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
self.setFixedSize(70, 70)
|
||||||
|
|
||||||
|
self.moved = False
|
||||||
|
self.start_position = None
|
||||||
|
|
||||||
|
self.icon_widget = QWidget()
|
||||||
|
self.icon_layout = QVBoxLayout(self.icon_widget)
|
||||||
|
self.icon_layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
|
||||||
|
self.icon_button = QPushButton("P")
|
||||||
|
self.icon_button.setFixedSize(50, 50)
|
||||||
|
self.icon_button.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: rgba(30, 30, 30, 220);
|
||||||
|
border-radius: 25px;
|
||||||
|
color: white;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 4px solid #8e44ad;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 100);
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: rgba(0, 0, 0, 100);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.icon_layout.addWidget(self.icon_button)
|
||||||
|
|
||||||
|
# self.shadow = QGraphicsDropShadowEffect()
|
||||||
|
# self.shadow.setBlurRadius(15)
|
||||||
|
# self.shadow.setColor(QColor('#FFFFFF'))
|
||||||
|
# self.shadow.setOffset(0, 0)
|
||||||
|
|
||||||
|
# self.icon_widget.setGraphicsEffect(self.shadow)
|
||||||
|
|
||||||
|
self.setCentralWidget(self.icon_widget)
|
||||||
|
|
||||||
|
# Настраиваем обработчики мыши для перемещения
|
||||||
|
self.icon_button.mousePressEvent = self.mouse_press_event
|
||||||
|
self.icon_button.mouseMoveEvent = self.mouse_move_event
|
||||||
|
self.icon_button.mouseReleaseEvent = self.mouse_release_event
|
||||||
|
|
||||||
|
def mouse_press_event(self, event: QMouseEvent):
|
||||||
|
"""Обработчик нажатия мыши на иконку"""
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
self.moved = True
|
||||||
|
self.start_position = self.icon_button.mapToGlobal(event.position().toPoint())
|
||||||
|
self.dragging = True
|
||||||
|
self.drag_offset = event.position().toPoint()
|
||||||
|
self.icon_button.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: rgba(0, 0, 0, 220);
|
||||||
|
border-radius: 25px;
|
||||||
|
color: white;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 3px solid #8e44ad;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
# self.shadow.setColor(QColor('#de94fd'))
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mouse_move_event(self, event: QMouseEvent):
|
||||||
|
"""Обработчик движения мыши для иконки"""
|
||||||
|
if self.dragging and event.buttons() & Qt.MouseButton.LeftButton:
|
||||||
|
global_pos = self.icon_button.mapToGlobal(event.position().toPoint())
|
||||||
|
new_pos = global_pos - self.drag_offset + QPoint(-10,-10)
|
||||||
|
self.move(new_pos)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mouse_release_event(self, event: QMouseEvent):
|
||||||
|
"""Обработчик отпускания мыши для иконки"""
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton and self.dragging:
|
||||||
|
# Решение конфликта событий mouse_press_event и clicked
|
||||||
|
end_position = self.icon_button.mapToGlobal(event.position().toPoint())
|
||||||
|
if abs((self.start_position - end_position).x()) + abs((self.start_position - end_position).y()) < 40:
|
||||||
|
self.icon_clicked.emit()
|
||||||
|
self.dragging = False
|
||||||
|
self.icon_button.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: rgba(30, 30, 30, 220);
|
||||||
|
border-radius: 25px;
|
||||||
|
color: white;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 4px solid #8e44ad;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 100);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
# self.shadow.setColor(QColor('#FFFFFF'))
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def show_icon(self):
|
||||||
|
"""Показывает иконку в левом нижнем углу"""
|
||||||
|
screen_geometry = self.screen().geometry()
|
||||||
|
x_position = screen_geometry.width() - 70
|
||||||
|
y_position = screen_geometry.height() - 120
|
||||||
|
self.move(x_position, y_position)
|
||||||
|
self.show()
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import torch
|
||||||
|
import sys
|
||||||
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
from PyQt6.QtGui import QFont
|
||||||
|
from main_window import TransparentWindow
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
app.setQuitOnLastWindowClosed(False)
|
||||||
|
|
||||||
|
font = QFont("Segoe UI", 9)
|
||||||
|
app.setFont(font)
|
||||||
|
|
||||||
|
window = TransparentWindow()
|
||||||
|
|
||||||
|
print("Input Helper запущен!")
|
||||||
|
print("Панель появляется при клике на любые поля ввода")
|
||||||
|
print("Иконка приложения находится в системном трее")
|
||||||
|
print("Для выхода используйте иконку в трее -> Выход")
|
||||||
|
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel, QProgressBar, QHBoxLayout
|
||||||
|
from PyQt6.QtCore import Qt, pyqtSignal, QSize
|
||||||
|
import qtawesome as qta
|
||||||
|
|
||||||
|
|
||||||
|
class MainInterface(QWidget):
|
||||||
|
"""Основной интерфейс приложения с кнопками и drop-областями"""
|
||||||
|
button_clicked = pyqtSignal(int)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.parent = parent
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Настраивает основной интерфейс"""
|
||||||
|
self.setObjectName("MainInterface")
|
||||||
|
|
||||||
|
|
||||||
|
self.main_layout = QVBoxLayout(self)
|
||||||
|
self.main_layout.setContentsMargins(8, 4, 8, 8)
|
||||||
|
self.main_layout.setSpacing(1)
|
||||||
|
|
||||||
|
|
||||||
|
# Прогресс-бар
|
||||||
|
self.progress_bar = QProgressBar()
|
||||||
|
self.progress_bar.setVisible(False)
|
||||||
|
self.progress_bar.setStyleSheet("""
|
||||||
|
QProgressBar {
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: rgba(180, 180, 180, 100);
|
||||||
|
font-size: 1px;
|
||||||
|
margin: 0px 3px;
|
||||||
|
}
|
||||||
|
QProgressBar::chunk {
|
||||||
|
background-color: rgba(255, 255, 255, 250);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.main_layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
|
# Кнопки основного интерфейса
|
||||||
|
button_texts = [
|
||||||
|
" Копировать жалобы", #mdi6.text-box-edit
|
||||||
|
" Копировать анамнез", #mdi6.text-box
|
||||||
|
" ЛОР-Статус", #mdi.ear-hearing
|
||||||
|
" Заключение ИИ", #mdi.robot-excited
|
||||||
|
" Новый случай" #mdi.refresh-circle
|
||||||
|
]
|
||||||
|
button_names = [
|
||||||
|
"complaints",
|
||||||
|
"history",
|
||||||
|
"ent_status",
|
||||||
|
"ai_conclusion",
|
||||||
|
"refresh"
|
||||||
|
]
|
||||||
|
|
||||||
|
button_default_icns = [
|
||||||
|
"mdi6.text-box-edit",
|
||||||
|
"mdi6.text-box",
|
||||||
|
"mdi.ear-hearing",
|
||||||
|
"mdi.robot-excited",
|
||||||
|
"mdi.refresh-circle"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
self.buttons = []
|
||||||
|
for i, text in enumerate(button_texts):
|
||||||
|
icn = qta.icon(button_default_icns[i], color=('#FFFFFF', 255))
|
||||||
|
button = QPushButton(icn, text)
|
||||||
|
button.setIconSize(QSize(25, 25))
|
||||||
|
button.setObjectName(button_names[i])
|
||||||
|
button.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
padding: 8px 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 650;
|
||||||
|
margin: 0px 3px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 100);
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: rgba(0, 0, 0, 220);
|
||||||
|
}
|
||||||
|
QPushButton:disabled {
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
button.clicked.connect(lambda checked, idx=i: self.button_clicked.emit(idx))
|
||||||
|
self.buttons.append(button)
|
||||||
|
self.main_layout.addWidget(button)
|
||||||
|
|
||||||
|
# Drop области
|
||||||
|
self.setup_drop_areas()
|
||||||
|
|
||||||
|
# Изначально скрываем кнопки и показываем compact область
|
||||||
|
self.hide_buttons()
|
||||||
|
self.show_compact_drop_area()
|
||||||
|
|
||||||
|
def setup_drop_areas(self):
|
||||||
|
"""Настраивает области для drag-n-drop"""
|
||||||
|
# Основная drop область для изображений
|
||||||
|
self.image_drop_area = QLabel("🖼️ Перетащите\nснимки сюда")
|
||||||
|
self.image_drop_area.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
# self.image_drop_area.setMinimumHeight(120)
|
||||||
|
self.image_drop_area.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
background-color: rgba(200, 200, 200, 100);
|
||||||
|
border: 2px dashed rgba(180, 180, 180, 150);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 7px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
QLabel:hover {
|
||||||
|
background-color: rgba(200, 200, 200, 150);
|
||||||
|
border: 2px dashed rgb(180, 180, 180);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.image_drop_area.hide()
|
||||||
|
|
||||||
|
# Drop области для текста
|
||||||
|
self.text_drop_layout = QVBoxLayout()
|
||||||
|
self.text_drop_layout.setSpacing(5)
|
||||||
|
|
||||||
|
self.complaints_drop_area = QLabel("📝 Перетащите текст\nжалоб сюда")
|
||||||
|
self.complaints_drop_area.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
# self.complaints_drop_area.setMinimumHeight(60)
|
||||||
|
self.complaints_drop_area.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
background-color: rgba(200, 200, 200, 100);
|
||||||
|
border: 2px dashed rgba(180, 180, 180, 150);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
QLabel:hover {
|
||||||
|
background-color: rgba(200, 200, 200, 150);
|
||||||
|
border: 2px dashed rgb(180, 180, 180);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.history_drop_area = QLabel("📋 Перетащите текст\nанамнеза сюда")
|
||||||
|
self.history_drop_area.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
# self.history_drop_area.setMinimumHeight(60)
|
||||||
|
self.history_drop_area.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
background-color: rgba(200, 200, 200, 100);
|
||||||
|
border: 2px dashed rgba(180, 180, 180, 150);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
QLabel:hover {
|
||||||
|
background-color: rgba(200, 200, 200, 150);
|
||||||
|
border: 2px dashed rgb(180, 180, 180);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.text_drop_layout.addWidget(self.complaints_drop_area)
|
||||||
|
self.text_drop_layout.addWidget(self.history_drop_area)
|
||||||
|
|
||||||
|
self.text_drop_widget = QWidget()
|
||||||
|
self.text_drop_widget.setLayout(self.text_drop_layout)
|
||||||
|
self.text_drop_widget.hide()
|
||||||
|
|
||||||
|
def show_compact_drop_area(self):
|
||||||
|
"""Показывает компактную drop область"""
|
||||||
|
self.hide_all_drop_areas()
|
||||||
|
|
||||||
|
if hasattr(self, 'compact_drop_widget'):
|
||||||
|
self.main_layout.removeWidget(self.compact_drop_widget)
|
||||||
|
self.compact_drop_widget.deleteLater()
|
||||||
|
|
||||||
|
compact_drop_label = QLabel("📁 Перетащите снимки\nили текст сюда")
|
||||||
|
compact_drop_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
# compact_drop_label.setMinimumHeight(80)
|
||||||
|
compact_drop_label.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
background-color: rgba(200, 200, 200, 100);
|
||||||
|
border: 2px dashed rgba(180, 180, 180, 150);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 7px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.compact_drop_widget = compact_drop_label
|
||||||
|
self.main_layout.insertWidget(0, self.compact_drop_widget)
|
||||||
|
self.compact_drop_widget.show()
|
||||||
|
|
||||||
|
def show_image_drop_interface(self):
|
||||||
|
"""Показывает интерфейс для перетаскивания изображений"""
|
||||||
|
self.hide_all_drop_areas()
|
||||||
|
if self.image_drop_area not in [self.main_layout.itemAt(i).widget() for i in range(self.main_layout.count())]:
|
||||||
|
self.main_layout.insertWidget(0, self.image_drop_area)
|
||||||
|
self.image_drop_area.show()
|
||||||
|
|
||||||
|
def show_text_drop_interface(self):
|
||||||
|
"""Показывает интерфейс для перетаскивания текста"""
|
||||||
|
self.hide_all_drop_areas()
|
||||||
|
if self.text_drop_widget not in [self.main_layout.itemAt(i).widget() for i in range(self.main_layout.count())]:
|
||||||
|
self.main_layout.insertWidget(0, self.text_drop_widget)
|
||||||
|
self.text_drop_widget.show()
|
||||||
|
|
||||||
|
def hide_all_drop_areas(self):
|
||||||
|
"""Скрывает все drop области"""
|
||||||
|
self.image_drop_area.hide()
|
||||||
|
self.text_drop_widget.hide()
|
||||||
|
if hasattr(self, 'compact_drop_widget'):
|
||||||
|
self.compact_drop_widget.hide()
|
||||||
|
|
||||||
|
def show_buttons(self):
|
||||||
|
"""Показывает все кнопки"""
|
||||||
|
for button in self.buttons:
|
||||||
|
button.show()
|
||||||
|
|
||||||
|
def hide_buttons(self):
|
||||||
|
"""Скрывает все кнопки"""
|
||||||
|
for button in self.buttons:
|
||||||
|
button.hide()
|
||||||
|
|
||||||
|
def set_processing_state(self, processing):
|
||||||
|
"""Устанавливает состояние обработки"""
|
||||||
|
self.progress_bar.setVisible(processing)
|
||||||
|
if processing:
|
||||||
|
self.progress_bar.setRange(0, 0)
|
||||||
|
else:
|
||||||
|
self.progress_bar.setRange(0, 100)
|
||||||
|
|
||||||
|
def set_buttons_enabled(self, enabled):
|
||||||
|
"""Включает/выключает кнопки"""
|
||||||
|
for button in self.buttons:
|
||||||
|
if button.objectName() != "🔄 Новый случай":
|
||||||
|
button.setEnabled(enabled)
|
||||||
+780
@@ -0,0 +1,780 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
from PyQt6.QtWidgets import (QMainWindow, QVBoxLayout, QWidget,
|
||||||
|
QSystemTrayIcon, QMenu, QApplication, QHBoxLayout, QPushButton)
|
||||||
|
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QPropertyAnimation, QEasingCurve, QPoint
|
||||||
|
from PyQt6.QtGui import QFont, QPainter, QIcon, QAction, QPixmap, QDragEnterEvent, QDropEvent, QMouseEvent
|
||||||
|
import uiautomation as auto
|
||||||
|
|
||||||
|
from ai_worker import AIWorker
|
||||||
|
from drag_drop_handler import DragDropHandler
|
||||||
|
from icon_window import IconWindow
|
||||||
|
from main_interface import MainInterface
|
||||||
|
import qtawesome as qta
|
||||||
|
import pyperclip
|
||||||
|
import keyboard
|
||||||
|
|
||||||
|
|
||||||
|
class TransparentWindow(QMainWindow):
|
||||||
|
text_to_insert = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.is_visible = False # Видимость окна (скрыто/показано)
|
||||||
|
self.is_expanded = False # Ширина окна (узкое/широкое)
|
||||||
|
self.hook_manager = None
|
||||||
|
|
||||||
|
self.dragging = False
|
||||||
|
self.drag_offset = QPoint()
|
||||||
|
self.drag_n_drop_active = False
|
||||||
|
|
||||||
|
self.setup_ui()
|
||||||
|
self.setup_tray()
|
||||||
|
self.setup_focus_monitor()
|
||||||
|
self.hide() # Изначально окно скрыто
|
||||||
|
self.last_focus_state = False
|
||||||
|
self.last_active_timestamp = time.time()
|
||||||
|
|
||||||
|
# Инициализация AI worker
|
||||||
|
self.ai_worker = AIWorker()
|
||||||
|
self.ai_worker.text_processed.connect(self.on_text_processed)
|
||||||
|
self.ai_worker.image_processed.connect(self.on_image_processed)
|
||||||
|
self.ai_worker.processing_finished.connect(self.on_processing_finished)
|
||||||
|
self.ai_worker.start()
|
||||||
|
|
||||||
|
# Инициализация drag-drop handler
|
||||||
|
self.drag_drop_handler = DragDropHandler(self)
|
||||||
|
|
||||||
|
# Переменные состояния
|
||||||
|
self.current_complaints_text = ""
|
||||||
|
self.current_history_text = ""
|
||||||
|
self.current_image_path = ""
|
||||||
|
self.is_processing = False
|
||||||
|
self.pending_text_type = None
|
||||||
|
|
||||||
|
# Настройка drag-and-drop
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Настраивает основной интерфейс окна"""
|
||||||
|
# Устанавливаем прозрачность и убираем рамку
|
||||||
|
self.setWindowFlags(Qt.WindowType.FramelessWindowHint |
|
||||||
|
Qt.WindowType.WindowStaysOnTopHint |
|
||||||
|
Qt.WindowType.Tool)
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
|
||||||
|
# Получаем размеры экрана
|
||||||
|
screen_geometry = QApplication.primaryScreen().geometry()
|
||||||
|
screen_width = screen_geometry.width()
|
||||||
|
screen_height = screen_geometry.height()
|
||||||
|
|
||||||
|
# Размеры окон
|
||||||
|
self.collapsed_width = 70
|
||||||
|
self.expanded_width = 200
|
||||||
|
self.window_height = 300
|
||||||
|
|
||||||
|
# Позиционируем основное окно слева по центру
|
||||||
|
main_x_position = 10
|
||||||
|
main_y_position = (screen_height - self.window_height) // 2
|
||||||
|
|
||||||
|
# Устанавливаем начальную позицию основного окна (свернутое состояние)
|
||||||
|
self.setGeometry(main_x_position, main_y_position, self.collapsed_width, self.window_height)
|
||||||
|
|
||||||
|
# Создаем центральный виджет
|
||||||
|
self.central_widget = QWidget()
|
||||||
|
self.central_widget.setObjectName("CentralWidget")
|
||||||
|
self.central_widget.setAcceptDrops(True)
|
||||||
|
self.setCentralWidget(self.central_widget)
|
||||||
|
|
||||||
|
self.central_widget.mousePressEvent = self.mouse_press_event
|
||||||
|
self.central_widget.mouseMoveEvent = self.mouse_move_event
|
||||||
|
self.central_widget.mouseReleaseEvent = self.mouse_release_event
|
||||||
|
|
||||||
|
# Основной layout
|
||||||
|
self.main_layout = QVBoxLayout(self.central_widget)
|
||||||
|
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.main_layout.setSpacing(0)
|
||||||
|
|
||||||
|
self.top_layout = QHBoxLayout()
|
||||||
|
self.top_layout.setContentsMargins(4, 4, 4, 0)
|
||||||
|
self.top_layout.setSpacing(0)
|
||||||
|
self.top_layout.addStretch()
|
||||||
|
icn = qta.icon("ph.gear-six-bold", color=('#FFFFFF', 255))
|
||||||
|
button_1 = QPushButton(icn, "")
|
||||||
|
button_1.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
padding: 4px 4px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 650;
|
||||||
|
margin: 0px 3px;
|
||||||
|
text-align: left;
|
||||||
|
max-width: 16px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 100);
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: rgba(0, 0, 0, 220);
|
||||||
|
}
|
||||||
|
QPushButton:disabled {
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
icn = qta.icon("ph.push-pin-bold", color=('#FFFFFF', 255))
|
||||||
|
button_2 = QPushButton(icn, "")
|
||||||
|
button_2.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
padding: 4px 4px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 650;
|
||||||
|
margin: 0px 3px;
|
||||||
|
text-align: left;
|
||||||
|
max-width: 16px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 100);
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: rgba(0, 0, 0, 220);
|
||||||
|
}
|
||||||
|
QPushButton:disabled {
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.top_layout.addWidget(button_1)
|
||||||
|
self.top_layout.addWidget(button_2)
|
||||||
|
self.main_layout.addLayout(self.top_layout)
|
||||||
|
|
||||||
|
# Создаем и показываем иконку
|
||||||
|
self.icon_window = IconWindow(self)
|
||||||
|
self.icon_window.icon_clicked.connect(self.toggle_window_visibility)
|
||||||
|
self.icon_window.show_icon()
|
||||||
|
|
||||||
|
|
||||||
|
# .addLayout(self.central_widget)
|
||||||
|
|
||||||
|
# Создаем основной интерфейс
|
||||||
|
self.main_interface = MainInterface(self)
|
||||||
|
self.main_interface.button_clicked.connect(self.on_button_click)
|
||||||
|
self.main_interface.hide() # Изначально скрыт
|
||||||
|
self.main_layout.addWidget(self.main_interface)
|
||||||
|
|
||||||
|
self.central_widget.setStyleSheet("""
|
||||||
|
#CentralWidget {
|
||||||
|
background: qlineargradient(
|
||||||
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 rgba(30, 30, 30, 220),
|
||||||
|
stop:1 rgba(40, 40, 40, 240)
|
||||||
|
);
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.text_to_insert.connect(self.insert_text)
|
||||||
|
|
||||||
|
def mouse_press_event(self, event: QMouseEvent):
|
||||||
|
"""Обработчик нажатия мыши на иконку"""
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
self.dragging = True
|
||||||
|
self.drag_offset = event.position().toPoint()
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mouse_move_event(self, event: QMouseEvent):
|
||||||
|
"""Обработчик движения мыши для иконки"""
|
||||||
|
if self.dragging and event.buttons() & Qt.MouseButton.LeftButton:
|
||||||
|
global_pos = self.central_widget.mapToGlobal(event.position().toPoint())
|
||||||
|
new_pos = global_pos - self.drag_offset + QPoint(-10,-10)
|
||||||
|
self.move(new_pos)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mouse_release_event(self, event: QMouseEvent):
|
||||||
|
"""Обработчик отпускания мыши для иконки"""
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton and self.dragging:
|
||||||
|
# Решение конфликта событий mouse_press_event и clicked
|
||||||
|
self.dragging = False
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def toggle_window_visibility(self):
|
||||||
|
"""Переключает видимость окна (скрыть/показать)"""
|
||||||
|
if self.is_visible:
|
||||||
|
self.hide_window()
|
||||||
|
else:
|
||||||
|
self.show_window()
|
||||||
|
|
||||||
|
def show_window(self):
|
||||||
|
"""Показывает окно в узком состоянии"""
|
||||||
|
if self.is_visible:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.is_visible = True
|
||||||
|
self.show()
|
||||||
|
self.raise_()
|
||||||
|
|
||||||
|
# Показываем основной интерфейс
|
||||||
|
self.main_interface.show()
|
||||||
|
|
||||||
|
# Устанавливаем узкую ширину и показываем compact drop область
|
||||||
|
# self.resize(self.collapsed_width, self.window_height)
|
||||||
|
# self.main_interface.hide_buttons()
|
||||||
|
# self.main_interface.show_compact_drop_area()
|
||||||
|
self.expand_for_text_field()
|
||||||
|
|
||||||
|
def hide_window(self):
|
||||||
|
"""Полностью скрывает окно"""
|
||||||
|
if not self.is_visible:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.is_visible = False
|
||||||
|
self.hide()
|
||||||
|
self.main_interface.hide_all_drop_areas()
|
||||||
|
|
||||||
|
def expand_for_drag_drop(self, mime_data):
|
||||||
|
"""Расширяет окно для drag-n-drop"""
|
||||||
|
|
||||||
|
if not self.is_visible:
|
||||||
|
self.show_window()
|
||||||
|
|
||||||
|
if not self.is_expanded:
|
||||||
|
self.animate_window_width(self.expanded_width)
|
||||||
|
self.is_expanded = True
|
||||||
|
|
||||||
|
# Показываем соответствующие drop области (без кнопок)
|
||||||
|
self.main_interface.hide_buttons()
|
||||||
|
self.main_interface.hide_all_drop_areas()
|
||||||
|
|
||||||
|
|
||||||
|
if mime_data.hasUrls():
|
||||||
|
urls = mime_data.urls()
|
||||||
|
if urls and urls[0].isLocalFile():
|
||||||
|
file_path = urls[0].toLocalFile()
|
||||||
|
if self.drag_drop_handler.is_image_file(file_path):
|
||||||
|
self.main_interface.show_image_drop_interface()
|
||||||
|
else:
|
||||||
|
self.main_interface.show_text_drop_interface()
|
||||||
|
else:
|
||||||
|
urls = mime_data.urls()
|
||||||
|
file_path = urls[0]
|
||||||
|
self.main_interface.show_image_drop_interface()
|
||||||
|
elif mime_data.hasText():
|
||||||
|
self.main_interface.show_text_drop_interface()
|
||||||
|
|
||||||
|
def expand_for_text_field(self):
|
||||||
|
"""Расширяет окно для работы с текстовым полем"""
|
||||||
|
if not self.is_visible:
|
||||||
|
self.show_window()
|
||||||
|
|
||||||
|
if not self.is_expanded:
|
||||||
|
self.animate_window_width(self.expanded_width)
|
||||||
|
self.is_expanded = True
|
||||||
|
|
||||||
|
# Показываем кнопки (без drop областей)
|
||||||
|
self.main_interface.show_buttons()
|
||||||
|
self.main_interface.hide_all_drop_areas()
|
||||||
|
|
||||||
|
def collapse_to_compact(self):
|
||||||
|
"""Сворачивает окно до компактного состояния"""
|
||||||
|
if not self.is_expanded:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.animate_window_width(self.collapsed_width)
|
||||||
|
self.is_expanded = False
|
||||||
|
|
||||||
|
# Показываем compact drop область
|
||||||
|
self.main_interface.hide_buttons()
|
||||||
|
self.main_interface.show_compact_drop_area()
|
||||||
|
|
||||||
|
def animate_window_width(self, target_width, callback=None):
|
||||||
|
"""Анимирует изменение ширины окна"""
|
||||||
|
|
||||||
|
# self.resize(target_width, self.window_height)
|
||||||
|
|
||||||
|
if callback:
|
||||||
|
callback()
|
||||||
|
|
||||||
|
def show_drag_drop_interface(self, mime_data):
|
||||||
|
"""Показывает интерфейс для drag-n-drop"""
|
||||||
|
self.expand_for_drag_drop(mime_data)
|
||||||
|
|
||||||
|
def handle_dropped_image(self, image_path):
|
||||||
|
"""Обрабатывает перетащенное изображение"""
|
||||||
|
try:
|
||||||
|
self.current_image_path = image_path
|
||||||
|
|
||||||
|
print("⚠️ NEW Image. URL:", image_path)
|
||||||
|
|
||||||
|
# Запускаем обработку изображения
|
||||||
|
self.set_processing_state(True)
|
||||||
|
self.main_interface.buttons[2].setText(" ЛОР-Статус (обработка...)")
|
||||||
|
self.main_interface.buttons[3].setText(" Заключение ИИ (обработка...)")
|
||||||
|
|
||||||
|
self.ai_worker.process_image(image_path)
|
||||||
|
self.main_interface.show_compact_drop_area()
|
||||||
|
except Exception as e:
|
||||||
|
self.show_error(f"Не удалось загрузить изображение: {str(e)}")
|
||||||
|
self.set_processing_state(False)
|
||||||
|
self.main_interface.show_compact_drop_area()
|
||||||
|
|
||||||
|
def handle_dropped_text(self, text, drop_type, y_position):
|
||||||
|
"""Обрабатывает перетащенный текст"""
|
||||||
|
if not text.strip() and drop_type == 'file_drop':
|
||||||
|
try:
|
||||||
|
text = "Текст из перетащенного файла"
|
||||||
|
except:
|
||||||
|
text = "Не удалось прочитать файл"
|
||||||
|
|
||||||
|
if y_position < self.window_height/2:
|
||||||
|
# Обрабатываем как жалобы
|
||||||
|
self.start_text_processing(text, 'complaints')
|
||||||
|
else:
|
||||||
|
# Обрабатываем как жалобы
|
||||||
|
self.start_text_processing(text, 'history')
|
||||||
|
self.main_interface.show_compact_drop_area()
|
||||||
|
|
||||||
|
def start_text_processing(self, text, processing_type):
|
||||||
|
"""Запускает обработку текста"""
|
||||||
|
self.set_processing_state(True)
|
||||||
|
|
||||||
|
if processing_type == 'complaints':
|
||||||
|
self.main_interface.buttons[0].setText(" Обработка...")
|
||||||
|
self.pending_text_type = 'complaints'
|
||||||
|
else:
|
||||||
|
self.main_interface.buttons[1].setText(" Обработка...")
|
||||||
|
self.pending_text_type = 'history'
|
||||||
|
|
||||||
|
self.ai_worker.process_text(text, processing_type)
|
||||||
|
|
||||||
|
def set_processing_state(self, processing):
|
||||||
|
"""Устанавливает состояние обработки"""
|
||||||
|
self.is_processing = processing
|
||||||
|
self.main_interface.set_buttons_enabled(not processing)
|
||||||
|
self.main_interface.set_processing_state(processing)
|
||||||
|
|
||||||
|
def on_text_processed(self, result, processing_type):
|
||||||
|
"""Обработчик завершения обработки текста"""
|
||||||
|
if processing_type == 'complaints':
|
||||||
|
self.current_complaints_text = result
|
||||||
|
self.main_interface.buttons[0].setText(" Вставить жалобы (ИИ)")
|
||||||
|
else:
|
||||||
|
self.current_history_text = result
|
||||||
|
self.main_interface.buttons[1].setText(" Вставить анамнез (ИИ)")
|
||||||
|
|
||||||
|
def on_image_processed(self, result):
|
||||||
|
"""Обработчик завершения обработки изображения"""
|
||||||
|
self.current_image_path = result
|
||||||
|
self.main_interface.buttons[2].setText(" ЛОР-Статус (ИИ)")
|
||||||
|
self.main_interface.buttons[3].setText(" Заключение ИИ (готово)")
|
||||||
|
|
||||||
|
def on_processing_finished(self, processing_type):
|
||||||
|
"""Обработчик завершения любой обработки"""
|
||||||
|
self.set_processing_state(False)
|
||||||
|
|
||||||
|
# Drag and Drop events
|
||||||
|
def dragEnterEvent(self, event: QDragEnterEvent):
|
||||||
|
"""Обработчик события перетаскивания в окно"""
|
||||||
|
self.drag_drop_handler.handle_drag_enter(event)
|
||||||
|
self.drag_n_drop_active = True
|
||||||
|
|
||||||
|
def dragLeaveEvent(self, event):
|
||||||
|
"""Обработчик события выхода перетаскивания из окна"""
|
||||||
|
if self.is_visible and not self.is_processing:
|
||||||
|
self.collapse_to_compact()
|
||||||
|
self.drag_n_drop_active = False
|
||||||
|
|
||||||
|
def dropEvent(self, event: QDropEvent):
|
||||||
|
"""Обработчик события отпускания перетаскиваемого объекта"""
|
||||||
|
self.drag_drop_handler.handle_drop(event)
|
||||||
|
self.drag_n_drop_active = False
|
||||||
|
|
||||||
|
def setup_tray(self):
|
||||||
|
"""Настраиваем системный трей"""
|
||||||
|
self.tray_icon = QSystemTrayIcon(self)
|
||||||
|
pixmap = QPixmap(16, 16)
|
||||||
|
pixmap.fill(Qt.GlobalColor.blue)
|
||||||
|
self.tray_icon.setIcon(QIcon(pixmap))
|
||||||
|
self.tray_icon.setToolTip("Input Helper\nАвтоматическая панель для ввода текста")
|
||||||
|
|
||||||
|
tray_menu = QMenu()
|
||||||
|
show_action = QAction("Показать панель", self)
|
||||||
|
show_action.triggered.connect(self.show_window)
|
||||||
|
hide_action = QAction("Скрыть панель", self)
|
||||||
|
hide_action.triggered.connect(self.hide_window)
|
||||||
|
quit_action = QAction("Выход", self)
|
||||||
|
quit_action.triggered.connect(self.quit_application)
|
||||||
|
|
||||||
|
tray_menu.addAction(show_action)
|
||||||
|
tray_menu.addAction(hide_action)
|
||||||
|
tray_menu.addSeparator()
|
||||||
|
tray_menu.addAction(quit_action)
|
||||||
|
|
||||||
|
self.tray_icon.setContextMenu(tray_menu)
|
||||||
|
self.tray_icon.activated.connect(self.tray_icon_activated)
|
||||||
|
self.tray_icon.show()
|
||||||
|
|
||||||
|
def is_editable_field(self):
|
||||||
|
try:
|
||||||
|
focused = auto.GetFocusedControl()
|
||||||
|
if not focused:
|
||||||
|
return False
|
||||||
|
|
||||||
|
control_type = focused.ControlType
|
||||||
|
if control_type in [auto.ControlType.EditControl, auto.ControlType.ComboBoxControl,
|
||||||
|
auto.ControlType.TextControl]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def setup_focus_monitor(self):
|
||||||
|
"""Мониторинг фокуса ввода через таймер"""
|
||||||
|
self.focus_timer = QTimer()
|
||||||
|
self.focus_timer.timeout.connect(self.check_focused_control)
|
||||||
|
self.focus_timer.start(200)
|
||||||
|
|
||||||
|
def check_focused_control(self):
|
||||||
|
"""Проверяет, есть ли фокус на редактируемом поле"""
|
||||||
|
try:
|
||||||
|
if self.drag_n_drop_active:
|
||||||
|
return 0
|
||||||
|
current_focus_state = self.is_editable_field()
|
||||||
|
if current_focus_state:
|
||||||
|
self.last_active_timestamp = time.time()
|
||||||
|
if not self.last_focus_state:
|
||||||
|
# При фокусе на текстовом поле - расширяем окно и показываем кнопки
|
||||||
|
self.expand_for_text_field()
|
||||||
|
else:
|
||||||
|
if self.geometry().width() < self.expanded_width:
|
||||||
|
self.animate_window_width(self.expanded_width)
|
||||||
|
self.is_expanded = True
|
||||||
|
self.main_interface.show_buttons()
|
||||||
|
self.main_interface.hide_all_drop_areas()
|
||||||
|
else:
|
||||||
|
if time.time() - self.last_active_timestamp > 5:
|
||||||
|
self.last_active_timestamp = time.time()
|
||||||
|
if not self.last_focus_state:
|
||||||
|
# При фокусе на текстовом поле - расширяем окно и показываем кнопки
|
||||||
|
self.expand_for_text_field()
|
||||||
|
else:
|
||||||
|
if self.geometry().width() < self.expanded_width:
|
||||||
|
self.animate_window_width(self.expanded_width)
|
||||||
|
self.is_expanded = True
|
||||||
|
self.main_interface.show_buttons()
|
||||||
|
self.main_interface.hide_all_drop_areas()
|
||||||
|
|
||||||
|
self.last_focus_state = current_focus_state
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_button_click(self, index):
|
||||||
|
"""Обработчик клика по кнопке"""
|
||||||
|
if index == len(self.main_interface.buttons) - 1: # Последняя кнопка - скрыть
|
||||||
|
self.restart_form()
|
||||||
|
elif index == 0: # Кнопка жалоб
|
||||||
|
self.handle_complaints_button()
|
||||||
|
elif index == 1: # Кнопка анамнеза
|
||||||
|
self.handle_history_button()
|
||||||
|
elif index == 2: # ЛОР-Статус
|
||||||
|
if "(ИИ)" in self.main_interface.buttons[2].text() and self.current_image_path:
|
||||||
|
self.insert_text(self.current_image_path)
|
||||||
|
else:
|
||||||
|
self.insert_text("""Отоскопия (эндоскопия уха):
|
||||||
|
Правое ухо: Наружный слуховой проход широкий, кожа розовая, чистая. Барабанная перепонка перламутрово-серого цвета, опознавательные знаки четко дифференцируются.
|
||||||
|
|
||||||
|
Левое ухо: Визуализируется обтурирующая масса в костном отделе наружного слухового прохода.""")
|
||||||
|
elif index == 3: # Заключение ИИ
|
||||||
|
if "(готово)" in self.main_interface.buttons[3].text() and self.current_image_path:
|
||||||
|
self.insert_text(self.current_image_path)
|
||||||
|
else:
|
||||||
|
self.insert_text("H68.0 Воспаление слуховой (евстахиевой) трубы")
|
||||||
|
|
||||||
|
def handle_complaints_button(self):
|
||||||
|
"""Обрабатывает нажатие кнопки жалоб"""
|
||||||
|
button = self.main_interface.buttons[0]
|
||||||
|
text = button.text()
|
||||||
|
|
||||||
|
if "Копировать" in text:
|
||||||
|
self.copy_current_text('complaints')
|
||||||
|
elif "Вставить" in text and self.current_complaints_text:
|
||||||
|
self.insert_text(self.current_complaints_text)
|
||||||
|
|
||||||
|
def handle_history_button(self):
|
||||||
|
"""Обрабатывает нажатие кнопки анамнеза"""
|
||||||
|
button = self.main_interface.buttons[1]
|
||||||
|
text = button.text()
|
||||||
|
|
||||||
|
if "Копировать" in text:
|
||||||
|
self.copy_current_text('history')
|
||||||
|
elif "Вставить" in text and self.current_history_text:
|
||||||
|
self.insert_text(self.current_history_text)
|
||||||
|
|
||||||
|
def copy_current_text(self, processing_type):
|
||||||
|
"""Копирует текст из активного поля и запускает обработку"""
|
||||||
|
try:
|
||||||
|
self.hide_window()
|
||||||
|
self.is_visible = False
|
||||||
|
|
||||||
|
if processing_type == 'complaints':
|
||||||
|
self.main_interface.buttons[0].setText(" Обработка...")
|
||||||
|
else:
|
||||||
|
self.main_interface.buttons[1].setText(" Обработка...")
|
||||||
|
|
||||||
|
self.set_processing_state(True)
|
||||||
|
|
||||||
|
QTimer.singleShot(50, lambda: self._do_copy_and_process(processing_type))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при копировании: {e}")
|
||||||
|
self.set_processing_state(False)
|
||||||
|
|
||||||
|
# def _do_copy_and_process(self, processing_type):
|
||||||
|
# """Копирует текст и запускает обработку"""
|
||||||
|
# try:
|
||||||
|
#
|
||||||
|
# original_text = pyperclip.paste()
|
||||||
|
#
|
||||||
|
# keyboard.press_and_release('ctrl+a')
|
||||||
|
# time.sleep(0.3)
|
||||||
|
# keyboard.press_and_release('ctrl+c')
|
||||||
|
# time.sleep(0.3)
|
||||||
|
# keyboard.press_and_release('end')
|
||||||
|
# time.sleep(0.3)
|
||||||
|
#
|
||||||
|
# copied_text = pyperclip.paste()
|
||||||
|
# # pyperclip.copy(original_text)
|
||||||
|
#
|
||||||
|
# print(copied_text)
|
||||||
|
# if copied_text: #and copied_text != original_text
|
||||||
|
# self.ai_worker.process_text(copied_text, processing_type)
|
||||||
|
# else:
|
||||||
|
# if processing_type == 'complaints':
|
||||||
|
# self.main_interface.buttons[0].setText(" Копировать жалобы")
|
||||||
|
# else:
|
||||||
|
# self.main_interface.buttons[1].setText(" Копировать анамнез")
|
||||||
|
# print("Не удалось скопировать текст из активного поля")
|
||||||
|
# self.show_error("Не удалось скопировать текст из активного поля")
|
||||||
|
# self.set_processing_state(False)
|
||||||
|
#
|
||||||
|
# QTimer.singleShot(100, self.show_window)
|
||||||
|
#
|
||||||
|
# except Exception as e:
|
||||||
|
# print(f"Ошибка при работе с pyperclip: {e}")
|
||||||
|
# self.set_processing_state(False)
|
||||||
|
# QTimer.singleShot(100, self.show_window)
|
||||||
|
|
||||||
|
def _do_copy_and_process(self, processing_type):
|
||||||
|
"""Копирует выделенный текст из любого окна (включая Chrome) через UI Automation"""
|
||||||
|
try:
|
||||||
|
copied_text = None
|
||||||
|
|
||||||
|
if auto is not None:
|
||||||
|
try:
|
||||||
|
selected_text = self._get_selected_text_uia()
|
||||||
|
if selected_text:
|
||||||
|
copied_text = selected_text
|
||||||
|
except Exception as e:
|
||||||
|
print(f"UIA сбой: {e}")
|
||||||
|
copied_text = None
|
||||||
|
print("Auto:", copied_text)
|
||||||
|
# Fallback: если UIA недоступен или не сработал — используем SendInput
|
||||||
|
if not copied_text:
|
||||||
|
copied_text = self._copy_via_sendinput()
|
||||||
|
print("SendInput:", copied_text)
|
||||||
|
|
||||||
|
# Обработка результата
|
||||||
|
if copied_text:
|
||||||
|
self.ai_worker.process_text(copied_text, processing_type)
|
||||||
|
else:
|
||||||
|
if processing_type == 'complaints':
|
||||||
|
self.main_interface.buttons[0].setText(" Копировать жалобы")
|
||||||
|
else:
|
||||||
|
self.main_interface.buttons[1].setText(" Копировать анамнез")
|
||||||
|
print("Не удалось скопировать текст из активного поля")
|
||||||
|
self.show_error("Не удалось скопировать текст из активного поля")
|
||||||
|
self.set_processing_state(False)
|
||||||
|
|
||||||
|
QTimer.singleShot(100, self.show_window)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при работе с pyperclip: {e}")
|
||||||
|
self.set_processing_state(False)
|
||||||
|
QTimer.singleShot(100, self.show_window)
|
||||||
|
|
||||||
|
def _get_selected_text_uia(self):
|
||||||
|
"""Получает весь текст из активного редактируемого поля через UI Automation"""
|
||||||
|
try:
|
||||||
|
focused = auto.GetFocusedControl()
|
||||||
|
if not focused:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Игнорируем известные "пустышки"
|
||||||
|
if focused.ClassName == "IHWindowClass" and "Input Capture" in (focused.Name or ""):
|
||||||
|
# RDP: невозможно получить текст локально
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Пытаемся получить TextPattern (даже у ComboBoxControl в Chrome он есть!)
|
||||||
|
text_pattern = focused.GetTextPattern()
|
||||||
|
if text_pattern is not None:
|
||||||
|
try:
|
||||||
|
full_range = text_pattern.DocumentRange
|
||||||
|
if full_range:
|
||||||
|
full_text = full_range.GetText(-1)
|
||||||
|
if isinstance(full_text, str):
|
||||||
|
return full_text.strip()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при получении DocumentRange: {e}")
|
||||||
|
|
||||||
|
# Fallback: если TextPattern недоступен, пробуем Value (осторожно!)
|
||||||
|
if hasattr(focused, 'LegacyIAccessible'):
|
||||||
|
acc = focused.LegacyIAccessible
|
||||||
|
value = getattr(acc, 'Value', None)
|
||||||
|
if isinstance(value, str) and value.strip():
|
||||||
|
# Дополнительная проверка: если Name похож на placeholder — не доверяем Value
|
||||||
|
# Но в большинстве нативных приложений Value = реальный текст
|
||||||
|
return value.strip()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"UIA: ошибка получения текста: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _copy_via_sendinput(self):
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Сохраняем оригинальный буфер
|
||||||
|
original_clipboard = pyperclip.paste()
|
||||||
|
# Очищаем буфер перед копированием (чтобы точно знать, что скопировалось)
|
||||||
|
pyperclip.copy("")
|
||||||
|
time.sleep(0.15)
|
||||||
|
|
||||||
|
# Выделяем всё и копируем
|
||||||
|
keyboard.press_and_release('ctrl+a', do_press=True, do_release=False)
|
||||||
|
time.sleep(0.2)
|
||||||
|
keyboard.press_and_release('ctrl+a', do_press=False, do_release=True)
|
||||||
|
time.sleep(0.2)
|
||||||
|
keyboard.press_and_release('ctrl+c', do_press=True, do_release=False)
|
||||||
|
time.sleep(0.2)
|
||||||
|
keyboard.press_and_release('ctrl+c', do_press=False, do_release=True)
|
||||||
|
time.sleep(0.4) # Достаточно для большинства случаев
|
||||||
|
|
||||||
|
# # Перемещаем курсор в конец (опционально)
|
||||||
|
# keyboard.press_and_release('end')
|
||||||
|
time.sleep(0.15)
|
||||||
|
|
||||||
|
# Получаем результат
|
||||||
|
copied_text = pyperclip.paste().strip()
|
||||||
|
|
||||||
|
# Восстанавливаем оригинальный буфер
|
||||||
|
pyperclip.copy(original_clipboard)
|
||||||
|
|
||||||
|
if copied_text:
|
||||||
|
return copied_text
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при копировании: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def insert_text(self, text):
|
||||||
|
"""Вставляет текст в активное поле ввода"""
|
||||||
|
try:
|
||||||
|
self.hide_window()
|
||||||
|
self.is_visible = False
|
||||||
|
|
||||||
|
if text:
|
||||||
|
QTimer.singleShot(50, lambda: self._do_insert_pyperclip(text))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при вставке: {e}")
|
||||||
|
|
||||||
|
def _do_insert_pyperclip(self, text):
|
||||||
|
"""Вставка текста с использованием pyperclip"""
|
||||||
|
try:
|
||||||
|
import pyperclip
|
||||||
|
|
||||||
|
original_text = pyperclip.paste()
|
||||||
|
pyperclip.copy(text)
|
||||||
|
|
||||||
|
QTimer.singleShot(30, lambda: self._paste_and_restore(original_text))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при работе с pyperclip: {e}")
|
||||||
|
QTimer.singleShot(100, self.show_window)
|
||||||
|
|
||||||
|
def _paste_and_restore(self, original_text):
|
||||||
|
"""Вставляет текст и восстанавливает буфер"""
|
||||||
|
try:
|
||||||
|
keyboard.press_and_release('ctrl+v')
|
||||||
|
QTimer.singleShot(100, lambda: pyperclip.copy(original_text))
|
||||||
|
QTimer.singleShot(100, self.show_window)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при вставке: {e}")
|
||||||
|
QTimer.singleShot(100, self.show_window)
|
||||||
|
|
||||||
|
def show_error(self, message):
|
||||||
|
"""Показывает сообщение об ошибке"""
|
||||||
|
print(f"Ошибка: {message}")
|
||||||
|
|
||||||
|
def tray_icon_activated(self, reason):
|
||||||
|
"""Обработчик активации иконки в трее"""
|
||||||
|
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
|
||||||
|
if self.is_visible:
|
||||||
|
self.hide_window()
|
||||||
|
else:
|
||||||
|
self.show_window()
|
||||||
|
|
||||||
|
def quit_application(self):
|
||||||
|
"""Корректный выход из приложения"""
|
||||||
|
if self.hook_manager:
|
||||||
|
try:
|
||||||
|
self.hook_manager.UnhookMouse()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.ai_worker.isRunning():
|
||||||
|
self.ai_worker.stop()
|
||||||
|
|
||||||
|
self.tray_icon.hide()
|
||||||
|
QApplication.quit()
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
"""Отрисовывает тень вокруг окна"""
|
||||||
|
painter = QPainter(self)
|
||||||
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
|
|
||||||
|
shadow_margin = 8
|
||||||
|
shadow_rect = self.rect().adjusted(
|
||||||
|
shadow_margin,
|
||||||
|
shadow_margin,
|
||||||
|
-shadow_margin,
|
||||||
|
-shadow_margin
|
||||||
|
)
|
||||||
|
|
||||||
|
painter.setPen(Qt.PenStyle.NoPen)
|
||||||
|
painter.setBrush(Qt.GlobalColor.black)
|
||||||
|
painter.setOpacity(0.2)
|
||||||
|
painter.drawRoundedRect(shadow_rect, 15, 15)
|
||||||
|
|
||||||
|
painter.end()
|
||||||
|
super().paintEvent(event)
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""Обработчик закрытия окна"""
|
||||||
|
event.ignore()
|
||||||
|
self.hide_window()
|
||||||
|
|
||||||
|
def restart_form(self):
|
||||||
|
self.main_interface.buttons[0].setText(" Копировать жалобы")
|
||||||
|
self.main_interface.buttons[1].setText(" Копировать анамнез")
|
||||||
|
self.main_interface.buttons[2].setText(" ЛОР-Статус")
|
||||||
|
self.main_interface.buttons[3].setText(" Заключение ИИ")
|
||||||
|
|
||||||
|
self.current_complaints_text = ""
|
||||||
|
self.current_history_text = ""
|
||||||
|
self.current_image_path = ""
|
||||||
+1706
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
|||||||
|
# Medical Input Helper 🏥
|
||||||
|
|
||||||
|
Умное приложение для автоматизации ввода медицинских текстов с поддержкой AI. Панель автоматически появляется при клике на поля ввода в любых приложениях (браузеры, текстовые редакторы, EHR системы).
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## ✨ Возможности
|
||||||
|
|
||||||
|
### 🤖 Интеллектуальная обработка текста
|
||||||
|
- **Автоматическое определение полей ввода** в любых приложениях
|
||||||
|
- **AI-обработка медицинских текстов** с использованием LLM
|
||||||
|
- **Поддержка русского языка** и медицинской терминологии
|
||||||
|
- **Автоматическое форматирование** жалоб и анамнеза
|
||||||
|
|
||||||
|
### 🖱️ Удобный интерфейс
|
||||||
|
- **Плавающая панель** с автоматическим появлением
|
||||||
|
- **Drag-and-drop** для текста и изображений
|
||||||
|
- **Системный трей** для фоновой работы
|
||||||
|
- **Перетаскиваемые окна** для удобного позиционирования
|
||||||
|
|
||||||
|
### 🏥 Медицинские функции
|
||||||
|
- **Обработка жалоб пациента** с AI-анализом
|
||||||
|
- **Форматирование анамнеза заболевания**
|
||||||
|
- **Подготовка ЛОР-статуса**
|
||||||
|
- **Генерация медицинских заключений**
|
||||||
|
|
||||||
|
## 🚀 Установка
|
||||||
|
|
||||||
|
### Требования
|
||||||
|
- Python 3.8 или выше
|
||||||
|
- 4GB+ оперативной памяти (8GB+ рекомендуется для LLM)
|
||||||
|
|
||||||
|
### Установка зависимостей
|
||||||
|
```bash
|
||||||
|
pip install PyQt6 qtawesome uiautomation pyperclip keyboard chromadb llama-cpp-python torch tiktoken psutil pillow requests
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import sys
|
||||||
|
from PyQt6.QtWidgets import QApplication, QWidget
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
|
class DropWidget(QWidget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
self.resize(400, 300)
|
||||||
|
self.setWindowTitle("Drag & Drop Monitor")
|
||||||
|
print("Окно готово к приему перетаскивания. Перетащите что-нибудь сюда.")
|
||||||
|
|
||||||
|
def dragEnterEvent(self, event):
|
||||||
|
mime_data = event.mimeData()
|
||||||
|
formats = mime_data.formats()
|
||||||
|
print("Типы перетаскиваемых данных:", [str(f) for f in formats])
|
||||||
|
|
||||||
|
if mime_data.hasUrls():
|
||||||
|
print("Перетаскиваются файлы или ссылки")
|
||||||
|
elif mime_data.hasText():
|
||||||
|
print("Перетаскивается текст")
|
||||||
|
elif mime_data.hasImage():
|
||||||
|
print("Перетаскивается изображение")
|
||||||
|
elif mime_data.hasHtml():
|
||||||
|
print("Перетаскивается HTML")
|
||||||
|
else:
|
||||||
|
print("Неизвестный тип данных")
|
||||||
|
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def dropEvent(self, event):
|
||||||
|
print("Объект сброшен.")
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
window = DropWidget()
|
||||||
|
window.show()
|
||||||
|
sys.exit(app.exec())
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import uiautomation as auto
|
||||||
|
import time
|
||||||
|
|
||||||
|
def is_editable_focused():
|
||||||
|
try:
|
||||||
|
focused = auto.GetFocusedControl()
|
||||||
|
if not focused:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Проверяем тип элемента
|
||||||
|
control_type = focused.ControlType
|
||||||
|
# EditControl — это текстовое поле ввода в Windows UI Automation
|
||||||
|
if control_type == auto.ControlType.EditControl:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Также можно проверить по классу (например, для браузеров)
|
||||||
|
class_name = focused.ClassName
|
||||||
|
# Например, Chrome использует класс 'Chrome_RenderWidgetHostHWND'
|
||||||
|
# Но это менее надёжно; лучше использовать UIA
|
||||||
|
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Отслеживание фокуса в текстовых полях...")
|
||||||
|
last_state = False
|
||||||
|
while True:
|
||||||
|
current_state = is_editable_focused()
|
||||||
|
if current_state and not last_state:
|
||||||
|
print("Текстовое поле получило фокус!")
|
||||||
|
last_state = current_state
|
||||||
|
time.sleep(0.5) # Проверка 2 раза в секунду
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
import requests
|
||||||
|
from io import BytesIO
|
||||||
|
import os
|
||||||
|
|
||||||
|
url = "https://www.k31.ru/uploads/articles/2025-08-15/689f35e92c4cf.jpg"
|
||||||
|
|
||||||
|
response = requests.get(url)
|
||||||
|
print(response.content)
|
||||||
|
img = Image.open(BytesIO(response.content))
|
||||||
|
img.save(f"D:\\WORK\\EVP_2.0\\SmartIntegration\\data\\{os.path.basename(url)}")
|
||||||
Reference in New Issue
Block a user