Skip to content

Commit a91f92c

Browse files
committed
🐛 fix: fix(a11y): native voice per language in ORCA language selector
- Display native language names as headings (e.g. "PortuguĂȘs, Brazil" instead of "Portuguese - Brazil") so screen readers announce each language in its own tongue - Use espeak-ng with per-language voices for non-pt_BR items, cancelling ORCA via speech-dispatcher (Scope.ALL) to avoid overlapping speech - Keep the default LetĂ­cia voice (ORCA) for pt_BR selection only - Hide flag icons and English caption from screen readers using PRESENTATION accessible role - Ensure en_US appears first in the language list by using explicit favorites ordering instead of alphabetical sort - Pre-create factory widgets in setup phase to avoid unnecessary recreation on each bind
1 parent bdff6dd commit a91f92c

2 files changed

Lines changed: 72 additions & 4 deletions

File tree

‎biglinux-livecd/usr/share/biglinux/calamares/src/utils/constants.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# Application Information
1010
APP_NAME = "BigLinux Calamares Config"
1111
APP_ID = "com.biglinux.calamares-config"
12-
APP_VERSION = "1.1.2"
12+
APP_VERSION = "1.1.3"
1313

1414
# Paths and Directories
1515
BASE_DIR = Path(__file__).parent.parent.parent

‎biglinux-livecd/usr/share/biglinux/livecd/ui/language_view.py‎

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
gi.require_version("Adw", "1")
55
from gi.repository import Gtk, Adw, Gio, GObject, Gdk, GLib
66
import json
7+
import subprocess
78
import unicodedata
89
from urllib.parse import parse_qs, urlparse
910
from translations import _
@@ -173,8 +174,75 @@ def _create_filtered_model(self):
173174
self.filter = Gtk.CustomFilter.new(self._filter_func, None)
174175
self.filter_model = Gtk.FilterListModel(model=self._store, filter=self.filter)
175176
selection_model = Gtk.SingleSelection(model=self.filter_model)
177+
selection_model.connect("selection-changed", self._on_selection_changed)
178+
self._espeak_proc = None
179+
self._speak_timeout_id = 0
180+
# speech-dispatcher client for fast cancel (avoids subprocess overhead)
181+
self._spd_client = None
182+
self._spd_scope_all = None
183+
try:
184+
import speechd
185+
186+
self._spd_client = speechd.SSIPClient("biglinux-wizard")
187+
self._spd_scope_all = speechd.Scope.ALL
188+
except Exception:
189+
pass
176190
return selection_model
177191

192+
def _cancel_orca(self):
193+
"""Cancel ALL speech-dispatcher clients (including ORCA) instantly."""
194+
if self._spd_client and self._spd_scope_all:
195+
try:
196+
self._spd_client.cancel(scope=self._spd_scope_all)
197+
except Exception:
198+
pass
199+
200+
def _on_selection_changed(self, selection_model, position, n_items):
201+
"""For pt_BR, let ORCA speak with the default LetĂ­cia voice.
202+
For other languages, cancel ORCA and use espeak-ng with the native voice."""
203+
# Cancel any pending delayed speak
204+
if self._speak_timeout_id > 0:
205+
GLib.source_remove(self._speak_timeout_id)
206+
self._speak_timeout_id = 0
207+
# Kill any ongoing espeak-ng process
208+
if self._espeak_proc and self._espeak_proc.poll() is None:
209+
self._espeak_proc.terminate()
210+
self._espeak_proc = None
211+
selected = selection_model.get_selected()
212+
if selected == Gtk.INVALID_LIST_POSITION:
213+
return
214+
item = selection_model.get_item(selected)
215+
if not item:
216+
return
217+
# For pt_BR: do nothing, let ORCA read with LetĂ­cia voice
218+
if item.code == "pt_BR":
219+
return
220+
# Immediately cancel ORCA speech via Python API (instant, no fork)
221+
self._cancel_orca()
222+
# Schedule espeak-ng after a brief delay to also cancel any ORCA re-queue
223+
parts = item.name.split(" - ", 1)
224+
country = parts[1] if len(parts) > 1 else ""
225+
native_name = _NATIVE_LANG_NAMES.get(item.code[:2], item.name_orig)
226+
text = f"{native_name}, {country}" if country else native_name
227+
voice = item.code.replace("_", "-") # "en_US" -> "en-US"
228+
self._speak_timeout_id = GLib.timeout_add(50, self._do_espeak, voice, text)
229+
230+
def _do_espeak(self, voice, text):
231+
"""Cancel ORCA speech and speak with espeak-ng in native voice."""
232+
self._speak_timeout_id = 0
233+
# Cancel any ORCA speech that was re-queued
234+
self._cancel_orca()
235+
# Speak with espeak-ng using the native voice
236+
try:
237+
self._espeak_proc = subprocess.Popen(
238+
["espeak-ng", "-v", voice, "--", text],
239+
stdout=subprocess.DEVNULL,
240+
stderr=subprocess.DEVNULL,
241+
)
242+
except FileNotFoundError:
243+
logger.debug("espeak-ng not found")
244+
return GLib.SOURCE_REMOVE
245+
178246
def _activate_item(self, item):
179247
if not item:
180248
return
@@ -248,7 +316,7 @@ def _on_factory_setup(self, factory, list_item):
248316
halign=Gtk.Align.START,
249317
valign=Gtk.Align.CENTER,
250318
)
251-
# Heading: ORCA reads this (native language name)
319+
# Heading: native name (ORCA reads this for pt_BR with LetĂ­cia voice)
252320
name_label = Gtk.Label(
253321
halign=Gtk.Align.START,
254322
wrap=True,
@@ -304,9 +372,9 @@ def _on_factory_bind(self, factory, list_item):
304372
native_name = _NATIVE_LANG_NAMES.get(item.code[:2], item.name_orig)
305373
heading_text = f"{native_name}, {country}" if country else native_name
306374

307-
# Heading: native name (ORCA reads this)
375+
# Heading: visual text
308376
root_box._name_label.set_label(heading_text)
309-
# Caption: English name (hidden from ORCA via PRESENTATION role)
377+
# Caption: English name (PRESENTATION — ORCA doesn't read)
310378
root_box._orig_label.set_label(item.name)
311379

312380
root_box._flag.set_from_icon_name(item.flag_icon_name)

0 commit comments

Comments
 (0)