649 lines
26 KiB
Python
649 lines
26 KiB
Python
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_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 not bool(cfg.models.allow_custom_models)
|
|
and not cfg.models.whisper_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.models.allow_custom_models = False
|
|
cfg.models.whisper_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)
|
|
|
|
safety_title = Gtk.Label()
|
|
safety_title.set_markup("<span weight='bold'>Output safety</span>")
|
|
safety_title.set_xalign(0.0)
|
|
box.pack_start(safety_title, False, False, 0)
|
|
|
|
self._safety_enabled_check = Gtk.CheckButton(
|
|
label="Enable fact-preservation guard (recommended)"
|
|
)
|
|
self._safety_enabled_check.connect("toggled", lambda *_: self._on_safety_guard_toggled())
|
|
box.pack_start(self._safety_enabled_check, False, False, 0)
|
|
|
|
self._safety_strict_check = Gtk.CheckButton(
|
|
label="Strict mode: reject output when facts are changed"
|
|
)
|
|
box.pack_start(self._safety_strict_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 the canonical editor model lifecycle for you. "
|
|
"Expert mode keeps Aman open-source friendly by letting you use custom Whisper paths."
|
|
)
|
|
)
|
|
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 Whisper path)")
|
|
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)
|
|
|
|
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)
|
|
|
|
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 set custom Whisper model paths.\n\n"
|
|
"Safety tips:\n"
|
|
"- Keep fact guard enabled to prevent accidental name/number changes.\n"
|
|
"- Strict safety blocks output on fact violations.\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))
|
|
self._safety_enabled_check.set_active(bool(self._config.safety.enabled))
|
|
self._safety_strict_check.set_active(bool(self._config.safety.strict))
|
|
self._on_safety_guard_toggled()
|
|
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._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 _on_safety_guard_toggled(self) -> None:
|
|
self._safety_strict_check.set_sensitive(self._safety_enabled_check.get_active())
|
|
|
|
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 custom Whisper path 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:
|
|
allow_custom = self._allow_custom_models_check.get_active()
|
|
custom_path_enabled = enabled and allow_custom
|
|
|
|
self._allow_custom_models_check.set_sensitive(enabled)
|
|
self._whisper_model_path_entry.set_sensitive(custom_path_enabled)
|
|
|
|
def _apply_canonical_runtime_defaults_to_widgets(self) -> None:
|
|
self._allow_custom_models_check.set_active(False)
|
|
self._whisper_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
|
|
|
|
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()
|
|
cfg.safety.enabled = self._safety_enabled_check.get_active()
|
|
cfg.safety.strict = self._safety_strict_check.get_active() and cfg.safety.enabled
|
|
if self._current_runtime_mode() == RUNTIME_MODE_MANAGED:
|
|
apply_canonical_runtime_defaults(cfg)
|
|
return cfg
|
|
|
|
cfg.stt.provider = DEFAULT_STT_PROVIDER
|
|
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()
|
|
else:
|
|
cfg.models.whisper_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. Keep fact guard enabled to prevent accidental fact changes. Aman-managed mode is "
|
|
"the canonical supported path; expert mode exposes custom Whisper model paths 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()
|