mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 19:06:11 +00:00
Merge pull request #50 from huseyin-emre-tigci/single-instance
Survive every way a dictation was being lost
This commit is contained in:
+95
-32
@@ -1,7 +1,9 @@
|
||||
"""Settings window."""
|
||||
|
||||
import functools
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from PyQt6.QtCore import QEvent, QObject, QRect, Qt, QUrl, pyqtSignal
|
||||
@@ -194,10 +196,12 @@ class LocalModelBox(QGroupBox):
|
||||
"""
|
||||
|
||||
_listed = pyqtSignal(list, str)
|
||||
_quants = pyqtSignal(list, str)
|
||||
# qint64 rather than int, which is C++'s 32-bit one: a 2.3 GB model is more
|
||||
# than fits in it, and the count comes out the far side negative.
|
||||
_progress = pyqtSignal("qint64", "qint64")
|
||||
# The tag says which label the numbers belong to: the program download and
|
||||
# a model download can run at once, and routing by a flag read when the
|
||||
# queued event lands put one job's bytes in the other's label.
|
||||
_progress = pyqtSignal(str, "qint64", "qint64")
|
||||
_finished = pyqtSignal(str, str)
|
||||
_installed = pyqtSignal(str, str)
|
||||
|
||||
@@ -245,7 +249,6 @@ class LocalModelBox(QGroupBox):
|
||||
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)
|
||||
@@ -348,9 +351,9 @@ class LocalModelBox(QGroupBox):
|
||||
def work():
|
||||
try:
|
||||
found = self._models(repo) if self._repos is not None else self._models()
|
||||
self._quants.emit([("models", found)], "")
|
||||
self._listed.emit([("models", found)], "")
|
||||
except ggml.LocalError as exc:
|
||||
self._quants.emit([], str(exc))
|
||||
self._listed.emit([], str(exc))
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
@@ -412,7 +415,9 @@ class LocalModelBox(QGroupBox):
|
||||
|
||||
def work():
|
||||
try:
|
||||
ggml.install_program(self.program, on_progress=self._report)
|
||||
ggml.install_program(
|
||||
self.program,
|
||||
on_progress=functools.partial(self._report, "program"))
|
||||
self._installed.emit("", "")
|
||||
except ggml.LocalError as exc:
|
||||
self._installed.emit("", str(exc))
|
||||
@@ -441,8 +446,17 @@ class LocalModelBox(QGroupBox):
|
||||
|
||||
def work():
|
||||
try:
|
||||
landed = ggml.download(item, self._model_path(item.name),
|
||||
on_progress=self._report,
|
||||
target_path = self._model_path(item.name)
|
||||
# A model the running server holds open cannot be replaced on
|
||||
# Windows, and the finished download would be thrown away over
|
||||
# it; a re-download lets go of the server first, and the next
|
||||
# run starts it again on the fresh file.
|
||||
if target_path.exists():
|
||||
(ggml.whisper if self.program is ggml.WHISPER
|
||||
else ggml.llm).stop()
|
||||
landed = ggml.download(item, target_path,
|
||||
on_progress=functools.partial(
|
||||
self._report, "model"),
|
||||
should_stop=lambda: self._stop)
|
||||
self._finished.emit(item.name if landed else "", "")
|
||||
except ggml.LocalError as exc:
|
||||
@@ -450,15 +464,15 @@ class LocalModelBox(QGroupBox):
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def _report(self, done, total):
|
||||
self._progress.emit(done, total)
|
||||
def _report(self, job, done, total):
|
||||
self._progress.emit(job, done, total)
|
||||
|
||||
def _on_progress(self, done, total):
|
||||
def _on_progress(self, job, 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:
|
||||
if job == "model":
|
||||
self.status.setText(text)
|
||||
else:
|
||||
self.program_label.setText(text)
|
||||
@@ -524,6 +538,15 @@ class SettingsWindow(QDialog):
|
||||
# hears about it from here rather than waiting for its own next check.
|
||||
update_found = pyqtSignal(object)
|
||||
|
||||
def _sources_once(self):
|
||||
"""One device listing per window build, shared by every combo.
|
||||
|
||||
Three listings at open were three subprocess runs on the main thread,
|
||||
which on a slow ffmpeg was most of the wait for the window."""
|
||||
if not hasattr(self, "_sources"):
|
||||
self._sources = audio.list_sources()
|
||||
return self._sources
|
||||
|
||||
_models_loaded = pyqtSignal(list, str)
|
||||
_transcribe_models_loaded = pyqtSignal(list, str)
|
||||
# Which key was tested, whether it worked, and what to write under it.
|
||||
@@ -650,7 +673,7 @@ class SettingsWindow(QDialog):
|
||||
|
||||
self.mic = QComboBox()
|
||||
self.mic.addItem(t("Default microphone"), "")
|
||||
for name, desc in audio.list_sources():
|
||||
for name, desc in self._sources_once():
|
||||
self.mic.addItem(desc, name)
|
||||
form.addRow(t("Microphone"), self.mic)
|
||||
|
||||
@@ -1115,7 +1138,7 @@ class SettingsWindow(QDialog):
|
||||
sources_form = QFormLayout(sources)
|
||||
self.meeting_mic = QComboBox()
|
||||
self.meeting_mic.addItem(t("Same as dictation"), "")
|
||||
for name, desc in audio.list_sources():
|
||||
for name, desc in self._sources_once():
|
||||
self.meeting_mic.addItem(desc, name)
|
||||
sources_form.addRow(t("Microphone"), self.meeting_mic)
|
||||
|
||||
@@ -1628,9 +1651,20 @@ class SettingsWindow(QDialog):
|
||||
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())
|
||||
# The defaults as they read NOW, kept for the save comparison: after a
|
||||
# language switch the boxes still hold the old language's default, and
|
||||
# comparing against the new one would store that text as a custom
|
||||
# prompt shadowing every future improvement.
|
||||
self._loaded_defaults = {
|
||||
"cleanup": cfg.default_cleanup_prompt(),
|
||||
"file": cfg.default_file_cleanup_prompt(),
|
||||
"assistant": cfg.default_assistant_prompt(),
|
||||
"meeting": cfg.default_meeting_prompt(),
|
||||
}
|
||||
self.cleanup_prompt.setPlainText(
|
||||
conf["cleanup_prompt"] or self._loaded_defaults["cleanup"])
|
||||
self.file_cleanup_prompt.setPlainText(
|
||||
conf["file_cleanup_prompt"] or cfg.default_file_cleanup_prompt()
|
||||
conf["file_cleanup_prompt"] or self._loaded_defaults["file"]
|
||||
)
|
||||
self.transcribe_prompt.setPlainText(conf["transcribe_prompt"])
|
||||
|
||||
@@ -1648,7 +1682,7 @@ class SettingsWindow(QDialog):
|
||||
self.assistant_paste.setChecked(conf["assistant_paste"])
|
||||
self.assistant_cleanup.setChecked(conf["assistant_cleanup"])
|
||||
self.assistant_prompt.setPlainText(
|
||||
conf["assistant_prompt"] or cfg.default_assistant_prompt()
|
||||
conf["assistant_prompt"] or self._loaded_defaults["assistant"]
|
||||
)
|
||||
|
||||
self._select_data(self.meeting_mic, conf["meeting_mic_target"])
|
||||
@@ -1660,10 +1694,11 @@ class SettingsWindow(QDialog):
|
||||
self._select_data(self.meeting_reasoning, conf["meeting_reasoning"])
|
||||
self._select_data(self.meeting_language, conf["meeting_language"])
|
||||
self.meeting_cleanup.setChecked(conf["meeting_cleanup"])
|
||||
self.meeting_max_minutes.setValue(max(5, int(conf["meeting_max_seconds"]) // 60))
|
||||
self._meeting_max_loaded = int(conf["meeting_max_seconds"])
|
||||
self.meeting_max_minutes.setValue(max(5, self._meeting_max_loaded // 60))
|
||||
self.meeting_keep_audio.setChecked(conf["meeting_keep_audio"])
|
||||
self.meeting_prompt.setPlainText(
|
||||
conf["meeting_prompt"] or cfg.default_meeting_prompt()
|
||||
conf["meeting_prompt"] or self._loaded_defaults["meeting"]
|
||||
)
|
||||
|
||||
self.file_timestamps.setChecked(conf["file_timestamps"])
|
||||
@@ -1726,13 +1761,18 @@ class SettingsWindow(QDialog):
|
||||
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.
|
||||
# Store an empty prompt when it matches a default: the one it was
|
||||
# loaded with, or today's (a Reset click in a session that switched
|
||||
# languages fills in the latter). Both count, so switching the
|
||||
# interface language keeps switching the prompt language.
|
||||
prompt = self.cleanup_prompt.toPlainText().strip()
|
||||
conf["cleanup_prompt"] = "" if prompt == cfg.default_cleanup_prompt() else prompt
|
||||
conf["cleanup_prompt"] = ("" if prompt in (
|
||||
self._loaded_defaults["cleanup"], cfg.default_cleanup_prompt())
|
||||
else prompt)
|
||||
file_prompt = self.file_cleanup_prompt.toPlainText().strip()
|
||||
conf["file_cleanup_prompt"] = ("" if file_prompt == cfg.default_file_cleanup_prompt()
|
||||
else file_prompt)
|
||||
conf["file_cleanup_prompt"] = ("" if file_prompt in (
|
||||
self._loaded_defaults["file"], cfg.default_file_cleanup_prompt())
|
||||
else file_prompt)
|
||||
conf["transcribe_prompt"] = self.transcribe_prompt.toPlainText().strip()
|
||||
|
||||
conf["assistant_provider"] = self.assistant_provider.currentData() or "claude"
|
||||
@@ -1759,8 +1799,9 @@ class SettingsWindow(QDialog):
|
||||
conf["assistant_paste"] = self.assistant_paste.isChecked()
|
||||
conf["assistant_cleanup"] = self.assistant_cleanup.isChecked()
|
||||
assistant_prompt = self.assistant_prompt.toPlainText().strip()
|
||||
conf["assistant_prompt"] = ("" if assistant_prompt == cfg.default_assistant_prompt()
|
||||
else assistant_prompt)
|
||||
conf["assistant_prompt"] = ("" if assistant_prompt in (
|
||||
self._loaded_defaults["assistant"], cfg.default_assistant_prompt())
|
||||
else assistant_prompt)
|
||||
|
||||
conf["meeting_mic_target"] = self.meeting_mic.currentData() or ""
|
||||
conf["meeting_system_target"] = self.meeting_system.currentData() or ""
|
||||
@@ -1772,11 +1813,16 @@ class SettingsWindow(QDialog):
|
||||
conf["meeting_reasoning"] = self.meeting_reasoning.currentData() or ""
|
||||
conf["meeting_language"] = self.meeting_language.currentData() or ""
|
||||
conf["meeting_cleanup"] = self.meeting_cleanup.isChecked()
|
||||
conf["meeting_max_seconds"] = self.meeting_max_minutes.value() * 60
|
||||
# Only when the dial was actually turned: the box speaks whole minutes
|
||||
# with a floor, and an unrelated Save must not rewrite a value the
|
||||
# command line set in seconds.
|
||||
if self.meeting_max_minutes.value() != max(5, self._meeting_max_loaded // 60):
|
||||
conf["meeting_max_seconds"] = self.meeting_max_minutes.value() * 60
|
||||
conf["meeting_keep_audio"] = self.meeting_keep_audio.isChecked()
|
||||
meeting_prompt = self.meeting_prompt.toPlainText().strip()
|
||||
conf["meeting_prompt"] = ("" if meeting_prompt == cfg.default_meeting_prompt()
|
||||
else meeting_prompt)
|
||||
conf["meeting_prompt"] = ("" if meeting_prompt in (
|
||||
self._loaded_defaults["meeting"], cfg.default_meeting_prompt())
|
||||
else meeting_prompt)
|
||||
|
||||
conf["file_timestamps"] = self.file_timestamps.isChecked()
|
||||
conf["file_cleanup"] = self.file_cleanup.isChecked()
|
||||
@@ -1790,7 +1836,16 @@ class SettingsWindow(QDialog):
|
||||
or hotkey.default_combo(which))
|
||||
conf["evdev_hotkey"] = self.evdev_enabled.isChecked()
|
||||
conf["history_limit"] = self.history_limit.value()
|
||||
conf.save()
|
||||
try:
|
||||
conf.save()
|
||||
except OSError as exc:
|
||||
# An antivirus or a sync tool holding the file for a beat is a
|
||||
# message, not an exit: an exception out of a Qt slot takes the
|
||||
# whole application down.
|
||||
QMessageBox.warning(self, "Dikte",
|
||||
t("Could not save the settings: {error}",
|
||||
error=exc))
|
||||
return
|
||||
# A lowered limit should bite now, not on the next dictation.
|
||||
try:
|
||||
cfg.trim_history(conf["history_limit"])
|
||||
@@ -2012,7 +2067,10 @@ class SettingsWindow(QDialog):
|
||||
"""
|
||||
self.conf["file_timestamps"] = self.file_timestamps.isChecked()
|
||||
self.conf["file_cleanup"] = self.file_cleanup.isChecked()
|
||||
self.conf.save()
|
||||
try:
|
||||
self.conf.save()
|
||||
except OSError as exc:
|
||||
print(f"dikte: could not save the settings: {exc}", file=sys.stderr)
|
||||
|
||||
def _run_file(self):
|
||||
if not getattr(self, "file_path", "") or self.transcriber.busy:
|
||||
@@ -2113,7 +2171,12 @@ class SettingsWindow(QDialog):
|
||||
QMessageBox.information(self, t("Shortcut"), message)
|
||||
if ok:
|
||||
self.conf[spec.setting] = combo
|
||||
self.conf.save()
|
||||
try:
|
||||
self.conf.save()
|
||||
except OSError as exc:
|
||||
QMessageBox.warning(self, "Dikte",
|
||||
t("Could not save the settings: {error}",
|
||||
error=exc))
|
||||
self._refresh_shortcut_status(which)
|
||||
|
||||
def _remove_shortcut(self, which):
|
||||
|
||||
Reference in New Issue
Block a user