public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: matiwari <matiwari@redhat.com>
To: git-commits@fedoraproject.org
Subject: [rpms/ibus-speech-to-text] f44: Update to release 1.0.0
Date: Fri, 07 Aug 2026 02:49:59 GMT	[thread overview]
Message-ID: <178607099989.1.699124242477025289.rpms-ibus-speech-to-text-be64b0282704@fedoraproject.org> (raw)

A new commit has been pushed.

Repo   : rpms/ibus-speech-to-text
Branch : f44
Commit : be64b02827044aee6a6f480b7e25728654a6349b
Author : matiwari <matiwari@redhat.com>
Date   : 2026-08-07T02:57:42+05:30
Stats  : +1475/-2 in 4 file(s)
URL    : https://src.fedoraproject.org/rpms/ibus-speech-to-text/c/be64b02827044aee6a6f480b7e25728654a6349b?branch=f44

Log:
Update to release 1.0.0

---
diff --git a/.gitignore b/.gitignore
index b1a134a..5586b2f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
 /0.6.0.tar.gz
 /0.7.0.tar.gz
 /0.9.0.tar.gz
+/1.0.0.tar.gz

diff --git a/ibus-speech-to-text.spec b/ibus-speech-to-text.spec
index bb7c194..1ca2384 100644
--- a/ibus-speech-to-text.spec
+++ b/ibus-speech-to-text.spec
@@ -1,12 +1,14 @@
 %global debug_package %{nil}
 Name:   ibus-speech-to-text
-Version:  0.9.0
+Version:  1.0.0
 Release:  1%{?dist}
 Summary:  A speech to text IBus Input Method using VOSK
 ExcludeArch: %{ix86}
 License:  GPL-3.0-or-later
 URL:     https://github.com/Manish7093/IBus-Speech-To-Text
 Source0: https://github.com/Manish7093/IBus-Speech-To-Text/archive/refs/tags/%{version}.tar.gz
+Patch0: ibus-stt-1_0_0-backend-notify.patch
+
 BuildRequires:  meson
 BuildRequires:  python3-devel
 BuildRequires:  ibus-devel >= 1.5.3
@@ -32,6 +34,7 @@ which can be used to dictate text to any application
 
 %prep
 %setup -q -n IBus-Speech-To-Text-%{version}
+%patch 0 -p1 -b .orig~
 
 %build
 %meson
@@ -54,6 +57,10 @@ desktop-file-validate %{buildroot}/%{_datadir}/applications/ibus-setup-stt.deskt
 %{_datadir}/glib-2.0/schemas/org.freedesktop.ibus.engine.stt.gschema.xml
 
 %changelog
+* Fri Aug 7 2026 Manish Tiwari <matiwari@redhat.com> 1.0.0-1
+- Update to release 1.0.0
+- Add ibus-stt-1_0_0-backend-notify.patch to support backend notification dialog
+
 * Thu May 7 2026 Manish Tiwari <matiwari@redhat.com> 0.9.0-1
 - Update to release 0.9.0
 

