presets: per-preset LLM engine selector + fix streaming freeze (v1.9.7)
- Add llm_engine field to Workflow (config.py load/save) - Daemon picks the named engine per-preset, falls back to active engine - Settings Presets tab: replace free-text model field with engine dropdown populated from configured engines; "(active engine)" = default - Fix LLM token streaming crash: coalesce GLib.idle_add calls so only one flush is ever queued (prevents GTK main loop flooding) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9e291437bd
commit
946d344090
@ -25,6 +25,9 @@ class Workflow:
|
||||
# Optional per-workflow overrides of the [rewrite] defaults.
|
||||
model: str | None = None
|
||||
temperature: float | None = None
|
||||
# Which LLM engine to use for this preset's rewrite step.
|
||||
# "" (empty) = use whichever engine is currently active in the Engines tab.
|
||||
llm_engine: str = ""
|
||||
# Cosmetic, used by the GUI.
|
||||
description: str = ""
|
||||
icon: str = "⚡"
|
||||
@ -245,6 +248,7 @@ def load(path: Path = CONFIG_PATH) -> Config:
|
||||
keywords=list(entry.get("keywords", [])),
|
||||
model=entry.get("model"),
|
||||
temperature=entry.get("temperature"),
|
||||
llm_engine=entry.get("llm_engine", ""),
|
||||
description=entry.get("description", ""),
|
||||
icon=entry.get("icon", "⚡"),
|
||||
)
|
||||
@ -396,6 +400,8 @@ def save(cfg: Config, path: Path = CONFIG_PATH) -> None:
|
||||
entry["model"] = wf.model
|
||||
if wf.temperature is not None:
|
||||
entry["temperature"] = wf.temperature
|
||||
if wf.llm_engine:
|
||||
entry["llm_engine"] = wf.llm_engine
|
||||
if wf.description:
|
||||
entry["description"] = wf.description
|
||||
if wf.icon and wf.icon != "⚡":
|
||||
|
||||
@ -520,9 +520,15 @@ class Daemon:
|
||||
_acc.append(delta)
|
||||
self.text_cb("".join(_acc))
|
||||
|
||||
# Use the preset's pinned engine when set, else the active one.
|
||||
engine_name = getattr(target, "llm_engine", "") or ""
|
||||
llm_engine = (
|
||||
next((e for e in self.cfg.llm_engines if e.name == engine_name), None)
|
||||
or self.cfg.active_llm
|
||||
)
|
||||
try:
|
||||
text = llm.chat(
|
||||
self.cfg.active_llm,
|
||||
llm_engine,
|
||||
target.prompt,
|
||||
text,
|
||||
model=target.model or None,
|
||||
|
||||
@ -846,8 +846,13 @@ notebook.bt-nb tab:checked label {
|
||||
beh_card = _card_section(page, "Behaviour")
|
||||
self.wf_mode = _labeled(beh_card, "Mode", _combo(["transcribe", "rewrite", "stream"]),
|
||||
tooltip="’transcribe’ types your words as-is. ‘rewrite’ sends them to the language model first. ‘stream’ shows live text from a realtime engine.")
|
||||
self.wf_model = _labeled(beh_card, "LLM model (opt.)", _entry(placeholder="blank = active LLM engine model"),
|
||||
tooltip="Override the language model just for this preset. Leave blank to use the active LLM engine.")
|
||||
# Engine dropdown — "(active engine)" + one entry per configured LLM engine.
|
||||
llm_names = [e.name for e in self.cfg.llm_engines]
|
||||
self.wf_llm_engine = _labeled(
|
||||
beh_card, "LLM engine",
|
||||
_combo(["(active engine)"] + llm_names),
|
||||
tooltip="Which LLM engine to use for the rewrite step. ‘(active engine)’ follows whatever is selected in the Engines tab.",
|
||||
)
|
||||
self.wf_temp = _labeled(beh_card, "Temperature (opt.)", _entry(placeholder="blank = engine default (e.g. 0.3)"),
|
||||
tooltip="Creativity of the rewrite, 0–1. Lower is more predictable. Blank uses the engine default.")
|
||||
|
||||
@ -872,7 +877,10 @@ notebook.bt-nb tab:checked label {
|
||||
self.wf_keywords.set_text(", ".join(wf.keywords))
|
||||
self.wf_hotkey.set_text(wf.hotkey)
|
||||
self.wf_mode.set_active(["transcribe", "rewrite", "stream"].index(wf.mode) if wf.mode in ("transcribe", "rewrite", "stream") else 0)
|
||||
self.wf_model.set_text(wf.model or "")
|
||||
llm_names = [e.name for e in self.cfg.llm_engines]
|
||||
engine = getattr(wf, "llm_engine", "") or ""
|
||||
idx_e = (llm_names.index(engine) + 1) if engine in llm_names else 0
|
||||
self.wf_llm_engine.set_active(idx_e)
|
||||
self.wf_temp.set_text("" if wf.temperature is None else str(wf.temperature))
|
||||
self.wf_prompt.get_buffer().set_text(wf.prompt)
|
||||
self._wf_idx = idx
|
||||
@ -888,7 +896,8 @@ notebook.bt-nb tab:checked label {
|
||||
wf.keywords = [k.strip() for k in self.wf_keywords.get_text().split(",") if k.strip()]
|
||||
wf.hotkey = self.wf_hotkey.get_text().strip()
|
||||
wf.mode = self.wf_mode.get_active_text() or "transcribe"
|
||||
wf.model = self.wf_model.get_text().strip() or None
|
||||
engine_text = self.wf_llm_engine.get_active_text() or ""
|
||||
wf.llm_engine = "" if engine_text == "(active engine)" else engine_text
|
||||
t = self.wf_temp.get_text().strip()
|
||||
wf.temperature = float(t) if _isfloat(t) else None
|
||||
b = self.wf_prompt.get_buffer()
|
||||
|
||||
@ -86,6 +86,11 @@ class Overlay:
|
||||
self._tick_id: int | None = None
|
||||
self._hide_id: int | None = None
|
||||
self._t0 = time.time()
|
||||
# Coalescing text updates: background LLM streaming can fire dozens of
|
||||
# set_text() calls per second. We buffer the latest text and only ever
|
||||
# have ONE idle_add pending — so the GTK main loop is never flooded.
|
||||
self._pending_text: str = ""
|
||||
self._text_flush_queued: bool = False
|
||||
|
||||
self._win = Gtk.Window(type=Gtk.WindowType.POPUP)
|
||||
self._win.set_app_paintable(True)
|
||||
@ -123,7 +128,18 @@ class Overlay:
|
||||
GLib.idle_add(self._set_level, float(level))
|
||||
|
||||
def set_text(self, text: str) -> None:
|
||||
GLib.idle_add(self._set_text, text or "")
|
||||
self._pending_text = text or ""
|
||||
# Only schedule a flush if none is already queued; this collapses a burst
|
||||
# of token callbacks (e.g. 50/s from LLM streaming) into a single GTK
|
||||
# redraw, preventing main-loop flooding and session freezes.
|
||||
if not self._text_flush_queued:
|
||||
self._text_flush_queued = True
|
||||
GLib.idle_add(self._flush_text)
|
||||
|
||||
def _flush_text(self) -> bool:
|
||||
self._text_flush_queued = False
|
||||
self._set_text(self._pending_text)
|
||||
return False
|
||||
|
||||
def set_preset(self, icon: str, name: str, keyword: str | None) -> None:
|
||||
"""Show the matched voice-routing preset on the overlay (emoji + name +
|
||||
|
||||
Loading…
Reference in New Issue
Block a user