mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 19:06:11 +00:00
Merge current master into local model status
This commit is contained in:
+1
-1
@@ -945,7 +945,7 @@ def cmd_doctor(opts):
|
|||||||
# Recording, the device list, and KDE's shortcut registry.
|
# Recording, the device list, and KDE's shortcut registry.
|
||||||
wanted += ["pw-record", "pactl", "kwriteconfig6"]
|
wanted += ["pw-record", "pactl", "kwriteconfig6"]
|
||||||
wanted += ["ffmpeg",
|
wanted += ["ffmpeg",
|
||||||
assistant.executable(assistant.provider(conf)) or "claude",
|
assistant.executable(assistant.provider(conf)),
|
||||||
cleanup.executable(cleanup.provider(conf))]
|
cleanup.executable(cleanup.provider(conf))]
|
||||||
programs = {name: shutil.which(name) or "" for name in wanted if name}
|
programs = {name: shutil.which(name) or "" for name in wanted if name}
|
||||||
target = conf.transcribe_target()
|
target = conf.transcribe_target()
|
||||||
|
|||||||
+1
-1
@@ -57,7 +57,7 @@ SHORTCUTS = {
|
|||||||
"Dikte: pause/resume the recording", "pause_shortcut", ""),
|
"Dikte: pause/resume the recording", "pause_shortcut", ""),
|
||||||
"cancel": Shortcut("cancel", CANCEL_DESKTOP_ID, "Dikte: discard the recording",
|
"cancel": Shortcut("cancel", CANCEL_DESKTOP_ID, "Dikte: discard the recording",
|
||||||
"cancel_shortcut", ""),
|
"cancel_shortcut", ""),
|
||||||
"ask": Shortcut("ask", ASK_DESKTOP_ID, "Dikte: ask Claude Code",
|
"ask": Shortcut("ask", ASK_DESKTOP_ID, "Dikte: ask the agent",
|
||||||
"assistant_shortcut", ""),
|
"assistant_shortcut", ""),
|
||||||
"meeting": Shortcut("meeting", MEETING_DESKTOP_ID,
|
"meeting": Shortcut("meeting", MEETING_DESKTOP_ID,
|
||||||
"Dikte: start/end a meeting recording",
|
"Dikte: start/end a meeting recording",
|
||||||
|
|||||||
@@ -819,6 +819,7 @@ TR = {
|
|||||||
"İşlemciye yüklendi: ekran kartı açık, ama kullanılamadı.",
|
"İşlemciye yüklendi: ekran kartı açık, ama kullanılamadı.",
|
||||||
"Not installed.": "Kurulu değil.",
|
"Not installed.": "Kurulu değil.",
|
||||||
"Installed on the system: {path}": "Sistemde kurulu: {path}",
|
"Installed on the system: {path}": "Sistemde kurulu: {path}",
|
||||||
|
"Using custom build: {path}": "Özel derleme kullanılıyor: {path}",
|
||||||
"Download again": "Yeniden indir",
|
"Download again": "Yeniden indir",
|
||||||
"Downloaded, version {version}.": "İndirildi, sürüm {version}.",
|
"Downloaded, version {version}.": "İndirildi, sürüm {version}.",
|
||||||
"Downloaded, version {version}. There was no Vulkan build, "
|
"Downloaded, version {version}. There was no Vulkan build, "
|
||||||
|
|||||||
+23
-6
@@ -223,7 +223,9 @@ class WheelGuard(QObject):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def eventFilter(self, box, event):
|
def eventFilter(self, box, event):
|
||||||
if event.type() == QEvent.Type.Wheel and not box.hasFocus():
|
win = box.window()
|
||||||
|
focused = box.hasFocus() or (win is not None and win.focusWidget() is box)
|
||||||
|
if event.type() == QEvent.Type.Wheel and not focused:
|
||||||
# Refused rather than swallowed. An unaccepted wheel event carries
|
# Refused rather than swallowed. An unaccepted wheel event carries
|
||||||
# on up the parents to the scroll area, so the page still moves.
|
# on up the parents to the scroll area, so the page still moves.
|
||||||
event.ignore()
|
event.ignore()
|
||||||
@@ -253,9 +255,11 @@ class LocalModelBox(QGroupBox):
|
|||||||
|
|
||||||
changed = pyqtSignal()
|
changed = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, program, title, models, model_path, repos=None, parent=None):
|
def __init__(self, program, title, models, model_path, binary=None,
|
||||||
|
repos=None, parent=None):
|
||||||
super().__init__(title, parent)
|
super().__init__(title, parent)
|
||||||
self.program = program
|
self.program = program
|
||||||
|
self._binary = binary # () -> a path set by hand, or ""
|
||||||
self._models = models # () -> [hub.Item], or (repo) -> [hub.Item]
|
self._models = models # () -> [hub.Item], or (repo) -> [hub.Item]
|
||||||
self._model_path = model_path # (name) -> Path
|
self._model_path = model_path # (name) -> Path
|
||||||
self._repos = repos # None, or () -> [repo id]
|
self._repos = repos # None, or () -> [repo id]
|
||||||
@@ -419,13 +423,23 @@ class LocalModelBox(QGroupBox):
|
|||||||
self._fill_repos(self.repository())
|
self._fill_repos(self.repository())
|
||||||
self._fetch_models(self.repository())
|
self._fetch_models(self.repository())
|
||||||
|
|
||||||
|
def _program_path(self):
|
||||||
|
return ggml.program_path(self.program,
|
||||||
|
self._binary() if self._binary else "")
|
||||||
|
|
||||||
def _show_program(self):
|
def _show_program(self):
|
||||||
path = ggml.program_path(self.program)
|
path = self._program_path()
|
||||||
if not path:
|
if not path:
|
||||||
self.program_label.setText(t("Not installed."))
|
self.program_label.setText(t("Not installed."))
|
||||||
self.install_button.setText(t("Download"))
|
self.install_button.setText(t("Download"))
|
||||||
self.install_button.setVisible(True)
|
self.install_button.setVisible(True)
|
||||||
return
|
return
|
||||||
|
if self._binary and self._binary():
|
||||||
|
# Neither a system copy nor one Dikte fetched, and "Downloaded"
|
||||||
|
# over a build someone made themselves is not true.
|
||||||
|
self.program_label.setText(t("Using custom build: {path}", path=path))
|
||||||
|
self.install_button.setVisible(False)
|
||||||
|
return
|
||||||
# A copy that is here is not a copy that is right. whisper.cpp releases
|
# A copy that is here is not a copy that is right. whisper.cpp releases
|
||||||
# every few weeks, and a graphics card installed after Dikte was
|
# every few weeks, and a graphics card installed after Dikte was
|
||||||
# changes which build this machine should be running; the button was
|
# changes which build this machine should be running; the button was
|
||||||
@@ -837,7 +851,7 @@ class LocalModelBox(QGroupBox):
|
|||||||
repo=self.repository(), cap=ggml.human_size(ggml.GGUF_MAX_BYTES)))
|
repo=self.repository(), cap=ggml.human_size(ggml.GGUF_MAX_BYTES)))
|
||||||
elif not name:
|
elif not name:
|
||||||
self.status.setText(t("Nothing downloaded yet."))
|
self.status.setText(t("Nothing downloaded yet."))
|
||||||
elif here and not ggml.program_path(self.program):
|
elif here and not self._program_path():
|
||||||
# The model alone runs nothing, and "Ready" over a missing program
|
# The model alone runs nothing, and "Ready" over a missing program
|
||||||
# reads as though it does.
|
# reads as though it does.
|
||||||
self.status.setText(t("{name} is here, but the program above is "
|
self.status.setText(t("{name} is here, but the program above is "
|
||||||
@@ -1248,7 +1262,8 @@ class SettingsWindow(QDialog):
|
|||||||
|
|
||||||
self.local_whisper = LocalModelBox(
|
self.local_whisper = LocalModelBox(
|
||||||
ggml.WHISPER, t("On this machine"),
|
ggml.WHISPER, t("On this machine"),
|
||||||
ggml.whisper_models, ggml.whisper_model_path)
|
ggml.whisper_models, ggml.whisper_model_path,
|
||||||
|
binary=lambda: self.conf["local_binary"])
|
||||||
stt_form.addRow(self.local_whisper)
|
stt_form.addRow(self.local_whisper)
|
||||||
|
|
||||||
self.local_gpu = QCheckBox(t("Use the graphics card"))
|
self.local_gpu = QCheckBox(t("Use the graphics card"))
|
||||||
@@ -1365,7 +1380,9 @@ class SettingsWindow(QDialog):
|
|||||||
|
|
||||||
self.local_llm = LocalModelBox(
|
self.local_llm = LocalModelBox(
|
||||||
ggml.LLAMA, t("On this machine"),
|
ggml.LLAMA, t("On this machine"),
|
||||||
ggml.llm_quants, ggml.llm_model_path, repos=ggml.llm_repos)
|
ggml.llm_quants, ggml.llm_model_path,
|
||||||
|
binary=lambda: self.conf["local_llm_binary"],
|
||||||
|
repos=ggml.llm_repos)
|
||||||
orr_form.addRow(self.local_llm)
|
orr_form.addRow(self.local_llm)
|
||||||
|
|
||||||
self.local_llm_gpu = QCheckBox(t("Use the graphics card"))
|
self.local_llm_gpu = QCheckBox(t("Use the graphics card"))
|
||||||
|
|||||||
@@ -578,6 +578,21 @@ class Doctor(DikteTest):
|
|||||||
self.run_doctor(as_json=False, cleanup_provider="codex",
|
self.run_doctor(as_json=False, cleanup_provider="codex",
|
||||||
cleanup_codex_model="gpt-5.4"))
|
cleanup_codex_model="gpt-5.4"))
|
||||||
|
|
||||||
|
def test_agent_on_hosted_provider_does_not_ask_for_a_cli_program(self):
|
||||||
|
for provider in ("openrouter", "opencode"):
|
||||||
|
with self.subTest(provider=provider):
|
||||||
|
reply = self.run_doctor(assistant_provider=provider)
|
||||||
|
self.assertEqual(reply["agent"]["provider"], provider)
|
||||||
|
for cli_name in ("claude", "codex", "agy"):
|
||||||
|
self.assertNotIn(cli_name, reply["programs"])
|
||||||
|
|
||||||
|
def test_agent_on_a_cli_asks_for_the_program(self):
|
||||||
|
for provider, binary in (("claude", "claude"), ("codex", "codex"), ("agy", "agy")):
|
||||||
|
with self.subTest(provider=provider):
|
||||||
|
reply = self.run_doctor(assistant_provider=provider)
|
||||||
|
self.assertEqual(reply["agent"]["provider"], provider)
|
||||||
|
self.assertIn(binary, reply["programs"])
|
||||||
|
|
||||||
|
|
||||||
class Devices(DikteTest):
|
class Devices(DikteTest):
|
||||||
def test_a_machine_with_nothing_names_its_own_missing_program(self):
|
def test_a_machine_with_nothing_names_its_own_missing_program(self):
|
||||||
|
|||||||
+69
-1
@@ -16,7 +16,7 @@ from unittest import mock
|
|||||||
|
|
||||||
from PyQt6.QtCore import QPoint, QPointF, QRect, Qt
|
from PyQt6.QtCore import QPoint, QPointF, QRect, Qt
|
||||||
from PyQt6.QtGui import QHideEvent, QShowEvent, QWheelEvent
|
from PyQt6.QtGui import QHideEvent, QShowEvent, QWheelEvent
|
||||||
from PyQt6.QtWidgets import QApplication, QMessageBox
|
from PyQt6.QtWidgets import QApplication, QComboBox, QMessageBox, QSpinBox, QWidget
|
||||||
|
|
||||||
from dikte import audio
|
from dikte import audio
|
||||||
from dikte import cleanup
|
from dikte import cleanup
|
||||||
@@ -230,6 +230,51 @@ class Settings(DikteTest):
|
|||||||
QApplication.sendEvent(box, self.wheel())
|
QApplication.sendEvent(box, self.wheel())
|
||||||
self.assertNotEqual(box.currentIndex(), before)
|
self.assertNotEqual(box.currentIndex(), before)
|
||||||
|
|
||||||
|
def test_the_wheel_uses_remembered_focus_in_an_inactive_window(self):
|
||||||
|
# Keep the window hidden so no desktop activation policy can give it
|
||||||
|
# keyboard focus. Its remembered focus still selects the wheel target.
|
||||||
|
for widget_type in (QComboBox, QSpinBox):
|
||||||
|
with self.subTest(widget=widget_type.__name__):
|
||||||
|
window = QWidget()
|
||||||
|
self.addCleanup(window.deleteLater)
|
||||||
|
box = widget_type(window)
|
||||||
|
other = QComboBox(window)
|
||||||
|
if isinstance(box, QComboBox):
|
||||||
|
box.addItems(["first", "second", "third"])
|
||||||
|
box.setCurrentIndex(1)
|
||||||
|
value = box.currentIndex
|
||||||
|
else:
|
||||||
|
box.setValue(5)
|
||||||
|
value = box.value
|
||||||
|
box.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
|
guard = settings_ui.WheelGuard(window)
|
||||||
|
box.installEventFilter(guard)
|
||||||
|
box.setFocus()
|
||||||
|
self.assertFalse(window.isActiveWindow())
|
||||||
|
self.assertFalse(box.hasFocus())
|
||||||
|
self.assertIs(window.focusWidget(), box)
|
||||||
|
before = value()
|
||||||
|
QApplication.sendEvent(box, self.wheel())
|
||||||
|
self.assertNotEqual(value(), before)
|
||||||
|
other.setFocus()
|
||||||
|
self.assertIs(window.focusWidget(), other)
|
||||||
|
before = value()
|
||||||
|
rolled = self.wheel()
|
||||||
|
QApplication.sendEvent(box, rolled)
|
||||||
|
self.assertEqual(value(), before)
|
||||||
|
self.assertFalse(rolled.isAccepted())
|
||||||
|
|
||||||
|
def test_the_wheel_is_refused_when_another_widget_has_focus(self):
|
||||||
|
window = self.window(cfg.Config())
|
||||||
|
box = window.ui_language
|
||||||
|
other = window.corner
|
||||||
|
other.setFocus()
|
||||||
|
before = box.currentIndex()
|
||||||
|
rolled = self.wheel()
|
||||||
|
QApplication.sendEvent(box, rolled)
|
||||||
|
self.assertEqual(box.currentIndex(), before)
|
||||||
|
self.assertFalse(rolled.isAccepted())
|
||||||
|
|
||||||
def test_a_wrapped_label_keeps_the_room_its_lines_need(self):
|
def test_a_wrapped_label_keeps_the_room_its_lines_need(self):
|
||||||
# The program path shares a row with a button, and a row is measured
|
# The program path shares a row with a button, and a row is measured
|
||||||
# before its width is known: the label has to claim the second line back
|
# before its width is known: the label has to claim the second line back
|
||||||
@@ -1578,6 +1623,29 @@ class LocalModels(DikteTest):
|
|||||||
self.assertNotIn("Ready", box.status.text())
|
self.assertNotIn("Ready", box.status.text())
|
||||||
self.assertIn("program", box.status.text())
|
self.assertIn("program", box.status.text())
|
||||||
|
|
||||||
|
def test_a_program_set_in_the_settings_is_not_called_downloaded(self):
|
||||||
|
mine = self.path("my-whisper-server")
|
||||||
|
mine.write_text("#!/bin/sh\n")
|
||||||
|
mine.chmod(0o755)
|
||||||
|
self.patch_attr(ggml.shutil, "which", lambda name: None)
|
||||||
|
box = self.window(self.config(local_binary=str(mine))).local_whisper
|
||||||
|
self.assertIn(str(mine), box.program_label.text())
|
||||||
|
self.assertFalse(box.install_button.isVisibleTo(box))
|
||||||
|
|
||||||
|
def test_a_model_over_a_program_set_by_hand_is_ready(self):
|
||||||
|
# The program is there, it is just named by the settings rather than
|
||||||
|
# downloaded, and the status line looked past it.
|
||||||
|
mine = self.path("my-whisper-server")
|
||||||
|
mine.write_text("#!/bin/sh\n")
|
||||||
|
mine.chmod(0o755)
|
||||||
|
self.patch_attr(ggml.shutil, "which", lambda name: None)
|
||||||
|
path = ggml.whisper_model_path("ggml-small.bin")
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_bytes(b"not really a model")
|
||||||
|
box = self.window(self.config(local_binary=str(mine))).local_whisper
|
||||||
|
box.load("ggml-small.bin")
|
||||||
|
self.assertIn("Ready", box.status.text())
|
||||||
|
|
||||||
def test_changing_the_publisher_changes_the_model(self):
|
def test_changing_the_publisher_changes_the_model(self):
|
||||||
# The model chosen under the old publisher is not published by the new
|
# The model chosen under the old publisher is not published by the new
|
||||||
# one. Carried over, it was added back as "not downloaded" and selected
|
# one. Carried over, it was added back as "not downloaded" and selected
|
||||||
|
|||||||
Reference in New Issue
Block a user