Browse Source

first commit

master
commit
c8a1cbf045
  1. 220
      .gitignore
  2. 429
      ai_worker.py
  3. 65
      drag_drop_handler.py
  4. 124
      icon_window.py
  5. 24
      main.py
  6. 251
      main_interface.py
  7. 780
      main_window.py
  8. 1706
      rag_corpus.json
  9. 37
      readme.md
  10. 39
      scripts/drag_n_drop_handler.py
  11. 37
      scripts/input_search.py
  12. 12
      scripts/test.py

220
.gitignore vendored

@ -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
ai_worker.py

@ -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Обнаружены патологические изменения. Рекомендуется консультация специалиста."

65
drag_drop_handler.py

@ -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
icon_window.py

@ -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()

24
main.py

@ -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()

251
main_interface.py

@ -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
main_window.py

@ -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
rag_corpus.json

File diff suppressed because it is too large Load Diff

37
readme.md

@ -0,0 +1,37 @@
# Medical Input Helper 🏥
Умное приложение для автоматизации ввода медицинских текстов с поддержкой AI. Панель автоматически появляется при клике на поля ввода в любых приложениях (браузеры, текстовые редакторы, EHR системы).
![Python](https://img.shields.io/badge/Python-3.12+-blue.svg)
![PyQt6](https://img.shields.io/badge/PyQt6-GUI-green.svg)
![AI](https://img.shields.io/badge/AI-LLM_Integrated-orange.svg)
## ✨ Возможности
### 🤖 Интеллектуальная обработка текста
- **Автоматическое определение полей ввода** в любых приложениях
- **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

39
scripts/drag_n_drop_handler.py

@ -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())

37
scripts/input_search.py

@ -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()

12
scripts/test.py

@ -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)}")
Loading…
Cancel
Save