Add multilingual STT support and config UI/runtime updates
This commit is contained in:
parent
ed950cb7c4
commit
4a69c3d333
26 changed files with 2207 additions and 465 deletions
728
src/config_ui.py
Normal file
728
src/config_ui.py
Normal file
|
|
@ -0,0 +1,728 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import gi
|
||||
|
||||
from config import (
|
||||
Config,
|
||||
DEFAULT_EXTERNAL_API_BASE_URL,
|
||||
DEFAULT_EXTERNAL_API_KEY_ENV_VAR,
|
||||
DEFAULT_EXTERNAL_API_MAX_RETRIES,
|
||||
DEFAULT_EXTERNAL_API_MODEL,
|
||||
DEFAULT_EXTERNAL_API_PROVIDER,
|
||||
DEFAULT_EXTERNAL_API_TIMEOUT_MS,
|
||||
DEFAULT_LLM_PROVIDER,
|
||||
DEFAULT_STT_PROVIDER,
|
||||
)
|
||||
from constants import DEFAULT_CONFIG_PATH
|
||||
from languages import COMMON_STT_LANGUAGE_OPTIONS, stt_language_label
|
||||
from recorder import list_input_devices, resolve_input_device, start_recording, stop_recording
|
||||
|
||||
gi.require_version("Gdk", "3.0")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gdk, Gtk # type: ignore[import-not-found]
|
||||
|
||||
|
||||
RUNTIME_MODE_MANAGED = "aman_managed"
|
||||
RUNTIME_MODE_EXPERT = "expert_custom"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigUiResult:
|
||||
saved: bool
|
||||
config: Config | None
|
||||
closed_reason: str | None = None
|
||||
|
||||
|
||||
def infer_runtime_mode(cfg: Config) -> str:
|
||||
is_canonical = (
|
||||
cfg.stt.provider.strip().lower() == DEFAULT_STT_PROVIDER
|
||||
and cfg.llm.provider.strip().lower() == DEFAULT_LLM_PROVIDER
|
||||
and not bool(cfg.external_api.enabled)
|
||||
and not bool(cfg.models.allow_custom_models)
|
||||
and not cfg.models.whisper_model_path.strip()
|
||||
and not cfg.models.llm_model_path.strip()
|
||||
)
|
||||
return RUNTIME_MODE_MANAGED if is_canonical else RUNTIME_MODE_EXPERT
|
||||
|
||||
|
||||
def apply_canonical_runtime_defaults(cfg: Config) -> None:
|
||||
cfg.stt.provider = DEFAULT_STT_PROVIDER
|
||||
cfg.llm.provider = DEFAULT_LLM_PROVIDER
|
||||
cfg.external_api.enabled = False
|
||||
cfg.external_api.provider = DEFAULT_EXTERNAL_API_PROVIDER
|
||||
cfg.external_api.base_url = DEFAULT_EXTERNAL_API_BASE_URL
|
||||
cfg.external_api.model = DEFAULT_EXTERNAL_API_MODEL
|
||||
cfg.external_api.timeout_ms = DEFAULT_EXTERNAL_API_TIMEOUT_MS
|
||||
cfg.external_api.max_retries = DEFAULT_EXTERNAL_API_MAX_RETRIES
|
||||
cfg.external_api.api_key_env_var = DEFAULT_EXTERNAL_API_KEY_ENV_VAR
|
||||
cfg.models.allow_custom_models = False
|
||||
cfg.models.whisper_model_path = ""
|
||||
cfg.models.llm_model_path = ""
|
||||
|
||||
|
||||
class ConfigWindow:
|
||||
def __init__(
|
||||
self,
|
||||
initial_cfg: Config,
|
||||
desktop,
|
||||
*,
|
||||
required: bool,
|
||||
config_path: str | Path | None,
|
||||
) -> None:
|
||||
self._desktop = desktop
|
||||
self._config = copy.deepcopy(initial_cfg)
|
||||
self._required = required
|
||||
self._config_path = Path(config_path) if config_path else DEFAULT_CONFIG_PATH
|
||||
self._devices = list_input_devices()
|
||||
self._device_by_id = {str(device["index"]): device for device in self._devices}
|
||||
self._row_to_section: dict[Gtk.ListBoxRow, str] = {}
|
||||
self._runtime_mode = infer_runtime_mode(self._config)
|
||||
|
||||
title = "Aman Settings (Required)" if required else "Aman Settings"
|
||||
self._dialog = Gtk.Dialog(title=title, flags=Gtk.DialogFlags.MODAL)
|
||||
self._dialog.set_default_size(880, 560)
|
||||
self._dialog.set_modal(True)
|
||||
self._dialog.set_keep_above(True)
|
||||
self._dialog.set_position(Gtk.WindowPosition.CENTER_ALWAYS)
|
||||
self._dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG)
|
||||
|
||||
self._dialog.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
||||
self._apply_button = self._dialog.add_button("Apply", Gtk.ResponseType.APPLY)
|
||||
self._dialog.set_default_response(Gtk.ResponseType.APPLY)
|
||||
|
||||
content = self._dialog.get_content_area()
|
||||
content.set_border_width(12)
|
||||
content.set_spacing(10)
|
||||
|
||||
if self._required:
|
||||
banner = Gtk.InfoBar()
|
||||
banner.set_show_close_button(False)
|
||||
banner.set_message_type(Gtk.MessageType.WARNING)
|
||||
banner_label = Gtk.Label(
|
||||
label="Aman needs saved settings before it can start recording."
|
||||
)
|
||||
banner_label.set_xalign(0.0)
|
||||
banner_label.set_line_wrap(True)
|
||||
banner.get_content_area().pack_start(banner_label, True, True, 0)
|
||||
content.pack_start(banner, False, False, 0)
|
||||
|
||||
body = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||
content.pack_start(body, True, True, 0)
|
||||
|
||||
self._navigation = Gtk.ListBox()
|
||||
self._navigation.set_selection_mode(Gtk.SelectionMode.SINGLE)
|
||||
self._navigation.set_activate_on_single_click(True)
|
||||
self._navigation.connect("row-selected", self._on_nav_selected)
|
||||
|
||||
nav_scroll = Gtk.ScrolledWindow()
|
||||
nav_scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
nav_scroll.set_min_content_width(210)
|
||||
nav_scroll.add(self._navigation)
|
||||
body.pack_start(nav_scroll, False, False, 0)
|
||||
|
||||
self._stack = Gtk.Stack()
|
||||
self._stack.set_hexpand(True)
|
||||
self._stack.set_vexpand(True)
|
||||
self._stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
|
||||
self._stack.set_transition_duration(120)
|
||||
body.pack_start(self._stack, True, True, 0)
|
||||
|
||||
self._general_page = self._build_general_page()
|
||||
self._audio_page = self._build_audio_page()
|
||||
self._advanced_page = self._build_advanced_page()
|
||||
self._help_page = self._build_help_page()
|
||||
self._about_page = self._build_about_page()
|
||||
|
||||
self._add_section("general", "General", self._general_page)
|
||||
self._add_section("audio", "Audio", self._audio_page)
|
||||
self._add_section("advanced", "Runtime & Models", self._advanced_page)
|
||||
self._add_section("help", "Help", self._help_page)
|
||||
self._add_section("about", "About", self._about_page)
|
||||
|
||||
self._initialize_widget_values()
|
||||
self._validate_hotkey()
|
||||
first_row = self._navigation.get_row_at_index(0)
|
||||
if first_row is not None:
|
||||
self._navigation.select_row(first_row)
|
||||
|
||||
def run(self) -> ConfigUiResult:
|
||||
self._dialog.show_all()
|
||||
while True:
|
||||
response = self._dialog.run()
|
||||
if response == Gtk.ResponseType.APPLY:
|
||||
if not self._validate_hotkey():
|
||||
continue
|
||||
if not self._validate_runtime_settings():
|
||||
continue
|
||||
cfg = self._build_result_config()
|
||||
self._dialog.destroy()
|
||||
return ConfigUiResult(saved=True, config=cfg, closed_reason="saved")
|
||||
reason = "cancelled" if response == Gtk.ResponseType.CANCEL else "closed"
|
||||
self._dialog.destroy()
|
||||
return ConfigUiResult(saved=False, config=None, closed_reason=reason)
|
||||
|
||||
def _add_section(self, name: str, title: str, widget: Gtk.Widget) -> None:
|
||||
row = Gtk.ListBoxRow()
|
||||
row_label = Gtk.Label(label=title)
|
||||
row_label.set_xalign(0.0)
|
||||
row_label.set_margin_start(10)
|
||||
row_label.set_margin_end(10)
|
||||
row_label.set_margin_top(8)
|
||||
row_label.set_margin_bottom(8)
|
||||
row.add(row_label)
|
||||
self._navigation.add(row)
|
||||
self._row_to_section[row] = name
|
||||
self._stack.add_titled(widget, name, title)
|
||||
|
||||
def _on_nav_selected(self, _listbox, row: Gtk.ListBoxRow | None) -> None:
|
||||
if row is None:
|
||||
return
|
||||
section = self._row_to_section.get(row)
|
||||
if section:
|
||||
self._stack.set_visible_child_name(section)
|
||||
|
||||
def _build_general_page(self) -> Gtk.Widget:
|
||||
grid = Gtk.Grid(column_spacing=12, row_spacing=10)
|
||||
grid.set_margin_start(14)
|
||||
grid.set_margin_end(14)
|
||||
grid.set_margin_top(14)
|
||||
grid.set_margin_bottom(14)
|
||||
|
||||
hotkey_label = Gtk.Label(label="Trigger hotkey")
|
||||
hotkey_label.set_xalign(0.0)
|
||||
self._hotkey_entry = Gtk.Entry()
|
||||
self._hotkey_entry.set_placeholder_text("Super+m")
|
||||
self._hotkey_entry.connect("changed", lambda *_: self._validate_hotkey())
|
||||
grid.attach(hotkey_label, 0, 0, 1, 1)
|
||||
grid.attach(self._hotkey_entry, 1, 0, 1, 1)
|
||||
|
||||
self._hotkey_error = Gtk.Label(label="")
|
||||
self._hotkey_error.set_xalign(0.0)
|
||||
self._hotkey_error.set_line_wrap(True)
|
||||
grid.attach(self._hotkey_error, 1, 1, 1, 1)
|
||||
|
||||
backend_label = Gtk.Label(label="Text injection")
|
||||
backend_label.set_xalign(0.0)
|
||||
self._backend_combo = Gtk.ComboBoxText()
|
||||
self._backend_combo.append("clipboard", "Clipboard paste (recommended)")
|
||||
self._backend_combo.append("injection", "Simulated typing")
|
||||
grid.attach(backend_label, 0, 2, 1, 1)
|
||||
grid.attach(self._backend_combo, 1, 2, 1, 1)
|
||||
|
||||
self._remove_clipboard_check = Gtk.CheckButton(
|
||||
label="Remove transcription from clipboard after paste"
|
||||
)
|
||||
self._remove_clipboard_check.set_hexpand(True)
|
||||
grid.attach(self._remove_clipboard_check, 1, 3, 1, 1)
|
||||
|
||||
language_label = Gtk.Label(label="Transcription language")
|
||||
language_label.set_xalign(0.0)
|
||||
self._language_combo = Gtk.ComboBoxText()
|
||||
for code, label in COMMON_STT_LANGUAGE_OPTIONS:
|
||||
self._language_combo.append(code, label)
|
||||
grid.attach(language_label, 0, 4, 1, 1)
|
||||
grid.attach(self._language_combo, 1, 4, 1, 1)
|
||||
|
||||
profile_label = Gtk.Label(label="Profile")
|
||||
profile_label.set_xalign(0.0)
|
||||
self._profile_combo = Gtk.ComboBoxText()
|
||||
self._profile_combo.append("default", "Default")
|
||||
self._profile_combo.append("fast", "Fast (lower latency)")
|
||||
self._profile_combo.append("polished", "Polished")
|
||||
grid.attach(profile_label, 0, 5, 1, 1)
|
||||
grid.attach(self._profile_combo, 1, 5, 1, 1)
|
||||
|
||||
self._show_notifications_check = Gtk.CheckButton(label="Enable tray notifications")
|
||||
self._show_notifications_check.set_hexpand(True)
|
||||
grid.attach(self._show_notifications_check, 1, 6, 1, 1)
|
||||
return grid
|
||||
|
||||
def _build_audio_page(self) -> Gtk.Widget:
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
box.set_margin_start(14)
|
||||
box.set_margin_end(14)
|
||||
box.set_margin_top(14)
|
||||
box.set_margin_bottom(14)
|
||||
|
||||
input_label = Gtk.Label(label="Input device")
|
||||
input_label.set_xalign(0.0)
|
||||
box.pack_start(input_label, False, False, 0)
|
||||
|
||||
self._mic_combo = Gtk.ComboBoxText()
|
||||
self._mic_combo.append("", "System default")
|
||||
for device in self._devices:
|
||||
self._mic_combo.append(str(device["index"]), f"{device['index']}: {device['name']}")
|
||||
box.pack_start(self._mic_combo, False, False, 0)
|
||||
|
||||
test_button = Gtk.Button(label="Test microphone")
|
||||
test_button.connect("clicked", lambda *_: self._on_test_microphone())
|
||||
box.pack_start(test_button, False, False, 0)
|
||||
|
||||
self._mic_status = Gtk.Label(label="")
|
||||
self._mic_status.set_xalign(0.0)
|
||||
self._mic_status.set_line_wrap(True)
|
||||
box.pack_start(self._mic_status, False, False, 0)
|
||||
return box
|
||||
|
||||
def _build_advanced_page(self) -> Gtk.Widget:
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
box.set_margin_start(14)
|
||||
box.set_margin_end(14)
|
||||
box.set_margin_top(14)
|
||||
box.set_margin_bottom(14)
|
||||
|
||||
self._strict_startup_check = Gtk.CheckButton(label="Fail fast on startup validation errors")
|
||||
box.pack_start(self._strict_startup_check, False, False, 0)
|
||||
|
||||
runtime_title = Gtk.Label()
|
||||
runtime_title.set_markup("<span weight='bold'>Runtime management</span>")
|
||||
runtime_title.set_xalign(0.0)
|
||||
box.pack_start(runtime_title, False, False, 0)
|
||||
|
||||
runtime_copy = Gtk.Label(
|
||||
label=(
|
||||
"Aman-managed mode handles model downloads, updates, and safe defaults for you. "
|
||||
"Expert mode keeps Aman open-source friendly by exposing custom providers and models."
|
||||
)
|
||||
)
|
||||
runtime_copy.set_xalign(0.0)
|
||||
runtime_copy.set_line_wrap(True)
|
||||
box.pack_start(runtime_copy, False, False, 0)
|
||||
|
||||
mode_label = Gtk.Label(label="Runtime mode")
|
||||
mode_label.set_xalign(0.0)
|
||||
box.pack_start(mode_label, False, False, 0)
|
||||
|
||||
self._runtime_mode_combo = Gtk.ComboBoxText()
|
||||
self._runtime_mode_combo.append(RUNTIME_MODE_MANAGED, "Aman-managed (recommended)")
|
||||
self._runtime_mode_combo.append(RUNTIME_MODE_EXPERT, "Expert mode (custom models/providers)")
|
||||
self._runtime_mode_combo.connect("changed", lambda *_: self._on_runtime_mode_changed(user_initiated=True))
|
||||
box.pack_start(self._runtime_mode_combo, False, False, 0)
|
||||
|
||||
self._runtime_status_label = Gtk.Label(label="")
|
||||
self._runtime_status_label.set_xalign(0.0)
|
||||
self._runtime_status_label.set_line_wrap(True)
|
||||
box.pack_start(self._runtime_status_label, False, False, 0)
|
||||
|
||||
self._expert_expander = Gtk.Expander(label="Expert options")
|
||||
self._expert_expander.set_expanded(False)
|
||||
box.pack_start(self._expert_expander, False, False, 0)
|
||||
|
||||
expert_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
expert_box.set_margin_start(10)
|
||||
expert_box.set_margin_end(10)
|
||||
expert_box.set_margin_top(8)
|
||||
expert_box.set_margin_bottom(8)
|
||||
self._expert_expander.add(expert_box)
|
||||
|
||||
expert_warning = Gtk.InfoBar()
|
||||
expert_warning.set_show_close_button(False)
|
||||
expert_warning.set_message_type(Gtk.MessageType.WARNING)
|
||||
warning_label = Gtk.Label(
|
||||
label=(
|
||||
"Expert mode is best-effort and may require manual troubleshooting. "
|
||||
"Aman-managed mode is the canonical supported path."
|
||||
)
|
||||
)
|
||||
warning_label.set_xalign(0.0)
|
||||
warning_label.set_line_wrap(True)
|
||||
expert_warning.get_content_area().pack_start(warning_label, True, True, 0)
|
||||
expert_box.pack_start(expert_warning, False, False, 0)
|
||||
|
||||
llm_provider_label = Gtk.Label(label="LLM provider")
|
||||
llm_provider_label.set_xalign(0.0)
|
||||
expert_box.pack_start(llm_provider_label, False, False, 0)
|
||||
|
||||
self._llm_provider_combo = Gtk.ComboBoxText()
|
||||
self._llm_provider_combo.append("local_llama", "Local llama.cpp")
|
||||
self._llm_provider_combo.append("external_api", "External API")
|
||||
self._llm_provider_combo.connect("changed", lambda *_: self._on_runtime_widgets_changed())
|
||||
expert_box.pack_start(self._llm_provider_combo, False, False, 0)
|
||||
|
||||
self._external_api_enabled_check = Gtk.CheckButton(label="Enable external API provider")
|
||||
self._external_api_enabled_check.connect("toggled", lambda *_: self._on_runtime_widgets_changed())
|
||||
expert_box.pack_start(self._external_api_enabled_check, False, False, 0)
|
||||
|
||||
external_model_label = Gtk.Label(label="External API model")
|
||||
external_model_label.set_xalign(0.0)
|
||||
expert_box.pack_start(external_model_label, False, False, 0)
|
||||
self._external_model_entry = Gtk.Entry()
|
||||
self._external_model_entry.connect("changed", lambda *_: self._on_runtime_widgets_changed())
|
||||
expert_box.pack_start(self._external_model_entry, False, False, 0)
|
||||
|
||||
external_base_url_label = Gtk.Label(label="External API base URL")
|
||||
external_base_url_label.set_xalign(0.0)
|
||||
expert_box.pack_start(external_base_url_label, False, False, 0)
|
||||
self._external_base_url_entry = Gtk.Entry()
|
||||
self._external_base_url_entry.connect("changed", lambda *_: self._on_runtime_widgets_changed())
|
||||
expert_box.pack_start(self._external_base_url_entry, False, False, 0)
|
||||
|
||||
external_key_env_label = Gtk.Label(label="External API key env var")
|
||||
external_key_env_label.set_xalign(0.0)
|
||||
expert_box.pack_start(external_key_env_label, False, False, 0)
|
||||
self._external_key_env_entry = Gtk.Entry()
|
||||
self._external_key_env_entry.connect("changed", lambda *_: self._on_runtime_widgets_changed())
|
||||
expert_box.pack_start(self._external_key_env_entry, False, False, 0)
|
||||
|
||||
self._allow_custom_models_check = Gtk.CheckButton(
|
||||
label="Allow custom local model paths"
|
||||
)
|
||||
self._allow_custom_models_check.connect("toggled", lambda *_: self._on_runtime_widgets_changed())
|
||||
expert_box.pack_start(self._allow_custom_models_check, False, False, 0)
|
||||
|
||||
whisper_model_path_label = Gtk.Label(label="Custom Whisper model path")
|
||||
whisper_model_path_label.set_xalign(0.0)
|
||||
expert_box.pack_start(whisper_model_path_label, False, False, 0)
|
||||
self._whisper_model_path_entry = Gtk.Entry()
|
||||
self._whisper_model_path_entry.connect("changed", lambda *_: self._on_runtime_widgets_changed())
|
||||
expert_box.pack_start(self._whisper_model_path_entry, False, False, 0)
|
||||
|
||||
llm_model_path_label = Gtk.Label(label="Custom LLM model path")
|
||||
llm_model_path_label.set_xalign(0.0)
|
||||
expert_box.pack_start(llm_model_path_label, False, False, 0)
|
||||
self._llm_model_path_entry = Gtk.Entry()
|
||||
self._llm_model_path_entry.connect("changed", lambda *_: self._on_runtime_widgets_changed())
|
||||
expert_box.pack_start(self._llm_model_path_entry, False, False, 0)
|
||||
|
||||
self._runtime_error = Gtk.Label(label="")
|
||||
self._runtime_error.set_xalign(0.0)
|
||||
self._runtime_error.set_line_wrap(True)
|
||||
expert_box.pack_start(self._runtime_error, False, False, 0)
|
||||
|
||||
path_label = Gtk.Label(label="Config path")
|
||||
path_label.set_xalign(0.0)
|
||||
box.pack_start(path_label, False, False, 0)
|
||||
|
||||
path_entry = Gtk.Entry()
|
||||
path_entry.set_editable(False)
|
||||
path_entry.set_text(str(self._config_path))
|
||||
box.pack_start(path_entry, False, False, 0)
|
||||
|
||||
note = Gtk.Label(
|
||||
label=(
|
||||
"Tip: after editing the file directly, use Reload Config from the tray to apply changes."
|
||||
)
|
||||
)
|
||||
note.set_xalign(0.0)
|
||||
note.set_line_wrap(True)
|
||||
box.pack_start(note, False, False, 0)
|
||||
return box
|
||||
|
||||
def _build_help_page(self) -> Gtk.Widget:
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
box.set_margin_start(14)
|
||||
box.set_margin_end(14)
|
||||
box.set_margin_top(14)
|
||||
box.set_margin_bottom(14)
|
||||
|
||||
help_text = Gtk.Label(
|
||||
label=(
|
||||
"Usage:\n"
|
||||
"- Press your hotkey to start recording.\n"
|
||||
"- Press the hotkey again to stop and process.\n"
|
||||
"- Press Esc while recording to cancel.\n\n"
|
||||
"Model/runtime tips:\n"
|
||||
"- Aman-managed mode (recommended) handles model lifecycle for you.\n"
|
||||
"- Expert mode lets you bring your own models/providers.\n\n"
|
||||
"Use the tray menu for pause/resume, config reload, and diagnostics."
|
||||
)
|
||||
)
|
||||
help_text.set_xalign(0.0)
|
||||
help_text.set_line_wrap(True)
|
||||
box.pack_start(help_text, False, False, 0)
|
||||
|
||||
about_button = Gtk.Button(label="Open About Dialog")
|
||||
about_button.connect("clicked", lambda *_: _present_about_dialog(self._dialog))
|
||||
box.pack_start(about_button, False, False, 0)
|
||||
return box
|
||||
|
||||
def _build_about_page(self) -> Gtk.Widget:
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
box.set_margin_start(14)
|
||||
box.set_margin_end(14)
|
||||
box.set_margin_top(14)
|
||||
box.set_margin_bottom(14)
|
||||
|
||||
title = Gtk.Label()
|
||||
title.set_markup("<span size='x-large' weight='bold'>Aman</span>")
|
||||
title.set_xalign(0.0)
|
||||
box.pack_start(title, False, False, 0)
|
||||
|
||||
subtitle = Gtk.Label(label="Local amanuensis for desktop dictation and rewriting.")
|
||||
subtitle.set_xalign(0.0)
|
||||
subtitle.set_line_wrap(True)
|
||||
box.pack_start(subtitle, False, False, 0)
|
||||
|
||||
about_button = Gtk.Button(label="About Aman")
|
||||
about_button.connect("clicked", lambda *_: _present_about_dialog(self._dialog))
|
||||
box.pack_start(about_button, False, False, 0)
|
||||
return box
|
||||
|
||||
def _initialize_widget_values(self) -> None:
|
||||
hotkey = self._config.daemon.hotkey.strip() or "Super+m"
|
||||
self._hotkey_entry.set_text(hotkey)
|
||||
|
||||
backend = (self._config.injection.backend or "clipboard").strip().lower()
|
||||
if backend not in {"clipboard", "injection"}:
|
||||
backend = "clipboard"
|
||||
self._backend_combo.set_active_id(backend)
|
||||
self._remove_clipboard_check.set_active(
|
||||
bool(self._config.injection.remove_transcription_from_clipboard)
|
||||
)
|
||||
language = (self._config.stt.language or "auto").strip().lower()
|
||||
if self._language_combo.get_active_id() is None:
|
||||
self._language_combo.set_active_id("auto")
|
||||
self._language_combo.set_active_id(language)
|
||||
if self._language_combo.get_active_id() != language:
|
||||
self._language_combo.append(language, stt_language_label(language))
|
||||
self._language_combo.set_active_id(language)
|
||||
|
||||
profile = (self._config.ux.profile or "default").strip().lower()
|
||||
if profile not in {"default", "fast", "polished"}:
|
||||
profile = "default"
|
||||
self._profile_combo.set_active_id(profile)
|
||||
self._show_notifications_check.set_active(bool(self._config.ux.show_notifications))
|
||||
self._strict_startup_check.set_active(bool(self._config.advanced.strict_startup))
|
||||
llm_provider = self._config.llm.provider.strip().lower()
|
||||
if llm_provider not in {"local_llama", "external_api"}:
|
||||
llm_provider = "local_llama"
|
||||
self._llm_provider_combo.set_active_id(llm_provider)
|
||||
self._external_api_enabled_check.set_active(bool(self._config.external_api.enabled))
|
||||
self._external_model_entry.set_text(self._config.external_api.model)
|
||||
self._external_base_url_entry.set_text(self._config.external_api.base_url)
|
||||
self._external_key_env_entry.set_text(self._config.external_api.api_key_env_var)
|
||||
self._allow_custom_models_check.set_active(bool(self._config.models.allow_custom_models))
|
||||
self._whisper_model_path_entry.set_text(self._config.models.whisper_model_path)
|
||||
self._llm_model_path_entry.set_text(self._config.models.llm_model_path)
|
||||
self._runtime_mode_combo.set_active_id(self._runtime_mode)
|
||||
self._sync_runtime_mode_ui(user_initiated=False)
|
||||
self._validate_runtime_settings()
|
||||
|
||||
resolved = resolve_input_device(self._config.recording.input)
|
||||
if resolved is None:
|
||||
self._mic_combo.set_active_id("")
|
||||
return
|
||||
resolved_id = str(resolved)
|
||||
self._mic_combo.set_active_id(resolved_id if resolved_id in self._device_by_id else "")
|
||||
|
||||
def _current_runtime_mode(self) -> str:
|
||||
mode = (self._runtime_mode_combo.get_active_id() or "").strip().lower()
|
||||
if mode in {RUNTIME_MODE_MANAGED, RUNTIME_MODE_EXPERT}:
|
||||
return mode
|
||||
return RUNTIME_MODE_MANAGED
|
||||
|
||||
def _on_runtime_mode_changed(self, *, user_initiated: bool) -> None:
|
||||
self._sync_runtime_mode_ui(user_initiated=user_initiated)
|
||||
self._validate_runtime_settings()
|
||||
|
||||
def _on_runtime_widgets_changed(self) -> None:
|
||||
self._sync_runtime_mode_ui(user_initiated=False)
|
||||
self._validate_runtime_settings()
|
||||
|
||||
def _sync_runtime_mode_ui(self, *, user_initiated: bool) -> None:
|
||||
mode = self._current_runtime_mode()
|
||||
self._runtime_mode = mode
|
||||
if mode == RUNTIME_MODE_MANAGED:
|
||||
if user_initiated:
|
||||
self._apply_canonical_runtime_defaults_to_widgets()
|
||||
self._runtime_status_label.set_text(
|
||||
"Aman-managed mode is active. Aman handles model lifecycle and keeps supported defaults."
|
||||
)
|
||||
self._expert_expander.set_expanded(False)
|
||||
self._expert_expander.set_visible(False)
|
||||
self._set_expert_controls_sensitive(False)
|
||||
self._runtime_error.set_text("")
|
||||
return
|
||||
|
||||
self._runtime_status_label.set_text(
|
||||
"Expert mode is active. You are responsible for provider, model, and environment compatibility."
|
||||
)
|
||||
self._expert_expander.set_visible(True)
|
||||
self._expert_expander.set_expanded(True)
|
||||
self._set_expert_controls_sensitive(True)
|
||||
|
||||
def _set_expert_controls_sensitive(self, enabled: bool) -> None:
|
||||
provider = (self._llm_provider_combo.get_active_id() or "local_llama").strip().lower()
|
||||
allow_custom = self._allow_custom_models_check.get_active()
|
||||
external_fields_enabled = enabled and provider == "external_api"
|
||||
custom_path_enabled = enabled and allow_custom
|
||||
|
||||
self._llm_provider_combo.set_sensitive(enabled)
|
||||
self._external_api_enabled_check.set_sensitive(enabled)
|
||||
self._external_model_entry.set_sensitive(external_fields_enabled)
|
||||
self._external_base_url_entry.set_sensitive(external_fields_enabled)
|
||||
self._external_key_env_entry.set_sensitive(external_fields_enabled)
|
||||
self._allow_custom_models_check.set_sensitive(enabled)
|
||||
self._whisper_model_path_entry.set_sensitive(custom_path_enabled)
|
||||
self._llm_model_path_entry.set_sensitive(custom_path_enabled)
|
||||
|
||||
def _apply_canonical_runtime_defaults_to_widgets(self) -> None:
|
||||
self._llm_provider_combo.set_active_id(DEFAULT_LLM_PROVIDER)
|
||||
self._external_api_enabled_check.set_active(False)
|
||||
self._external_model_entry.set_text(DEFAULT_EXTERNAL_API_MODEL)
|
||||
self._external_base_url_entry.set_text(DEFAULT_EXTERNAL_API_BASE_URL)
|
||||
self._external_key_env_entry.set_text(DEFAULT_EXTERNAL_API_KEY_ENV_VAR)
|
||||
self._allow_custom_models_check.set_active(False)
|
||||
self._whisper_model_path_entry.set_text("")
|
||||
self._llm_model_path_entry.set_text("")
|
||||
|
||||
def _validate_runtime_settings(self) -> bool:
|
||||
mode = self._current_runtime_mode()
|
||||
if mode == RUNTIME_MODE_MANAGED:
|
||||
self._runtime_error.set_text("")
|
||||
return True
|
||||
|
||||
provider = (self._llm_provider_combo.get_active_id() or "local_llama").strip().lower()
|
||||
if provider == "external_api" and not self._external_api_enabled_check.get_active():
|
||||
self._runtime_error.set_text(
|
||||
"Expert mode: enable External API provider when LLM provider is set to External API."
|
||||
)
|
||||
return False
|
||||
if provider == "external_api" and not self._external_model_entry.get_text().strip():
|
||||
self._runtime_error.set_text("Expert mode: External API model is required.")
|
||||
return False
|
||||
if provider == "external_api" and not self._external_base_url_entry.get_text().strip():
|
||||
self._runtime_error.set_text("Expert mode: External API base URL is required.")
|
||||
return False
|
||||
if provider == "external_api" and not self._external_key_env_entry.get_text().strip():
|
||||
self._runtime_error.set_text("Expert mode: External API key env var is required.")
|
||||
return False
|
||||
self._runtime_error.set_text("")
|
||||
return True
|
||||
|
||||
def _selected_input_spec(self) -> str | int | None:
|
||||
selected = self._mic_combo.get_active_id()
|
||||
if not selected:
|
||||
return ""
|
||||
if selected.isdigit():
|
||||
return int(selected)
|
||||
return selected
|
||||
|
||||
def _on_test_microphone(self) -> None:
|
||||
input_spec = self._selected_input_spec()
|
||||
self._mic_status.set_text("Testing microphone...")
|
||||
while Gtk.events_pending():
|
||||
Gtk.main_iteration()
|
||||
try:
|
||||
stream, record = start_recording(input_spec)
|
||||
time.sleep(0.35)
|
||||
audio = stop_recording(stream, record)
|
||||
if getattr(audio, "size", 0) > 0:
|
||||
self._mic_status.set_text("Microphone test successful.")
|
||||
return
|
||||
self._mic_status.set_text("No audio captured. Try another device.")
|
||||
except Exception as exc:
|
||||
self._mic_status.set_text(f"Microphone test failed: {exc}")
|
||||
|
||||
def _validate_hotkey(self) -> bool:
|
||||
hotkey = self._hotkey_entry.get_text().strip()
|
||||
if not hotkey:
|
||||
self._hotkey_error.set_text("Hotkey is required.")
|
||||
self._apply_button.set_sensitive(False)
|
||||
return False
|
||||
try:
|
||||
self._desktop.validate_hotkey(hotkey)
|
||||
except Exception as exc:
|
||||
self._hotkey_error.set_text(f"Hotkey is not available: {exc}")
|
||||
self._apply_button.set_sensitive(False)
|
||||
return False
|
||||
self._hotkey_error.set_text("")
|
||||
self._apply_button.set_sensitive(True)
|
||||
return True
|
||||
|
||||
def _build_result_config(self) -> Config:
|
||||
cfg = copy.deepcopy(self._config)
|
||||
cfg.daemon.hotkey = self._hotkey_entry.get_text().strip()
|
||||
cfg.recording.input = self._selected_input_spec()
|
||||
cfg.injection.backend = self._backend_combo.get_active_id() or "clipboard"
|
||||
cfg.injection.remove_transcription_from_clipboard = self._remove_clipboard_check.get_active()
|
||||
cfg.stt.language = self._language_combo.get_active_id() or "auto"
|
||||
cfg.ux.profile = self._profile_combo.get_active_id() or "default"
|
||||
cfg.ux.show_notifications = self._show_notifications_check.get_active()
|
||||
cfg.advanced.strict_startup = self._strict_startup_check.get_active()
|
||||
if self._current_runtime_mode() == RUNTIME_MODE_MANAGED:
|
||||
apply_canonical_runtime_defaults(cfg)
|
||||
return cfg
|
||||
|
||||
cfg.stt.provider = DEFAULT_STT_PROVIDER
|
||||
cfg.llm.provider = self._llm_provider_combo.get_active_id() or DEFAULT_LLM_PROVIDER
|
||||
cfg.external_api.enabled = self._external_api_enabled_check.get_active()
|
||||
cfg.external_api.model = self._external_model_entry.get_text().strip()
|
||||
cfg.external_api.base_url = self._external_base_url_entry.get_text().strip()
|
||||
cfg.external_api.api_key_env_var = self._external_key_env_entry.get_text().strip()
|
||||
cfg.models.allow_custom_models = self._allow_custom_models_check.get_active()
|
||||
if cfg.models.allow_custom_models:
|
||||
cfg.models.whisper_model_path = self._whisper_model_path_entry.get_text().strip()
|
||||
cfg.models.llm_model_path = self._llm_model_path_entry.get_text().strip()
|
||||
else:
|
||||
cfg.models.whisper_model_path = ""
|
||||
cfg.models.llm_model_path = ""
|
||||
return cfg
|
||||
|
||||
|
||||
def run_config_ui(
|
||||
initial_cfg: Config,
|
||||
desktop,
|
||||
*,
|
||||
required: bool,
|
||||
config_path: str | Path | None = None,
|
||||
) -> ConfigUiResult:
|
||||
try:
|
||||
Gtk.init([])
|
||||
except Exception:
|
||||
pass
|
||||
logging.info("opening settings ui")
|
||||
window = ConfigWindow(
|
||||
initial_cfg,
|
||||
desktop,
|
||||
required=required,
|
||||
config_path=config_path,
|
||||
)
|
||||
return window.run()
|
||||
|
||||
|
||||
def show_help_dialog() -> None:
|
||||
try:
|
||||
Gtk.init([])
|
||||
except Exception:
|
||||
pass
|
||||
dialog = Gtk.MessageDialog(
|
||||
None,
|
||||
Gtk.DialogFlags.MODAL,
|
||||
Gtk.MessageType.INFO,
|
||||
Gtk.ButtonsType.OK,
|
||||
"Aman Help",
|
||||
)
|
||||
dialog.set_title("Aman Help")
|
||||
dialog.format_secondary_text(
|
||||
"Press your hotkey to record, press it again to process, and press Esc while recording to "
|
||||
"cancel. Aman-managed mode is the canonical supported path; expert mode exposes custom "
|
||||
"providers/models for advanced users."
|
||||
)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
|
||||
def show_about_dialog() -> None:
|
||||
try:
|
||||
Gtk.init([])
|
||||
except Exception:
|
||||
pass
|
||||
_present_about_dialog(None)
|
||||
|
||||
|
||||
def _present_about_dialog(parent) -> None:
|
||||
about = Gtk.AboutDialog(transient_for=parent, modal=True)
|
||||
about.set_program_name("Aman")
|
||||
about.set_version("pre-release")
|
||||
about.set_comments("Local amanuensis for desktop dictation and rewriting.")
|
||||
about.set_license("MIT")
|
||||
about.set_wrap_license(True)
|
||||
about.run()
|
||||
about.destroy()
|
||||
Loading…
Add table
Add a link
Reference in a new issue