Speculative fix on preferences weirdness.

This commit is contained in:
Storm Dragon
2026-03-09 01:14:01 -04:00
parent c7b8e4a30d
commit a7cd1d033a
9 changed files with 538 additions and 16 deletions
+15
View File
@@ -14,6 +14,16 @@ This repository is a screen reader. Prioritize accessibility, correctness, and s
- `meson compile -C _build`
- `meson install -C _build`
## Runtime target and testing rules
- Make source changes in this repo, not in `~/.local/lib/python*/site-packages/cthulhu/`, unless the user explicitly asks for an installed-package hotfix.
- If you confirm the active import comes from `~/.local/...`, that does **not** mean you should edit there. It means you should update the repo and then run `./build-local.sh` to replace the installed copy for testing.
- Default test/apply workflow for local Cthulhu fixes:
- edit repo files
- run `./build-local.sh`
- reproduce/test against the refreshed `~/.local` install
- If repo and installed behavior differ, prefer rebuilding with `./build-local.sh` over patching the installed package directly.
- Treat direct edits under `~/.local/.../cthulhu/` as an exception path that requires explicit user approval.
## Coding guidelines
- **When modifying existing code:** follow the surrounding codes conventions.
- **When writing new code from scratch:** prefer
@@ -55,3 +65,8 @@ This repository is a screen reader. Prioritize accessibility, correctness, and s
## Meson install reminder (important)
- If you add new Python modules under `src/cthulhu/`, update `src/cthulhu/meson.build` so they get installed (otherwise imports can fail after install).
- If you add a new plugin directory, update `src/cthulhu/plugins/meson.build` and add a `meson.build` in the plugin directory.
## Common Cthulhu agent mistakes
- Checking the import origin, seeing `~/.local/...`, and then editing the installed package instead of the repo.
- Forgetting that `./build-local.sh` is the normal way to apply repo changes into the installed copy for testing.
- Making repo fixes and then diagnosing the old installed copy without rebuilding.
+2 -2
View File
@@ -571,7 +571,7 @@
<property name="use_underline">True</property>
<property name="mnemonic_widget">timeFormatCombo</property>
<accessibility>
<relation type="label-for" target="availableProfilesComboBox1"/>
<relation type="label-for" target="timeFormatCombo"/>
</accessibility>
</object>
<packing>
@@ -588,7 +588,7 @@
<property name="use_underline">True</property>
<property name="mnemonic_widget">dateFormatCombo</property>
<accessibility>
<relation type="label-for" target="availableProfilesComboBox2"/>
<relation type="label-for" target="dateFormatCombo"/>
</accessibility>
</object>
<packing>
+209 -4
View File
@@ -71,6 +71,7 @@ from . import text_attribute_names
from . import sound_theme_manager
from . import script_manager
from .ax_object import AXObject
from .ax_utilities import AXUtilities
_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager
@@ -202,6 +203,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.profilesCombo = None
self.profilesComboModel = None
self.startingProfileCombo = None
self._initialFocusSyncAttempts = 0
self._capturedKey = []
self.script = None
@@ -1506,6 +1508,16 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
#
if not serverInfo:
serverInfo = speech.getInfo()
if serverInfo and len(serverInfo) >= 2 and serverInfo[1] == 'default':
defaultFamily = None
voices = self.prefsDict.get("voices", {})
defaultVoice = voices.get(settings.DEFAULT_VOICE) if voices else None
if defaultVoice:
defaultFamily = acss.ACSS(defaultVoice).get(acss.ACSS.FAMILY)
resolved = self._resolveSpeechDispatcherServerForFamily(defaultFamily)
if resolved is not None:
serverInfo = resolved.getInfo()
valueSet = False
i = 0
@@ -1720,6 +1732,118 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
}
self.echoVoice['established'] = True
def _resolveSpeechDispatcherServerForFamily(self, family):
"""Returns a concrete Speech Dispatcher server matching the voice family."""
if not family or not self.speechServersChoices:
return None
name = family.get(speechserver.VoiceFamily.NAME)
language = family.get(speechserver.VoiceFamily.LANG)
dialect = family.get(speechserver.VoiceFamily.DIALECT)
variant = family.get(speechserver.VoiceFamily.VARIANT)
if not name:
return None
for server in self.speechServersChoices:
info = server.getInfo()
if not info or len(info) < 2 or info[1] == 'default':
continue
try:
families = server.getVoiceFamilies()
except Exception:
debug.printException(debug.LEVEL_FINEST)
continue
for candidate in families:
if candidate.get(speechserver.VoiceFamily.NAME) != name:
continue
if candidate.get(speechserver.VoiceFamily.LANG) != language:
continue
if candidate.get(speechserver.VoiceFamily.DIALECT) != dialect:
continue
if candidate.get(speechserver.VoiceFamily.VARIANT) != variant:
continue
tokens = [
"PREFERENCES DIALOG: Resolved voice family",
name,
"to speech server",
info,
]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return server
tokens = [
"PREFERENCES DIALOG: Could not resolve speech server for family",
family,
]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return None
def _getSpeechServerChoiceForSave(self):
"""Returns the server choice that should be persisted for speech settings."""
server = self.speechServersChoice
if not server or self.speechSystemsChoice != self._getSpeechDispatcherFactory():
return server
info = server.getInfo()
if not info or len(info) < 2 or info[1] != 'default':
return server
defaultFamily = None
if self.defaultVoice is not None:
defaultFamily = self.defaultVoice.get(acss.ACSS.FAMILY)
resolved = self._resolveSpeechDispatcherServerForFamily(defaultFamily)
return resolved or server
def _getEchoSpeechServerFamilyForSave(self):
"""Returns the most specific echo family to use for server resolution."""
family = None
if self.echoVoice is not None:
family = self.echoVoice.get(acss.ACSS.FAMILY)
if family:
return family
if self.defaultVoice is not None:
return self.defaultVoice.get(acss.ACSS.FAMILY)
return None
def _getEchoSpeechServerChoiceForSave(self):
"""Returns the echo speech server choice that should be persisted."""
server = self.echoSpeechServersChoice
if server is None:
return None
info = server.getInfo()
if not info or len(info) < 2 or info[1] != 'default':
return server
family = self._getEchoSpeechServerFamilyForSave()
resolved = self._resolveSpeechDispatcherServerForFamily(family)
if resolved is not None:
return resolved
speechServer = self._getSpeechServerChoiceForSave()
if speechServer is not None:
speechInfo = speechServer.getInfo()
if speechInfo and len(speechInfo) >= 2 and speechInfo[1] != 'default':
tokens = [
"PREFERENCES DIALOG: Falling back to main speech server for echo",
speechInfo,
]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return speechServer
return server
def _populateEchoSpeechFamilies(self, families):
"""Populate the echo family combobox from the provided families list."""
@@ -1873,6 +1997,23 @@ print(json.dumps(result))
self._setupEchoSpeechFamilies()
return
if serverInfo and len(serverInfo) >= 2 and serverInfo[1] == 'default':
family = self._getEchoSpeechServerFamilyForSave()
resolved = self._resolveSpeechDispatcherServerForFamily(family)
if resolved is None:
speechServer = self._getSpeechServerChoiceForSave()
if speechServer is not None:
speechInfo = speechServer.getInfo()
if speechInfo and len(speechInfo) >= 2 and speechInfo[1] != 'default':
resolved = speechServer
tokens = [
"PREFERENCES DIALOG: Reusing main speech server for echo",
speechInfo,
]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if resolved is not None:
serverInfo = resolved.getInfo()
valueSet = False
for i, server in enumerate(self.echoSpeechServersChoices):
info = server.getInfo()
@@ -3148,6 +3289,68 @@ print(json.dumps(result))
cthulhuSetupWindow.set_title(title)
cthulhuSetupWindow.show()
self._initialFocusSyncAttempts = 0
GLib.idle_add(self._set_initial_window_state)
GLib.idle_add(self._set_initial_gtk_focus)
GLib.timeout_add(50, self._set_initial_window_state)
def _set_initial_gtk_focus(self):
"""Give GTK focus to a real preferences control as soon as the dialog appears."""
candidateIds = [
"generalDesktopButton",
"generalLaptopButton",
"availableProfilesComboBox1",
"speechSupportCheckButton",
"notebook",
]
cthulhuSetupWindow = self.get_widget("cthulhuSetupWindow")
for widgetId in candidateIds:
widget = self.get_widget(widgetId)
if not widget.get_visible() or not widget.get_sensitive():
continue
if not widget.get_can_focus():
continue
debug.printMessage(
debug.LEVEL_INFO,
f"PREFERENCES DIALOG: Setting initial GTK focus to {widgetId}",
True,
)
cthulhuSetupWindow.set_focus(widget)
widget.grab_focus()
return False
debug.printMessage(
debug.LEVEL_INFO,
"PREFERENCES DIALOG: No focusable initial GTK widget found",
True,
)
return False
def _set_initial_window_state(self):
"""Sync Cthulhu's active window to preferences without forcing dialog focus."""
self._initialFocusSyncAttempts += 1
activeWindow = AXUtilities.find_active_window()
if activeWindow is None:
return self._initialFocusSyncAttempts < 5
app = AXObject.get_application(activeWindow)
appName = (AXObject.get_name(app) or "").lower()
if appName != "cthulhu":
return self._initialFocusSyncAttempts < 5
debug.printTokens(
debug.LEVEL_INFO,
["PREFERENCES DIALOG: Syncing active window to", activeWindow],
True,
)
cthulhu.setActiveWindow(activeWindow, notifyScript=False)
tokens = ["PREFERENCES DIALOG: Synced initial window state to", activeWindow]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return False
def _initComboBox(self, combobox):
"""Initialize the given combo box to take a list of int/str pairs.
@@ -4648,9 +4851,10 @@ print(json.dumps(result))
self.prefsDict["speechServerFactory"] = \
self.speechSystemsChoice.__name__
if self.speechServersChoice:
speechServerChoice = self._getSpeechServerChoiceForSave()
if speechServerChoice:
self.prefsDict["speechServerInfo"] = \
self.speechServersChoice.getInfo()
speechServerChoice.getInfo()
if self.defaultVoice is not None:
self.prefsDict["voices"] = {
@@ -4684,8 +4888,9 @@ print(json.dumps(result))
self.echoVoice['established'] = True
self.prefsDict["echoVoice"] = acss.ACSS(self.echoVoice)
if self.echoSpeechServersChoice:
self.prefsDict["echoSpeechServerInfo"] = self.echoSpeechServersChoice.getInfo()
echoSpeechServerChoice = self._getEchoSpeechServerChoiceForSave()
if echoSpeechServerChoice:
self.prefsDict["echoSpeechServerInfo"] = echoSpeechServerChoice.getInfo()
else:
self.prefsDict["echoSpeechServerInfo"] = None
+5
View File
@@ -47,6 +47,11 @@ __license__ = "LGPL"
#
locusOfFocus: Optional[Any] = None # Actually: Optional[Atspi.Accessible]
# A pending focused object from a Cthulhu-owned window that should be used
# for keyboard-event context until the queued focus event is processed.
#
pendingSelfHostedFocus: Optional[Any] = None # Actually: Optional[Atspi.Accessible]
# The currently active window.
#
activeWindow: Optional[Any] = None # Actually: Optional[Atspi.Accessible]
+141 -1
View File
@@ -70,7 +70,9 @@ class EventManager:
self._dequeueCount: int = 0
self._cmdlineCache: Dict[int, str] = {}
self._eventQueue: queue.Queue[Any] = queue.Queue(0)
self._prioritizedEvent: Optional[Atspi.Event] = None
self._gidleId: int = 0
self._prioritizedIdleId: int = 0
self._gidleLock: threading.Lock = threading.Lock()
self._gilSleepTime: float = 0.00001
self._synchronousToolkits: List[str] = ['VCL']
@@ -284,6 +286,12 @@ class EventManager:
if self._isDuplicateEvent(event):
return _ignore_with_reason("duplicate", "duplicate event")
if self._isSelfHostedFocusClearedEvent(event):
return _ignore_with_reason("self-hosted-focus-cleared", "self-hosted focused=false event")
if self._isRedundantSelfHostedPropertyEvent(event):
return _ignore_with_reason("self-hosted-redundant-property", "self-hosted redundant property event")
# Thunderbird spams us with these when a message list thread is expanded or collapsed.
if event.type.endswith('system') \
and AXObject.get_name(app).lower().startswith('thunderbird'):
@@ -596,6 +604,11 @@ class EventManager:
self._enqueueCount -= 1
return
if isObjectEvent and self._prioritizeSelfHostedFocusedEvent(e):
if debug.debugEventQueue:
self._enqueueCount -= 1
return
self._queuePrintln(e)
if self._inFlood() and self._prioritizeDuringFlood(e):
@@ -628,6 +641,73 @@ class EventManager:
if debug.debugEventQueue:
self._enqueueCount -= 1
def _isSelfHostedFocusedEvent(self, event: Atspi.Event) -> bool:
"""Returns True if the event is a newly-focused event from Cthulhu."""
if not event.type.startswith("object:state-changed:focused") or not event.detail1:
return False
app = AXObject.get_application(event.source)
appName = (AXObject.get_name(app) or "").lower()
return appName == "cthulhu"
def _isSelfHostedFocusClearedEvent(self, event: Atspi.Event) -> bool:
"""Returns True if the event is a focus-lost event from Cthulhu."""
if not event.type.startswith("object:state-changed:focused") or event.detail1:
return False
app = AXObject.get_application(event.source)
appName = (AXObject.get_name(app) or "").lower()
return appName == "cthulhu"
def _isRedundantSelfHostedPropertyEvent(self, event: Atspi.Event) -> bool:
"""Returns True if the event is redundant prefs startup noise from Cthulhu."""
app = AXObject.get_application(event.source)
appName = (AXObject.get_name(app) or "").lower()
if appName != "cthulhu":
return False
if event.type.startswith("object:property-change:accessible-name"):
return AXUtilities.is_combo_box(event.source) or AXUtilities.is_table_cell(event.source)
if event.type.startswith("object:property-change:accessible-value"):
return AXObject.get_role(event.source) == Atspi.Role.SLIDER \
and event.source != cthulhu_state.locusOfFocus
return False
def _prioritizeSelfHostedFocusedEvent(self, event: Atspi.Event) -> bool:
"""Schedules the latest focused-child event from Cthulhu ahead of the normal queue."""
if not self._isSelfHostedFocusedEvent(event):
return False
tokens = ["EVENT MANAGER: Prioritizing self-hosted focused event for", event.source]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
script = self._get_scriptForEvent(event)
if script is not None:
script.eventCache[event.type] = (event, time.time())
self._gidleLock.acquire()
try:
replaced = self._prioritizedEvent is not None
self._prioritizedEvent = event
cthulhu_state.pendingSelfHostedFocus = event.source
if not self._prioritizedIdleId:
self._prioritizedIdleId = GLib.idle_add(
self._dequeuePrioritizedEvent,
priority=GLib.PRIORITY_HIGH_IDLE,
)
finally:
self._gidleLock.release()
msg = f"EVENT MANAGER: Prioritized self-hosted focused event. Replaced pending event: {replaced}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
def _isNoFocus(self) -> bool:
if cthulhu_state.locusOfFocus or cthulhu_state.activeWindow or cthulhu_state.activeScript:
return False
@@ -645,6 +725,60 @@ class EventManager:
defaultScript.idleMessage()
return False
def _dequeuePrioritizedEvent(self) -> bool:
"""Handles prioritized focused events from Cthulhu-owned windows."""
self._gidleLock.acquire()
try:
event = self._prioritizedEvent
self._prioritizedEvent = None
self._prioritizedIdleId = 0
if event is not None and cthulhu_state.pendingSelfHostedFocus == event.source:
cthulhu_state.pendingSelfHostedFocus = None
finally:
self._gidleLock.release()
if event is None:
return False
tokens = ["EVENT MANAGER: Dequeued prioritized event", event]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
debugging = not debug.eventDebugFilter or debug.eventDebugFilter.match(event.type)
if debugging:
startTime = time.time()
msg = (
f"\nvvvvv PROCESS OBJECT EVENT {event.type} "
f"(prioritized) vvvvv"
)
debug.printMessage(debug.eventDebugLevel, msg, False)
try:
self._processObjectEvent(event)
if self._didSuspendEventsFor(event):
self._unsuspendEvents(event)
elif self._eventsSuspended and self._shouldUnsuspendEventsFor(event):
self._unsuspendEvents(event, force=True)
except Exception:
debug.printException(debug.LEVEL_SEVERE)
if debugging:
msg = (
f"TOTAL PROCESSING TIME: {time.time() - startTime:.4f}"
f"\n^^^^^ PROCESS OBJECT EVENT {event.type} ^^^^^\n"
)
debug.printMessage(debug.eventDebugLevel, msg, False)
self._gidleLock.acquire()
try:
hasMore = self._prioritizedEvent is not None
if not hasMore and self._eventQueue.qsize() and not self._gidleId:
self._gidleId = GLib.idle_add(self._dequeue)
finally:
self._gidleLock.release()
return hasMore
def _dequeue(self) -> bool:
"""Handles all events destined for scripts. Called by the GTK
idle thread."""
@@ -657,7 +791,13 @@ class EventManager:
self._dequeueCount += 1
try:
event = self._eventQueue.get_nowait()
fromPriority = False
self._gidleLock.acquire()
try:
event = self._eventQueue.get_nowait()
finally:
self._gidleLock.release()
self._queuePrintln(event, isEnqueue=False)
inputEvents = (input_event.KeyboardEvent, input_event.BrailleEvent)
if isinstance(event, inputEvents):
+34 -6
View File
@@ -116,6 +116,13 @@ class FocusManager:
_log_tokens(["Focused object in", self._window, "is", result])
return result
def active_window_is_cthulhu(self) -> bool:
"""Returns True if the active window belongs to Cthulhu itself."""
app = AXObject.get_application(self._window)
appName = (AXObject.get_name(app) or "").lower()
return appName == "cthulhu"
def focus_and_window_are_unknown(self) -> bool:
"""Returns True if we have no knowledge about what is focused."""
@@ -350,18 +357,39 @@ class FocusManager:
self._window = frame
cthulhu_state.activeWindow = frame
contextObject = self._focus
if set_window_as_focus:
self.set_locus_of_focus(None, self._window, notify_script)
elif not (self.focus_is_active_window() or self.focus_is_in_active_window()):
_log_tokens(["Focus", self._focus, "is not in", self._window], stack=True)
# Don't update the focus to the active window if we can't get to the active window
# from the focused object. https://bugreports.qt.io/browse/QTBUG-130116
if not AXObject.has_broken_ancestry(self._focus):
self.set_locus_of_focus(None, self._window, notify_script=True)
if self.active_window_is_cthulhu():
_log_tokens(
["Skipping focused-object lookup and script activation for self-hosted window", self._window],
"self-window",
)
return
else:
focusedObject = self.find_focused_object()
if focusedObject is not None and focusedObject != self._window:
_log_tokens(["Using focused object", focusedObject, "from active window", self._window])
self.set_locus_of_focus(None, focusedObject, notify_script=True)
contextObject = focusedObject
elif self._focus is None:
_log_tokens(["No previous focus. Falling back to active window", self._window])
self.set_locus_of_focus(None, self._window, notify_script=True)
contextObject = self._window
# Don't update the focus to the active window if we can't get to the active window
# from the focused object. https://bugreports.qt.io/browse/QTBUG-130116
elif not AXObject.has_broken_ancestry(self._focus):
self.set_locus_of_focus(None, self._window, notify_script=True)
contextObject = self._window
app = _get_ax_utilities().get_application(self._focus)
self.app.scriptManager.activate_script_for_context(app, self._focus, "focus: active-window")
if contextObject is None:
contextObject = self._window
app = _get_ax_utilities().get_application(contextObject)
self.app.scriptManager.activate_script_for_context(app, contextObject, "focus: active-window")
@dbus_service.command
def toggle_presentation_mode(
+6 -2
View File
@@ -306,6 +306,10 @@ class InputEventManager:
return False
manager = focus_manager.get_manager()
pendingFocus = cthulhu_state.pendingSelfHostedFocus
if pendingFocus is not None:
tokens = ["INPUT EVENT MANAGER: Using pending self-hosted focus for keyboard event:", pendingFocus]
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
if pressed:
window = manager.get_active_window()
if not AXUtilities.can_be_active_window(window, clear_cache=True):
@@ -322,7 +326,7 @@ class InputEventManager:
window = None
manager.set_active_window(None, notify_script=True)
event.set_window(window)
event.set_object(manager.get_locus_of_focus())
event.set_object(pendingFocus or manager.get_locus_of_focus())
event.set_script(script_manager.get_manager().get_active_script())
elif self.last_event_was_keyboard():
assert isinstance(self._last_input_event, input_event.KeyboardEvent)
@@ -331,7 +335,7 @@ class InputEventManager:
event.set_script(self._last_input_event.get_script())
else:
event.set_window(manager.get_active_window())
event.set_object(manager.get_locus_of_focus())
event.set_object(pendingFocus or manager.get_locus_of_focus())
event.set_script(script_manager.get_manager().get_active_script())
event._finalize_initialization()
+8
View File
@@ -1925,6 +1925,14 @@ class Script(script.Script):
self.pointOfReference = {}
cthulhu.setActiveWindow(window)
app = AXObject.get_application(window)
appName = (AXObject.get_name(app) or "").lower()
if appName == "cthulhu":
msg = "DEFAULT: Self-hosted window activated. Waiting for focused child event."
debug.printMessage(debug.LEVEL_INFO, msg, True)
return
if self.utilities.isKeyGrabEvent(event):
msg = "DEFAULT: Ignoring event. Likely from key grab."
debug.printMessage(debug.LEVEL_INFO, msg, True)
+118 -1
View File
@@ -82,6 +82,108 @@ def _isSpeechDispatcherFactory(moduleName: Optional[str]) -> bool:
return False
return moduleName.split(".")[-1] == "speechdispatcherfactory"
def _matchesVoiceFamily(candidateFamily: Any, targetFamily: Any) -> bool:
if not candidateFamily or not targetFamily:
return False
for key in (VoiceFamily.NAME, VoiceFamily.LANG, VoiceFamily.DIALECT, VoiceFamily.VARIANT):
if candidateFamily.get(key) != targetFamily.get(key):
return False
return True
def _resolveSpeechDispatcherServerInfo(
moduleName: Optional[str],
speechServerInfo: Optional[Any],
voice: Optional[Any] = None,
fallbackServerInfo: Optional[Any] = None,
) -> Optional[Any]:
if not _isSpeechDispatcherFactory(moduleName):
return speechServerInfo
if speechServerInfo and len(speechServerInfo) >= 2 and speechServerInfo[1] != "default":
return speechServerInfo
if fallbackServerInfo and len(fallbackServerInfo) >= 2 and fallbackServerInfo[1] != "default":
tokens = [
"SPEECH: Resolving Speech Dispatcher server via fallback server info:",
fallbackServerInfo,
]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return fallbackServerInfo
family = None
if voice:
try:
family = ACSS(voice).get(ACSS.FAMILY)
except Exception:
debug.printException(debug.LEVEL_INFO)
if not family:
try:
family = ACSS(settings.voices.get(settings.DEFAULT_VOICE, {})).get(ACSS.FAMILY)
except Exception:
debug.printException(debug.LEVEL_INFO)
family = None
if not family:
return speechServerInfo
factory = None
try:
factory = importlib.import_module(f"cthulhu.{moduleName}")
except Exception:
try:
factory = importlib.import_module(moduleName)
except Exception:
debug.printException(debug.LEVEL_INFO)
if factory is None:
return speechServerInfo
try:
servers = factory.SpeechServer.getSpeechServers() # type: ignore[attr-defined]
except Exception:
debug.printException(debug.LEVEL_INFO)
return speechServerInfo
for server in servers:
try:
info = server.getInfo()
except Exception:
debug.printException(debug.LEVEL_INFO)
continue
if not info or len(info) < 2 or info[1] == "default":
continue
try:
families = server.getVoiceFamilies() or []
except Exception:
debug.printException(debug.LEVEL_INFO)
continue
for candidate in families:
if not _matchesVoiceFamily(candidate, family):
continue
tokens = [
"SPEECH: Resolved Speech Dispatcher server info",
info,
"for voice family",
family,
]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return info
tokens = [
"SPEECH: Could not resolve Speech Dispatcher server info for voice family",
family,
]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return speechServerInfo
def _initSpeechServer(moduleName: Optional[str], speechServerInfo: Optional[Any]) -> SpeechServer:
if not moduleName:
@@ -125,9 +227,24 @@ def _refreshEchoSpeechServer() -> None:
return
try:
resolvedInfo = _resolveSpeechDispatcherServerInfo(
settings.speechServerFactory,
settings.echoSpeechServerInfo,
settings.echoVoice,
settings.speechServerInfo,
)
if resolvedInfo != settings.echoSpeechServerInfo:
tokens = [
"SPEECH: Updating echo speech server info from",
settings.echoSpeechServerInfo,
"to",
resolvedInfo,
]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
settings.echoSpeechServerInfo = resolvedInfo
_echoSpeechserver = _initSpeechServer(
settings.speechServerFactory,
settings.echoSpeechServerInfo
resolvedInfo
)
except Exception:
debug.printException(debug.LEVEL_INFO)