diff --git a/ibus-stt-1_0_0-backend-notify.patch b/ibus-stt-1_0_0-backend-notify.patch
new file mode 100644
index 0000000..f1d9c28
--- /dev/null
+++ b/ibus-stt-1_0_0-backend-notify.patch
@@ -0,0 +1,1465 @@
+diff -urN IBus-Speech-To-Text-1.0.0.orig/engine/meson.build IBus-Speech-To-Text-1.0.0/engine/meson.build
+--- IBus-Speech-To-Text-1.0.0.orig/engine/meson.build	2026-07-14 03:00:00.000000000 +0530
++++ IBus-Speech-To-Text-1.0.0/engine/meson.build	2026-08-07 01:42:43.021671212 +0530
+@@ -18,6 +18,7 @@
+     'sttshortcutdialog.py',
+     'sttutterancerow.py',
+     'sttvad.py',
++    'sttbackenddeps.py',
+     'sttmodelchooserdialog.py',
+     'sttvoskmodelmanagers.py',
+     'sttwhispermodelmanagers.py',
+diff -urN IBus-Speech-To-Text-1.0.0.orig/engine/sttbackenddeps.py IBus-Speech-To-Text-1.0.0/engine/sttbackenddeps.py
+--- IBus-Speech-To-Text-1.0.0.orig/engine/sttbackenddeps.py	1970-01-01 05:30:00.000000000 +0530
++++ IBus-Speech-To-Text-1.0.0/engine/sttbackenddeps.py	2026-08-07 01:44:45.299598828 +0530
+@@ -0,0 +1,173 @@
++import importlib
++import importlib.machinery
++import importlib.util
++import logging
++import sys
++
++LOG_MSG = logging.getLogger()
++
++class STTBackendDependency:
++
++    def __init__(self, module, distribution, extras=None):
++        self.module = module
++        self.distribution = distribution
++        self.extras = tuple(extras) if extras is not None else ()
++
++    @property
++    def requirement(self):
++        if not self.extras:
++            return self.distribution
++        return "%s[%s]" % (self.distribution, ",".join(self.extras))
++
++    @property
++    def pip_argument(self):
++        if not self.extras:
++            return self.distribution
++        return "'%s'" % self.requirement
++
++    def available(self):
++        #True when the module can be found on sys.path right now
++        try:
++            spec = importlib.machinery.PathFinder.find_spec(self.module,
++                                                            sys.path)
++        except (ImportError, ValueError, TypeError, AttributeError) as error:
++            LOG_MSG.debug("cannot look up module %s (%s)", self.module, error)
++            return False
++
++        return spec is not None
++
++    def __repr__(self):
++        return "<STTBackendDependency %s>" % self.requirement
++
++# NOTE: keep the keys in sync with the "backend" GSettings key and with the
++# check buttons of sttconfigdialog.ui.
++_BACKENDS = {
++    "vosk": {
++        "name": "Vosk",
++        "dependencies": (),
++        "components": {
++            "engine":  ("sttgstvosk", "STTGstVosk"),
++            "model":   ("sttvoskmodel", "STTVoskModel"),
++            "manager": ("sttvoskmodelmanagers", "stt_vosk_online_model_manager"),
++        },
++    },
++    "whisper": {
++        "name": "Whisper",
++        "dependencies": (),
++        "components": {
++            "engine":  ("sttgstwhisper", "STTGstWhisper"),
++            "model":   ("sttwhispermodel", "STTWhisperModel"),
++            "manager": ("sttwhispermodelmanagers", "stt_whisper_online_model_manager"),
++        },
++    },
++    "onnxasr": {
++        "name": "onnx-asr",
++        "dependencies": (
++            STTBackendDependency("onnx_asr", "onnx-asr", ("cpu", "hub")),
++        ),
++        "components": {
++            "engine":  ("sttgstonnxasr", "STTGstOnnxAsr"),
++            "model":   ("sttonnxasrmodel", "STTOnnxAsrModel"),
++            "manager": ("sttonnxasrmodelmanagers", "stt_onnxasr_online_model_manager"),
++        },
++    },
++    "moonshine": {
++        "name": "Moonshine",
++        "dependencies": (
++            STTBackendDependency("moonshine_voice", "moonshine-voice"),
++        ),
++        "components": {
++            "engine":  ("sttgstmoonshine", "STTGstMoonshine"),
++            "model":   ("sttmoonshinemodel", "STTMoonshineModel"),
++            "manager": ("sttmoonshinemodelmanagers", "stt_moonshine_online_model_manager"),
++        },
++    },
++}
++
++STT_DEFAULT_BACKEND = "vosk"
++
++# Cache the lookups: they are queried from callbacks that can run on every
++# model/locale change. Dropped by stt_invalidate_availability_cache().
++_availability_cache = {}
++
++def stt_backends():
++    return tuple(_BACKENDS.keys())
++
++def stt_backend_display_name(backend):
++    entry = _BACKENDS.get(backend)
++    if entry is None:
++        return backend
++    return entry["name"]
++
++def stt_backend_dependencies(backend):
++    entry = _BACKENDS.get(backend)
++    if entry is None:
++        return ()
++    return entry["dependencies"]
++
++def stt_invalidate_availability_cache():
++    _availability_cache.clear()
++    importlib.invalidate_caches()
++
++def stt_backend_missing_dependencies(backend, refresh=False):
++    if refresh:
++        stt_invalidate_availability_cache()
++
++    missing = []
++    for dependency in stt_backend_dependencies(backend):
++        available = _availability_cache.get(dependency.module)
++        if available is None:
++            available = dependency.available()
++            _availability_cache[dependency.module] = available
++            LOG_MSG.debug("module %s available: %s",
++                          dependency.module, available)
++        if not available:
++            missing.append(dependency)
++
++    return missing
++
++def stt_backend_is_available(backend, refresh=False):
++    return not stt_backend_missing_dependencies(backend, refresh=refresh)
++
++def stt_backend_install_command(backend):
++    missing = stt_backend_missing_dependencies(backend)
++    if not missing:
++        return ""
++
++    return "pip install %s" % " ".join(
++        dependency.pip_argument for dependency in missing)
++
++def stt_backend_component(backend, component):
++    entry = _BACKENDS.get(backend)
++    if entry is None:
++        LOG_MSG.warning("unknown backend (%s)", backend)
++        return None
++
++    location = entry["components"].get(component)
++    if location is None:
++        return None
++
++    module_name, attribute = location
++    try:
++        module = importlib.import_module(module_name)
++    except Exception as error:
++        LOG_MSG.warning("cannot import %s for backend %s (%s)",
++                        module_name, backend, error)
++        return None
++
++    value = getattr(module, attribute, None)
++    if value is None:
++        LOG_MSG.warning("%s has no attribute %s", module_name, attribute)
++    return value
++
++def stt_backend_model_manager(backend):
++    manager = stt_backend_component(backend, "manager")
++    if manager is None:
++        return None
++
++    try:
++        return manager()
++    except Exception as error:
++        LOG_MSG.warning("cannot create model manager for %s (%s)",
++                        backend, error)
++        return None
+diff -urN IBus-Speech-To-Text-1.0.0.orig/engine/sttconfigdialog.py IBus-Speech-To-Text-1.0.0/engine/sttconfigdialog.py
+--- IBus-Speech-To-Text-1.0.0.orig/engine/sttconfigdialog.py	2026-07-14 03:00:00.000000000 +0530
++++ IBus-Speech-To-Text-1.0.0/engine/sttconfigdialog.py	2026-08-07 01:45:25.241228485 +0530
+@@ -27,27 +27,23 @@
+ gi.require_version('Gtk', '4.0')
+ gi.require_version('Adw', '1')
+ 
+-from gi.repository import Gtk, Gio, Adw
++from gi.repository import Gtk, Gio, Gdk, GObject, Adw
+ 
+ from sttutils import *
+ from sttshortcutrow import STTShortcutRow
+ from sttshortcutdialog import STTShortcutDialog
+ 
+ from sttcurrentlocale import stt_current_locale
+-from sttvoskmodelmanagers import stt_vosk_online_model_manager
+-from sttwhispermodelmanagers import stt_whisper_online_model_manager
+-from sttonnxasrmodelmanagers import stt_onnxasr_online_model_manager
+-from sttmoonshinemodelmanagers import stt_moonshine_online_model_manager
+-from sttvoskmodel import STTVoskModel
+-from sttwhispermodel import STTWhisperModel
+-from sttonnxasrmodel import STTOnnxAsrModel
+-from sttmoonshinemodel import STTMoonshineModel
+ from sttmodelchooserdialog import STTModelChooserDialog
+ 
+-from sttgstvosk import STTGstVosk
+-from sttgstwhisper import STTGstWhisper
+-from sttgstonnxasr import STTGstOnnxAsr
+-from sttgstmoonshine import STTGstMoonshine
++from sttbackenddeps import (STT_DEFAULT_BACKEND,
++                            stt_backend_component,
++                            stt_backend_display_name,
++                            stt_backend_install_command,
++                            stt_backend_is_available,
++                            stt_backend_missing_dependencies,
++                            stt_backend_model_manager,
++                            stt_invalidate_availability_cache)
+ 
+ LOG_MSG=logging.getLogger()
+ 
+@@ -64,6 +60,11 @@
+     onnxasr_check = Gtk.Template.Child()
+     moonshine_check = Gtk.Template.Child()
+ 
++    vosk_action_row      = Gtk.Template.Child()
++    whisper_action_row   = Gtk.Template.Child()
++    onnxasr_action_row   = Gtk.Template.Child()
++    moonshine_action_row = Gtk.Template.Child()
++
+     tab_stack    = Gtk.Template.Child()
+     tab_switcher = Gtk.Template.Child()
+ 
+@@ -102,18 +103,20 @@
+         self._utterances_dict = {}
+         self._no_model_toast = None
+         self._unsupported_locale_toast = None
++        self._missing_deps_toast = None
+         self._model = None
+         self._suppress_language_cb = False
++        self._suppress_engine_cb = False
+         self._engine = None
+ 
+         self._settings=Gio.Settings.new("org.freedesktop.ibus.engine.stt")
+         self._settings.bind("preload", self.preload_model_switch, "active", Gio.SettingsBindFlags.DEFAULT)
+         self._settings.bind("active-on-start", self.active_on_start_switch, "active", Gio.SettingsBindFlags.DEFAULT)
+ 
+-        stt_vosk_online_model_manager()
+-        stt_whisper_online_model_manager()
+-        stt_onnxasr_online_model_manager()
+-        stt_moonshine_online_model_manager()
++        self._backend_subtitles = {
++            backend: (row.get_subtitle() or "")
++            for backend, row in self._backend_action_rows().items()
++        }
+ 
+         # Load current locale
+         self._current_locale = stt_current_locale()
+@@ -121,25 +124,11 @@
+         self._override_file_changed_id=self._current_locale.connect("override-file-changed", self._override_file_changed_cb)
+         self._override_file_written=False
+ 
+-        backend = self._settings.get_string("backend")
+-        self._suppress_engine_cb = True
+-        if backend == "whisper":
+-            self.whisper_check.set_active(True)
+-        elif backend == "onnxasr":
+-            self.onnxasr_check.set_active(True)
+-        elif backend == "moonshine":
+-            self.moonshine_check.set_active(True)
+-        else:
+-            self.vosk_check.set_active(True)
+-        self._suppress_engine_cb = False
++        self._select_backend_button(self._backend())
+ 
+         self._locale_list = []
+         self._locale_names = Gtk.StringList()
+-        self._populate_locale_list()
+-        self._suppress_language_cb = True
+-        self.language_dropdown.set_model(self._locale_names)
+-        self._suppress_language_cb = False
+-        self._select_current_locale_in_dropdown()
++        self._refresh_locale_dropdown()
+ 
+         if self._current_locale.default_locale:
+             self._suppress_language_cb = True
+@@ -156,17 +145,81 @@
+ 
+         self._update_voice_commands_visibility()
+ 
+-        if self._model is None or not self._model.available():
+-            self._engine_has_no_model()
+-        elif self._valid_formatting_file == False:
+-            self._unsupported_locale()
+-
+         self._toast_action=Gio.SimpleAction.new("manage_model", None)
++        self._install_action=Gio.SimpleAction.new("install_backend", None)
+         action_group=Gio.SimpleActionGroup.new()
+         action_group.insert(self._toast_action)
++        action_group.insert(self._install_action)
+         self.insert_action_group("toast", action_group)
+         self._toast_action.connect("activate",
+                                    self._manage_model_action_activated)
++        self._install_action.connect("activate",
++                                     self._install_action_activated)
++
++        self._update_backend_rows()
++        self._refresh_status_messages()
++        self._backend_was_available = stt_backend_is_available(self._backend())
++        self.connect("notify::is-active", self._window_active_cb)
++
++    def _window_active_cb(self, *_args):
++        if not self.get_property("is-active"):
++            return
++
++        stt_invalidate_availability_cache()
++        available = stt_backend_is_available(self._backend())
++        if available == self._backend_was_available:
++            self._update_backend_rows()
++            return
++
++        self._backend_was_available = available
++        self._reload_backend()
++
++    def _backend(self):
++        backend = self._settings.get_string("backend")
++        if backend in ("", None):
++            return STT_DEFAULT_BACKEND
++        return backend
++
++    def _backend_check_buttons(self):
++        return {
++            "vosk":      self.vosk_check,
++            "whisper":   self.whisper_check,
++            "onnxasr":   self.onnxasr_check,
++            "moonshine": self.moonshine_check,
++        }
++
++    def _backend_action_rows(self):
++        return {
++            "vosk":      self.vosk_action_row,
++            "whisper":   self.whisper_action_row,
++            "onnxasr":   self.onnxasr_action_row,
++            "moonshine": self.moonshine_action_row,
++        }
++
++    def _backend_for_check_button(self, button):
++        for backend, check in self._backend_check_buttons().items():
++            if check == button:
++                return backend
++        return None
++
++    def _select_backend_button(self, backend):
++        button = self._backend_check_buttons().get(backend)
++        if button is None:
++            button = self.vosk_check
++
++        self._suppress_engine_cb = True
++        button.set_active(True)
++        self._suppress_engine_cb = False
++
++    def _update_backend_rows(self):
++        for backend, row in self._backend_action_rows().items():
++            subtitle = self._backend_subtitles.get(backend, "")
++            if stt_backend_is_available(backend):
++                row.set_subtitle(subtitle)
++            elif subtitle:
++                row.set_subtitle(_("%s · Not installed") % subtitle)
++            else:
++                row.set_subtitle(_("Not installed"))
+ 
+ 
+     def _create_engine(self):
+@@ -180,15 +233,23 @@
+             self._engine.destroy()
+             self._engine = None
+ 
+-        backend = self._settings.get_string("backend")
+-        if backend == "whisper":
+-            self._engine = STTGstWhisper(current_locale=self._current_locale)
+-        elif backend == "onnxasr":
+-            self._engine = STTGstOnnxAsr(current_locale=self._current_locale)
+-        elif backend == "moonshine":
+-            self._engine = STTGstMoonshine(current_locale=self._current_locale)
+-        else:
+-            self._engine = STTGstVosk(current_locale=self._current_locale)
++        backend = self._backend()
++        if not stt_backend_is_available(backend):
++            LOG_MSG.warning("backend %s is missing its dependencies, "
++                            "no engine created", backend)
++            return
++
++        engine_class = stt_backend_component(backend, "engine")
++        if engine_class is None:
++            return
++
++        try:
++            self._engine = engine_class(current_locale=self._current_locale)
++        except Exception as error:
++            LOG_MSG.error("cannot create engine for backend %s (%s)",
++                          backend, error)
++            self._engine = None
++            return
+ 
+         self._engine.connect("model-changed", self._engine_model_changed_cb)
+         self._engine.preload()
+@@ -204,11 +265,13 @@
+                 and self._current_locale.locale not in self._locale_list):
+             self._append_locale_option(self._current_locale.locale)
+ 
+-        backend = self._settings.get_string("backend")
++        backend = self._backend()
+         if backend == "moonshine":
+-            supported = stt_moonshine_online_model_manager().supported_locales()
++            manager = stt_backend_model_manager("moonshine")
+         else:
+-            supported = stt_vosk_online_model_manager().supported_locales()
++            manager = stt_backend_model_manager("vosk")
++
++        supported = manager.supported_locales() if manager is not None else []
+ 
+         _EXCLUDED = {"multilingual"}
+ 
+@@ -218,6 +281,16 @@
+             if loc not in self._locale_list:
+                 self._append_locale_option(loc)
+ 
++    def _refresh_locale_dropdown(self):
++        # Rebuild the language list and hand it to the dropdown.
++        self._populate_locale_list()
++
++        self._suppress_language_cb = True
++        self.language_dropdown.set_model(self._locale_names)
++        self._suppress_language_cb = False
++
++        self._select_current_locale_in_dropdown()
++
+     def _append_locale_option(self, locale_str):
+         if locale_str in (None, "", "None", "multilingual"):
+             return
+@@ -253,23 +326,36 @@
+                 self._model.disconnect_by_func(self._model_changed_cb)
+             except TypeError:
+                 pass
++            self._model = None
+ 
+-        backend = self._settings.get_string("backend")
++        backend = self._backend()
+         locale_str = self._current_locale.locale
+ 
+-        if backend == "whisper":
+-            self._model = STTWhisperModel(locale_str=locale_str)
+-        elif backend == "onnxasr":
+-            self._model = STTOnnxAsrModel(locale_str=locale_str)
+-        elif backend == "moonshine":
+-            self._model = STTMoonshineModel(locale_str=locale_str)
+-        else:
+-            self._model = STTVoskModel(locale_str=locale_str)
++        if stt_backend_is_available(backend):
++            model_class = stt_backend_component(backend, "model")
++            if model_class is not None:
++                try:
++                    self._model = model_class(locale_str=locale_str)
++                except Exception as error:
++                    LOG_MSG.error("cannot create model for backend %s (%s)",
++                                  backend, error)
++
++        if self._model is not None:
++            self._model.connect("changed", self._model_changed_cb)
+ 
+-        self._model.connect("changed", self._model_changed_cb)
+         self._update_model_info()
+ 
+     def _update_model_info(self):
++        backend = self._backend()
++
++        if stt_backend_missing_dependencies(backend):
++            self.model_info_row.set_title(
++                _("%s is not installed") % stt_backend_display_name(backend))
++            self.model_info_row.set_subtitle(
++                stt_backend_install_command(backend))
++            self.change_model_button.set_label(_("Install…"))
++            return
++
+         if self._model is None or not self._model.available():
+             self.model_info_row.set_title(_("No model downloaded"))
+             self.model_info_row.set_subtitle(
+@@ -288,16 +374,8 @@
+                 else _("Installed manually"))
+             return
+ 
+-        backend = self._settings.get_string("backend")
+-        if backend == "whisper":
+-            manager = stt_whisper_online_model_manager()
+-        elif backend == "onnxasr":
+-            manager = stt_onnxasr_online_model_manager()
+-        elif backend == "moonshine":
+-            manager = stt_moonshine_online_model_manager()
+-        else:
+-            manager = stt_vosk_online_model_manager()
+-        desc = manager.get_model_description(model_name)
++        manager = stt_backend_model_manager(backend)
++        desc = manager.get_model_description(model_name) if manager else None
+ 
+         self.model_info_row.set_title(model_name)
+ 
+@@ -347,6 +425,9 @@
+                     self.model_info_row.set_subtitle(size)
+ 
+     def _auto_prompt_model_download(self):
++        if not stt_backend_is_available(self._backend()):
++            return
++
+         if self._model is not None and not self._model.available():
+             dialog = STTModelChooserDialog(model=self._model)
+             dialog.set_transient_for(self)
+@@ -355,9 +436,134 @@
+     def _model_changed_cb(self, model):
+         self._update_model_info()
+ 
++    def _copy_to_clipboard(self, text):
++        clipboard = self.get_clipboard()
++        if clipboard is None:
++            return
++
++        try:
++            clipboard.set_content(
++                Gdk.ContentProvider.new_for_value(GObject.Value(str, text)))
++        except Exception as error:
++            LOG_MSG.warning("cannot copy to clipboard (%s)", error)
++
++    def _install_dialog_body(self, backend, missing):
++        modules = "\n".join("    • %s" % dep.requirement for dep in missing)
++        return _("The %(backend)s backend needs python modules that are not "
++                 "installed on this system:\n\n"
++                 "%(modules)s\n\n"
++                 "Install them in a terminal with the following command, "
++                 "then press “Check Again”:\n\n"
++                 "    %(command)s") % {
++                     "backend": stt_backend_display_name(backend),
++                     "modules": modules,
++                     "command": stt_backend_install_command(backend)}
++
++    def _present_install_dialog(self, backend):
++        missing = stt_backend_missing_dependencies(backend, refresh=True)
++        if not missing:
++            return False
++
++        heading = _("%s Is Not Installed") % stt_backend_display_name(backend)
++        body = self._install_dialog_body(backend, missing)
++        command = stt_backend_install_command(backend)
++
++        # Adw.AlertDialog needs libadwaita 1.5, fall back on Adw.MessageDialog.
++        if hasattr(Adw, "AlertDialog"):
++            dialog = Adw.AlertDialog(heading=heading, body=body)
++            self._setup_install_dialog(dialog, backend, command)
++            dialog.present(self)
++        else:
++            dialog = Adw.MessageDialog(transient_for=self, modal=True,
++                                       heading=heading, body=body)
++            self._setup_install_dialog(dialog, backend, command)
++            dialog.present()
++
++        return True
++
++    def _setup_install_dialog(self, dialog, backend, command):
++        dialog.add_response("close", _("Close"))
++        dialog.add_response("copy", _("Copy Command"))
++        dialog.add_response("recheck", _("Check Again"))
++        dialog.set_response_appearance("recheck",
++                                       Adw.ResponseAppearance.SUGGESTED)
++        dialog.set_default_response("recheck")
++        dialog.set_close_response("close")
++        dialog.connect("response", self._install_dialog_response_cb,
++                       backend, command)
++
++    def _install_dialog_response_cb(self, dialog, response, backend, command):
++        if response == "copy":
++            self._copy_to_clipboard(command)
++            self.toast_overlay.add_toast(
++                Adw.Toast(title=_("Command copied to the clipboard"),
++                          timeout=3))
++            return
++
++        if response != "recheck":
++            return
++
++        if not stt_backend_is_available(backend, refresh=True):
++            self.toast_overlay.add_toast(
++                Adw.Toast(title=_("%s is still not installed")
++                          % stt_backend_display_name(backend), timeout=5))
++            return
++
++        self._update_backend_rows()
++        if backend != self._backend():
++            self._apply_backend(backend)
++        else:
++            self._reload_backend()
++
++    def _install_action_activated(self, _action, _param):
++        self._present_install_dialog(self._backend())
++
++    def _backend_not_installed(self):
++        if self._missing_deps_toast != None:
++            return
++
++        if self._no_model_toast != None:
++            self._no_model_toast.dismiss()
++            self._no_model_toast = None
++        if self._unsupported_locale_toast != None:
++            self._unsupported_locale_toast.dismiss()
++            self._unsupported_locale_toast = None
++
++        self._missing_deps_toast = Adw.Toast(
++            title=_("%s is not installed")
++                  % stt_backend_display_name(self._backend()),
++            timeout=0,
++            button_label=_("How to Install"),
++            action_name="toast.install_backend")
++        self._missing_deps_toast.connect("dismissed", self._toast_dismissed)
++        self.toast_overlay.add_toast(self._missing_deps_toast)
++
++    def _refresh_status_messages(self):
++        if not stt_backend_is_available(self._backend()):
++            self._backend_not_installed()
++            return
++
++        if self._missing_deps_toast != None:
++            self._missing_deps_toast.dismiss()
++            self._missing_deps_toast = None
++
++        if (self._model is None or not self._model.available()
++                or self._engine is None or not self._engine.has_model()):
++            self._engine_has_no_model()
++            return
++
++        if self._no_model_toast != None:
++            self._no_model_toast.dismiss()
++            self._no_model_toast = None
++
++        if self._valid_formatting_file == False:
++            self._unsupported_locale()
++        elif self._unsupported_locale_toast != None:
++            self._unsupported_locale_toast.dismiss()
++            self._unsupported_locale_toast = None
+ 
+     def _update_voice_commands_visibility(self):
+-        is_vosk = (self._settings.get_string("backend") == "vosk")
++        is_vosk = (self._backend() == "vosk")
+         self.vc_whisper_warning.set_visible(not is_vosk)
+         self.voice_commands_group.set_visible(is_vosk)
+ 
+@@ -372,40 +578,40 @@
+         if getattr(self, '_suppress_engine_cb', False):
+             return
+ 
+-        if button == self.vosk_check:
+-            backend = "vosk"
+-        elif button == self.moonshine_check:
+-            backend = "moonshine"
+-        elif button == self.whisper_check:
+-            backend = "whisper"
+-        else:
+-            backend = "onnxasr"
++        backend = self._backend_for_check_button(button)
++        if backend is None:
++            return
+ 
+-        current = self._settings.get_string("backend")
++        current = self._backend()
+         if backend == current:
+             return
+ 
+-        self._settings.set_string("backend", backend)
+-        old_locale = self._current_locale.locale
+-        self._populate_locale_list()
+-        self._suppress_language_cb = True
+-        self.language_dropdown.set_model(self._locale_names)
+-        self._suppress_language_cb = False
++        if not stt_backend_is_available(backend, refresh=True):
++            self._select_backend_button(current)
++            self._update_backend_rows()
++            self._present_install_dialog(backend)
++            return
+ 
+-        if old_locale in self._locale_list:
+-            self._suppress_language_cb = True
+-            self.language_dropdown.set_selected(
+-                self._locale_list.index(old_locale))
+-            self._suppress_language_cb = False
++        self._apply_backend(backend)
+ 
+-        self._init_model()
++    def _apply_backend(self, backend):
++        self._settings.set_string("backend", backend)
++        self._select_backend_button(backend)
++        self._reload_backend(prompt_download=True)
+ 
++    def _reload_backend(self, prompt_download=False):
++        self._refresh_locale_dropdown()
++        self._init_model()
+         self._create_engine()
+         self._update_voice_commands_visibility()
+         self._empty_shortcut_page()
+         self._load_utterances()
++        self._update_backend_rows()
++        self._refresh_status_messages()
+ 
+-        if self._model is not None and not self._model.available():
++        if (prompt_download
++                and self._model is not None
++                and not self._model.available()):
+             self._auto_prompt_model_download()
+ 
+     @Gtk.Template.Callback()
+@@ -441,6 +647,14 @@
+ 
+     @Gtk.Template.Callback()
+     def change_model_clicked_cb(self, *_args):
++        backend = self._backend()
++        if not stt_backend_is_available(backend, refresh=True):
++            self._present_install_dialog(backend)
++            return
++
++        if self._model is None:
++            self._reload_backend()
++
+         if self._model != None:
+             dialog = STTModelChooserDialog(model=self._model)
+             dialog.set_transient_for(self)
+@@ -458,11 +672,7 @@
+         self._select_current_locale_in_dropdown()
+ 
+         if self._current_locale.locale not in self._locale_list:
+-            self._populate_locale_list()
+-            self._suppress_language_cb = True
+-            self.language_dropdown.set_model(self._locale_names)
+-            self._suppress_language_cb = False
+-            self._select_current_locale_in_dropdown()
++            self._refresh_locale_dropdown()
+ 
+         self._init_model()
+         self._load_current_locale()
+@@ -541,20 +751,7 @@
+     def _load_current_locale(self):
+         self._empty_shortcut_page()
+         self._load_utterances()
+-
+-        if not self._engine.has_model():
+-            self._engine_has_no_model()
+-            return
+-
+-        if self._no_model_toast != None:
+-            self._no_model_toast.dismiss()
+-            self._no_model_toast = None
+-
+-        if not self._valid_formatting_file:
+-            self._unsupported_locale()
+-        elif self._unsupported_locale_toast != None:
+-            self._unsupported_locale_toast.dismiss()
+-            self._unsupported_locale_toast = None
++        self._refresh_status_messages()
+ 
+     def _apply_change(self):
+         LOG_MSG.debug("override file being written")
+@@ -608,6 +805,10 @@
+         self._present_shortcut_dialog(row)
+ 
+     def _present_shortcut_dialog(self, row):
++        if self._engine is None:
++            LOG_MSG.warning("no engine available, cannot edit shortcuts")
++            return
++
+         dialog = STTShortcutDialog(
+             row=row, engine=self._engine, transient_for=self)
+         dialog.connect("response", self._shortcut_dialog_response_cb)
+@@ -781,7 +982,9 @@
+         self._auto_prompt_model_download()
+ 
+     def _toast_dismissed(self, toast):
+-        if toast == self._no_model_toast:
++        if toast == self._missing_deps_toast:
++            self._missing_deps_toast=None
++        elif toast == self._no_model_toast:
+             self._no_model_toast=None
+ 
+             # Display the other message if needed
+@@ -792,11 +995,14 @@
+ 
+     def _unsupported_locale(self):
+         # Careful: we can have no formatting file but an overriding one !!
++        if self._missing_deps_toast != None:
++            return
++
+         if self._no_model_toast != None:
+             return
+ 
+         # Formatting files only exist for vosk; other engines have no files to find
+-        if self._settings.get_string("backend") != "vosk":
++        if self._backend() != "vosk":
+             if self._unsupported_locale_toast is not None:
+                 self._unsupported_locale_toast.dismiss()
+                 self._unsupported_locale_toast = None
+@@ -816,6 +1022,9 @@
+         self.toast_overlay.add_toast(self._unsupported_locale_toast)
+ 
+     def _engine_has_no_model(self):
++        if self._missing_deps_toast != None:
++            return
++
+         if self._no_model_toast != None:
+             return
+ 
+diff -urN IBus-Speech-To-Text-1.0.0.orig/engine/sttgstfactory.py IBus-Speech-To-Text-1.0.0/engine/sttgstfactory.py
+--- IBus-Speech-To-Text-1.0.0.orig/engine/sttgstfactory.py	2026-07-14 03:00:00.000000000 +0530
++++ IBus-Speech-To-Text-1.0.0/engine/sttgstfactory.py	2026-08-07 01:46:01.026792604 +0530
+@@ -29,6 +29,10 @@
+ from sttgstonnxasr import STTGstOnnxAsr
+ from sttgstmoonshine import STTGstMoonshine
+ 
++from sttbackenddeps import (stt_backend_display_name,
++                            stt_backend_install_command,
++                            stt_backend_is_available)
++
+ LOG_MSG=logging.getLogger()
+ 
+ class STTGstFactory(GObject.GObject):
+@@ -45,11 +49,21 @@
+         self.__update_preloaded_engine()
+ 
+     def new_engine(self):
+-        engine=None if self._current_engine is None else self._current_engine()
++        engine = None if self._current_engine is None else self._current_engine()
++        if engine is not None and engine.pipeline is None:
++            LOG_MSG.debug("cached engine was already destroyed, creating a new one")
++            engine = None
+         if engine is None:
+             LOG_MSG.debug("new engine")
+             # Check backend setting
+             backend = self.__settings.get_string("backend")
++            if not stt_backend_is_available(backend):
++                LOG_MSG.error("%s backend is selected but not installed. "
++                              "Install it with: %s",
++                              stt_backend_display_name(backend),
++                              stt_backend_install_command(backend))
++                LOG_MSG.error("falling back to the Vosk backend")
++                backend = "vosk"
+             if backend == "whisper":
+                 LOG_MSG.info("Using Whisper backend")
+                 engine=STTGstWhisper()
+diff -urN IBus-Speech-To-Text-1.0.0.orig/engine/sttgstmoonshine.py IBus-Speech-To-Text-1.0.0/engine/sttgstmoonshine.py
+--- IBus-Speech-To-Text-1.0.0.orig/engine/sttgstmoonshine.py	2026-07-14 03:00:00.000000000 +0530
++++ IBus-Speech-To-Text-1.0.0/engine/sttgstmoonshine.py	2026-08-07 01:38:55.356082244 +0530
+@@ -13,38 +13,39 @@
+ 
+ SAMPLE_RATE = 16000
+ 
+-try:
+-    from moonshine_voice import (
+-        Transcriber,
+-        TranscriptEventListener,
+-        ModelArch,
+-    )
+-    MOONSHINE_AVAILABLE = True
+-except ImportError:
+-    LOG_MSG.warning("moonshine_voice not available. Install with: pip install moonshine-voice")
+-    MOONSHINE_AVAILABLE = False
+-    TranscriptEventListener = object
++_LISTENER_CLASS = None
+ 
+-class _LineListener(TranscriptEventListener):
++def _line_listener_class():
++    #Build the listener subclass on first use
++    global _LISTENER_CLASS
++    if _LISTENER_CLASS is not None:
++        return _LISTENER_CLASS
+ 
+-    def __init__(self, engine):
+-        if MOONSHINE_AVAILABLE:
++    from moonshine_voice import TranscriptEventListener
++
++    class _LineListener(TranscriptEventListener):
++
++        def __init__(self, engine):
+             super().__init__()
+-        self._engine = engine
+-        self._emitted_lines = []
++            self._engine = engine
++            self._emitted_lines = []
+ 
+-    def reset(self):
+-        self._emitted_lines.clear()
++        def reset(self):
++            self._emitted_lines.clear()
++
++        def on_line_completed(self, event):
++            line = event.line
++            if any(line is seen for seen in self._emitted_lines):
++                return
++            self._emitted_lines.append(line)
++            text = (line.text or "").strip()
++            if text:
++                LOG_MSG.info("Moonshine transcription result: '%s'", text)
++                GLib.idle_add(self._engine._emit_text, text)
++
++    _LISTENER_CLASS = _LineListener
++    return _LISTENER_CLASS
+ 
+-    def on_line_completed(self, event):
+-        line = event.line
+-        if any(line is seen for seen in self._emitted_lines):
+-            return
+-        self._emitted_lines.append(line)
+-        text = (line.text or "").strip()
+-        if text:
+-            LOG_MSG.info("Moonshine transcription result: '%s'", text)
+-            GLib.idle_add(self._engine._emit_text, text)
+ 
+ class STTGstMoonshine(STTGstBase):
+     __gtype_name__ = 'STTGstMoonshine'
+@@ -93,6 +94,7 @@
+         self._tx_lock = threading.Lock()
+         self._session_active = False
+         self._stopping = False
++        self._destroyed = False
+ 
+         self._process_queue = queue.Queue()
+         self._stop_processing = False
+@@ -103,11 +105,11 @@
+ 
+     def __del__(self):
+         try:
+-            if LOG_MSG is not None:
+-                LOG_MSG.info("Moonshine __del__")
+             self._stop_processing = True
+-            if self._process_thread is not None:
+-                self._process_thread.join(timeout=2.0)
++            thread = self._process_thread
++            self._process_thread = None
++            if thread is not None:
++                thread.join(timeout=2.0)
+         except Exception:
+             pass
+         try:
+@@ -116,24 +118,36 @@
+             pass
+ 
+     def destroy(self):
++        if self._destroyed:
++            LOG_MSG.debug("Moonshine engine already destroyed")
++            return
++        self._destroyed = True
++
+         self._stop_processing = True
+-        if self._process_thread is not None:
+-            self._process_thread.join(timeout=2.0)
+ 
+-        self._current_locale.disconnect(self._locale_id)
+-        self._locale_id = 0
++        thread = self._process_thread
++        self._process_thread = None
++        if thread is not None:
++            thread.join(timeout=2.0)
++            if thread.is_alive():
++                LOG_MSG.warning("Moonshine worker thread did not stop in time")
++
++        if self._locale_id != 0:
++            self._current_locale.disconnect(self._locale_id)
++            self._locale_id = 0
+ 
+-        if self._model_id != 0:
++        if self._model_id != 0 and self._model is not None:
+             self._model.disconnect(self._model_id)
+             self._model_id = 0
+ 
+         with self._tx_lock:
+             self._teardown_transcriber()
+ 
+-        if self._appsink is not None and getattr(self, "_new_sample_id", 0) !=0:
+-            self._appsink.disconnect(self._new_sample_id)
+-            self._new_sample_id = 0
++        if self._appsink is not None and self._on_new_sample_id != 0:
++            self._appsink.disconnect(self._on_new_sample_id)
++            self._on_new_sample_id = 0
+         self._appsink = None
++
+         LOG_MSG.info("Moonshine.destroy() called")
+         super().destroy()
+ 
+@@ -150,14 +164,18 @@
+         self._listener = None
+ 
+     def _load_moonshine_model(self, model_path, model_arch):
+-        if not MOONSHINE_AVAILABLE:
+-            LOG_MSG.error("moonshine_voice not available")
++        try:
++            from moonshine_voice import ModelArch, Transcriber
++        except Exception as e:
++            LOG_MSG.error("moonshine_voice not available (%s). "
++                          "Install with: pip install moonshine-voice", e)
+             return False
++
+         try:
+-            arch = model_arch if model_arch is not None else ModelArch.BASE
++            arch = (ModelArch(model_arch) if model_arch is not None else ModelArch.BASE)
+             LOG_MSG.info("Loading Moonshine model: %s (arch=%s)", model_path, arch)
+             transcriber = Transcriber(model_path=model_path, model_arch=arch)
+-            listener = _LineListener(self)
++            listener = _line_listener_class()(self)
+             transcriber.add_listener(listener)
+             with self._tx_lock:
+                 self._teardown_transcriber()
+diff -urN IBus-Speech-To-Text-1.0.0.orig/engine/sttmoonshinemodelmanagers.py IBus-Speech-To-Text-1.0.0/engine/sttmoonshinemodelmanagers.py
+--- IBus-Speech-To-Text-1.0.0.orig/engine/sttmoonshinemodelmanagers.py	2026-07-14 03:00:00.000000000 +0530
++++ IBus-Speech-To-Text-1.0.0/engine/sttmoonshinemodelmanagers.py	2026-08-07 01:46:55.086648952 +0530
+@@ -1,5 +1,9 @@
+ import os
++import sys
++import importlib.machinery
+ import logging
++import importlib
++import importlib.util
+ import threading
+ 
+ from pathlib import Path
+@@ -9,23 +13,14 @@
+ 
+ LOG_MSG = logging.getLogger()
+ 
+-
+-try:
+-    from moonshine_voice import ModelArch
+-    from moonshine_voice.download import (
+-        MODEL_INFO,
+-        find_model_info,
+-        get_components_for_model_info,
+-        get_model_for_language,
+-    )
+-    from moonshine_voice.download_file import get_cache_dir
+-    MOONSHINE_AVAILABLE = True
+-except Exception as e:
+-    LOG_MSG.warning("moonshine_voice model catalog unavailable (%s). "
+-                    "Install/upgrade with: pip install -U moonshine-voice", e)
+-    MOONSHINE_AVAILABLE = False
+-    MODEL_INFO = {}
+-    ModelArch = None
++_ARCH_NAMES = {
++    0: "tiny",
++    1: "base",
++    2: "tiny-streaming",
++    3: "base-streaming",
++    4: "small-streaming",
++    5: "medium-streaming",
++}
+ 
+ _ARCH_SIZES = {
+     "tiny":             "~50 MB",
+@@ -46,15 +41,9 @@
+ }
+ 
+ def _arch_to_string(model_arch):
+-    return {
+-        0: "tiny",
+-        1: "base",
+-        2: "tiny-streaming",
+-        3: "base-streaming",
+-        4: "small-streaming",
+-        5: "medium-streaming",
+-    }.get(int(model_arch), "base")
+-
++    if model_arch is None:
++        return "base"
++    return _ARCH_NAMES.get(int(model_arch), "base")
+ 
+ class STTDownloadState(float, Enum):
+     STOPPED = -1.0
+@@ -67,26 +56,116 @@
+         return None
+     return locale_str[0:2].lower()
+ 
+-def _all_model_infos():
+-    for lang, entry in MODEL_INFO.items():
+-        for model in entry.get("models", []):
+-            yield model["model_name"], lang, model
+-
+-def _expected_model_path(model_info):
+-    cache_dir = get_cache_dir()
+-    folder = model_info["download_url"].replace("https://", "")
+-    return Path(cache_dir, folder)
+-
+-def _model_present(model_info):
+-    root = _expected_model_path(model_info)
+-    if not root.is_dir():
++def moonshine_installed():
++    try:
++        if importlib.machinery.PathFinder.find_spec("moonshine_voice", sys.path):
++            return True
++        importlib.invalidate_caches()
++        return bool(importlib.machinery.PathFinder.find_spec("moonshine_voice",
++                                                             sys.path))
++    except (ImportError, ValueError, TypeError, AttributeError) as error:
++        LOG_MSG.debug("cannot look up moonshine_voice (%s)", error)
+         return False
++
++def _moonshine_cache_dir():
++    #Same directory as moonshine_voice.download_file.get_cache_dir(), computed without importing the package
++    override = os.environ.get("MOONSHINE_VOICE_CACHE")
++    if override:
++        return Path(override)
++
++    xdg = os.environ.get("XDG_CACHE_HOME")
++    if xdg:
++        return Path(xdg) / "moonshine_voice"
++
++    return Path.home() / ".cache" / "moonshine_voice"
++
++_CATALOG = None          # {model_name: {...}}
++_CATALOG_LOCALES = None  # {lang: [model_name, ...]}
++
++def _build_catalog():
++    from moonshine_voice.download import find_model_info, supported_languages
++
++    models = {}
++    locales = {}
++
++    for lang in supported_languages():
++        for arch_value in sorted(_ARCH_NAMES):
++            try:
++                info = find_model_info(lang, arch_value)
++            except Exception:
++                # This language simply has no model for that architecture.
++                continue
++
++            arch_name = _ARCH_NAMES[arch_value]
++            name = "%s-%s" % (arch_name, lang)
++            models[name] = {
++                "name": name,
++                "lang": lang,
++                "arch": arch_value,
++                "arch_name": arch_name,
++                "download_url": info.get("download_url", ""),
++            }
++            locales.setdefault(lang, []).append(name)
++
++    return models, locales
++
++def _catalog():
++    global _CATALOG, _CATALOG_LOCALES
++
++    if not moonshine_installed():
++        _CATALOG = None
++        _CATALOG_LOCALES = None
++        return {}, {}
++
++    if _CATALOG is not None:
++        return _CATALOG, _CATALOG_LOCALES
++
++    try:
++        models, locales = _build_catalog()
++    except Exception as error:
++        LOG_MSG.warning("moonshine_voice model catalog unavailable (%s). "
++                        "Install/upgrade with: pip install -U moonshine-voice",
++                        error)
++        return {}, {}
++
++    if not models:
++        LOG_MSG.warning("moonshine_voice returned an empty model catalog")
++        return {}, {}
++
++    _CATALOG = models
++    _CATALOG_LOCALES = locales
++    LOG_MSG.debug("moonshine catalog built (%d models, %d languages)",
++                  len(models), len(locales))
++    return _CATALOG, _CATALOG_LOCALES
++
++def _model_root(entry):
++    url = entry.get("download_url") or ""
++    if not url:
++        return None
++    return Path(_moonshine_cache_dir(), url.replace("https://", ""))
++
++def _model_present(entry):
++    root = _model_root(entry)
++    if root is None or not root.is_dir():
++        return False
++
++    components = None
+     try:
+-        components = get_components_for_model_info(model_info)
+-    except Exception:
+-        components = ["tokenizer.bin"]
+-    return all((root / component).is_file() for component in components)
++        from moonshine_voice.download import (find_model_info,
++                                              get_components_for_model_info)
++        components = get_components_for_model_info(
++            find_model_info(entry["lang"], entry["arch"]))
++    except Exception as error:
++        LOG_MSG.debug("cannot list components of %s (%s)",
++                      entry["name"], error)
+ 
++    if components:
++        return all((root / component).is_file() for component in components)
++
++    try:
++        return any(root.iterdir())
++    except OSError:
++        return False
+ 
+ class STTMoonshineModelDescription(GObject.Object):
+     __gtype_name__ = "STTMoonshineModelDescription"
+@@ -106,21 +185,32 @@
+         self.quality = init_model.quality if init_model is not None else ""
+ 
+         self._operation = None
++        self._downloaded_path = None
+         self.download_progress = STTDownloadState.STOPPED
+ 
+     def _download_finished(self):
+         self._operation = None
+         self.download_progress = STTDownloadState.STOPPED
+-        info = find_model_info(self.lang, self.arch)
+-        if _model_present(info):
+-            path = str(_expected_model_path(info))
++
++        path = self._downloaded_path
++        self._downloaded_path = None
++
++        if path is None:
++            entry = _catalog()[0].get(self.name)
++            if entry is not None and _model_present(entry):
++                path = str(_model_root(entry))
++
++        if path is not None:
+             self.paths = [path]
+-            stt_moonshine_local_model_manager()._notify_added(self.name, path, self.lang)
++            stt_moonshine_local_model_manager()._notify_added(
++                self.name, path, self.lang)
+         return False
+ 
+     def _download_thread(self, cancelled):
+         try:
+-            get_model_for_language(self.lang, self.arch)
++            from moonshine_voice import get_model_for_language
++            path, _arch = get_model_for_language(self.lang, self.arch)
++            self._downloaded_path = str(path)
+         except Exception as e:
+             LOG_MSG.error("Moonshine download failed (%s): %s", self.name, e)
+         if not cancelled.is_set():
+@@ -131,7 +221,7 @@
+     def start_downloading(self):
+         if self._operation is not None:
+             return
+-        if not MOONSHINE_AVAILABLE:
++        if not moonshine_installed():
+             LOG_MSG.error("cannot download, moonshine_voice not installed")
+             return
+ 
+@@ -186,16 +276,18 @@
+         super().__init__()
+         self._present = {}
+         self._custom_paths = {}
+-        self._scan_present_models()
++        self._scanned_models = 0
+ 
+     def _scan_present_models(self):
+-        if not MOONSHINE_AVAILABLE:
++        catalog, _locales = _catalog()
++        if not catalog or self._scanned_models == len(catalog):
+             return
+-        for model_name, lang, info in _all_model_infos():
+-            info = dict(info, language=lang)
+-            if _model_present(info):
+-                self._present[model_name] = str(_expected_model_path(info))
+-                LOG_MSG.debug("moonshine model present on disk (%s)", model_name)
++
++        self._scanned_models = len(catalog)
++        for name, entry in catalog.items():
++            if _model_present(entry):
++                self._present[name] = str(_model_root(entry))
++                LOG_MSG.debug("moonshine model present on disk (%s)", name)
+ 
+     def _notify_added(self, model_name, path, lang=None):
+         self._present[model_name] = path
+@@ -206,44 +298,38 @@
+         self.emit("removed", model_name, path)
+ 
+     def path_available(self, model_path):
+-        return model_path in self._present.values() or model_path in self._custom_paths
++        self._scan_present_models()
++        return (model_path in self._present.values()
++                or model_path in self._custom_paths)
+ 
+     def get_best_path_for_model(self, model_name):
+         if model_name is None:
+             return None
++        self._scan_present_models()
+         return self._present.get(model_name, None)
+ 
+     def get_arch_for_model(self, model_name):
+-        if not MOONSHINE_AVAILABLE:
+-            return None
+-        for name, lang, info in _all_model_infos():
+-            if name == model_name:
+-                return info["model_arch"]
+-        return None
++        entry = _catalog()[0].get(model_name)
++        return entry["arch"] if entry is not None else None
+ 
+     def get_lang_for_model(self, model_name):
+-        if not MOONSHINE_AVAILABLE:
+-            return None
+-        for name, lang, info in _all_model_infos():
+-            if name == model_name:
+-                return lang
+-        return None
++        entry = _catalog()[0].get(model_name)
++        return entry["lang"] if entry is not None else None
+ 
+     @staticmethod
+     def _infer_arch_for_folder(model_path):
+         root = Path(model_path)
++        name = root.name.lower()
++
+         if (root / "streaming_config.json").is_file():
+-            name = root.name.lower()
+-            for token, arch in (("medium", ModelArch.MEDIUM_STREAMING),
+-                                ("small", ModelArch.SMALL_STREAMING),
+-                                ("base", ModelArch.BASE_STREAMING),
+-                                ("tiny", ModelArch.TINY_STREAMING)):
++            for token, arch in (("medium", 5), ("small", 4),
++                                ("base", 3), ("tiny", 2)):
+                 if token in name:
+                     return arch
+-            return ModelArch.SMALL_STREAMING
+-        if "tiny" in root.name.lower():
+-            return ModelArch.TINY
+-        return ModelArch.BASE
++            return 4
++        if "tiny" in name:
++            return 0
++        return 1
+ 
+     def register_custom_model_path(self, model_path, locale_str):
+         self._custom_paths[model_path] = locale_str
+@@ -252,7 +338,8 @@
+         self._custom_paths.pop(model_path, None)
+ 
+     def custom_path_available(self, model_path):
+-        return Path(model_path).is_dir() and (Path(model_path) / "tokenizer.bin").is_file()
++        root = Path(model_path)
++        return root.is_dir() and (root / "tokenizer.bin").is_file()
+ 
+ 
+ _GLOBAL_LOCAL_MANAGER = None
+@@ -276,35 +363,43 @@
+         super().__init__()
+         self._models = {}
+         self._locales_dict = {}
+-        self._build_catalog()
+ 
+         local = stt_moonshine_local_model_manager()
+         local.connect("added", self._model_path_added_cb)
+         local.connect("removed", self._model_path_removed_cb)
+ 
+-    def _build_catalog(self):
+-        if not MOONSHINE_AVAILABLE:
++    def _ensure_catalog(self):
++        catalog, locales = _catalog()
++        if not catalog:
++            self._models = {}
++            self._locales_dict = {}
++            return
++        if len(self._models) == len(catalog):
+             return
+-        for model_name, lang, info in _all_model_infos():
+-            arch = info["model_arch"]
+-            arch_str = _arch_to_string(arch)
++
++        local = stt_moonshine_local_model_manager()
++        self._models = {}
++        self._locales_dict = {}
++
++        for name, entry in catalog.items():
++            arch_name = entry["arch_name"]
+ 
+             desc = STTMoonshineModelDescription()
+-            desc.name = model_name
+-            desc.lang = lang
+-            desc.locale = lang
+-            desc.arch = arch
+-            desc.type = arch_str
+-            desc.url = info["download_url"]
+-            desc.size = _ARCH_SIZES.get(arch_str, "")
+-            desc.quality = _ARCH_QUALITY.get(arch_str, "")
++            desc.name = name
++            desc.lang = entry["lang"]
++            desc.locale = entry["lang"]
++            desc.arch = entry["arch"]
++            desc.type = arch_name
++            desc.url = entry["download_url"]
++            desc.size = _ARCH_SIZES.get(arch_name, "")
++            desc.quality = _ARCH_QUALITY.get(arch_name, "")
+ 
+-            path = stt_moonshine_local_model_manager().get_best_path_for_model(model_name)
++            path = local.get_best_path_for_model(name)
+             if path is not None:
+                 desc.paths = [path]
+ 
+-            self._models[model_name] = desc
+-            self._locales_dict.setdefault(lang, []).append(desc)
++            self._models[name] = desc
++            self._locales_dict.setdefault(entry["lang"], []).append(desc)
+ 
+     def _model_path_added_cb(self, manager, model_name, model_path):
+         desc = self._models.get(model_name, None)
+@@ -322,15 +417,16 @@
+         self.emit("changed", desc)
+ 
+     def get_model_description(self, model_name):
++        self._ensure_catalog()
+         return self._models.get(model_name, None)
+ 
+     def get_models_for_locale(self, locale_str):
++        self._ensure_catalog()
+         lang = _lang_of_locale(locale_str)
+         return list(self._locales_dict.get(lang, []))
+ 
+     def supported_locales(self):
+-        if not MOONSHINE_AVAILABLE:
+-            return []
++        self._ensure_catalog()
+         return list(self._locales_dict.keys())
+ 
+ _GLOBAL_ONLINE_MANAGER = None
+diff -urN IBus-Speech-To-Text-1.0.0.orig/engine/sttmoonshinemodel.py IBus-Speech-To-Text-1.0.0/engine/sttmoonshinemodel.py
+--- IBus-Speech-To-Text-1.0.0.orig/engine/sttmoonshinemodel.py	2026-07-14 03:00:00.000000000 +0530
++++ IBus-Speech-To-Text-1.0.0/engine/sttmoonshinemodel.py	2026-08-07 01:39:38.932769188 +0530
+@@ -4,10 +4,7 @@
+ from pathlib import Path
+ from gi.repository import GObject, Gio
+ 
+-from sttmoonshinemodelmanagers import (
+-    stt_moonshine_local_model_manager,
+-    MOONSHINE_AVAILABLE,
+-)
++from sttmoonshinemodelmanagers import stt_moonshine_local_model_manager
+ 
+ LOG_MSG = logging.getLogger()
+ 
+@@ -79,7 +76,7 @@
+                 return
+             self._model_name = None
+             self._model_path = model
+-            self._model_arch = local._infer_arch_for_folder(model) if MOONSHINE_AVAILABLE else None
++            self._model_arch = local._infer_arch_for_folder(model)
+             local.register_custom_model_path(model, self._locale_str)
+             self._valid_model = local.custom_path_available(model)
+         else:

diff --git a/sources b/sources
index c1b314a..3d7d1cf 100644
--- a/sources
+++ b/sources
@@ -1 +1 @@
-SHA512 (0.9.0.tar.gz) = 24c80f0f17b4a669731974f42d2b55ccdbdfd8e800d87ec9df8fd6c9a4e9288e680e71521e2d1beb65f7b1b41453a7425b745f2784b3134760d34074ae075116
+SHA512 (1.0.0.tar.gz) = 7b5d6cbea06cfcdb03420cf7160738ef435726af5192b3d4435a58afe902edb1b8bbc50b5b0b594fc640749f60622a6cf81d79b9fec6c6b2ceb6977551f884c8

                 reply	other threads:[~2026-08-07  2:49 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=178607099989.1.699124242477025289.rpms-ibus-speech-to-text-be64b0282704@fedoraproject.org \
    --to=matiwari@redhat.com \
    --cc=git-commits@fedoraproject.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox