settings: info buttons, Manual tab, engine quickstart templates, human-readable type names (v1.9.3)
- Every _labeled and _switch_row field now gets a clickable ⓘ info button
that opens a plain-language help popover — targeted at non-technical users
- New Manual tab in Settings renders MANUAL.md directly inside the dialog
- STT and LLM toolbars each get a "Quickstart ▾" button: a menu of common
providers (OpenAI, Groq, OpenRouter, Ollama, LM Studio, vLLM, llama-swap,
faster-whisper-server, NVIDIA Riva) that pre-fills the engine form in one click
- Engine type combos now show human-readable labels ("Internal — faster-whisper",
"LAN server — runs on your machine", "GPU (CUDA)", "int8 — fast, less memory")
while storing the same internal key values (no config migration needed)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d2c6d352a5
commit
dd617f625e
@ -9,6 +9,29 @@ The version is defined in [`blitztext/__init__.py`](blitztext/__init__.py).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.9.3] - 2026-06-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **ⓘ info buttons on every settings field.** Each field in every tab now has a
|
||||||
|
small information icon that opens a plain-language help popover when clicked —
|
||||||
|
so non-technical users can understand what each setting does without hovering
|
||||||
|
or reading the manual.
|
||||||
|
- **Manual tab in Settings.** A new "Manual" tab shows the full `MANUAL.md`
|
||||||
|
reference doc directly inside the Settings window.
|
||||||
|
- **Quickstart templates for engines.** A "Quickstart ▾" button in the STT and
|
||||||
|
LLM engine toolbars opens a menu of common services (OpenAI, Groq, OpenRouter,
|
||||||
|
Ollama, LM Studio, vLLM, llama-swap, faster-whisper-server, NVIDIA Riva) and
|
||||||
|
pre-fills the form — one click to configure a provider.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Engine type names are now human-readable.** STT types now read "Internal —
|
||||||
|
faster-whisper, runs inside the app", "Server — OpenAI-compatible API (LAN or
|
||||||
|
cloud)", and "Realtime — NVIDIA Riva / NIM streaming" instead of the raw
|
||||||
|
identifiers. LLM types read "LAN server — runs on your machine or local
|
||||||
|
network" and "Cloud service — OpenAI, Groq, OpenRouter, …".
|
||||||
|
- **Device selector now shows "GPU (CUDA)" instead of "cuda"**, and compute
|
||||||
|
types have plain-language descriptions (e.g. "int8 — fast, less memory").
|
||||||
|
|
||||||
## [1.9.2] - 2026-06-09
|
## [1.9.2] - 2026-06-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -6,4 +6,4 @@ counterpart to the macOS Blitztext menu bar app: it runs natively on the host
|
|||||||
(not in a container) so it can type into any application via xdotool.
|
(not in a container) so it can type into any application via xdotool.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "1.9.2"
|
__version__ = "1.9.3"
|
||||||
|
|||||||
@ -34,6 +34,45 @@ RESP_SAVE = 1
|
|||||||
RESP_SAVE_RESTART = 2
|
RESP_SAVE_RESTART = 2
|
||||||
GREEN, RED, GREY = "#34c759", "#ff3b30", "#b8b8be"
|
GREEN, RED, GREY = "#34c759", "#ff3b30", "#b8b8be"
|
||||||
|
|
||||||
|
# (stored_value, display_label) pairs used by _type_combo / _type_key
|
||||||
|
_STT_TYPES: list[tuple[str, str]] = [
|
||||||
|
("local", "Internal — faster-whisper, runs inside the app"),
|
||||||
|
("openai", "Server — OpenAI-compatible API (LAN or cloud)"),
|
||||||
|
("riva_realtime", "Realtime — NVIDIA Riva / NIM streaming"),
|
||||||
|
]
|
||||||
|
_LLM_TYPES: list[tuple[str, str]] = [
|
||||||
|
("local", "LAN server — runs on your machine or local network"),
|
||||||
|
("cloud", "Cloud service — OpenAI, Groq, OpenRouter, …"),
|
||||||
|
]
|
||||||
|
_DEVICE_OPTIONS: list[tuple[str, str]] = [
|
||||||
|
("auto", "Auto (try GPU / CUDA first, fall back to CPU)"),
|
||||||
|
("cpu", "CPU"),
|
||||||
|
("cuda", "GPU (CUDA)"),
|
||||||
|
]
|
||||||
|
_COMPUTE_OPTIONS: list[tuple[str, str]] = [
|
||||||
|
("auto", "Auto"),
|
||||||
|
("int8", "int8 — fast, less memory"),
|
||||||
|
("float16", "float16 — accurate, needs more VRAM"),
|
||||||
|
("int8_float16", "int8_float16 — balanced"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# (name, url, api_key_env, type_key, default_model) — quickstart templates
|
||||||
|
_LLM_TEMPLATES: list[tuple[str, str, str, str, str]] = [
|
||||||
|
("OpenAI", "https://api.openai.com/v1", "OPENAI_API_KEY", "cloud", "gpt-4o-mini"),
|
||||||
|
("Groq", "https://api.groq.com/openai/v1", "GROQ_API_KEY", "cloud", "llama3-8b-8192"),
|
||||||
|
("OpenRouter", "https://openrouter.ai/api/v1", "OPENROUTER_API_KEY", "cloud", "openai/gpt-4o-mini"),
|
||||||
|
("Ollama", "http://localhost:11434/v1", "", "local", ""),
|
||||||
|
("LM Studio", "http://localhost:1234/v1", "", "local", ""),
|
||||||
|
("vLLM", "http://localhost:8000/v1", "", "local", ""),
|
||||||
|
("llama-swap", "http://localhost:28080/v1", "", "local", ""),
|
||||||
|
]
|
||||||
|
_STT_TEMPLATES: list[tuple[str, str, str, str, str]] = [
|
||||||
|
("OpenAI Whisper", "https://api.openai.com/v1", "OPENAI_API_KEY", "openai", "whisper-1"),
|
||||||
|
("Groq Whisper", "https://api.groq.com/openai/v1", "GROQ_API_KEY", "openai", "whisper-large-v3-turbo"),
|
||||||
|
("faster-whisper-server", "http://localhost:8010/v1", "", "openai", ""),
|
||||||
|
("Realtime (Riva / NIM)", "http://localhost:8006/v1", "", "riva_realtime", ""),
|
||||||
|
]
|
||||||
|
|
||||||
# (cat_emoji, label, [emojis]) — standard Unicode categories, WhatsApp-style
|
# (cat_emoji, label, [emojis]) — standard Unicode categories, WhatsApp-style
|
||||||
_EMOJI_CATEGORIES: list[tuple[str, str, list[str]]] = [
|
_EMOJI_CATEGORIES: list[tuple[str, str, list[str]]] = [
|
||||||
("😀", "Smileys", [
|
("😀", "Smileys", [
|
||||||
@ -172,6 +211,8 @@ def _labeled(parent: Gtk.Box, label: str, widget: Gtk.Widget, width: int = 130,
|
|||||||
|
|
||||||
row.pack_start(lbl, False, False, 0)
|
row.pack_start(lbl, False, False, 0)
|
||||||
row.pack_start(widget, True, True, 0)
|
row.pack_start(widget, True, True, 0)
|
||||||
|
if tooltip:
|
||||||
|
row.pack_start(_info_btn(tooltip), False, False, 0)
|
||||||
parent.pack_start(row, False, False, 0)
|
parent.pack_start(row, False, False, 0)
|
||||||
return widget
|
return widget
|
||||||
|
|
||||||
@ -196,6 +237,8 @@ def _switch_row(parent: Gtk.Box, label: str, switch: Gtk.Switch, description: st
|
|||||||
switch.set_halign(Gtk.Align.END)
|
switch.set_halign(Gtk.Align.END)
|
||||||
switch.set_valign(Gtk.Align.CENTER)
|
switch.set_valign(Gtk.Align.CENTER)
|
||||||
row.pack_end(switch, False, False, 0)
|
row.pack_end(switch, False, False, 0)
|
||||||
|
if description:
|
||||||
|
row.pack_end(_info_btn(description), False, False, 0)
|
||||||
|
|
||||||
# ATK accessibility + tooltips
|
# ATK accessibility + tooltips
|
||||||
if hasattr(lbl, "set_mnemonic_widget"):
|
if hasattr(lbl, "set_mnemonic_widget"):
|
||||||
@ -250,6 +293,42 @@ def _combo(options, active=None) -> Gtk.ComboBoxText:
|
|||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def _info_btn(text: str) -> Gtk.Button:
|
||||||
|
"""Small ⓘ button that shows a help popover when clicked."""
|
||||||
|
btn = Gtk.Button()
|
||||||
|
icon = Gtk.Image.new_from_icon_name("dialog-information-symbolic", Gtk.IconSize.SMALL_TOOLBAR)
|
||||||
|
btn.add(icon)
|
||||||
|
btn.set_relief(Gtk.ReliefStyle.NONE)
|
||||||
|
btn.set_tooltip_text(text)
|
||||||
|
def _show(_b):
|
||||||
|
pop = Gtk.Popover(relative_to=btn)
|
||||||
|
lbl = Gtk.Label(label=text, xalign=0.0)
|
||||||
|
lbl.set_line_wrap(True)
|
||||||
|
lbl.set_max_width_chars(46)
|
||||||
|
lbl.set_margin_top(10); lbl.set_margin_bottom(10)
|
||||||
|
lbl.set_margin_start(12); lbl.set_margin_end(12)
|
||||||
|
pop.add(lbl)
|
||||||
|
pop.show_all()
|
||||||
|
btn.connect("clicked", _show)
|
||||||
|
return btn
|
||||||
|
|
||||||
|
|
||||||
|
def _type_combo(types: list[tuple[str, str]], stored: str = "") -> Gtk.ComboBoxText:
|
||||||
|
"""Combo that shows human-readable labels but maps to/from internal key values via index."""
|
||||||
|
c = Gtk.ComboBoxText()
|
||||||
|
for _, label in types:
|
||||||
|
c.append_text(label)
|
||||||
|
idx = next((i for i, (k, _) in enumerate(types) if k == stored), 0)
|
||||||
|
c.set_active(idx)
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def _type_key(combo: Gtk.ComboBoxText, types: list[tuple[str, str]], fallback: str = "") -> str:
|
||||||
|
"""Read the stored key for a combo built with _type_combo."""
|
||||||
|
i = combo.get_active()
|
||||||
|
return types[i][0] if 0 <= i < len(types) else fallback
|
||||||
|
|
||||||
|
|
||||||
class ModelPicker(Gtk.Box):
|
class ModelPicker(Gtk.Box):
|
||||||
"""An editable model field + a ▾ button opening a popover with a search bar
|
"""An editable model field + a ▾ button opening a popover with a search bar
|
||||||
on top and a scrollable, filtered list of models."""
|
on top and a scrollable, filtered list of models."""
|
||||||
@ -387,6 +466,7 @@ def _app_paths() -> dict[str, list[Path]]:
|
|||||||
return {
|
return {
|
||||||
"changelog": [linux_dir / "CHANGELOG.md", Path("/opt/blitztext/CHANGELOG.md")],
|
"changelog": [linux_dir / "CHANGELOG.md", Path("/opt/blitztext/CHANGELOG.md")],
|
||||||
"license": [repo_dir / "LICENSE", Path("/usr/share/doc/blitztext/copyright")],
|
"license": [repo_dir / "LICENSE", Path("/usr/share/doc/blitztext/copyright")],
|
||||||
|
"manual": [repo_dir / "MANUAL.md", Path("/opt/blitztext/MANUAL.md")],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -436,6 +516,7 @@ class SettingsDialog:
|
|||||||
("General", self._build_general),
|
("General", self._build_general),
|
||||||
("Benchmark", self._build_benchmark),
|
("Benchmark", self._build_benchmark),
|
||||||
("Log", self._build_log),
|
("Log", self._build_log),
|
||||||
|
("Manual", self._build_manual),
|
||||||
("About", self._build_about)):
|
("About", self._build_about)):
|
||||||
self._pending_tabs[_page(nb, title)] = builder
|
self._pending_tabs[_page(nb, title)] = builder
|
||||||
self._build_tab(nb.get_nth_page(0)) # the visible tab, eagerly
|
self._build_tab(nb.get_nth_page(0)) # the visible tab, eagerly
|
||||||
@ -666,7 +747,7 @@ class SettingsDialog:
|
|||||||
|
|
||||||
def _stt_section(self) -> Gtk.Box:
|
def _stt_section(self) -> Gtk.Box:
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
box.pack_start(Gtk.Label(label="Speech-to-text engine", xalign=0.0), False, False, 2)
|
box.pack_start(Gtk.Label(label="STT Speech-to-text engine", xalign=0.0), False, False, 2)
|
||||||
self._stt_idx = 0
|
self._stt_idx = 0
|
||||||
bar = Gtk.Box(spacing=8)
|
bar = Gtk.Box(spacing=8)
|
||||||
self.stt_combo = Gtk.ComboBoxText()
|
self.stt_combo = Gtk.ComboBoxText()
|
||||||
@ -681,23 +762,42 @@ class SettingsDialog:
|
|||||||
("Delete", self._stt_delete), ("Test", self._stt_test),
|
("Delete", self._stt_delete), ("Test", self._stt_test),
|
||||||
("Refresh", lambda _b: self._refresh_status())):
|
("Refresh", lambda _b: self._refresh_status())):
|
||||||
b = Gtk.Button(label=label); b.connect("clicked", cb); bar.pack_start(b, False, False, 0)
|
b = Gtk.Button(label=label); b.connect("clicked", cb); bar.pack_start(b, False, False, 0)
|
||||||
|
qs = Gtk.Button(label="Quickstart ▾")
|
||||||
|
qs.set_tooltip_text("Fill the form from a common service template")
|
||||||
|
qs.connect("clicked", self._show_stt_templates)
|
||||||
|
bar.pack_start(qs, False, False, 0)
|
||||||
box.pack_start(bar, False, False, 2)
|
box.pack_start(bar, False, False, 2)
|
||||||
|
|
||||||
form = Gtk.Box(orientation=Gtk.Orientation.VERTICAL); box.pack_start(form, False, False, 2)
|
form = Gtk.Box(orientation=Gtk.Orientation.VERTICAL); box.pack_start(form, False, False, 2)
|
||||||
self.stt_name = _labeled(form, "Name", _entry(placeholder="e.g. faster-whisper GPU"))
|
self.stt_name = _labeled(form, "Name", _entry(placeholder="e.g. faster-whisper GPU"),
|
||||||
self.stt_type = _labeled(form, "Type", _combo(["local", "openai", "riva_realtime"]))
|
tooltip="A label for this engine, shown in the dropdown.")
|
||||||
|
self.stt_type = _labeled(form, "Type", _type_combo(_STT_TYPES, "local"),
|
||||||
|
tooltip="Internal: faster-whisper runs inside Blitztext, no server needed. "
|
||||||
|
"Server: any OpenAI-compatible /v1 endpoint on your LAN or a cloud API. "
|
||||||
|
"Realtime: live streaming via NVIDIA Riva or NIM.")
|
||||||
self.stt_url = _url_field(form, "URL", "http://localhost:8010/v1 · realtime: http://localhost:8006/v1",
|
self.stt_url = _url_field(form, "URL", "http://localhost:8010/v1 · realtime: http://localhost:8006/v1",
|
||||||
lambda: self._populate_models(self.stt_model, self.stt_url.get_text().strip(), self.stt_key.get_text().strip()))
|
lambda: self._populate_models(self.stt_model, self.stt_url.get_text().strip(), self.stt_key.get_text().strip()))
|
||||||
self.stt_model = _labeled(form, "Model", _model_combo("blank = server default · tiny/base/small… for local"))
|
self.stt_model = _labeled(form, "Model", _model_combo("blank = server default · tiny/base/small… for local"),
|
||||||
self.stt_key = _labeled(form, "API key env", _entry(placeholder="env var name, e.g. GROQ_API_KEY (optional)"))
|
tooltip="Which Whisper model to load. For Internal: tiny (fastest) → large-v3 (most accurate). "
|
||||||
|
"For a server, press ⟳ to fetch available models from its URL.")
|
||||||
|
self.stt_key = _labeled(form, "API key env", _entry(placeholder="env var name, e.g. GROQ_API_KEY (optional)"),
|
||||||
|
tooltip="Name of the environment variable that holds your API key "
|
||||||
|
"(e.g. GROQ_API_KEY). Leave blank for local servers that don't need one.")
|
||||||
self.stt_url.connect("changed", lambda _e: self._schedule_models("stt"))
|
self.stt_url.connect("changed", lambda _e: self._schedule_models("stt"))
|
||||||
self.stt_key.connect("changed", lambda _e: self._schedule_models("stt"))
|
self.stt_key.connect("changed", lambda _e: self._schedule_models("stt"))
|
||||||
self.stt_type.connect("changed", self._stt_type_changed)
|
self.stt_type.connect("changed", self._stt_type_changed)
|
||||||
|
|
||||||
sep = Gtk.Separator(); sep.set_margin_top(4); form.pack_start(sep, False, False, 4)
|
sep = Gtk.Separator(); sep.set_margin_top(4); form.pack_start(sep, False, False, 4)
|
||||||
form.pack_start(Gtk.Label(label="Local engine (faster-whisper) — device & precision", xalign=0.0), False, False, 0)
|
form.pack_start(Gtk.Label(label="Local engine (faster-whisper) — device & precision", xalign=0.0), False, False, 0)
|
||||||
self.stt_device = _labeled(form, "Device", _combo(["auto", "cpu", "cuda"], self.cfg.device))
|
self.stt_device = _labeled(form, "Device", _type_combo(_DEVICE_OPTIONS, self.cfg.device),
|
||||||
self.stt_compute = _labeled(form, "Compute type", _combo(["auto", "int8", "float16", "int8_float16"], self.cfg.compute_type))
|
tooltip="Which processor runs the speech model. "
|
||||||
|
"Auto tries your GPU first and falls back to CPU. "
|
||||||
|
"GPU (CUDA) requires an NVIDIA GPU and CUDA toolkit — much faster than CPU.")
|
||||||
|
self.stt_compute = _labeled(form, "Compute type", _type_combo(_COMPUTE_OPTIONS, self.cfg.compute_type),
|
||||||
|
tooltip="Numerical precision of the model. "
|
||||||
|
"int8 is fastest and uses least memory. "
|
||||||
|
"float16 is most accurate but needs more GPU memory. "
|
||||||
|
"Auto lets Blitztext choose based on your hardware.")
|
||||||
|
|
||||||
self.stt_result = Gtk.Label(xalign=0.0); self.stt_result.set_line_wrap(True)
|
self.stt_result = Gtk.Label(xalign=0.0); self.stt_result.set_line_wrap(True)
|
||||||
box.pack_start(self.stt_result, False, False, 2)
|
box.pack_start(self.stt_result, False, False, 2)
|
||||||
@ -706,7 +806,7 @@ class SettingsDialog:
|
|||||||
|
|
||||||
def _llm_section(self) -> Gtk.Box:
|
def _llm_section(self) -> Gtk.Box:
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
box.pack_start(Gtk.Label(label="Language model (rewrite)", xalign=0.0), False, False, 2)
|
box.pack_start(Gtk.Label(label="LLM - Language model (rewrite)", xalign=0.0), False, False, 2)
|
||||||
self._llm_idx = 0
|
self._llm_idx = 0
|
||||||
bar = Gtk.Box(spacing=8)
|
bar = Gtk.Box(spacing=8)
|
||||||
self.llm_combo = Gtk.ComboBoxText()
|
self.llm_combo = Gtk.ComboBoxText()
|
||||||
@ -720,16 +820,30 @@ class SettingsDialog:
|
|||||||
for label, cb in (("+ Add", self._llm_add), ("Delete", self._llm_delete),
|
for label, cb in (("+ Add", self._llm_add), ("Delete", self._llm_delete),
|
||||||
("Refresh", lambda _b: self._refresh_status())):
|
("Refresh", lambda _b: self._refresh_status())):
|
||||||
b = Gtk.Button(label=label); b.connect("clicked", cb); bar.pack_start(b, False, False, 0)
|
b = Gtk.Button(label=label); b.connect("clicked", cb); bar.pack_start(b, False, False, 0)
|
||||||
|
qs = Gtk.Button(label="Quickstart ▾")
|
||||||
|
qs.set_tooltip_text("Fill the form from a common service template")
|
||||||
|
qs.connect("clicked", self._show_llm_templates)
|
||||||
|
bar.pack_start(qs, False, False, 0)
|
||||||
box.pack_start(bar, False, False, 2)
|
box.pack_start(bar, False, False, 2)
|
||||||
|
|
||||||
form = Gtk.Box(orientation=Gtk.Orientation.VERTICAL); box.pack_start(form, False, False, 2)
|
form = Gtk.Box(orientation=Gtk.Orientation.VERTICAL); box.pack_start(form, False, False, 2)
|
||||||
self.llm_name = _labeled(form, "Name", _entry(placeholder="e.g. Local Qwen"))
|
self.llm_name = _labeled(form, "Name", _entry(placeholder="e.g. Local Qwen"),
|
||||||
self.llm_type = _labeled(form, "Type", _combo(["local", "cloud"]))
|
tooltip="A label for this language model engine, shown in the dropdown.")
|
||||||
|
self.llm_type = _labeled(form, "Type", _type_combo(_LLM_TYPES, "cloud"),
|
||||||
|
tooltip="LAN server: a model running on your own machine or network "
|
||||||
|
"(Ollama, vLLM, LM Studio, llama-swap, …). "
|
||||||
|
"Cloud service: a remote paid API like OpenAI or Groq.")
|
||||||
self.llm_url = _url_field(form, "Base URL", "http://localhost:28080/v1 · https://api.openai.com/v1",
|
self.llm_url = _url_field(form, "Base URL", "http://localhost:28080/v1 · https://api.openai.com/v1",
|
||||||
lambda: self._populate_models(self.llm_model, self.llm_url.get_text().strip(), self.llm_key.get_text().strip()))
|
lambda: self._populate_models(self.llm_model, self.llm_url.get_text().strip(), self.llm_key.get_text().strip()))
|
||||||
self.llm_model = _labeled(form, "Model", _model_combo("pick after entering URL"))
|
self.llm_model = _labeled(form, "Model", _model_combo("pick after entering URL"),
|
||||||
self.llm_key = _labeled(form, "API key env", _entry(placeholder="env var name, e.g. OPENAI_API_KEY (blank for local)"))
|
tooltip="Which language model to use for rewriting. Press ⟳ on the URL field "
|
||||||
self.llm_temp = _labeled(form, "Temperature", _entry(placeholder="0.3"))
|
"to fetch available models from the server.")
|
||||||
|
self.llm_key = _labeled(form, "API key env", _entry(placeholder="env var name, e.g. OPENAI_API_KEY (blank for local)"),
|
||||||
|
tooltip="Name of the environment variable holding your API key "
|
||||||
|
"(e.g. OPENAI_API_KEY). Leave blank for local servers.")
|
||||||
|
self.llm_temp = _labeled(form, "Temperature", _entry(placeholder="0.3"),
|
||||||
|
tooltip="How creative the rewrite is, from 0.0 (very predictable) to 1.0 (more varied). "
|
||||||
|
"0.3 is a good default — lower for formal text, higher for creative writing.")
|
||||||
self.llm_url.connect("changed", lambda _e: self._schedule_models("llm"))
|
self.llm_url.connect("changed", lambda _e: self._schedule_models("llm"))
|
||||||
self.llm_key.connect("changed", lambda _e: self._schedule_models("llm"))
|
self.llm_key.connect("changed", lambda _e: self._schedule_models("llm"))
|
||||||
self._llm_load(self.llm_combo.get_active())
|
self._llm_load(self.llm_combo.get_active())
|
||||||
@ -741,7 +855,7 @@ class SettingsDialog:
|
|||||||
return
|
return
|
||||||
e = self.cfg.stt_engines[idx]
|
e = self.cfg.stt_engines[idx]
|
||||||
self.stt_name.set_text(e.name)
|
self.stt_name.set_text(e.name)
|
||||||
self.stt_type.set_active(["local", "openai", "riva_realtime"].index(e.type) if e.type in ("local", "openai", "riva_realtime") else 0)
|
self.stt_type.set_active(next((i for i, (k, _) in enumerate(_STT_TYPES) if k == e.type), 0))
|
||||||
self.stt_url.set_text(e.url); self.stt_key.set_text(e.api_key_env)
|
self.stt_url.set_text(e.url); self.stt_key.set_text(e.api_key_env)
|
||||||
if e.type == "local":
|
if e.type == "local":
|
||||||
_fill_combo(self.stt_model, ["tiny", "base", "small", "medium", "large-v3"], e.model or self.cfg.model)
|
_fill_combo(self.stt_model, ["tiny", "base", "small", "medium", "large-v3"], e.model or self.cfg.model)
|
||||||
@ -759,7 +873,7 @@ class SettingsDialog:
|
|||||||
e = self.cfg.stt_engines[idx]
|
e = self.cfg.stt_engines[idx]
|
||||||
new_name = self.stt_name.get_text().strip() or e.name
|
new_name = self.stt_name.get_text().strip() or e.name
|
||||||
e.name = new_name
|
e.name = new_name
|
||||||
e.type = self.stt_type.get_active_text() or "local"
|
e.type = _type_key(self.stt_type, _STT_TYPES, "local")
|
||||||
e.url = self.stt_url.get_text().strip().rstrip("/")
|
e.url = self.stt_url.get_text().strip().rstrip("/")
|
||||||
e.model = _combo_text(self.stt_model)
|
e.model = _combo_text(self.stt_model)
|
||||||
e.api_key_env = self.stt_key.get_text().strip()
|
e.api_key_env = self.stt_key.get_text().strip()
|
||||||
@ -842,7 +956,7 @@ class SettingsDialog:
|
|||||||
return
|
return
|
||||||
e = self.cfg.llm_engines[idx]
|
e = self.cfg.llm_engines[idx]
|
||||||
self.llm_name.set_text(e.name)
|
self.llm_name.set_text(e.name)
|
||||||
self.llm_type.set_active(["local", "cloud"].index(e.type) if e.type in ("local", "cloud") else 1)
|
self.llm_type.set_active(next((i for i, (k, _) in enumerate(_LLM_TYPES) if k == e.type), 0))
|
||||||
self.llm_url.set_text(e.url)
|
self.llm_url.set_text(e.url)
|
||||||
self.llm_key.set_text(e.api_key_env); self.llm_temp.set_text(str(e.temperature))
|
self.llm_key.set_text(e.api_key_env); self.llm_temp.set_text(str(e.temperature))
|
||||||
_fill_combo(self.llm_model, [], e.model)
|
_fill_combo(self.llm_model, [], e.model)
|
||||||
@ -856,7 +970,7 @@ class SettingsDialog:
|
|||||||
e = self.cfg.llm_engines[idx]
|
e = self.cfg.llm_engines[idx]
|
||||||
new_name = self.llm_name.get_text().strip() or e.name
|
new_name = self.llm_name.get_text().strip() or e.name
|
||||||
e.name = new_name
|
e.name = new_name
|
||||||
e.type = self.llm_type.get_active_text() or "cloud"
|
e.type = _type_key(self.llm_type, _LLM_TYPES, "cloud")
|
||||||
e.url = self.llm_url.get_text().strip().rstrip("/")
|
e.url = self.llm_url.get_text().strip().rstrip("/")
|
||||||
e.model = _combo_text(self.llm_model)
|
e.model = _combo_text(self.llm_model)
|
||||||
e.api_key_env = self.llm_key.get_text().strip()
|
e.api_key_env = self.llm_key.get_text().strip()
|
||||||
@ -885,8 +999,48 @@ class SettingsDialog:
|
|||||||
self.llm_combo.remove(self._llm_idx); self._llm_idx = -1
|
self.llm_combo.remove(self._llm_idx); self._llm_idx = -1
|
||||||
self.llm_combo.set_active(0); self._llm_load(0)
|
self.llm_combo.set_active(0); self._llm_load(0)
|
||||||
|
|
||||||
|
def _show_stt_templates(self, btn: Gtk.Button) -> None:
|
||||||
|
menu = Gtk.Menu()
|
||||||
|
for tpl in _STT_TEMPLATES:
|
||||||
|
item = Gtk.MenuItem(label=tpl[0])
|
||||||
|
item.connect("activate", lambda _i, t=tpl: self._stt_apply_template(t))
|
||||||
|
menu.append(item)
|
||||||
|
menu.show_all()
|
||||||
|
menu.popup_at_widget(btn, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, None)
|
||||||
|
|
||||||
|
def _stt_apply_template(self, tpl: tuple) -> None:
|
||||||
|
name, url, key, type_key, model = tpl
|
||||||
|
self.stt_name.set_text(name)
|
||||||
|
self.stt_url.set_text(url)
|
||||||
|
self.stt_key.set_text(key)
|
||||||
|
self.stt_type.set_active(next((i for i, (k, _) in enumerate(_STT_TYPES) if k == type_key), 0))
|
||||||
|
if model:
|
||||||
|
_fill_combo(self.stt_model, [model], model)
|
||||||
|
elif url:
|
||||||
|
self._populate_models(self.stt_model, url, key)
|
||||||
|
|
||||||
|
def _show_llm_templates(self, btn: Gtk.Button) -> None:
|
||||||
|
menu = Gtk.Menu()
|
||||||
|
for tpl in _LLM_TEMPLATES:
|
||||||
|
item = Gtk.MenuItem(label=tpl[0])
|
||||||
|
item.connect("activate", lambda _i, t=tpl: self._llm_apply_template(t))
|
||||||
|
menu.append(item)
|
||||||
|
menu.show_all()
|
||||||
|
menu.popup_at_widget(btn, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, None)
|
||||||
|
|
||||||
|
def _llm_apply_template(self, tpl: tuple) -> None:
|
||||||
|
name, url, key, type_key, model = tpl
|
||||||
|
self.llm_name.set_text(name)
|
||||||
|
self.llm_url.set_text(url)
|
||||||
|
self.llm_key.set_text(key)
|
||||||
|
self.llm_type.set_active(next((i for i, (k, _) in enumerate(_LLM_TYPES) if k == type_key), 0))
|
||||||
|
if model:
|
||||||
|
_fill_combo(self.llm_model, [model], model)
|
||||||
|
elif url:
|
||||||
|
self._populate_models(self.llm_model, url, key)
|
||||||
|
|
||||||
def _stt_type_changed(self, _c) -> None:
|
def _stt_type_changed(self, _c) -> None:
|
||||||
typ = self.stt_type.get_active_text()
|
typ = _type_key(self.stt_type, _STT_TYPES, "local")
|
||||||
if typ == "local":
|
if typ == "local":
|
||||||
_fill_combo(self.stt_model, ["tiny", "base", "small", "medium", "large-v3"], _combo_text(self.stt_model))
|
_fill_combo(self.stt_model, ["tiny", "base", "small", "medium", "large-v3"], _combo_text(self.stt_model))
|
||||||
elif typ == "openai":
|
elif typ == "openai":
|
||||||
@ -914,7 +1068,7 @@ class SettingsDialog:
|
|||||||
|
|
||||||
def fire():
|
def fire():
|
||||||
setattr(self, attr, 0)
|
setattr(self, attr, 0)
|
||||||
if which == "stt" and self.stt_type.get_active_text() == "openai":
|
if which == "stt" and _type_key(self.stt_type, _STT_TYPES) == "openai":
|
||||||
self._populate_models(self.stt_model, self.stt_url.get_text().strip(),
|
self._populate_models(self.stt_model, self.stt_url.get_text().strip(),
|
||||||
self.stt_key.get_text().strip())
|
self.stt_key.get_text().strip())
|
||||||
elif which == "llm":
|
elif which == "llm":
|
||||||
@ -1038,7 +1192,7 @@ class SettingsDialog:
|
|||||||
_labeled(page, "Strip trailing punctuation", self.q_strip, width=LW,
|
_labeled(page, "Strip trailing punctuation", self.q_strip, width=LW,
|
||||||
tooltip="Remove ending periods from pasted text, useful for code inserts.")
|
tooltip="Remove ending periods from pasted text, useful for code inserts.")
|
||||||
page.pack_start(Gtk.Separator(), False, False, 8)
|
page.pack_start(Gtk.Separator(), False, False, 8)
|
||||||
page.pack_start(Gtk.Label(label="Hands-free (Wakeword)", xalign=0.0), False, False, 2)
|
page.pack_start(Gtk.Label(label="WW - Wakeword (Hands-free)", xalign=0.0), False, False, 2)
|
||||||
self.ww_enabled = Gtk.Switch(); self.ww_enabled.set_active(self.cfg.wakeword_enabled); self.ww_enabled.set_halign(Gtk.Align.START)
|
self.ww_enabled = Gtk.Switch(); self.ww_enabled.set_active(self.cfg.wakeword_enabled); self.ww_enabled.set_halign(Gtk.Align.START)
|
||||||
_labeled(page, "Enable wakeword", self.ww_enabled, width=LW)
|
_labeled(page, "Enable wakeword", self.ww_enabled, width=LW)
|
||||||
self.ww_dot = Gtk.Label(); self.ww_dot.set_markup(_dot(GREY))
|
self.ww_dot = Gtk.Label(); self.ww_dot.set_markup(_dot(GREY))
|
||||||
@ -1049,7 +1203,7 @@ class SettingsDialog:
|
|||||||
self.ww_uri.set_text(self.cfg.wakeword_uri)
|
self.ww_uri.set_text(self.cfg.wakeword_uri)
|
||||||
self.ww_uri.connect("focus-out-event", self._on_ww_uri_leave)
|
self.ww_uri.connect("focus-out-event", self._on_ww_uri_leave)
|
||||||
self._probe_dot(self.ww_dot, self.cfg.wakeword_uri, 10400)
|
self._probe_dot(self.ww_dot, self.cfg.wakeword_uri, 10400)
|
||||||
self.ww_model = _labeled(page, "Model name", _model_combo("Search models…"), width=LW)
|
self.ww_model = _labeled(page, "Wakeword Model name", _model_combo("Search models…"), width=LW)
|
||||||
_fill_combo(self.ww_model, [], self.cfg.wakeword_model)
|
_fill_combo(self.ww_model, [], self.cfg.wakeword_model)
|
||||||
|
|
||||||
self.ww_mic_level = Gtk.LevelBar(); self.ww_mic_level.set_min_value(0); self.ww_mic_level.set_max_value(1)
|
self.ww_mic_level = Gtk.LevelBar(); self.ww_mic_level.set_min_value(0); self.ww_mic_level.set_max_value(1)
|
||||||
@ -1084,14 +1238,14 @@ class SettingsDialog:
|
|||||||
"multi-word phrase (e.g. your wakeword + ‘send’). Empty = off.")
|
"multi-word phrase (e.g. your wakeword + ‘send’). Empty = off.")
|
||||||
|
|
||||||
self.ww_snd_detected = self._sound_field(
|
self.ww_snd_detected = self._sound_field(
|
||||||
page, "Sound: detected", self.cfg.wakeword_sound_detected,
|
page, "Wakeword detected", self.cfg.wakeword_sound_detected,
|
||||||
"HANDS-FREE ONLY. Plays the instant the wake word is recognised and recording starts "
|
"HANDS-FREE ONLY. Plays the instant the wake word is recognised and recording starts "
|
||||||
"— your ‘speak now’ cue. (Keyboard/hotkey dictation ignores this and uses ‘Play before’.)",
|
"— your ‘speak now’ cue. (Keyboard/hotkey dictation ignores this and uses ‘Play before’.)",
|
||||||
empty_note="Leave empty for no sound. These wakeword cues are independent of the "
|
empty_note="Leave empty for no sound. These wakeword cues are independent of the "
|
||||||
"’Play audio cues’ switch below.",
|
"’Play audio cues’ switch below.",
|
||||||
clear_tip="Clear — no sound", width=LW)
|
clear_tip="Clear — no sound", width=LW)
|
||||||
self.ww_snd_done = self._sound_field(
|
self.ww_snd_done = self._sound_field(
|
||||||
page, "Sound: captured", self.cfg.wakeword_sound_done,
|
page, "Speech captured", self.cfg.wakeword_sound_done,
|
||||||
"HANDS-FREE ONLY. Plays when your spoken command is captured and recording stops "
|
"HANDS-FREE ONLY. Plays when your spoken command is captured and recording stops "
|
||||||
"(on silence or stop). (Keyboard/hotkey dictation ignores this and uses ‘Play after’.)",
|
"(on silence or stop). (Keyboard/hotkey dictation ignores this and uses ‘Play after’.)",
|
||||||
empty_note="Leave empty for no sound.",
|
empty_note="Leave empty for no sound.",
|
||||||
@ -1104,11 +1258,11 @@ class SettingsDialog:
|
|||||||
tooltip="On/off for the MANUAL start/stop chimes below (keyboard/hotkey dictation). "
|
tooltip="On/off for the MANUAL start/stop chimes below (keyboard/hotkey dictation). "
|
||||||
"The hands-free wakeword sounds above are separate and always play when set.")
|
"The hands-free wakeword sounds above are separate and always play when set.")
|
||||||
self.snd_before = self._sound_field(
|
self.snd_before = self._sound_field(
|
||||||
page, "Play before", self.cfg.sound_before,
|
page, "Hotkey detected", self.cfg.sound_before,
|
||||||
"MANUAL (keyboard/hotkey) dictation only. Plays when recording starts. "
|
"MANUAL (keyboard/hotkey) dictation only. Plays when recording starts. "
|
||||||
"(Hands-free sessions use ‘Sound: detected’ instead.)", width=LW)
|
"(Hands-free sessions use ‘Sound: detected’ instead.)", width=LW)
|
||||||
self.snd_after = self._sound_field(
|
self.snd_after = self._sound_field(
|
||||||
page, "Play after", self.cfg.sound_after,
|
page, "Capture done", self.cfg.sound_after,
|
||||||
"MANUAL (keyboard/hotkey) dictation only. Plays when recording stops "
|
"MANUAL (keyboard/hotkey) dictation only. Plays when recording stops "
|
||||||
"(paste, paste+Enter, or auto-stop on silence). (Hands-free uses ‘Sound: captured’ instead.)",
|
"(paste, paste+Enter, or auto-stop on silence). (Hands-free uses ‘Sound: captured’ instead.)",
|
||||||
width=LW)
|
width=LW)
|
||||||
@ -1174,7 +1328,7 @@ class SettingsDialog:
|
|||||||
_switch_row(page, "Notifications", self.gen_notify,
|
_switch_row(page, "Notifications", self.gen_notify,
|
||||||
"Desktop pop-ups for recording, transcription, and errors (manual dictation).")
|
"Desktop pop-ups for recording, transcription, and errors (manual dictation).")
|
||||||
self.gen_notify_routing = Gtk.Switch(); self.gen_notify_routing.set_active(self.cfg.notify_routing)
|
self.gen_notify_routing = Gtk.Switch(); self.gen_notify_routing.set_active(self.cfg.notify_routing)
|
||||||
_switch_row(page, "Announce matched preset", self.gen_notify_routing,
|
_switch_row(page, "Announce preset", self.gen_notify_routing,
|
||||||
"After a voice command, show which preset and keyword matched — even hands-free.")
|
"After a voice command, show which preset and keyword matched — even hands-free.")
|
||||||
self.gen_overlay = Gtk.Switch(); self.gen_overlay.set_active(self.cfg.overlay_enabled)
|
self.gen_overlay = Gtk.Switch(); self.gen_overlay.set_active(self.cfg.overlay_enabled)
|
||||||
_switch_row(page, "Visual overlay", self.gen_overlay,
|
_switch_row(page, "Visual overlay", self.gen_overlay,
|
||||||
@ -1534,6 +1688,14 @@ class SettingsDialog:
|
|||||||
f"{res.seconds:.0f}s\n<small>per voice: {voice_bits}</small>")
|
f"{res.seconds:.0f}s\n<small>per voice: {voice_bits}</small>")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# ===== Manual ===========================================================
|
||||||
|
def _build_manual(self, page: Gtk.Box) -> None:
|
||||||
|
text = _read_first(_app_paths()["manual"],
|
||||||
|
fallback="Manual not available in this install.\n\n"
|
||||||
|
"See the online documentation at "
|
||||||
|
"github.com/mARTin-B78/blitztext-app-linux")
|
||||||
|
page.pack_start(_text_panel(text, monospace=False, height=420), True, True, 0)
|
||||||
|
|
||||||
# ===== About ============================================================
|
# ===== About ============================================================
|
||||||
def _build_about(self, page: Gtk.Box) -> None:
|
def _build_about(self, page: Gtk.Box) -> None:
|
||||||
_infobox(page, "About Blitztext: version, source code, recent changes, and licence.")
|
_infobox(page, "About Blitztext: version, source code, recent changes, and licence.")
|
||||||
@ -1664,8 +1826,8 @@ class SettingsDialog:
|
|||||||
c.notify = self.gen_notify.get_active()
|
c.notify = self.gen_notify.get_active()
|
||||||
c.notify_routing = self.gen_notify_routing.get_active()
|
c.notify_routing = self.gen_notify_routing.get_active()
|
||||||
c.overlay_enabled = self.gen_overlay.get_active()
|
c.overlay_enabled = self.gen_overlay.get_active()
|
||||||
c.device = self.stt_device.get_active_text() or "auto"
|
c.device = _type_key(self.stt_device, _DEVICE_OPTIONS, "auto")
|
||||||
c.compute_type = self.stt_compute.get_active_text() or "auto"
|
c.compute_type = _type_key(self.stt_compute, _COMPUTE_OPTIONS, "auto")
|
||||||
autostart.set_enabled(self.gen_boot.get_active())
|
autostart.set_enabled(self.gen_boot.get_active())
|
||||||
|
|
||||||
if c.wakeword_enabled:
|
if c.wakeword_enabled:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user