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] rawhide: Update to 1.0.0 release
Date: Thu, 06 Aug 2026 21:17:58 GMT [thread overview]
Message-ID: <178605107815.1.11891097024841474046.rpms-ibus-speech-to-text-ce50ee95ff6e@fedoraproject.org> (raw)
A new commit has been pushed.
Repo : rpms/ibus-speech-to-text
Branch : rawhide
Commit : ce50ee95ff6ea1d72dcae5dc1810d3f272da551f
Author : matiwari <matiwari@redhat.com>
Date : 2026-08-07T02:32:20+05:30
Stats : +1475/-3 in 4 file(s)
URL : https://src.fedoraproject.org/rpms/ibus-speech-to-text/c/ce50ee95ff6ea1d72dcae5dc1810d3f272da551f?branch=rawhide
Log:
Update to 1.0.0 release
---
diff --git a/.gitignore b/.gitignore
index f1098b9..98a27e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@
/0.7.0.tar.gz
/0.8.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 355790c..f856071 100644
--- a/ibus-speech-to-text.spec
+++ b/ibus-speech-to-text.spec
@@ -1,12 +1,13 @@
%global debug_package %{nil}
Name: ibus-speech-to-text
-Version: 0.9.0
-Release: 2%{?dist}
+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
@@ -33,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
@@ -55,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 Jul 16 2026 Fedora Release Engineering <releng@fedoraproject.org> - 0.9.0-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_45_Mass_Rebuild
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-06 21:17 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=178605107815.1.11891097024841474046.rpms-ibus-speech-to-text-ce50ee95ff6e@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