STT plugin based on onnx-asr

This commit is contained in:
hueso
2026-08-07 04:23:44 -03:00
parent cc63a25fe0
commit a0f300068e
8 changed files with 521 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
# This file is part of Gajim.
#
# Gajim is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Any
from typing import TYPE_CHECKING
from gi.repository import Gtk
from gajim.gtk.const import Setting
from gajim.gtk.const import SettingKind
from gajim.gtk.const import SettingType
from gajim.gtk.settings import SettingsDialog
from gajim.plugins.plugins_i18n import _
if TYPE_CHECKING:
from ..stt_voice_messages import STTVoiceMessagesPlugin
class STTVoiceMessagesConfigDialog(SettingsDialog):
def __init__(self, plugin: STTVoiceMessagesPlugin, parent: Gtk.Window) -> None:
self.plugin = plugin
model = plugin.model
if not model.available():
status = _('onnx-asr is not installed. Run: pip install "onnx-asr[hub]"')
elif model.is_loaded:
status = _("Model is loaded.")
elif model.will_download:
status = _("Model files will be downloaded on first use.")
else:
status = _("Model files are downloaded and ready.")
settings = [
Setting(
SettingKind.SWITCH,
_("Transcribe automatically"),
SettingType.VALUE,
bool(plugin.config["auto_transcribe"]),
callback=self._on_setting,
data="auto_transcribe",
desc=_("Transcribe voice messages as soon as they are displayed"),
),
Setting(
SettingKind.ENTRY,
_("Model"),
SettingType.VALUE,
str(plugin.config["model_id"]),
callback=self._on_setting,
data="model_id",
desc=_("onnx-asr model name or Hugging Face repository"),
),
Setting(
SettingKind.DROPDOWN,
_("Quantization"),
SettingType.VALUE,
str(plugin.config["quantization"]),
callback=self._on_setting,
data="quantization",
props={
"data": {
"int8": _("int8 (fast, ~600 MB download)"),
"fp32": _("Full precision (~2.4 GB download)"),
}
},
desc=_("Applied the next time the model is loaded"),
),
Setting(SettingKind.GENERIC, _("Status"), SettingType.VALUE, desc=status),
]
SettingsDialog.__init__(
self,
parent,
_("STT Voice Messages Configuration"),
Gtk.DialogFlags.MODAL,
settings,
"",
)
def _on_setting(self, value: Any, data: Any) -> None:
self.plugin.config[data] = value
if data in ("model_id", "quantization"):
self.plugin.model.set_config(
str(self.plugin.config["model_id"]),
str(self.plugin.config["quantization"]),
)
+155
View File
@@ -0,0 +1,155 @@
# This file is part of Gajim.
#
# Gajim is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Any
import logging
from pathlib import Path
from gi.repository import Adw
from gi.repository import Gio
from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gtk
from gajim.plugins.plugins_i18n import _
from ..model import OnnxAsrModel
log = logging.getLogger("gajim.p.stt_voice_messages_sttbox")
class STTBox(Gtk.Box):
def __init__(self, model: OnnxAsrModel, audio_file: Path) -> None:
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=6)
self.set_margin_top(6)
self.set_halign(Gtk.Align.CENTER)
self.set_visible(False)
self._model = model
self._audio_file = audio_file
self._task: Gio.Task | None = None
self._transcribe_button = Gtk.Button.new_from_icon_name(
"lucide-square-pen-symbolic"
)
self._transcribe_button.set_tooltip_text(_("Transcribe voice message"))
self._transcribe_button.set_valign(Gtk.Align.CENTER)
self._transcribe_button.connect("clicked", self._on_transcribe_clicked)
self._spinner = Adw.Spinner(valign=Gtk.Align.CENTER, visible=False)
self._transcription_label = Gtk.Label()
self._transcription_label.set_max_width_chars(40)
self._transcription_label.set_wrap(True)
self._transcription_label.set_xalign(0)
self._transcription_label.set_selectable(True)
self.append(self._spinner)
self.append(self._transcription_label)
@property
def button(self) -> Gtk.Button:
return self._transcribe_button
def transcribe(self) -> None:
self._on_transcribe_clicked(self._transcribe_button)
def _on_transcribe_clicked(self, _button: Gtk.Button) -> None:
if not self._model.available():
self._show_status(
_('onnx-asr is not installed. Run: pip install "onnx-asr[hub]"'),
busy=False,
)
return
if self._model.is_loaded:
text = _("Transcribing…")
elif self._model.will_download:
text = _("Downloading model…")
else:
text = _("Loading model…")
self._show_status(text, busy=True)
self._run_bg(self._model.load, self._on_load_done)
def _on_load_done(self) -> None:
try:
self._finish()
except Exception as e:
self._show_status(_("Error: {}").format(e), busy=False)
return
self._show_status(_("Transcribing…"), busy=True)
self._run_bg(
lambda: self._model.recognize(self._audio_file, self._on_partial),
self._show_result,
)
def _on_partial(self, text: str) -> None:
GLib.idle_add(self._set_partial, text)
def _set_partial(self, text: str) -> None:
self._transcription_label.set_text(text)
def _show_result(self) -> None:
try:
text = self._finish()
except Exception as e:
self._show_status(_("Error: {}").format(e), busy=False)
return
text = text.strip()
if not text:
text = _("No speech detected.")
self._show_status(text, busy=False)
def _run_bg(self, fn, on_done) -> None:
self._bg_fn = fn
def _cb(_source: GObject.Object, _result: Gio.AsyncResult, _data: None) -> None:
on_done()
self._task = Gio.Task.new(self, None, _cb, None)
self._task.run_in_thread(self._thread_cb)
@staticmethod
def _thread_cb(
task: Gio.Task,
source: "STTBox",
_data: None,
_cancel: Gio.Cancellable,
) -> None:
try:
task.return_value(source._bg_fn())
except Exception as e:
task.return_value(e)
def _finish(self) -> Any:
task = self._task
self._task = None
value = task.propagate_value().value
if isinstance(value, Exception):
raise value
return value
def _show_status(self, text: str, busy: bool) -> None:
self._transcription_label.set_text(text)
self._transcription_label.set_visible(True)
self.set_visible(True)
self._spinner.set_visible(busy)
self._transcribe_button.set_sensitive(not busy)