Trans_for_doctors / app /gui_app.py
Mintik24's picture
asd
b216c95
"""
Medical Transcription GUI Application
Полнофункциональное приложение для транскрибирования медицинских диктовок
с автоматической генерацией отчётов
"""
import sys
import logging
from pathlib import Path
from typing import Optional, Dict, Any
import threading
import traceback
from datetime import datetime
import os
from PyQt6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QLineEdit, QTextEdit, QFileDialog,
QComboBox, QSpinBox, QCheckBox, QProgressBar, QMessageBox,
QTabWidget, QFormLayout, QGroupBox, QDialog, QScrollArea
)
from PyQt6.QtCore import Qt, pyqtSignal, QObject, QThread
from PyQt6.QtGui import QFont, QIcon, QColor
from PyQt6.QtCore import QTimer
# Import common utilities
from common import (
get_logger,
UIColors,
UIDimensions,
Messages,
Placeholders,
AudioFileException,
TranscriptionException,
ValidationException
)
# Import pipeline config for type hints
from pipeline import PipelineConfig
logger = get_logger(__name__)
class WorkerSignals(QObject):
"""Сигналы для воркера обработки"""
progress = pyqtSignal(str)
finished = pyqtSignal(dict)
error = pyqtSignal(str)
class TranscriptionWorker(QThread):
"""Воркер для обработки аудио в отдельном потоке"""
signals = WorkerSignals()
def __init__(
self,
audio_path: str,
config: PipelineConfig,
patient_data: Dict[str, Any]
) -> None:
super().__init__()
self.audio_path: str = audio_path
self.config: PipelineConfig = config
self.patient_data: Dict[str, Any] = patient_data
def run(self) -> None:
try:
# Импортируем здесь, чтобы избежать циклических зависимостей
from pipeline.medical_pipeline import MedicalTranscriptionPipeline
self.signals.progress.emit("Инициализация пайплайна...")
pipeline = MedicalTranscriptionPipeline(self.config)
self.signals.progress.emit("Запуск транскрибирования...")
result = pipeline.process(
audio_path=self.audio_path,
patient_name=self.patient_data.get("patient_name"),
patient_dob=self.patient_data.get("patient_dob"),
study_area=self.patient_data.get("study_area"),
study_number=self.patient_data.get("study_number"),
study_date=self.patient_data.get("study_date"),
doctor_name=self.patient_data.get("doctor_name"),
generate_report=self.config.generate_report
)
self.signals.progress.emit("Обработка завершена!")
self.signals.finished.emit(result)
except Exception as e:
logger.error(f"Error in transcription worker: {e}\n{traceback.format_exc()}")
self.signals.error.emit(str(e))
class PatientDataDialog(QDialog):
"""Диалог для ввода данных пациента"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Данные пациента")
self.setGeometry(100, 100, 500, 400)
self.init_ui()
self.result = None
def init_ui(self):
layout = QFormLayout()
self.patient_name = QLineEdit()
self.patient_name.setPlaceholderText("Фамилия Имя Отчество")
self.patient_dob = QLineEdit()
self.patient_dob.setPlaceholderText("ДД.MM.YYYY")
self.study_area = QLineEdit()
self.study_area.setPlaceholderText("Область исследования (напр. МРТ головы)")
self.study_number = QLineEdit()
self.study_number.setPlaceholderText("Номер исследования")
self.study_date = QLineEdit()
self.study_date.setPlaceholderText("ДД.MM.YYYY")
self.study_date.setText(datetime.now().strftime("%d.%m.%Y"))
self.doctor_name = QLineEdit()
self.doctor_name.setPlaceholderText("ФИО врача")
layout.addRow("ФИО пациента:", self.patient_name)
layout.addRow("Дата рождения:", self.patient_dob)
layout.addRow("Область исследования:", self.study_area)
layout.addRow("Номер исследования:", self.study_number)
layout.addRow("Дата исследования:", self.study_date)
layout.addRow("ФИО врача:", self.doctor_name)
# Кнопки
button_layout = QHBoxLayout()
ok_btn = QPushButton("OK")
cancel_btn = QPushButton("Отмена")
ok_btn.clicked.connect(self.accept)
cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(ok_btn)
button_layout.addWidget(cancel_btn)
layout.addRow(button_layout)
self.setLayout(layout)
def get_data(self):
"""Получить введённые данные"""
return {
"patient_name": self.patient_name.text(),
"patient_dob": self.patient_dob.text(),
"study_area": self.study_area.text(),
"study_number": self.study_number.text(),
"study_date": self.study_date.text(),
"doctor_name": self.doctor_name.text()
}
class MedicalTranscriptionApp(QMainWindow):
"""Главное окно приложения"""
def __init__(self):
super().__init__()
self.setWindowTitle(Messages.APP_TITLE)
self.setGeometry(100, 100, UIDimensions.WINDOW_WIDTH, UIDimensions.WINDOW_HEIGHT)
# Переменные
self.audio_path = None
self.model_path = Path(__file__).parent.parent
self.worker = None
self.patient_data = {}
self.init_ui()
self.setup_logging()
# Установка стилей
self.apply_styles()
def setup_logging(self):
"""Настройка логирования"""
from common import configure_logging
configure_logging()
def init_ui(self):
"""Инициализация интерфейса"""
main_widget = QWidget()
self.setCentralWidget(main_widget)
# Создание табов
tabs = QTabWidget()
# Таб 1: Транскрибирование
transcription_tab = self.create_transcription_tab()
tabs.addTab(transcription_tab, "Транскрибирование")
# Таб 2: Настройки
settings_tab = self.create_settings_tab()
tabs.addTab(settings_tab, "Настройки")
# Главный layout
main_layout = QVBoxLayout()
main_layout.addWidget(tabs)
main_widget.setLayout(main_layout)
def create_transcription_tab(self):
"""Создание вкладки транскрибирования"""
widget = QWidget()
layout = QVBoxLayout()
# --- Выбор аудиофайла ---
file_group = QGroupBox("1. Выбор аудиофайла")
file_layout = QHBoxLayout()
self.file_path_label = QLineEdit()
self.file_path_label.setReadOnly(True)
self.file_path_label.setPlaceholderText("Аудиофайл не выбран")
browse_btn = QPushButton("Обзор...")
browse_btn.clicked.connect(self.browse_audio_file)
file_layout.addWidget(QLabel("Файл:"))
file_layout.addWidget(self.file_path_label, 1)
file_layout.addWidget(browse_btn)
file_group.setLayout(file_layout)
layout.addWidget(file_group)
# --- Данные пациента ---
patient_group = QGroupBox("2. Данные пациента")
patient_layout = QVBoxLayout()
self.patient_info_label = QLabel("Данные пациента не заполнены")
patient_info_font = QFont()
patient_info_font.setItalic(True)
self.patient_info_label.setFont(patient_info_font)
patient_btn = QPushButton("Заполнить данные пациента...")
patient_btn.clicked.connect(self.open_patient_dialog)
patient_layout.addWidget(self.patient_info_label)
patient_layout.addWidget(patient_btn)
patient_group.setLayout(patient_layout)
layout.addWidget(patient_group)
# --- Опции обработки ---
options_group = QGroupBox("3. Опции обработки")
options_layout = QFormLayout()
self.llm_checkbox = QCheckBox("Использовать LLM-коррекцию")
self.llm_checkbox.setChecked(True)
self.report_checkbox = QCheckBox("Автоматически создать отчёт")
self.report_checkbox.setChecked(True)
self.save_original_checkbox = QCheckBox("Сохранить оригинальную транскрипцию")
self.save_original_checkbox.setChecked(True)
options_layout.addRow(self.llm_checkbox)
options_layout.addRow(self.report_checkbox)
options_layout.addRow(self.save_original_checkbox)
options_group.setLayout(options_layout)
layout.addWidget(options_group)
# --- Прогресс ---
progress_group = QGroupBox("4. Статус обработки")
progress_layout = QVBoxLayout()
self.progress_label = QLabel("Готов к обработке")
self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)
self.progress_bar.setVisible(False)
progress_layout.addWidget(self.progress_label)
progress_layout.addWidget(self.progress_bar)
progress_group.setLayout(progress_layout)
layout.addWidget(progress_group)
# --- Результаты ---
results_group = QGroupBox("5. Результаты")
results_layout = QVBoxLayout()
self.results_text = QTextEdit()
self.results_text.setReadOnly(True)
self.results_text.setPlaceholderText("Результаты обработки появятся здесь")
self.results_text.setMinimumHeight(200)
results_layout.addWidget(self.results_text)
results_group.setLayout(results_layout)
layout.addWidget(results_group)
# --- Кнопки управления ---
button_layout = QHBoxLayout()
self.start_btn = QPushButton("▶ Начать транскрибирование")
self.start_btn.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
font-weight: bold;
padding: 10px;
border-radius: 5px;
}
QPushButton:hover {
background-color: #45a049;
}
QPushButton:disabled {
background-color: #cccccc;
}
""")
self.start_btn.clicked.connect(self.start_transcription)
clear_btn = QPushButton("🗑 Очистить результаты")
clear_btn.clicked.connect(lambda: self.results_text.clear())
button_layout.addWidget(self.start_btn, 1)
button_layout.addWidget(clear_btn)
layout.addLayout(button_layout)
widget.setLayout(layout)
return widget
def create_settings_tab(self):
"""Создание вкладки настроек"""
widget = QWidget()
layout = QVBoxLayout()
# --- Модель Whisper ---
model_group = QGroupBox("Модель Whisper")
model_layout = QFormLayout()
self.model_path_input = QLineEdit()
self.model_path_input.setText(str(self.model_path))
browse_model_btn = QPushButton("Обзор...")
browse_model_btn.clicked.connect(self.browse_model_path)
model_path_layout = QHBoxLayout()
model_path_layout.addWidget(self.model_path_input, 1)
model_path_layout.addWidget(browse_model_btn)
model_layout.addRow("Путь к модели:", model_path_layout)
self.device_combo = QComboBox()
self.device_combo.addItems(["auto", "cuda", "cpu"])
model_layout.addRow("Устройство:", self.device_combo)
self.dtype_combo = QComboBox()
self.dtype_combo.addItems(["float32", "float16", "bfloat16"])
model_layout.addRow("Тип данных:", self.dtype_combo)
model_group.setLayout(model_layout)
layout.addWidget(model_group)
# --- OpenRouter API ---
api_group = QGroupBox("OpenRouter API (для LLM-коррекции)")
api_layout = QFormLayout()
self.api_key_input = QLineEdit()
self.api_key_input.setEchoMode(QLineEdit.EchoMode.Password)
self.api_key_input.setPlaceholderText("Введите ваш API ключ OpenRouter")
api_layout.addRow("API Ключ:", self.api_key_input)
self.model_combo = QComboBox()
self.model_combo.addItems([
"gpt-4o",
"claude-3-opus",
"gemini-pro",
"gpt-4-turbo"
])
api_layout.addRow("Модель LLM:", self.model_combo)
api_group.setLayout(api_layout)
layout.addWidget(api_group)
# --- Медицинские термины ---
terms_group = QGroupBox("База медицинских терминов")
terms_layout = QFormLayout()
self.terms_path_input = QLineEdit()
self.terms_path_input.setText(str(Path(__file__).parent.parent / "medical_terms.txt"))
browse_terms_btn = QPushButton("Обзор...")
browse_terms_btn.clicked.connect(self.browse_terms_path)
terms_path_layout = QHBoxLayout()
terms_path_layout.addWidget(self.terms_path_input, 1)
terms_path_layout.addWidget(browse_terms_btn)
terms_layout.addRow("Путь к файлу терминов:", terms_path_layout)
terms_group.setLayout(terms_layout)
layout.addWidget(terms_group)
layout.addStretch()
# Кнопка сохранения
save_settings_btn = QPushButton("💾 Сохранить настройки")
save_settings_btn.clicked.connect(self.save_settings)
layout.addWidget(save_settings_btn)
widget.setLayout(layout)
return widget
def apply_styles(self):
"""Применение стилей к приложению"""
style = f"""
QMainWindow {{
background-color: {UIColors.BACKGROUND};
}}
QPushButton {{
background-color: {UIColors.PRIMARY};
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
}}
QPushButton:hover {{
background-color: {UIColors.PRIMARY_HOVER};
}}
QPushButton:disabled {{
background-color: {UIColors.DISABLED};
color: {UIColors.TEXT_DISABLED};
}}
QProgressBar {{
border: 1px solid {UIColors.BORDER};
border-radius: 5px;
text-align: center;
}}
QProgressBar::chunk {{
background-color: {UIColors.SUCCESS};
}}
QGroupBox {{
font-weight: bold;
border: 1px solid {UIColors.BORDER};
border-radius: 5px;
margin-top: 10px;
padding-top: 10px;
}}
QGroupBox::title {{
subcontrol-origin: margin;
left: 10px;
padding: 0 3px 0 3px;
}}
QLineEdit, QTextEdit, QComboBox, QSpinBox {{
border: 1px solid {UIColors.BORDER};
border-radius: 4px;
padding: 5px;
background-color: white;
}}
QLabel {{
color: {UIColors.TEXT};
}}
"""
self.setStyleSheet(style)
def browse_audio_file(self):
"""Выбор аудиофайла"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"Выберите аудиофайл",
"",
"Audio Files (*.wav *.mp3 *.m4a);;All Files (*)"
)
if file_path:
self.audio_path = file_path
self.file_path_label.setText(file_path)
def browse_model_path(self):
"""Выбор пути к модели"""
path = QFileDialog.getExistingDirectory(
self,
"Выберите папку с моделью Whisper"
)
if path:
self.model_path_input.setText(path)
def browse_terms_path(self):
"""Выбор пути к файлу терминов"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"Выберите файл с медицинскими терминами",
"",
"Text Files (*.txt);;All Files (*)"
)
if file_path:
self.terms_path_input.setText(file_path)
def open_patient_dialog(self):
"""Открытие диалога ввода данных пациента"""
dialog = PatientDataDialog(self)
if dialog.exec() == QDialog.DialogCode.Accepted:
self.patient_data = dialog.get_data()
self.update_patient_info_label()
def update_patient_info_label(self):
"""Обновление метки с информацией о пациенте"""
if self.patient_data:
text = f"Пациент: {self.patient_data.get('patient_name', 'Не указано')}"
self.patient_info_label.setText(text)
self.patient_info_label.setStyleSheet("color: #4CAF50; font-weight: bold;")
else:
self.patient_info_label.setText("Данные пациента не заполнены")
self.patient_info_label.setStyleSheet("color: #ff9800; font-style: italic;")
def save_settings(self):
"""Сохранение настроек"""
try:
# Здесь можно добавить сохранение настроек в конфиг файл
QMessageBox.information(
self,
"Успешно",
"Настройки сохранены!"
)
except Exception as e:
QMessageBox.critical(
self,
"Ошибка",
f"Ошибка при сохранении настроек: {e}"
)
def start_transcription(self):
"""Запуск транскрибирования"""
# Проверка выбран ли файл
if not self.audio_path:
QMessageBox.warning(
self,
"Ошибка",
"Пожалуйста, выберите аудиофайл!"
)
return
# Проверка наличие файла
if not Path(self.audio_path).exists():
QMessageBox.critical(
self,
"Ошибка",
f"Файл не найден: {self.audio_path}"
)
return
# Проверка данных пациента если нужен отчёт
if self.report_checkbox.isChecked() and not self.patient_data:
QMessageBox.warning(
self,
"Ошибка",
"Для создания отчёта необходимо заполнить данные пациента!"
)
return
# Отключение кнопки запуска
self.start_btn.setEnabled(False)
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
# Создание конфига пайплайна
try:
from pipeline.pipeline_config import PipelineConfig
config = PipelineConfig(
model_path=Path(self.model_path_input.text()),
device=self.device_combo.currentText(),
dtype=self.dtype_combo.currentText(),
medical_terms_file=Path(self.terms_path_input.text()),
openai_api_key=self.api_key_input.text() or None,
openai_model=self.model_combo.currentText(),
correction_enabled=self.llm_checkbox.isChecked(),
save_original=self.save_original_checkbox.isChecked(),
save_corrected=True,
generate_report=self.report_checkbox.isChecked()
)
except Exception as e:
QMessageBox.critical(
self,
"Ошибка конфигурации",
f"Ошибка при создании конфига: {e}"
)
self.start_btn.setEnabled(True)
self.progress_bar.setVisible(False)
return
# Запуск воркера
self.worker = TranscriptionWorker(
self.audio_path,
config,
self.patient_data
)
self.worker.signals.progress.connect(self.on_progress)
self.worker.signals.finished.connect(self.on_finished)
self.worker.signals.error.connect(self.on_error)
self.worker.start()
def on_progress(self, message: str):
"""Обновление прогресса"""
self.progress_label.setText(message)
self.progress_bar.setValue(min(self.progress_bar.value() + 20, 90))
def on_finished(self, result: dict):
"""Завершение обработки"""
self.progress_bar.setValue(100)
self.start_btn.setEnabled(True)
# Вывод результатов
output = "=" * 60 + "\n"
output += "РЕЗУЛЬТАТЫ ОБРАБОТКИ\n"
output += "=" * 60 + "\n\n"
if "transcription_original" in result:
output += "ОРИГИНАЛЬНАЯ ТРАНСКРИПЦИЯ:\n"
output += "-" * 40 + "\n"
output += result["transcription_original"] + "\n\n"
if "transcription_corrected" in result:
output += "СКОРРЕКТИРОВАННАЯ ТРАНСКРИПЦИЯ:\n"
output += "-" * 40 + "\n"
output += result["transcription_corrected"] + "\n\n"
if "report_path" in result:
output += "✓ Отчёт успешно создан:\n"
output += f" {result['report_path']}\n\n"
output += "=" * 60 + "\n"
output += "Обработка завершена успешно!"
self.results_text.setText(output)
QMessageBox.information(
self,
"Успешно",
"Транскрибирование завершено!"
)
def on_error(self, error_message: str):
"""Обработка ошибки"""
self.progress_bar.setVisible(False)
self.start_btn.setEnabled(True)
self.results_text.setText(f"ОШИБКА:\n{error_message}")
QMessageBox.critical(
self,
"Ошибка обработки",
f"Произошла ошибка:\n{error_message}"
)
def main():
"""Запуск приложения"""
from PyQt6.QtWidgets import QApplication
app = QApplication(sys.argv)
window = MedicalTranscriptionApp()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
# Базовое логирование
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
main()