mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 19:06:11 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb80d85332 | ||
|
|
fff9cd1c55 | ||
|
|
cfeed2af8c |
+64
-6
@@ -83,6 +83,10 @@ LLAMA = Program("llama", "ggml-org/llama.cpp", "llama-server", "/health")
|
||||
WHISPER_MODELS_REPO = "ggerganov/whisper.cpp"
|
||||
LLM_AUTHOR = "ggml-org"
|
||||
|
||||
# The file llama.cpp attaches to its version releases in place of the binaries:
|
||||
# a line naming the nightly tag those are published under.
|
||||
NIGHTLY_TAG = "nightly-tag.txt"
|
||||
|
||||
# What the whisper repository holds besides models: Core ML encoders for Apple
|
||||
# hardware and the odd loose file.
|
||||
WHISPER_PREFIX = "ggml-"
|
||||
@@ -277,6 +281,65 @@ def _wanted_assets(program):
|
||||
return (f"bin-ubuntu-{arch}.tar.gz",)
|
||||
|
||||
|
||||
def _matching_asset(program, assets):
|
||||
"""The archive this machine wants out of one release's files, or None."""
|
||||
for ending in _wanted_assets(program):
|
||||
item = next((a for a in assets if a.name.endswith(ending)), None)
|
||||
if item:
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
def _pick_asset(program, tag="", refresh=False):
|
||||
"""(tag, Item) for the release archive to install. Item is None when there
|
||||
is none for this machine.
|
||||
|
||||
A named tag is taken as given. For the newest, what GitHub answers is not
|
||||
always where the builds are: llama.cpp's latest release is a version marker
|
||||
carrying a single nightly-tag.txt, which names the tag the archives are
|
||||
actually attached to, and those are prereleases that "latest" never points
|
||||
at. The pointer is followed when it is there, and when it is not, the newest
|
||||
release that does carry a build for this machine is taken instead.
|
||||
"""
|
||||
named = bool(tag) and tag != "latest"
|
||||
missing = None
|
||||
try:
|
||||
tag, assets = hub.release(program.repo, tag or "latest", refresh=refresh)
|
||||
except hub.HubError as exc:
|
||||
# A release carrying no files at all is the case the search below exists
|
||||
# for, not a reason to stop before it: the build for this machine may be
|
||||
# attached to a prerelease that "latest" never points at. The failure is
|
||||
# kept rather than dropped, because an unreachable GitHub arrives here
|
||||
# the same way and that one is the message the caller wants.
|
||||
if named:
|
||||
raise
|
||||
missing, assets = exc, []
|
||||
item = _matching_asset(program, assets)
|
||||
if item or named:
|
||||
return tag, item
|
||||
# Best effort from here on: a machine this project publishes nothing for is
|
||||
# not a failed lookup, and the caller's message about that is the useful
|
||||
# one. Whatever goes wrong while looking further leaves it standing.
|
||||
try:
|
||||
pointer = next((a for a in assets if a.name == NIGHTLY_TAG), None)
|
||||
if pointer:
|
||||
nightly = hub.text(pointer.url).strip()
|
||||
if nightly:
|
||||
found, assets = hub.release(program.repo, nightly, refresh=refresh)
|
||||
item = _matching_asset(program, assets)
|
||||
if item:
|
||||
return found, item
|
||||
for found, assets in hub.releases(program.repo, refresh=refresh):
|
||||
item = _matching_asset(program, assets)
|
||||
if item:
|
||||
return found, item
|
||||
except hub.HubError:
|
||||
pass
|
||||
if missing is not None:
|
||||
raise missing
|
||||
return tag, None
|
||||
|
||||
|
||||
def _install_record(program):
|
||||
return BIN_DIR / program.name / "installed.json"
|
||||
|
||||
@@ -375,15 +438,10 @@ def install_program(program, tag="", on_progress=None, should_stop=None,
|
||||
whisper.cpp has one.
|
||||
"""
|
||||
try:
|
||||
tag, assets = hub.release(program.repo, tag or "latest", refresh=refresh)
|
||||
tag, item = _pick_asset(program, tag, refresh=refresh)
|
||||
except hub.HubError as exc:
|
||||
raise LocalError(str(exc)) from exc
|
||||
|
||||
item = None
|
||||
for ending in _wanted_assets(program):
|
||||
item = next((a for a in assets if a.name.endswith(ending)), None)
|
||||
if item:
|
||||
break
|
||||
if item is None:
|
||||
# Nothing to download and nothing to install for you: whisper.cpp
|
||||
# publishes no macOS binary, and Homebrew's whisper-cpp is configured
|
||||
|
||||
+48
-4
@@ -121,6 +121,13 @@ def _digest(value):
|
||||
return value.split(":", 1)[1] if value.startswith("sha256:") else value
|
||||
|
||||
|
||||
def _assets(data):
|
||||
return [Item(a.get("name") or "", a.get("browser_download_url") or "",
|
||||
int(a.get("size") or 0), _digest(a.get("digest")))
|
||||
for a in (data.get("assets") or [])
|
||||
if a.get("browser_download_url")]
|
||||
|
||||
|
||||
def release(repo, tag="latest", refresh=False):
|
||||
"""(tag, [Item]) for one GitHub release, newest when no tag is given."""
|
||||
where = "latest" if tag in ("", "latest") else f"tags/{tag}"
|
||||
@@ -128,10 +135,47 @@ def release(repo, tag="latest", refresh=False):
|
||||
f"{GITHUB_API}/repos/{repo}/releases/{where}", refresh=refresh)
|
||||
if not isinstance(data, dict) or not data.get("assets"):
|
||||
raise HubError(t("{repo} has no downloadable release.", repo=repo))
|
||||
assets = [Item(a.get("name") or "", a.get("browser_download_url") or "",
|
||||
int(a.get("size") or 0), _digest(a.get("digest")))
|
||||
for a in data["assets"] if a.get("browser_download_url")]
|
||||
return data.get("tag_name") or tag, assets
|
||||
return data.get("tag_name") or tag, _assets(data)
|
||||
|
||||
|
||||
def releases(repo, limit=20, refresh=False):
|
||||
"""[(tag, [Item])] for the recent releases, newest first, with their files.
|
||||
|
||||
"latest" is one release and this is the list behind it, prereleases
|
||||
included: a project that attaches its builds to a prerelease is invisible
|
||||
to release() above, and its newest usable build is in here.
|
||||
"""
|
||||
data = _fetch(f"gh-list-{repo}-{limit}",
|
||||
f"{GITHUB_API}/repos/{repo}/releases?per_page={limit}",
|
||||
refresh=refresh)
|
||||
if not isinstance(data, list):
|
||||
raise HubError(t("{repo} has no downloadable release.", repo=repo))
|
||||
out = []
|
||||
for entry in data:
|
||||
tag, items = entry.get("tag_name") or "", _assets(entry)
|
||||
if tag and items:
|
||||
out.append((tag, items))
|
||||
return out
|
||||
|
||||
|
||||
def text(url, limit=4096, timeout=20):
|
||||
"""A small text file from a release, as a string.
|
||||
|
||||
Not cached and not checksummed, because what it carries is a pointer: a few
|
||||
bytes naming the release the actual archives are attached to, read once on
|
||||
the way to a download that is checked in full.
|
||||
"""
|
||||
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||
return response.read(limit).decode("utf-8", "replace")
|
||||
except urllib.error.HTTPError as exc:
|
||||
exc.close()
|
||||
raise HubError(t("{url} answered HTTP {code}.",
|
||||
url=urllib.parse.urlsplit(url).netloc, code=exc.code)) from exc
|
||||
except (urllib.error.URLError, OSError, ValueError) as exc:
|
||||
raise HubError(t("Could not reach {url}: {error}",
|
||||
url=urllib.parse.urlsplit(url).netloc, error=exc)) from exc
|
||||
|
||||
|
||||
def newest_release(repo, refresh=False):
|
||||
|
||||
@@ -787,6 +787,12 @@ TR = {
|
||||
"Ready: {name}.": "Hazır: {name}.",
|
||||
"Nothing downloaded yet.": "Henüz bir şey indirilmedi.",
|
||||
"{name} has not been downloaded yet.": "{name} henüz indirilmedi.",
|
||||
"{name} is here, but the program above is not. Download it first.":
|
||||
"{name} burada, ama yukarıdaki program değil. Önce onu indirin.",
|
||||
"{name} is not on this machine and this publisher does not offer it. "
|
||||
"Choose another model, or another publisher.":
|
||||
"{name} bu makinede yok ve bu yayıncı da sunmuyor. Başka bir model, "
|
||||
"ya da başka bir yayıncı seçin.",
|
||||
"downloaded": "indirildi",
|
||||
"not downloaded": "indirilmedi",
|
||||
"Delete model": "Modeli sil",
|
||||
|
||||
+59
-8
@@ -6,7 +6,7 @@ import shutil
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from PyQt6.QtCore import QEvent, QObject, QRect, Qt, QUrl, pyqtSignal
|
||||
from PyQt6.QtCore import QEvent, QObject, QRect, Qt, QTimer, QUrl, pyqtSignal
|
||||
from PyQt6.QtGui import QDesktopServices, QGuiApplication, QKeySequence, QShortcut
|
||||
from PyQt6.QtWidgets import (
|
||||
QAbstractItemView, QAbstractSpinBox, QCheckBox, QComboBox, QDialog,
|
||||
@@ -247,6 +247,13 @@ class LocalModelBox(QGroupBox):
|
||||
self._pending = False
|
||||
self._stop = False
|
||||
self._wanted = "" # the model to select once a list arrives
|
||||
self._chosen_in = "" # the publisher the selected model is from
|
||||
# Typing or arrowing through the publisher box changes its text a
|
||||
# character at a time, and each of those would otherwise be a request.
|
||||
self._later = QTimer(self)
|
||||
self._later.setSingleShot(True)
|
||||
self._later.setInterval(400)
|
||||
self._later.timeout.connect(self._later_fetch)
|
||||
|
||||
form = QFormLayout(self)
|
||||
|
||||
@@ -328,6 +335,8 @@ class LocalModelBox(QGroupBox):
|
||||
self._wanted = model
|
||||
self._pending = True
|
||||
self._show_program()
|
||||
self._chosen_in = repo or (ggml.SUGGESTED_LLM[0] if self._repos is not None
|
||||
else "")
|
||||
if self._repos is not None:
|
||||
self.repo.blockSignals(True)
|
||||
self.repo.clear()
|
||||
@@ -367,11 +376,18 @@ class LocalModelBox(QGroupBox):
|
||||
|
||||
def _fill_repos(self, current):
|
||||
def work():
|
||||
self._listed.emit([("repos", ggml.llm_repos())], "")
|
||||
self._listed.emit([("repos", ggml.llm_repos(), "")], "")
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def _repo_changed(self):
|
||||
if not self._downloading:
|
||||
self._later.start()
|
||||
|
||||
def _later_fetch(self):
|
||||
# A download that started inside the wait was not there to be seen when
|
||||
# the timer went off, and rebuilding the rows underneath one is exactly
|
||||
# what the guard above is for.
|
||||
if not self._downloading:
|
||||
self._fetch_models(self.repository())
|
||||
|
||||
@@ -381,18 +397,29 @@ class LocalModelBox(QGroupBox):
|
||||
def work():
|
||||
try:
|
||||
found = self._models(repo) if self._repos is not None else self._models()
|
||||
self._listed.emit([("models", found)], "")
|
||||
self._listed.emit([("models", found, repo)], "")
|
||||
except ggml.LocalError as exc:
|
||||
self._listed.emit([], str(exc))
|
||||
self._listed.emit([("models", [], repo)], str(exc))
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def _on_listed(self, payload, error):
|
||||
kind, found, repo = payload[0] if payload else ("repos", [], "")
|
||||
# A publisher changed while its predecessor's list was still on the way
|
||||
# would otherwise be answered with the wrong models, whichever request
|
||||
# happened to come back last.
|
||||
if kind == "models" and repo != self.repository():
|
||||
return
|
||||
if error:
|
||||
# The list is the publisher's, so a failed one leaves the box no
|
||||
# longer showing this publisher's models: emptying it is what keeps
|
||||
# the two boxes saying the same thing. The message goes on after,
|
||||
# because filling the box writes a status of its own.
|
||||
if kind == "models":
|
||||
self._fill_models([])
|
||||
self._refresh_buttons()
|
||||
self.status.setText(error)
|
||||
self._refresh_buttons()
|
||||
return
|
||||
kind, found = payload[0]
|
||||
if kind == "repos":
|
||||
current = self.repo.currentText()
|
||||
self.repo.blockSignals(True)
|
||||
@@ -406,7 +433,12 @@ class LocalModelBox(QGroupBox):
|
||||
|
||||
def _fill_models(self, items):
|
||||
"""One row per model, saying what it weighs and whether it is here."""
|
||||
wanted = self._wanted or self.selected()
|
||||
# The selection is only worth carrying over within the publisher it was
|
||||
# made in. Carried across one, a model this repository does not publish
|
||||
# would be added back as "not downloaded" and selected again, and
|
||||
# changing the publisher would leave the model box looking untouched.
|
||||
same = self._repos is None or self.repository() == self._chosen_in
|
||||
wanted = self._wanted or (self.selected() if same else "")
|
||||
here = [name for name in (self._model_path(i.name).name for i in items)]
|
||||
self.model.blockSignals(True)
|
||||
self.model.clear()
|
||||
@@ -431,6 +463,7 @@ class LocalModelBox(QGroupBox):
|
||||
self.model.blockSignals(False)
|
||||
self._fit_popup(self.model)
|
||||
self._wanted = ""
|
||||
self._chosen_in = self.repository()
|
||||
self._model_changed()
|
||||
|
||||
def _on_disk(self):
|
||||
@@ -459,6 +492,9 @@ class LocalModelBox(QGroupBox):
|
||||
self._show_program()
|
||||
if error:
|
||||
self.program_label.setText(error)
|
||||
# The model line says whether the program is here, so installing one
|
||||
# changes what it should read.
|
||||
self._refresh_buttons()
|
||||
self.changed.emit()
|
||||
|
||||
def _current_item(self):
|
||||
@@ -545,15 +581,30 @@ class LocalModelBox(QGroupBox):
|
||||
def _refresh_buttons(self):
|
||||
name = self.selected()
|
||||
here = bool(name) and ggml.have_model(self._model_path(name))
|
||||
# A row carries what it takes to fetch it. The ones that do not are the
|
||||
# models found on this disk and the one the settings name but the list
|
||||
# does not offer: there is nothing to press Download for on those, and
|
||||
# a button that can only do nothing is worse than one that is out.
|
||||
item = self._current_item()
|
||||
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))
|
||||
self.download_button.setEnabled(self._downloading or (item is not None
|
||||
and not here))
|
||||
if self._downloading:
|
||||
return
|
||||
if not name:
|
||||
self.status.setText(t("Nothing downloaded yet."))
|
||||
elif here and not ggml.program_path(self.program):
|
||||
# The model alone runs nothing, and "Ready" over a missing program
|
||||
# reads as though it does.
|
||||
self.status.setText(t("{name} is here, but the program above is "
|
||||
"not. Download it first.", name=name))
|
||||
elif here:
|
||||
self.status.setText(t("Ready: {name}.", name=name))
|
||||
elif item is None:
|
||||
self.status.setText(t("{name} is not on this machine and this "
|
||||
"publisher does not offer it. Choose another "
|
||||
"model, or another publisher.", name=name))
|
||||
else:
|
||||
self.status.setText(t("{name} has not been downloaded yet.", name=name))
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ socket is faked, and everything that runs locally runs for real.
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
import webbrowser
|
||||
from typing import ClassVar
|
||||
@@ -668,7 +669,11 @@ class WithoutAnInstance(DikteTest):
|
||||
def run_verb(self, argv):
|
||||
# launch_gui replaces this process with the application, so it never
|
||||
# comes back in real use and must not be allowed to here.
|
||||
# `ask` with no text reads what was piped in, and the runner's own
|
||||
# stdin is not that: under pytest it is an object that refuses to be
|
||||
# read at all.
|
||||
with mock.patch.object(ipc, "send", return_value=None), \
|
||||
mock.patch.object(sys, "stdin", io.StringIO()), \
|
||||
mock.patch.object(cli, "launch_gui") as launch, \
|
||||
captured() as (out, err):
|
||||
code = cli.run(argv)
|
||||
|
||||
@@ -238,6 +238,45 @@ class InstallProgram(Local):
|
||||
"whisper-bin-ubuntu-x64.tar.gz")
|
||||
self.assertTrue(urls[1].endswith("whisper-bin-ubuntu-x64.tar.gz"))
|
||||
|
||||
def test_the_nightly_pointer_is_followed_to_where_the_builds_are(self):
|
||||
"""llama.cpp's latest release carries a tag name, not the binaries."""
|
||||
self.patch_attr(ggml, "_arch", lambda: "x64")
|
||||
self.patch_attr(ggml, "_has_vulkan", lambda: False)
|
||||
marker = self.release(ggml.NIGHTLY_TAG)
|
||||
nightly = dict(self.release("llama-b10809-bin-ubuntu-x64.tar.gz"),
|
||||
tag_name="b10809")
|
||||
|
||||
def opener(request, timeout=None):
|
||||
url = request.full_url
|
||||
if url.endswith("/releases/latest"):
|
||||
return json_body(marker)
|
||||
if url.endswith("/releases/tags/b10809"):
|
||||
return json_body(nightly)
|
||||
if url.endswith(ggml.NIGHTLY_TAG):
|
||||
return body(b"b10809\n")
|
||||
return body(self.archive)
|
||||
|
||||
with mock.patch("urllib.request.urlopen", side_effect=opener):
|
||||
tag, found = ggml._pick_asset(ggml.LLAMA)
|
||||
self.assertEqual(tag, "b10809")
|
||||
self.assertEqual(found.name, "llama-b10809-bin-ubuntu-x64.tar.gz")
|
||||
|
||||
def test_without_a_pointer_the_newest_release_that_has_a_build_is_taken(self):
|
||||
self.patch_attr(ggml, "_arch", lambda: "x64")
|
||||
self.patch_attr(ggml, "_has_vulkan", lambda: False)
|
||||
marker = self.release("source.zip")
|
||||
listing = [dict(self.release("llama-b2-bin-win-cpu-x64.zip"), tag_name="b2"),
|
||||
dict(self.release("llama-b1-bin-ubuntu-x64.tar.gz"), tag_name="b1")]
|
||||
|
||||
def opener(request, timeout=None):
|
||||
url = request.full_url
|
||||
return json_body(listing if "per_page" in url else marker)
|
||||
|
||||
with mock.patch("urllib.request.urlopen", side_effect=opener):
|
||||
tag, found = ggml._pick_asset(ggml.LLAMA)
|
||||
self.assertEqual(tag, "b1")
|
||||
self.assertEqual(found.name, "llama-b1-bin-ubuntu-x64.tar.gz")
|
||||
|
||||
def test_a_release_with_nothing_for_this_machine_says_so(self):
|
||||
self.patch_attr(ggml, "_arch", lambda: "x64")
|
||||
with fake_urlopen(self.release("whisper-bin-Win32.zip")):
|
||||
|
||||
+5
-2
@@ -42,9 +42,12 @@ class Directories(unittest.TestCase):
|
||||
|
||||
def test_a_mac_does_not_read_the_xdg_variables(self):
|
||||
"""A Mac with them set from some other tool still stores in one place."""
|
||||
with mock.patch.dict(os.environ, {"XDG_CONFIG_HOME": "/c"}):
|
||||
# Something no temporary directory can be called: the home this runs
|
||||
# under is a mkdtemp path, and a two-letter needle matched the "/c" in
|
||||
# somebody's TMPDIR rather than the variable being read.
|
||||
with mock.patch.dict(os.environ, {"XDG_CONFIG_HOME": "/xdg-elsewhere"}):
|
||||
config_dir, _ = paths.directories("darwin")
|
||||
self.assertNotIn("/c", config_dir.as_posix())
|
||||
self.assertNotIn("xdg-elsewhere", config_dir.as_posix())
|
||||
|
||||
def test_windows_keeps_the_models_out_of_the_roaming_profile(self):
|
||||
"""Settings roam with the account; several gigabytes must not."""
|
||||
|
||||
@@ -8,6 +8,7 @@ next time anybody presses Save. That is the failure this catches.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
from typing import ClassVar
|
||||
from unittest import mock
|
||||
@@ -21,6 +22,7 @@ from dikte import cleanup
|
||||
from dikte import config as cfg
|
||||
from dikte import ggml
|
||||
from dikte import hotkey
|
||||
from dikte import hub
|
||||
from dikte import ipc
|
||||
from dikte import overlay as overlay_module
|
||||
from dikte import paste
|
||||
@@ -1214,6 +1216,75 @@ class LocalModels(DikteTest):
|
||||
for row in range(box.repo.count()))
|
||||
self.assertGreaterEqual(view.minimumWidth(), widest)
|
||||
|
||||
@staticmethod
|
||||
def _item(name, size=1 << 20):
|
||||
return hub.Item(name, f"https://example.invalid/{name}", size, "")
|
||||
|
||||
def test_a_row_with_nothing_to_fetch_does_not_offer_a_download(self):
|
||||
# The model the settings name is not in the list any more, so its row
|
||||
# was rebuilt from the name alone and carries no file to fetch. The
|
||||
# button stayed lit and the press did nothing at all.
|
||||
box = self.window(self.config(local_llm_model="gone.gguf")).local_llm
|
||||
box.load("gone.gguf", "ggml-org/SmolLM3-3B-GGUF")
|
||||
self.assertEqual(box.selected(), "gone.gguf")
|
||||
self.assertFalse(box.download_button.isEnabled())
|
||||
self.assertIn("gone.gguf", box.status.text())
|
||||
self.assertIn("publisher", box.status.text())
|
||||
|
||||
def test_a_model_without_its_program_does_not_say_it_is_ready(self):
|
||||
# The model runs on the program above it, and "Ready" over a missing
|
||||
# one is what had people asking why nothing transcribed.
|
||||
box = self.window(cfg.Config()).local_whisper
|
||||
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.load("ggml-small.bin")
|
||||
self.assertFalse(ggml.program_path(ggml.WHISPER))
|
||||
self.assertNotIn("Ready", box.status.text())
|
||||
self.assertIn("program", box.status.text())
|
||||
|
||||
def test_changing_the_publisher_changes_the_model(self):
|
||||
# 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
|
||||
# again, and the box looked as though the change had not taken.
|
||||
box = self.window(self.config(local_llm_model="gemma-3-4b-it-Q4_K_M.gguf",
|
||||
local_llm_repo="ggml-org/gemma-3-4b-it-GGUF")).local_llm
|
||||
box.load("gemma-3-4b-it-Q4_K_M.gguf", "ggml-org/gemma-3-4b-it-GGUF")
|
||||
box.repo.blockSignals(True)
|
||||
box.repo.setCurrentText("ggml-org/SmolLM3-3B-GGUF")
|
||||
box.repo.blockSignals(False)
|
||||
box._on_listed([("models", [self._item("SmolLM3-Q4_K_M.gguf")],
|
||||
"ggml-org/SmolLM3-3B-GGUF")], "")
|
||||
self.assertEqual(box.selected(), "SmolLM3-Q4_K_M.gguf")
|
||||
self.assertEqual(box.model.count(), 1)
|
||||
|
||||
def test_a_list_for_a_publisher_that_is_no_longer_chosen_is_dropped(self):
|
||||
# Every change starts its own request, and they do not come back in the
|
||||
# order they went out.
|
||||
box = self.window(cfg.Config()).local_llm
|
||||
box.load("", "ggml-org/SmolLM3-3B-GGUF")
|
||||
box.repo.blockSignals(True)
|
||||
box.repo.setCurrentText("ggml-org/SmolLM3-3B-GGUF")
|
||||
box.repo.blockSignals(False)
|
||||
box._on_listed([("models", [self._item("SmolLM3-Q4_K_M.gguf")],
|
||||
"ggml-org/SmolLM3-3B-GGUF")], "")
|
||||
box._on_listed([("models", [self._item("gemma-3-4b-it-Q4_K_M.gguf")],
|
||||
"ggml-org/gemma-3-4b-it-GGUF")], "")
|
||||
self.assertEqual(box.selected(), "SmolLM3-Q4_K_M.gguf")
|
||||
|
||||
def test_the_publisher_box_is_not_asked_on_every_keystroke(self):
|
||||
box = self.window(cfg.Config()).local_llm
|
||||
with mock.patch.object(box, "_fetch_models") as fetch:
|
||||
for text in ("g", "gg", "ggm", "ggml-org/SmolLM3-3B-GGUF"):
|
||||
box.repo.setCurrentText(text)
|
||||
fetch.assert_not_called()
|
||||
box._later.setInterval(0)
|
||||
box._later.start()
|
||||
_app.processEvents()
|
||||
time.sleep(0.05)
|
||||
_app.processEvents()
|
||||
self.assertEqual(fetch.call_count, 1)
|
||||
|
||||
def test_only_the_chosen_transcriber_is_on_screen(self):
|
||||
window = self.window(self.config(transcribe_provider="openai"))
|
||||
self.assertTrue(window.stt_form.isRowVisible(window.transcribe_model_row))
|
||||
|
||||
Reference in New Issue
Block a user