mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Transcribe and clean up on this machine, without installing anything first
whisper-server is started on --inference-path /v1/audio/transcriptions, which is exactly the path api.py already builds for the hosted providers, and llama-server answers /chat/completions the way OpenRouter does. So the local half is one more base URL rather than a second code path: worker.py, filetranscribe.py and meeting.py are untouched, and dictation, subtitles and meetings all work here on the first try. Three findings worth naming, none of them in the new code: whisper.cpp cuts segments on tokens, which in Turkish lands inside a word about as often as between two. Pasted raw that gives "akraba değ\niller."; in a subtitle it gives a cue reading "değ". Whisper marks the start of a word with a leading space, so a piece that does not begin with one continues the word above it. A small model will repeat the transcript until the context is full, and every one of those tokens is a second of somebody waiting: measured at 206 seconds, and 25 with a ceiling on the reply. Hosted models are left alone, where the same runaway is rare and a ceiling would cut the minutes short. A server outlives SIGTERM and SIGKILL holding its model in memory. Signals are now turned into an event Qt delivers, since Qt blocks in C where a Python handler never runs, and a pid file lets the next start sweep up what a SIGKILL left behind. The minutes keep their own provider rather than following cleanup's. The two jobs are not the same size: a 4B model here will strip the filler words out of a dictation and will not write up an hour long meeting. The suite runs offline now: a test that reaches the network says so instead of quietly going there.
This commit is contained in:
+438
-14
@@ -18,6 +18,7 @@ import assistant
|
||||
import audio
|
||||
import config as cfg
|
||||
import filetranscribe
|
||||
import ggml
|
||||
import hotkey
|
||||
import meeting
|
||||
from filetranscribe import FileTranscriber
|
||||
@@ -29,7 +30,10 @@ LANGUAGES = [
|
||||
("German", "de"), ("French", "fr"), ("Spanish", "es"), ("Arabic", "ar"),
|
||||
]
|
||||
CORNERS = ["bottom-left", "bottom-right", "top-left", "top-right"]
|
||||
TRANSCRIBE_PROVIDERS = [("OpenAI", "openai"), ("OpenRouter", "openrouter")]
|
||||
TRANSCRIBE_PROVIDERS = [("This machine (whisper.cpp)", "local"),
|
||||
("OpenAI", "openai"), ("OpenRouter", "openrouter")]
|
||||
CLEANUP_PROVIDERS = [("OpenRouter", "openrouter"),
|
||||
("This machine (llama.cpp)", "local")]
|
||||
# Starting points for the model box; "Fetch model list" replaces them with
|
||||
# whatever the provider offers today.
|
||||
TRANSCRIBE_MODELS = {
|
||||
@@ -103,6 +107,316 @@ AUDIO_FILTER = ("*.mp3 *.wav *.m4a *.ogg *.opus *.flac *.aac *.wma "
|
||||
"*.mp4 *.mkv *.webm *.mov *.avi")
|
||||
|
||||
|
||||
class LocalModelBox(QGroupBox):
|
||||
"""The program, the model, and the two downloads that put them there.
|
||||
|
||||
One class for whisper.cpp and llama.cpp, because the job is the same one
|
||||
twice: say whether the program is here, offer the models somebody publishes,
|
||||
fetch the chosen one, and stay usable while a gigabyte arrives. Nothing is
|
||||
listed in the source; `repos` and `models` are asked at the moment the box is
|
||||
opened, so a model published this morning is in the list this afternoon.
|
||||
"""
|
||||
|
||||
_listed = pyqtSignal(list, str)
|
||||
_quants = pyqtSignal(list, str)
|
||||
_progress = pyqtSignal(int, int)
|
||||
_finished = pyqtSignal(str, str)
|
||||
_installed = pyqtSignal(str, str)
|
||||
|
||||
changed = pyqtSignal()
|
||||
|
||||
def __init__(self, program, title, models, model_path, repos=None, parent=None):
|
||||
super().__init__(title, parent)
|
||||
self.program = program
|
||||
self._models = models # () -> [hub.Item], or (repo) -> [hub.Item]
|
||||
self._model_path = model_path # (name) -> Path
|
||||
self._repos = repos # None, or () -> [repo id]
|
||||
self._downloading = False
|
||||
self._pending = False
|
||||
self._stop = False
|
||||
self._wanted = "" # the model to select once a list arrives
|
||||
|
||||
form = QFormLayout(self)
|
||||
|
||||
self.program_label = QLabel("")
|
||||
self.program_label.setWordWrap(True)
|
||||
self.install_button = QPushButton(t("Download"))
|
||||
self.install_button.clicked.connect(self._install_program)
|
||||
form.addRow(t("Program"), self._side_by_side(self.program_label,
|
||||
self.install_button))
|
||||
|
||||
if self._repos is not None:
|
||||
self.repo = QComboBox()
|
||||
self.repo.setEditable(True)
|
||||
self.repo.setToolTip(t("A Hugging Face repository of GGUF files. The "
|
||||
"list is fetched; any other one can be typed in."))
|
||||
self.repo.currentTextChanged.connect(self._repo_changed)
|
||||
form.addRow(t("Publisher"), self.repo)
|
||||
|
||||
self.model = QComboBox()
|
||||
self.download_button = QPushButton(t("Download"))
|
||||
self.download_button.clicked.connect(self._download)
|
||||
self.delete_button = QPushButton(t("Delete"))
|
||||
self.delete_button.clicked.connect(self._delete)
|
||||
form.addRow(t("Model"), self._side_by_side(self.model,
|
||||
self.download_button,
|
||||
self.delete_button))
|
||||
self.model.currentIndexChanged.connect(self._model_changed)
|
||||
|
||||
self.status = QLabel("")
|
||||
self.status.setWordWrap(True)
|
||||
form.addRow(self.status)
|
||||
|
||||
self._listed.connect(self._on_listed)
|
||||
self._quants.connect(self._on_listed)
|
||||
self._progress.connect(self._on_progress)
|
||||
self._finished.connect(self._on_finished)
|
||||
self._installed.connect(self._on_installed)
|
||||
|
||||
@staticmethod
|
||||
def _side_by_side(*widgets):
|
||||
layout = QHBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
for index, widget in enumerate(widgets):
|
||||
layout.addWidget(widget, 1 if index == 0 else 0)
|
||||
holder = QWidget()
|
||||
holder.setLayout(layout)
|
||||
return holder
|
||||
|
||||
# ---- what is here ----------------------------------------------------
|
||||
|
||||
def selected(self):
|
||||
return self.model.currentData() or ""
|
||||
|
||||
def repository(self):
|
||||
return self.repo.currentText().strip() if self._repos is not None else ""
|
||||
|
||||
def load(self, model, repo=""):
|
||||
"""Show what is stored. What else is on offer is asked for on the way up.
|
||||
|
||||
Nothing is fetched here: building the settings window is not the same as
|
||||
opening it, and a list nobody is looking at is not worth a request. What
|
||||
is already on this disk is shown straight away either way.
|
||||
"""
|
||||
self._wanted = model
|
||||
self._pending = True
|
||||
self._show_program()
|
||||
if self._repos is not None:
|
||||
self.repo.blockSignals(True)
|
||||
self.repo.clear()
|
||||
self.repo.addItems(list(ggml.SUGGESTED_LLM))
|
||||
self.repo.setCurrentText(repo or ggml.SUGGESTED_LLM[0])
|
||||
self.repo.blockSignals(False)
|
||||
self._fill_models([])
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
if self._pending:
|
||||
self._pending = False
|
||||
if self._repos is not None:
|
||||
self._fill_repos(self.repository())
|
||||
self._fetch_models(self.repository())
|
||||
|
||||
def _show_program(self):
|
||||
path = ggml.program_path(self.program)
|
||||
if not path:
|
||||
self.program_label.setText(t("Not installed."))
|
||||
self.install_button.setVisible(True)
|
||||
return
|
||||
self.install_button.setVisible(not ggml.installed_program(self.program)
|
||||
and not ggml.system_program(self.program))
|
||||
if ggml.system_program(self.program):
|
||||
# Worth saying which one is running: a distribution package is built
|
||||
# for this machine and may reach the graphics card, while the
|
||||
# released binaries carry processor backends only.
|
||||
self.program_label.setText(t("Installed on the system: {path}", path=path))
|
||||
else:
|
||||
self.program_label.setText(
|
||||
t("Downloaded, version {version}.",
|
||||
version=ggml.installed_version(self.program) or "?"))
|
||||
|
||||
# ---- the lists -------------------------------------------------------
|
||||
|
||||
def _fill_repos(self, current):
|
||||
def work():
|
||||
self._listed.emit([("repos", ggml.llm_repos())], "")
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def _repo_changed(self):
|
||||
if not self._downloading:
|
||||
self._fetch_models(self.repository())
|
||||
|
||||
def _fetch_models(self, repo=""):
|
||||
self.status.setText(t("Fetching the model list…"))
|
||||
|
||||
def work():
|
||||
try:
|
||||
found = self._models(repo) if self._repos is not None else self._models()
|
||||
self._quants.emit([("models", found)], "")
|
||||
except ggml.LocalError as exc:
|
||||
self._quants.emit([], str(exc))
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def _on_listed(self, payload, error):
|
||||
if error:
|
||||
self.status.setText(error)
|
||||
self._refresh_buttons()
|
||||
return
|
||||
kind, found = payload[0]
|
||||
if kind == "repos":
|
||||
current = self.repo.currentText()
|
||||
self.repo.blockSignals(True)
|
||||
self.repo.clear()
|
||||
self.repo.addItems(found)
|
||||
self.repo.setCurrentText(current)
|
||||
self.repo.blockSignals(False)
|
||||
return
|
||||
self._fill_models(found)
|
||||
|
||||
def _fill_models(self, items):
|
||||
"""One row per model, saying what it weighs and whether it is here."""
|
||||
wanted = self._wanted or self.selected()
|
||||
here = [name for name in (self._model_path(i.name).name for i in items)]
|
||||
self.model.blockSignals(True)
|
||||
self.model.clear()
|
||||
for item, name in zip(items, here):
|
||||
mark = (t("downloaded") if ggml.have_model(self._model_path(item.name))
|
||||
else ggml.human_size(item.size))
|
||||
self.model.addItem(f"{name} ({mark})", name)
|
||||
self.model.setItemData(self.model.count() - 1, item, Qt.ItemDataRole.UserRole + 1)
|
||||
# A model that was downloaded and then dropped from the list upstream is
|
||||
# still on this disk and still works, so it stays on offer.
|
||||
for name in self._on_disk():
|
||||
if self.model.findData(name) < 0:
|
||||
self.model.addItem(f"{name} ({t('downloaded')})", name)
|
||||
# And one that is chosen but not here, because the file was deleted from
|
||||
# underneath or the settings came from another machine, stays chosen:
|
||||
# Save reads this box, and a row missing here would quietly empty the
|
||||
# setting rather than showing that the model needs downloading again.
|
||||
if wanted and self.model.findData(wanted) < 0:
|
||||
self.model.addItem(f"{wanted} ({t('not downloaded')})", wanted)
|
||||
index = self.model.findData(wanted)
|
||||
self.model.setCurrentIndex(max(index, 0))
|
||||
self.model.blockSignals(False)
|
||||
self._wanted = ""
|
||||
self._model_changed()
|
||||
|
||||
def _on_disk(self):
|
||||
return (ggml.installed_whisper_models() if self.program is ggml.WHISPER
|
||||
else ggml.installed_llm_models())
|
||||
|
||||
# ---- fetching --------------------------------------------------------
|
||||
|
||||
def _install_program(self):
|
||||
self.install_button.setEnabled(False)
|
||||
self.program_label.setText(t("Downloading…"))
|
||||
|
||||
def work():
|
||||
try:
|
||||
ggml.install_program(self.program, on_progress=self._report)
|
||||
self._installed.emit("", "")
|
||||
except ggml.LocalError as exc:
|
||||
self._installed.emit("", str(exc))
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def _on_installed(self, _, error):
|
||||
self.install_button.setEnabled(True)
|
||||
self._show_program()
|
||||
if error:
|
||||
self.program_label.setText(error)
|
||||
self.changed.emit()
|
||||
|
||||
def _current_item(self):
|
||||
return self.model.currentData(Qt.ItemDataRole.UserRole + 1)
|
||||
|
||||
def _download(self):
|
||||
if self._downloading:
|
||||
self._stop = True
|
||||
return
|
||||
item = self._current_item()
|
||||
if item is None:
|
||||
return
|
||||
self._downloading, self._stop = True, False
|
||||
self._refresh_buttons()
|
||||
|
||||
def work():
|
||||
try:
|
||||
landed = ggml.download(item, self._model_path(item.name),
|
||||
on_progress=self._report,
|
||||
should_stop=lambda: self._stop)
|
||||
self._finished.emit(item.name if landed else "", "")
|
||||
except ggml.LocalError as exc:
|
||||
self._finished.emit("", str(exc))
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def _report(self, done, total):
|
||||
self._progress.emit(done, total)
|
||||
|
||||
def _on_progress(self, done, total):
|
||||
share = f" ({done * 100 // total}%)" if total else ""
|
||||
text = t("Downloading: {done} of {total}{share}",
|
||||
done=ggml.human_size(done), total=ggml.human_size(total or done),
|
||||
share=share)
|
||||
if self._downloading:
|
||||
self.status.setText(text)
|
||||
else:
|
||||
self.program_label.setText(text)
|
||||
|
||||
def _on_finished(self, name, error):
|
||||
self._downloading = False
|
||||
if error:
|
||||
self.status.setText(error)
|
||||
elif not name:
|
||||
self.status.setText(t("Download stopped."))
|
||||
self._fill_models_from_current()
|
||||
self.changed.emit()
|
||||
|
||||
def _fill_models_from_current(self):
|
||||
"""Redraw the rows without asking anybody anything again."""
|
||||
items = [self.model.itemData(i, Qt.ItemDataRole.UserRole + 1)
|
||||
for i in range(self.model.count())]
|
||||
self._wanted = self.selected()
|
||||
self._fill_models([i for i in items if i is not None])
|
||||
|
||||
def _delete(self):
|
||||
name = self.selected()
|
||||
if not name or not ggml.have_model(self._model_path(name)):
|
||||
return
|
||||
if QMessageBox.question(self, t("Delete model"),
|
||||
t("Delete {name} from this machine?", name=name)) \
|
||||
!= QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
try:
|
||||
ggml.delete_model(self._model_path(name))
|
||||
except ggml.LocalError as exc:
|
||||
self.status.setText(str(exc))
|
||||
self._fill_models_from_current()
|
||||
self.changed.emit()
|
||||
|
||||
def _model_changed(self):
|
||||
self._refresh_buttons()
|
||||
self.changed.emit()
|
||||
|
||||
def _refresh_buttons(self):
|
||||
name = self.selected()
|
||||
here = bool(name) and ggml.have_model(self._model_path(name))
|
||||
self.delete_button.setEnabled(here and not self._downloading)
|
||||
self.download_button.setText(t("Stop") if self._downloading else t("Download"))
|
||||
self.download_button.setEnabled(self._downloading or (bool(name) and not here))
|
||||
if self._downloading:
|
||||
return
|
||||
if not name:
|
||||
self.status.setText(t("Nothing downloaded yet."))
|
||||
elif here:
|
||||
self.status.setText(t("Ready: {name}.", name=name))
|
||||
else:
|
||||
self.status.setText(t("{name} has not been downloaded yet.", name=name))
|
||||
|
||||
|
||||
class SettingsWindow(QDialog):
|
||||
applied = pyqtSignal()
|
||||
|
||||
@@ -127,9 +441,9 @@ class SettingsWindow(QDialog):
|
||||
self.setWindowTitle(t("Dikte Settings"))
|
||||
self.resize(680, 640)
|
||||
|
||||
tabs = QTabWidget(self)
|
||||
tabs = self.tabs = QTabWidget(self)
|
||||
tabs.addTab(self._general_tab(), t("General"))
|
||||
tabs.addTab(self._api_tab(), t("API and models"))
|
||||
self.api_tab_index = tabs.addTab(self._api_tab(), t("API and models"))
|
||||
tabs.addTab(self._prompt_tab(), t("Cleanup rules"))
|
||||
tabs.addTab(self._assistant_tab(), t("Agent"))
|
||||
tabs.addTab(self._meeting_tab(), t("Meeting"))
|
||||
@@ -161,6 +475,10 @@ class SettingsWindow(QDialog):
|
||||
self.meetings.finished.connect(self._on_minutes_finished)
|
||||
self.meetings.failed.connect(self._on_minutes_failed)
|
||||
self._load()
|
||||
# On a machine where nothing can transcribe yet, this window was opened
|
||||
# because of that, so open it on the tab that fixes it.
|
||||
if not conf.transcribe_ready():
|
||||
self.tabs.setCurrentIndex(self.api_tab_index)
|
||||
|
||||
# ---- tabs ----------------------------------------------------------
|
||||
|
||||
@@ -269,20 +587,55 @@ class SettingsWindow(QDialog):
|
||||
stt_form = QFormLayout(stt)
|
||||
self.transcribe_provider = QComboBox()
|
||||
for label, value in TRANSCRIBE_PROVIDERS:
|
||||
self.transcribe_provider.addItem(label, value)
|
||||
self.transcribe_provider.addItem(t(label), value)
|
||||
stt_form.addRow(t("Provider"), self.transcribe_provider)
|
||||
|
||||
# The hosted providers take any model id that is typed at them; the
|
||||
# local one offers what has been published, so the two are separate
|
||||
# blocks and only one of them is ever visible.
|
||||
self.hosted_stt = QWidget()
|
||||
hosted_form = QFormLayout(self.hosted_stt)
|
||||
hosted_form.setContentsMargins(0, 0, 0, 0)
|
||||
self.transcribe_model = QComboBox()
|
||||
self.transcribe_model.setEditable(True)
|
||||
self.refresh_transcribe_models = QPushButton(t("Fetch model list"))
|
||||
self.refresh_transcribe_models.clicked.connect(self._load_transcribe_models)
|
||||
stt_form.addRow(t("Model"),
|
||||
self._row(self.transcribe_model, self.refresh_transcribe_models))
|
||||
hosted_form.addRow(t("Model"),
|
||||
self._row(self.transcribe_model,
|
||||
self.refresh_transcribe_models))
|
||||
# A spanning row: in the narrow field column a wrapped label gets a
|
||||
# height that fits one line, and the rest of the text is cut off.
|
||||
self.transcribe_status = QLabel("")
|
||||
self.transcribe_status.setWordWrap(True)
|
||||
stt_form.addRow(self.transcribe_status)
|
||||
hosted_form.addRow(self.transcribe_status)
|
||||
stt_form.addRow(self.hosted_stt)
|
||||
|
||||
self.local_whisper = LocalModelBox(
|
||||
ggml.WHISPER, t("On this machine"),
|
||||
ggml.whisper_models, ggml.whisper_model_path)
|
||||
stt_form.addRow(self.local_whisper)
|
||||
|
||||
self.local_gpu = QCheckBox(t("Use the graphics card"))
|
||||
self.local_gpu.setToolTip(
|
||||
t("whisper.cpp reaches the card through CUDA, ROCm or Vulkan when the "
|
||||
"build it is running was made with one. A build without any of them "
|
||||
"runs on the processor whatever this says."))
|
||||
self.local_preload = QCheckBox(t("Load the model when Dikte starts"))
|
||||
self.local_preload.setToolTip(
|
||||
t("A large model takes a second or two to load. Loading it up front "
|
||||
"spends that once instead of on the first dictation, at the cost of "
|
||||
"the memory it sits in."))
|
||||
self.local_threads = QSpinBox()
|
||||
self.local_threads.setRange(0, 64)
|
||||
self.local_threads.setSpecialValueText(t("Automatic"))
|
||||
self.local_options = QWidget()
|
||||
options_form = QFormLayout(self.local_options)
|
||||
options_form.setContentsMargins(0, 0, 0, 0)
|
||||
options_form.addRow("", self.local_gpu)
|
||||
options_form.addRow("", self.local_preload)
|
||||
options_form.addRow(t("Threads"), self.local_threads)
|
||||
stt_form.addRow(self.local_options)
|
||||
|
||||
self.transcribe_provider.currentIndexChanged.connect(self._provider_changed)
|
||||
outer.addWidget(stt)
|
||||
|
||||
@@ -291,12 +644,22 @@ class SettingsWindow(QDialog):
|
||||
self.cleanup_enabled = QCheckBox(t("Clean the transcript with a model"))
|
||||
orr_form.addRow("", self.cleanup_enabled)
|
||||
|
||||
self.cleanup_provider = QComboBox()
|
||||
for label, value in CLEANUP_PROVIDERS:
|
||||
self.cleanup_provider.addItem(t(label), value)
|
||||
self.cleanup_provider.currentIndexChanged.connect(self._cleanup_provider_changed)
|
||||
orr_form.addRow(t("Provider"), self.cleanup_provider)
|
||||
|
||||
self.hosted_cleanup = QWidget()
|
||||
cleanup_form = QFormLayout(self.hosted_cleanup)
|
||||
cleanup_form.setContentsMargins(0, 0, 0, 0)
|
||||
self.cleanup_model = QComboBox()
|
||||
self.cleanup_model.setEditable(True)
|
||||
self.cleanup_model.addItems(CLEANUP_MODELS)
|
||||
self.refresh_models = QPushButton(t("Fetch model list"))
|
||||
self.refresh_models.clicked.connect(self._load_models)
|
||||
orr_form.addRow(t("Model"), self._row(self.cleanup_model, self.refresh_models))
|
||||
cleanup_form.addRow(t("Model"), self._row(self.cleanup_model,
|
||||
self.refresh_models))
|
||||
|
||||
self.cleanup_reasoning = QComboBox()
|
||||
for label, value in REASONING_LEVELS:
|
||||
@@ -306,11 +669,38 @@ class SettingsWindow(QDialog):
|
||||
"a light job, so more thinking mostly costs time and tokens. Models "
|
||||
"that cannot think ignore this.")
|
||||
)
|
||||
orr_form.addRow(t("Thinking"), self.cleanup_reasoning)
|
||||
cleanup_form.addRow(t("Thinking"), self.cleanup_reasoning)
|
||||
|
||||
self.models_label = QLabel(t("Runs on OpenRouter."))
|
||||
self.models_label.setWordWrap(True)
|
||||
orr_form.addRow(self.models_label)
|
||||
cleanup_form.addRow(self.models_label)
|
||||
orr_form.addRow(self.hosted_cleanup)
|
||||
|
||||
self.local_llm = LocalModelBox(
|
||||
ggml.LLAMA, t("On this machine"),
|
||||
ggml.llm_quants, ggml.llm_model_path, repos=ggml.llm_repos)
|
||||
orr_form.addRow(self.local_llm)
|
||||
|
||||
self.local_llm_gpu = QCheckBox(t("Use the graphics card"))
|
||||
self.local_llm_preload = QCheckBox(t("Load the model when Dikte starts"))
|
||||
self.local_llm_preload.setToolTip(
|
||||
t("An LLM is slower to load than a whisper model and sits in more "
|
||||
"memory. Off means it is loaded on the first cleanup instead."))
|
||||
self.local_llm_reasoning = QComboBox()
|
||||
for label, value in REASONING_LEVELS:
|
||||
self.local_llm_reasoning.addItem(t(label), value)
|
||||
self.local_llm_reasoning.setToolTip(
|
||||
t("A model trained to think will think unless it is told not to, and "
|
||||
"spending 300 tokens of reasoning on a comma is 300 tokens of "
|
||||
"waiting. Off is what cleanup wants."))
|
||||
self.local_llm_options = QWidget()
|
||||
llm_form = QFormLayout(self.local_llm_options)
|
||||
llm_form.setContentsMargins(0, 0, 0, 0)
|
||||
llm_form.addRow("", self.local_llm_gpu)
|
||||
llm_form.addRow("", self.local_llm_preload)
|
||||
llm_form.addRow(t("Thinking"), self.local_llm_reasoning)
|
||||
orr_form.addRow(self.local_llm_options)
|
||||
|
||||
outer.addWidget(orr)
|
||||
outer.addStretch(1)
|
||||
return page
|
||||
@@ -926,9 +1316,20 @@ class SettingsWindow(QDialog):
|
||||
self._shown_provider = ""
|
||||
self._select_data(self.transcribe_provider, conf["transcribe_provider"])
|
||||
self._provider_changed() # selecting index 0 fires no signal
|
||||
self.local_gpu.setChecked(conf["local_gpu"])
|
||||
self.local_preload.setChecked(conf["local_preload"])
|
||||
self.local_threads.setValue(int(conf["local_threads"]))
|
||||
self.local_whisper.load(conf["local_model"])
|
||||
|
||||
self.cleanup_enabled.setChecked(conf["cleanup_enabled"])
|
||||
self._select_data(self.cleanup_provider, conf["cleanup_provider"])
|
||||
self._cleanup_provider_changed()
|
||||
self.cleanup_model.setCurrentText(conf["cleanup_model"])
|
||||
self._select_data(self.cleanup_reasoning, conf["cleanup_reasoning"])
|
||||
self.local_llm_gpu.setChecked(conf["local_llm_gpu"])
|
||||
self.local_llm_preload.setChecked(conf["local_llm_preload"])
|
||||
self._select_data(self.local_llm_reasoning, conf["local_llm_reasoning"])
|
||||
self.local_llm.load(conf["local_llm_model"], conf["local_llm_repo"])
|
||||
self.cleanup_prompt.setPlainText(conf["cleanup_prompt"] or cfg.default_cleanup_prompt())
|
||||
self.file_cleanup_prompt.setPlainText(
|
||||
conf["file_cleanup_prompt"] or cfg.default_file_cleanup_prompt()
|
||||
@@ -1003,16 +1404,27 @@ class SettingsWindow(QDialog):
|
||||
conf["openai_api_key"] = self.openai_key.text().strip()
|
||||
conf["openrouter_api_key"] = self.openrouter_key.text().strip()
|
||||
|
||||
provider = self.transcribe_provider.currentData() or "openai"
|
||||
self._models[provider] = self.transcribe_model.currentText().strip()
|
||||
provider = self.transcribe_provider.currentData() or "local"
|
||||
if provider in TRANSCRIBE_MODELS:
|
||||
self._models[provider] = self.transcribe_model.currentText().strip()
|
||||
conf["transcribe_provider"] = provider
|
||||
for key, name in (("openai", "transcribe_model"),
|
||||
("openrouter", "openrouter_transcribe_model")):
|
||||
conf[name] = self._models[key].strip() or cfg.DEFAULTS[name]
|
||||
conf["local_model"] = self.local_whisper.selected()
|
||||
conf["local_gpu"] = self.local_gpu.isChecked()
|
||||
conf["local_preload"] = self.local_preload.isChecked()
|
||||
conf["local_threads"] = self.local_threads.value()
|
||||
|
||||
conf["cleanup_enabled"] = self.cleanup_enabled.isChecked()
|
||||
conf["cleanup_provider"] = self.cleanup_provider.currentData() or "openrouter"
|
||||
conf["cleanup_model"] = self.cleanup_model.currentText().strip()
|
||||
conf["cleanup_reasoning"] = self.cleanup_reasoning.currentData() or ""
|
||||
conf["local_llm_model"] = self.local_llm.selected()
|
||||
conf["local_llm_repo"] = self.local_llm.repository()
|
||||
conf["local_llm_gpu"] = self.local_llm_gpu.isChecked()
|
||||
conf["local_llm_preload"] = self.local_llm_preload.isChecked()
|
||||
conf["local_llm_reasoning"] = self.local_llm_reasoning.currentData() or ""
|
||||
|
||||
# Store an empty prompt when it matches the default, so switching the
|
||||
# interface language also switches the prompt language.
|
||||
@@ -1093,15 +1505,27 @@ class SettingsWindow(QDialog):
|
||||
|
||||
def _provider_changed(self):
|
||||
"""Swap the model box over to the newly chosen provider's own model."""
|
||||
if self._shown_provider:
|
||||
if self._shown_provider in TRANSCRIBE_MODELS:
|
||||
self._models[self._shown_provider] = self.transcribe_model.currentText().strip()
|
||||
provider = self.transcribe_provider.currentData() or "openai"
|
||||
provider = self.transcribe_provider.currentData() or "local"
|
||||
self._shown_provider = provider
|
||||
local = provider == "local"
|
||||
self.hosted_stt.setVisible(not local)
|
||||
self.local_whisper.setVisible(local)
|
||||
self.local_options.setVisible(local)
|
||||
if local:
|
||||
return
|
||||
self.transcribe_model.clear()
|
||||
self.transcribe_model.addItems(TRANSCRIBE_MODELS[provider])
|
||||
self.transcribe_model.setCurrentText(self._models[provider])
|
||||
self.transcribe_status.setText("")
|
||||
|
||||
def _cleanup_provider_changed(self):
|
||||
local = (self.cleanup_provider.currentData() or "openrouter") == "local"
|
||||
self.hosted_cleanup.setVisible(not local)
|
||||
self.local_llm.setVisible(local)
|
||||
self.local_llm_options.setVisible(local)
|
||||
|
||||
def _load_transcribe_models(self):
|
||||
"""The model list of whichever provider is selected."""
|
||||
provider = self.transcribe_provider.currentData() or "openai"
|
||||
|
||||
Reference in New Issue
Block a user