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