You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
184 lines
7.1 KiB
184 lines
7.1 KiB
"""Modal editor for non-secret settings and the stored API key.""" |
|
|
|
from __future__ import annotations |
|
|
|
import os |
|
from pathlib import Path |
|
|
|
from PySide6.QtCore import Signal |
|
from PySide6.QtWidgets import ( |
|
QDialog, |
|
QDialogButtonBox, |
|
QCheckBox, |
|
QFileDialog, |
|
QFormLayout, |
|
QHBoxLayout, |
|
QLineEdit, |
|
QMessageBox, |
|
QPushButton, |
|
QSpinBox, |
|
QToolButton, |
|
QVBoxLayout, |
|
QWidget, |
|
) |
|
|
|
from voice_transcriptor.models import AppSettings |
|
from voice_transcriptor.services.credentials import CredentialError, CredentialService |
|
from voice_transcriptor.services.settings import SettingsError, SettingsRepository |
|
|
|
|
|
class SettingsDialog(QDialog): |
|
"""Edit application settings while keeping the API key out of normal storage.""" |
|
|
|
settings_saved = Signal(AppSettings) |
|
|
|
def __init__( |
|
self, |
|
settings: AppSettings, |
|
repository: SettingsRepository, |
|
credentials: CredentialService, |
|
parent=None, |
|
) -> None: |
|
super().__init__(parent) |
|
self._repository = repository |
|
self._credentials = credentials |
|
self.setWindowTitle("Settings") |
|
|
|
self.model_input = QLineEdit(settings.model, self) |
|
self.model_input.setObjectName("modelInput") |
|
self.language_input = QLineEdit(settings.language, self) |
|
self.language_input.setObjectName("languageInput") |
|
self.api_key_input = QLineEdit(self) |
|
self.api_key_input.setObjectName("apiKeyInput") |
|
self.api_key_input.setEchoMode(QLineEdit.EchoMode.Password) |
|
self.output_directory_input = QLineEdit(str(settings.output_directory), self) |
|
self.output_directory_input.setObjectName("outputDirectoryInput") |
|
self.choose_directory_button = QPushButton("Browse…", self) |
|
self.choose_directory_button.setObjectName("chooseDirectoryButton") |
|
self.choose_directory_button.clicked.connect(self.choose_output_directory) |
|
|
|
output_directory_layout = QHBoxLayout() |
|
output_directory_layout.addWidget(self.output_directory_input) |
|
output_directory_layout.addWidget(self.choose_directory_button) |
|
|
|
form = QFormLayout() |
|
form.addRow("OpenAI API key", self.api_key_input) |
|
form.addRow("Model", self.model_input) |
|
form.addRow("Language", self.language_input) |
|
form.addRow("Output directory", output_directory_layout) |
|
|
|
self.advanced_toggle = QToolButton(self) |
|
self.advanced_toggle.setText("Advanced") |
|
self.advanced_toggle.setCheckable(True) |
|
self.advanced_toggle.setObjectName("advancedToggle") |
|
self.advanced_panel = QWidget(self) |
|
self.advanced_panel.setObjectName("advancedPanel") |
|
advanced_form = QFormLayout(self.advanced_panel) |
|
self.chunk_duration_input = QSpinBox(self.advanced_panel) |
|
self.chunk_duration_input.setObjectName("chunkDurationInput") |
|
self.chunk_duration_input.setRange(1, 86400) |
|
self.chunk_duration_input.setSuffix(" seconds") |
|
self.chunk_duration_input.setValue(settings.chunk_duration_seconds) |
|
self.chunk_overlap_input = QSpinBox(self.advanced_panel) |
|
self.chunk_overlap_input.setObjectName("chunkOverlapInput") |
|
self.chunk_overlap_input.setRange(0, 3600) |
|
self.chunk_overlap_input.setSuffix(" seconds") |
|
self.chunk_overlap_input.setValue(settings.chunk_overlap_seconds) |
|
self.retain_temporary_files_input = QCheckBox("Retain temporary files for debugging", self.advanced_panel) |
|
self.retain_temporary_files_input.setObjectName("retainTemporaryFilesInput") |
|
self.retain_temporary_files_input.setChecked(settings.retain_temporary_files) |
|
advanced_form.addRow("Chunk duration", self.chunk_duration_input) |
|
advanced_form.addRow("Overlap", self.chunk_overlap_input) |
|
advanced_form.addRow(self.retain_temporary_files_input) |
|
self.advanced_panel.hide() |
|
self.advanced_toggle.toggled.connect(self.advanced_panel.setVisible) |
|
|
|
self.button_box = QDialogButtonBox( |
|
QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel, |
|
parent=self, |
|
) |
|
self.save_button = self.button_box.button(QDialogButtonBox.StandardButton.Save) |
|
self.save_button.setObjectName("saveButton") |
|
self.cancel_button = self.button_box.button(QDialogButtonBox.StandardButton.Cancel) |
|
self.cancel_button.setObjectName("cancelButton") |
|
self.button_box.accepted.connect(self.save) |
|
self.button_box.rejected.connect(self.reject) |
|
|
|
layout = QVBoxLayout(self) |
|
layout.addLayout(form) |
|
layout.addWidget(self.advanced_toggle) |
|
layout.addWidget(self.advanced_panel) |
|
layout.addWidget(self.button_box) |
|
|
|
def choose_output_directory(self) -> None: |
|
directory = QFileDialog.getExistingDirectory( |
|
self, |
|
"Choose output directory", |
|
self.output_directory_input.text(), |
|
) |
|
if directory: |
|
self.output_directory_input.setText(directory) |
|
|
|
def save(self) -> None: |
|
"""Persist valid settings and, only when supplied, replace the API key.""" |
|
api_key = self.api_key_input.text().strip() |
|
self.api_key_input.clear() |
|
|
|
output_directory = self._output_directory() |
|
if output_directory is None: |
|
QMessageBox.warning( |
|
self, |
|
"Invalid output directory", |
|
"Choose an accessible output directory.", |
|
) |
|
return |
|
|
|
model = self.model_input.text().strip() |
|
language = self.language_input.text().strip() |
|
if not model or not language: |
|
QMessageBox.warning(self, "Invalid settings", "Model and language are required.") |
|
return |
|
|
|
chunk_duration = self.chunk_duration_input.value() |
|
overlap = self.chunk_overlap_input.value() |
|
if overlap >= chunk_duration: |
|
QMessageBox.warning(self, "Invalid settings", "Overlap must be smaller than chunk duration.") |
|
return |
|
updated_settings = AppSettings( |
|
model, language, output_directory, chunk_duration, overlap, |
|
self.retain_temporary_files_input.isChecked(), |
|
) |
|
try: |
|
self._repository.save(updated_settings) |
|
except SettingsError: |
|
QMessageBox.warning(self, "Could not save settings", "Settings could not be saved.") |
|
return |
|
|
|
if api_key: |
|
try: |
|
self._credentials.set_api_key(api_key) |
|
except CredentialError: |
|
QMessageBox.warning( |
|
self, |
|
"Could not save settings", |
|
"The API key could not be saved.", |
|
) |
|
return |
|
|
|
self.settings_saved.emit(updated_settings) |
|
self.accept() |
|
|
|
def _output_directory(self) -> Path | None: |
|
value = self.output_directory_input.text().strip() |
|
if not value: |
|
return None |
|
|
|
directory = Path(value) |
|
try: |
|
if not directory.is_dir(): |
|
return None |
|
if not os.access(directory, os.R_OK | os.W_OK): |
|
return None |
|
except OSError: |
|
return None |
|
return directory
|
|
|