Requested feature added. It is now possible to customize speech used for echo settings including speech module, voice, rate, volume, pitch.

This commit is contained in:
Storm Dragon
2026-02-22 01:44:19 -05:00
parent 11bd7107d2
commit 1d2d727fa2
11 changed files with 1018 additions and 22 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org> # Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=cthulhu pkgname=cthulhu
pkgver=2026.02.17 pkgver=2026.02.22
pkgrel=1 pkgrel=1
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca" pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
url="https://git.stormux.org/storm/cthulhu" url="https://git.stormux.org/storm/cthulhu"
+1 -1
View File
@@ -1,5 +1,5 @@
project('cthulhu', project('cthulhu',
version: '2026.02.17-master', version: '2026.02.22-testing',
meson_version: '>= 1.0.0', meson_version: '>= 1.0.0',
) )
+34
View File
@@ -222,6 +222,24 @@ class Backend:
return settingsDict return settingsDict
def _hasLikelyCustomEchoVoice(self, echoVoice, defaultVoice):
"""Return True when echoVoice appears intentionally customized."""
if not echoVoice:
return False
echoEstablished = bool(echoVoice.get('established', False))
if not defaultVoice:
return echoEstablished or bool(echoVoice.get(acss.ACSS.FAMILY))
for key in [acss.ACSS.RATE, acss.ACSS.AVERAGE_PITCH, acss.ACSS.GAIN, acss.ACSS.FAMILY]:
echoValue = echoVoice.get(key)
defaultValue = defaultVoice.get(key)
if echoValue is not None and echoValue != defaultValue:
return True
return echoEstablished and bool(echoVoice.get(acss.ACSS.FAMILY))
def getGeneral(self, profile=None): def getGeneral(self, profile=None):
""" Get general settings from default settings and """ Get general settings from default settings and
override with profile values. """ override with profile values. """
@@ -241,8 +259,24 @@ class Backend:
if key == 'voices': if key == 'voices':
for voiceType, voiceDef in value.items(): for voiceType, voiceDef in value.items():
value[voiceType] = acss.ACSS(voiceDef) value[voiceType] = acss.ACSS(voiceDef)
if key == 'echoVoice' and isinstance(value, dict):
value = acss.ACSS(value)
if key not in ['startingProfile', 'activeProfile']: if key not in ['startingProfile', 'activeProfile']:
generalSettings[key] = value generalSettings[key] = value
# Backward compatibility: recover custom echo behavior when legacy
# profiles have echoVoice but are missing custom-echo toggle keys.
if 'useCustomEchoVoice' not in profileSettings:
voices = generalSettings.get('voices') or {}
defaultVoice = voices.get(settings.DEFAULT_VOICE)
echoVoice = generalSettings.get('echoVoice')
if self._hasLikelyCustomEchoVoice(echoVoice, defaultVoice):
generalSettings['useCustomEchoVoice'] = True
if 'useCustomEchoForKey' not in profileSettings:
generalSettings['useCustomEchoForKey'] = True
if 'useCustomEchoForCharacter' not in profileSettings:
generalSettings['useCustomEchoForCharacter'] = True
try: try:
generalSettings['activeProfile'] = profileSettings['profile'] generalSettings['activeProfile'] = profileSettings['profile']
except KeyError: except KeyError:
+310 -3
View File
@@ -136,6 +136,24 @@
<property name="step_increment">0.10000000149</property> <property name="step_increment">0.10000000149</property>
<property name="page_increment">1</property> <property name="page_increment">1</property>
</object> </object>
<object class="GtkAdjustment" id="echoPitchAdjustment">
<property name="upper">10</property>
<property name="value">5</property>
<property name="step_increment">0.10000000149</property>
<property name="page_increment">1</property>
</object>
<object class="GtkAdjustment" id="echoRateAdjustment">
<property name="upper">100</property>
<property name="value">50</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkAdjustment" id="echoVolumeAdjustment">
<property name="upper">10</property>
<property name="value">10</property>
<property name="step_increment">0.10000000149</property>
<property name="page_increment">1</property>
</object>
<object class="GtkDialog" id="cthulhuSetupWindow"> <object class="GtkDialog" id="cthulhuSetupWindow">
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="title" translatable="yes">Cthulhu Preferences</property> <property name="title" translatable="yes">Cthulhu Preferences</property>
@@ -3037,6 +3055,295 @@
<property name="top_attach">1</property> <property name="top_attach">1</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkFrame" id="echoVoiceFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkAlignment" id="echoVoiceAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<object class="GtkGrid" id="echoVoiceGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">10</property>
<property name="column_spacing">8</property>
<child>
<object class="GtkCheckButton" id="useCustomEchoVoiceCheckButton">
<property name="label" translatable="yes">Use c_ustom echo voice settings</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="useCustomEchoVoiceToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="useCustomEchoSpeechServerCheckButton">
<property name="label" translatable="yes">Use custom _speech-dispatcher module for echo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="useCustomEchoSpeechServerToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="echoSpeechServersLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Echo _module:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">echoSpeechServers</property>
<accessibility>
<relation type="label-for" target="echoSpeechServers"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="echoSpeechServers">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="changed" handler="echoSpeechServersChanged" swapped="no"/>
<accessibility>
<relation type="labelled-by" target="echoSpeechServersLabel"/>
</accessibility>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="echoSpeechFamiliesLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Echo _voice:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">echoSpeechFamilies</property>
<accessibility>
<relation type="label-for" target="echoSpeechFamilies"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="echoSpeechFamilies">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="changed" handler="echoSpeechFamiliesChanged" swapped="no"/>
<accessibility>
<relation type="labelled-by" target="echoSpeechFamiliesLabel"/>
</accessibility>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="echoRateLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Echo ra_te:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">echoRateScale</property>
<accessibility>
<relation type="label-for" target="echoRateScale"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkScale" id="echoRateScale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">echoRateAdjustment</property>
<property name="round_digits">0</property>
<property name="digits">0</property>
<property name="value_pos">right</property>
<signal name="value-changed" handler="echoRateValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="echoPitchLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Echo pi_tch:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">echoPitchScale</property>
<accessibility>
<relation type="label-for" target="echoPitchScale"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">5</property>
</packing>
</child>
<child>
<object class="GtkScale" id="echoPitchScale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">echoPitchAdjustment</property>
<property name="round_digits">1</property>
<property name="value_pos">right</property>
<signal name="value-changed" handler="echoPitchValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">5</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="echoVolumeLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Echo vo_lume:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">echoVolumeScale</property>
<accessibility>
<relation type="label-for" target="echoVolumeScale"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">6</property>
</packing>
</child>
<child>
<object class="GtkScale" id="echoVolumeScale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">echoVolumeAdjustment</property>
<property name="round_digits">1</property>
<property name="value_pos">right</property>
<signal name="value-changed" handler="echoVolumeValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">6</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="useCustomEchoForKeyCheckButton">
<property name="label" translatable="yes">Use custom voice for _key echo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="checkButtonToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">7</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="useCustomEchoForCharacterCheckButton">
<property name="label" translatable="yes">Use custom voice for _character echo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="checkButtonToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">8</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="useCustomEchoForWordCheckButton">
<property name="label" translatable="yes">Use custom voice for _word echo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="checkButtonToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">9</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="useCustomEchoForSentenceCheckButton">
<property name="label" translatable="yes">Use custom voice for _sentence echo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="checkButtonToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">10</property>
<property name="width">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="echoVoiceLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Echo Voice</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child> <child>
<object class="GtkCheckButton" id="enableEchoByCharacterCheckButton"> <object class="GtkCheckButton" id="enableEchoByCharacterCheckButton">
<property name="label" translatable="yes" comments="Translators: When this option is enabled, inserted text of length 1 is spoken.">Enable echo by cha_racter</property> <property name="label" translatable="yes" comments="Translators: When this option is enabled, inserted text of length 1 is spoken.">Enable echo by cha_racter</property>
@@ -3049,7 +3356,7 @@
</object> </object>
<packing> <packing>
<property name="left_attach">0</property> <property name="left_attach">0</property>
<property name="top_attach">2</property> <property name="top_attach">3</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -3064,7 +3371,7 @@
</object> </object>
<packing> <packing>
<property name="left_attach">0</property> <property name="left_attach">0</property>
<property name="top_attach">3</property> <property name="top_attach">4</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -3079,7 +3386,7 @@
</object> </object>
<packing> <packing>
<property name="left_attach">0</property> <property name="left_attach">0</property>
<property name="top_attach">4</property> <property name="top_attach">5</property>
</packing> </packing>
</child> </child>
</object> </object>
+2 -2
View File
@@ -23,5 +23,5 @@
# Forked from Orca screen reader. # Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu # Cthulhu project: https://git.stormux.org/storm/cthulhu
version = "2026.02.17" version = "2026.02.22"
codeName = "master" codeName = "testing"
+439
View File
@@ -38,6 +38,9 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Atspi from gi.repository import Atspi
import os import os
import json
import subprocess
import sys
import threading import threading
from gi.repository import Gdk from gi.repository import Gdk
from gi.repository import GLib from gi.repository import GLib
@@ -173,6 +176,16 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.speechSystemsChoice = None self.speechSystemsChoice = None
self.speechSystemsChoices = None self.speechSystemsChoices = None
self.speechSystemsModel = None self.speechSystemsModel = None
self.echoVoice = None
self.echoSpeechFamiliesChoice = None
self.echoSpeechFamiliesChoices = None
self.echoSpeechFamiliesModel = None
self.echoSpeechServersChoice = None
self.echoSpeechServersChoices = None
self.echoSpeechServersModel = None
self.initializingEchoSpeech = False
self._echoVoiceFetchToken = 0
self._updatingEchoSpeechFamilies = False
self.systemVoice = None self.systemVoice = None
self.uppercaseVoice = None self.uppercaseVoice = None
self.window = None self.window = None
@@ -386,7 +399,12 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._initComboBox(self.get_widget("speechLanguages")) self._initComboBox(self.get_widget("speechLanguages"))
self.speechFamiliesModel = \ self.speechFamiliesModel = \
self._initComboBox(self.get_widget("speechFamilies")) self._initComboBox(self.get_widget("speechFamilies"))
self.echoSpeechServersModel = \
self._initComboBox(self.get_widget("echoSpeechServers"))
self.echoSpeechFamiliesModel = \
self._initComboBox(self.get_widget("echoSpeechFamilies"))
self._initSpeechState() self._initSpeechState()
self._initEchoSpeechState()
# TODO - JD: Will this ever be the case?? # TODO - JD: Will this ever be the case??
self._isInitialSetup = \ self._isInitialSetup = \
@@ -1671,6 +1689,261 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._setupSpeechSystems(factories) self._setupSpeechSystems(factories)
self.initializingSpeech = False self.initializingSpeech = False
def _getSpeechDispatcherFactory(self):
"""Returns the Speech Dispatcher factory if available."""
factories = cthulhu.cthulhuApp.settingsManager.getSpeechServerFactories()
for factory in factories:
try:
if factory.SpeechServer.getFactoryName() == guilabels.SPEECH_DISPATCHER:
return factory
except Exception:
debug.printException(debug.LEVEL_FINEST)
return None
def _setEchoVoiceFamily(self, family):
"""Sets echo voice family from the selected server family object."""
if not family:
return
name = family.get(speechserver.VoiceFamily.NAME)
language = family.get(speechserver.VoiceFamily.LANG)
dialect = family.get(speechserver.VoiceFamily.DIALECT)
variant = family.get(speechserver.VoiceFamily.VARIANT)
self.echoVoice[acss.ACSS.FAMILY] = {
speechserver.VoiceFamily.NAME: name,
speechserver.VoiceFamily.LANG: language,
speechserver.VoiceFamily.DIALECT: dialect,
speechserver.VoiceFamily.VARIANT: variant,
}
self.echoVoice['established'] = True
def _populateEchoSpeechFamilies(self, families):
"""Populate the echo family combobox from the provided families list."""
combobox = self.get_widget("echoSpeechFamilies")
combobox.set_model(None)
self.echoSpeechFamiliesModel.clear()
self.echoSpeechFamiliesChoices = list(families or [])
self.echoSpeechFamiliesChoice = None
selectedIndex = 0
selectedMatchFound = False
selectedFamily = self.echoVoice.get(acss.ACSS.FAMILY)
selectedName = selectedLanguage = selectedDialect = selectedVariant = None
if selectedFamily:
selectedName = selectedFamily.get(speechserver.VoiceFamily.NAME)
selectedLanguage = selectedFamily.get(speechserver.VoiceFamily.LANG)
selectedDialect = selectedFamily.get(speechserver.VoiceFamily.DIALECT)
selectedVariant = selectedFamily.get(speechserver.VoiceFamily.VARIANT)
for i, family in enumerate(self.echoSpeechFamiliesChoices):
name = family.get(speechserver.VoiceFamily.NAME) or ""
language = family.get(speechserver.VoiceFamily.LANG) or ""
dialect = family.get(speechserver.VoiceFamily.DIALECT) or ""
variant = family.get(speechserver.VoiceFamily.VARIANT)
display = name
locale = language
if dialect:
locale = f"{language}-{dialect}" if language else dialect
if locale:
display = f"{name} ({locale})"
self.echoSpeechFamiliesModel.append((i, display))
if selectedName == name and selectedLanguage == language \
and selectedDialect == dialect and selectedVariant == variant:
selectedIndex = i
selectedMatchFound = True
combobox.set_model(self.echoSpeechFamiliesModel)
if self.echoSpeechFamiliesChoices:
self._updatingEchoSpeechFamilies = True
try:
combobox.set_active(selectedIndex)
finally:
self._updatingEchoSpeechFamilies = False
self.echoSpeechFamiliesChoice = self.echoSpeechFamiliesChoices[selectedIndex]
def _fetchEchoSpeechFamiliesWorker(self, serverInfo, requestToken):
"""Fetch module-specific voices in a subprocess and apply asynchronously."""
queryScript = """
import json
import sys
from cthulhu import speechdispatcherfactory
from cthulhu import speechserver
serverInfo = json.loads(sys.argv[1])
if isinstance(serverInfo, list):
serverInfo = tuple(serverInfo)
server = speechdispatcherfactory.SpeechServer.getSpeechServer(serverInfo)
families = server.getVoiceFamilies() if server else []
result = []
for family in families:
result.append({
"name": family.get(speechserver.VoiceFamily.NAME),
"lang": family.get(speechserver.VoiceFamily.LANG),
"dialect": family.get(speechserver.VoiceFamily.DIALECT),
"variant": family.get(speechserver.VoiceFamily.VARIANT),
})
print(json.dumps(result))
"""
familyDefs = None
try:
completed = subprocess.run(
[sys.executable, "-c", queryScript, json.dumps(serverInfo)],
capture_output=True,
text=True,
timeout=4.0,
check=False,
)
if completed.returncode == 0 and completed.stdout.strip():
familyDefs = json.loads(completed.stdout.strip())
else:
tokens = [
"PREFERENCES DIALOG: Echo voice fetch failed for",
serverInfo,
"rc=",
completed.returncode,
]
debug.printTokens(debug.LEVEL_WARNING, tokens, True)
except subprocess.TimeoutExpired:
tokens = ["PREFERENCES DIALOG: Echo voice fetch timed out for", serverInfo]
debug.printTokens(debug.LEVEL_WARNING, tokens, True)
except Exception:
debug.printException(debug.LEVEL_WARNING)
GLib.idle_add(self._applyFetchedEchoSpeechFamilies, requestToken, familyDefs)
def _applyFetchedEchoSpeechFamilies(self, requestToken, familyDefs):
"""Apply fetched echo families if this is still the active request."""
if requestToken != self._echoVoiceFetchToken:
return False
if not isinstance(familyDefs, list):
return False
families = []
for familyDef in familyDefs:
if not isinstance(familyDef, dict):
continue
families.append(
speechserver.VoiceFamily({
speechserver.VoiceFamily.NAME: familyDef.get("name"),
speechserver.VoiceFamily.LANG: familyDef.get("lang"),
speechserver.VoiceFamily.DIALECT: familyDef.get("dialect"),
speechserver.VoiceFamily.VARIANT: familyDef.get("variant"),
})
)
if not families:
return False
self._populateEchoSpeechFamilies(families)
return False
def _setupEchoSpeechFamilies(self):
"""Populate echo voice family list for the current echo server."""
self._echoVoiceFetchToken += 1
requestToken = self._echoVoiceFetchToken
useCustomModule = self.get_widget("useCustomEchoSpeechServerCheckButton").get_active()
fallbackFamilies = list(self.speechFamiliesChoices or [])
self._populateEchoSpeechFamilies(fallbackFamilies)
if useCustomModule and self.echoSpeechServersChoice:
serverInfo = self.echoSpeechServersChoice.getInfo()
thread = threading.Thread(
target=self._fetchEchoSpeechFamiliesWorker,
args=(serverInfo, requestToken),
daemon=True,
)
thread.start()
def _setEchoSpeechServersChoice(self, serverInfo):
"""Set the active echo module based on stored server info."""
if not self.echoSpeechServersChoices:
self.echoSpeechServersChoice = None
self._setupEchoSpeechFamilies()
return
valueSet = False
for i, server in enumerate(self.echoSpeechServersChoices):
info = server.getInfo()
if serverInfo and info == serverInfo:
self.get_widget("echoSpeechServers").set_active(i)
self.echoSpeechServersChoice = server
valueSet = True
break
if not valueSet:
self.get_widget("echoSpeechServers").set_active(0)
self.echoSpeechServersChoice = self.echoSpeechServersChoices[0]
self._setupEchoSpeechFamilies()
def _setupEchoSpeechServers(self):
"""Populate available speech-dispatcher modules for echo."""
combobox = self.get_widget("echoSpeechServers")
combobox.set_model(None)
self.echoSpeechServersModel.clear()
self.echoSpeechServersChoices = []
self.echoSpeechServersChoice = None
if not self.prefsDict.get('enableSpeech', True):
combobox.set_model(self.echoSpeechServersModel)
self._setupEchoSpeechFamilies()
return
factory = self._getSpeechDispatcherFactory()
if factory is None:
combobox.set_model(self.echoSpeechServersModel)
self._setupEchoSpeechFamilies()
return
try:
self.echoSpeechServersChoices = factory.SpeechServer.getSpeechServers()
except Exception:
debug.printException(debug.LEVEL_WARNING)
self.echoSpeechServersChoices = []
for i, server in enumerate(self.echoSpeechServersChoices):
name = server.getInfo()[0]
self.echoSpeechServersModel.append((i, name))
combobox.set_model(self.echoSpeechServersModel)
self._setEchoSpeechServersChoice(self.prefsDict.get("echoSpeechServerInfo"))
def _initEchoSpeechState(self):
"""Initialize echo voice controls and choices."""
self.echoVoice = acss.ACSS(
self.prefsDict.get("echoVoice", settings.echoVoice) or {})
baseVoice = self.defaultVoice or acss.ACSS(
self.prefsDict.get("voices", {}).get(settings.DEFAULT_VOICE, {}))
rate = self.echoVoice.get(acss.ACSS.RATE, baseVoice.get(acss.ACSS.RATE, 50.0))
pitch = self.echoVoice.get(acss.ACSS.AVERAGE_PITCH, baseVoice.get(acss.ACSS.AVERAGE_PITCH, 5.0))
volume = self.echoVoice.get(acss.ACSS.GAIN, baseVoice.get(acss.ACSS.GAIN, 10.0))
self.get_widget("echoRateScale").set_value(rate)
self.get_widget("echoPitchScale").set_value(pitch)
self.get_widget("echoVolumeScale").set_value(volume)
self.initializingEchoSpeech = True
self._setupEchoSpeechServers()
self.initializingEchoSpeech = False
self._setEchoVoiceItems()
def _setSpokenTextAttributes(self, view, setAttributes, def _setSpokenTextAttributes(self, view, setAttributes,
state, moveToTop=False): state, moveToTop=False):
"""Given a set of spoken text attributes, update the model used by the """Given a set of spoken text attributes, update the model used by the
@@ -2404,6 +2677,19 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
prefs["enableEchoByWord"]) prefs["enableEchoByWord"])
self.get_widget("enableEchoBySentenceCheckButton").set_active( \ self.get_widget("enableEchoBySentenceCheckButton").set_active( \
prefs["enableEchoBySentence"]) prefs["enableEchoBySentence"])
self.get_widget("useCustomEchoVoiceCheckButton").set_active(
prefs.get("useCustomEchoVoice", settings.useCustomEchoVoice))
self.get_widget("useCustomEchoSpeechServerCheckButton").set_active(
prefs.get("useCustomEchoSpeechServer", settings.useCustomEchoSpeechServer))
self.get_widget("useCustomEchoForKeyCheckButton").set_active(
prefs.get("useCustomEchoForKey", settings.useCustomEchoForKey))
self.get_widget("useCustomEchoForCharacterCheckButton").set_active(
prefs.get("useCustomEchoForCharacter", settings.useCustomEchoForCharacter))
self.get_widget("useCustomEchoForWordCheckButton").set_active(
prefs.get("useCustomEchoForWord", settings.useCustomEchoForWord))
self.get_widget("useCustomEchoForSentenceCheckButton").set_active(
prefs.get("useCustomEchoForSentence", settings.useCustomEchoForSentence))
self._setEchoVoiceItems()
# Text attributes pane. # Text attributes pane.
# #
@@ -2906,6 +3192,45 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.get_widget("enableNavigationKeysCheckButton").set_sensitive(enable) self.get_widget("enableNavigationKeysCheckButton").set_sensitive(enable)
self.get_widget("enableDiacriticalKeysCheckButton").set_sensitive( \ self.get_widget("enableDiacriticalKeysCheckButton").set_sensitive( \
enable) enable)
self._setEchoVoiceItems()
def _setEchoVoiceItems(self):
"""[In]sensitize echo voice controls based on current state."""
useCustomVoice = self.get_widget("useCustomEchoVoiceCheckButton").get_active()
useCustomModule = self.get_widget("useCustomEchoSpeechServerCheckButton").get_active()
keyEchoEnabled = self.get_widget("keyEchoCheckButton").get_active()
charEchoEnabled = self.get_widget("enableEchoByCharacterCheckButton").get_active()
wordEchoEnabled = self.get_widget("enableEchoByWordCheckButton").get_active()
sentenceEchoEnabled = self.get_widget("enableEchoBySentenceCheckButton").get_active()
speechEnabled = self.get_widget("speechSupportCheckButton").get_active()
speechSystemIsDispatcher = False
if self.speechSystemsChoice:
try:
speechSystemIsDispatcher = \
self.speechSystemsChoice.SpeechServer.getFactoryName() == guilabels.SPEECH_DISPATCHER
except Exception:
speechSystemIsDispatcher = False
voiceControlsEnabled = useCustomVoice and speechEnabled
moduleOverrideAvailable = voiceControlsEnabled and speechSystemIsDispatcher
moduleControlsEnabled = moduleOverrideAvailable and useCustomModule
self.get_widget("useCustomEchoSpeechServerCheckButton").set_sensitive(moduleOverrideAvailable)
self.get_widget("echoSpeechServers").set_sensitive(moduleControlsEnabled)
self.get_widget("echoSpeechFamilies").set_sensitive(voiceControlsEnabled)
self.get_widget("echoRateScale").set_sensitive(useCustomVoice)
self.get_widget("echoPitchScale").set_sensitive(useCustomVoice)
self.get_widget("echoVolumeScale").set_sensitive(useCustomVoice)
self.get_widget("useCustomEchoForKeyCheckButton").set_sensitive(useCustomVoice and keyEchoEnabled)
self.get_widget("useCustomEchoForCharacterCheckButton").set_sensitive(
useCustomVoice and charEchoEnabled)
self.get_widget("useCustomEchoForWordCheckButton").set_sensitive(useCustomVoice and wordEchoEnabled)
self.get_widget("useCustomEchoForSentenceCheckButton").set_sensitive(
useCustomVoice and sentenceEchoEnabled)
def _presentMessage(self, text, interrupt=False, voice=None): def _presentMessage(self, text, interrupt=False, voice=None):
"""If the text field is not None, presents the given text, optionally """If the text field is not None, presents the given text, optionally
@@ -3232,6 +3557,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
enable = widget.get_active() enable = widget.get_active()
self.prefsDict["enableSpeech"] = enable self.prefsDict["enableSpeech"] = enable
self.get_widget("speechOptionsGrid").set_sensitive(enable) self.get_widget("speechOptionsGrid").set_sensitive(enable)
self._setupEchoSpeechServers()
self._setEchoVoiceItems()
def onlySpeakDisplayedTextToggled(self, widget): def onlySpeakDisplayedTextToggled(self, widget):
"""Signal handler for the "toggled" signal for the GtkCheckButton """Signal handler for the "toggled" signal for the GtkCheckButton
@@ -3263,6 +3590,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
selectedIndex = widget.get_active() selectedIndex = widget.get_active()
self.speechSystemsChoice = self.speechSystemsChoices[selectedIndex] self.speechSystemsChoice = self.speechSystemsChoices[selectedIndex]
self._setupSpeechServers() self._setupSpeechServers()
self._setupEchoSpeechServers()
self._setEchoVoiceItems()
def speechServersChanged(self, widget): def speechServersChanged(self, widget):
"""Signal handler for the "changed" signal for the speechServers """Signal handler for the "changed" signal for the speechServers
@@ -3295,6 +3624,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
pass pass
self._setupVoices() self._setupVoices()
self._setEchoVoiceItems()
def speechLanguagesChanged(self, widget): def speechLanguagesChanged(self, widget):
"""Signal handler for the "value_changed" signal for the languages """Signal handler for the "value_changed" signal for the languages
@@ -3433,6 +3763,75 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
voices.get(settings.DEFAULT_VOICE, {})[acss.ACSS.GAIN] = volume voices.get(settings.DEFAULT_VOICE, {})[acss.ACSS.GAIN] = volume
cthulhu.cthulhuApp.settingsManager.setSetting('voices', voices) cthulhu.cthulhuApp.settingsManager.setSetting('voices', voices)
def useCustomEchoVoiceToggled(self, widget):
"""Signal handler for enabling/disabling custom echo voice settings."""
self.prefsDict["useCustomEchoVoice"] = widget.get_active()
self._setEchoVoiceItems()
self._setupEchoSpeechFamilies()
def useCustomEchoSpeechServerToggled(self, widget):
"""Signal handler for enabling/disabling custom echo module override."""
self.prefsDict["useCustomEchoSpeechServer"] = widget.get_active()
self._setEchoVoiceItems()
self._setupEchoSpeechFamilies()
def echoSpeechServersChanged(self, widget):
"""Signal handler for selecting the echo speech-dispatcher module."""
if self.initializingEchoSpeech:
return
selectedIndex = widget.get_active()
if selectedIndex < 0:
self.echoSpeechServersChoice = None
self.prefsDict["echoSpeechServerInfo"] = None
self._setupEchoSpeechFamilies()
return
self.echoSpeechServersChoice = self.echoSpeechServersChoices[selectedIndex]
self.prefsDict["echoSpeechServerInfo"] = self.echoSpeechServersChoice.getInfo()
self._setupEchoSpeechFamilies()
def echoSpeechFamiliesChanged(self, widget):
"""Signal handler for selecting the echo voice family."""
if self.initializingEchoSpeech or self._updatingEchoSpeechFamilies:
return
selectedIndex = widget.get_active()
if selectedIndex < 0:
self.echoSpeechFamiliesChoice = None
return
self.echoSpeechFamiliesChoice = self.echoSpeechFamiliesChoices[selectedIndex]
self._setEchoVoiceFamily(self.echoSpeechFamiliesChoice)
def echoRateValueChanged(self, widget):
"""Signal handler for changing custom echo rate."""
if self.echoVoice is None:
self.echoVoice = acss.ACSS({})
self.echoVoice[acss.ACSS.RATE] = widget.get_value()
self.echoVoice['established'] = True
def echoPitchValueChanged(self, widget):
"""Signal handler for changing custom echo pitch."""
if self.echoVoice is None:
self.echoVoice = acss.ACSS({})
self.echoVoice[acss.ACSS.AVERAGE_PITCH] = widget.get_value()
self.echoVoice['established'] = True
def echoVolumeValueChanged(self, widget):
"""Signal handler for changing custom echo volume."""
if self.echoVoice is None:
self.echoVoice = acss.ACSS({})
self.echoVoice[acss.ACSS.GAIN] = widget.get_value()
self.echoVoice['established'] = True
def checkButtonToggled(self, widget): def checkButtonToggled(self, widget):
"""Signal handler for "toggled" signal for basic GtkCheckButton """Signal handler for "toggled" signal for basic GtkCheckButton
widgets. The user has altered the state of the checkbox. widgets. The user has altered the state of the checkbox.
@@ -3450,6 +3849,15 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
# strip "CheckButton" from the end. # strip "CheckButton" from the end.
settingName = settingName[:-11] settingName = settingName[:-11]
self.prefsDict[settingName] = widget.get_active() self.prefsDict[settingName] = widget.get_active()
if settingName in [
"enableEchoByCharacter",
"enableEchoByWord",
"enableEchoBySentence",
"useCustomEchoForKey",
"useCustomEchoForCharacter",
"useCustomEchoForWord",
"useCustomEchoForSentence"]:
self._setEchoVoiceItems()
def keyEchoChecked(self, widget): def keyEchoChecked(self, widget):
"""Signal handler for the "toggled" signal for the """Signal handler for the "toggled" signal for the
@@ -4252,6 +4660,35 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
settings.SYSTEM_VOICE: acss.ACSS(self.systemVoice), settings.SYSTEM_VOICE: acss.ACSS(self.systemVoice),
} }
self.prefsDict["useCustomEchoVoice"] = \
self.get_widget("useCustomEchoVoiceCheckButton").get_active()
self.prefsDict["useCustomEchoSpeechServer"] = \
self.get_widget("useCustomEchoSpeechServerCheckButton").get_active()
self.prefsDict["useCustomEchoForKey"] = \
self.get_widget("useCustomEchoForKeyCheckButton").get_active()
self.prefsDict["useCustomEchoForCharacter"] = \
self.get_widget("useCustomEchoForCharacterCheckButton").get_active()
self.prefsDict["useCustomEchoForWord"] = \
self.get_widget("useCustomEchoForWordCheckButton").get_active()
self.prefsDict["useCustomEchoForSentence"] = \
self.get_widget("useCustomEchoForSentenceCheckButton").get_active()
if self.echoVoice is None:
self.echoVoice = acss.ACSS({})
# Persist slider values directly so saving does not depend on
# value-changed signal timing.
self.echoVoice[acss.ACSS.RATE] = self.get_widget("echoRateScale").get_value()
self.echoVoice[acss.ACSS.AVERAGE_PITCH] = self.get_widget("echoPitchScale").get_value()
self.echoVoice[acss.ACSS.GAIN] = self.get_widget("echoVolumeScale").get_value()
self.echoVoice['established'] = True
self.prefsDict["echoVoice"] = acss.ACSS(self.echoVoice)
if self.echoSpeechServersChoice:
self.prefsDict["echoSpeechServerInfo"] = self.echoSpeechServersChoice.getInfo()
else:
self.prefsDict["echoSpeechServerInfo"] = None
def applyButtonClicked(self, widget): def applyButtonClicked(self, widget):
"""Signal handler for the "clicked" signal for the applyButton """Signal handler for the "clicked" signal for the applyButton
GtkButton widget. The user has clicked the Apply button. GtkButton widget. The user has clicked the Apply button.
@@ -4284,6 +4721,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._refresh_dynamic_plugin_tabs() self._refresh_dynamic_plugin_tabs()
braille.checkBrailleSetting() braille.checkBrailleSetting()
self._initSpeechState() self._initSpeechState()
self._initEchoSpeechState()
self._populateKeyBindings() self._populateKeyBindings()
self.__initProfileCombo() self.__initProfileCombo()
@@ -4546,6 +4984,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
braille.checkBrailleSetting() braille.checkBrailleSetting()
self._initSpeechState() self._initSpeechState()
self._initEchoSpeechState()
self._populateKeyBindings() self._populateKeyBindings()
+3
View File
@@ -5122,6 +5122,9 @@ class Utilities:
if not event.type.startswith("object:text-changed:insert"): if not event.type.startswith("object:text-changed:insert"):
return False return False
if not cthulhu.cthulhuApp.settingsManager.getSetting("enableKeyEcho"):
return False
if AXUtilities.is_focusable(event.source) \ if AXUtilities.is_focusable(event.source) \
and not AXUtilities.is_focused(event.source) \ and not AXUtilities.is_focused(event.source) \
and event.source != cthulhu_state.locusOfFocus: and event.source != cthulhu_state.locusOfFocus:
+66 -4
View File
@@ -44,6 +44,7 @@ from gi.repository import Gdk
import re import re
import time import time
import cthulhu.acss as acss
import cthulhu.braille as braille import cthulhu.braille as braille
import cthulhu.cmdnames as cmdnames import cthulhu.cmdnames as cmdnames
import cthulhu.dbus_service as dbus_service import cthulhu.dbus_service as dbus_service
@@ -1818,6 +1819,9 @@ class Script(script.Script):
if len(string) != 1: if len(string) != 1:
return return
if not cthulhu.cthulhuApp.settingsManager.getSetting('enableKeyEcho'):
return
if cthulhu.cthulhuApp.settingsManager.getSetting('enableEchoBySentence') \ if cthulhu.cthulhuApp.settingsManager.getSetting('enableEchoBySentence') \
and self.echoPreviousSentence(event.source): and self.echoPreviousSentence(event.source):
return return
@@ -2228,9 +2232,12 @@ class Script(script.Script):
sentence = self.utilities.substring(obj, sentenceStartOffset + 1, sentence = self.utilities.substring(obj, sentenceStartOffset + 1,
sentenceEndOffset + 1) sentenceEndOffset + 1)
voice = self.speechGenerator.voice(obj=obj, string=sentence)
sentence = self.utilities.adjustForRepeats(sentence) sentence = self.utilities.adjustForRepeats(sentence)
self.speakMessage(sentence, voice) if self._shouldUseCustomEchoVoice("sentence"):
speech.speakEchoText(sentence, self._getCustomEchoVoice())
else:
voice = self.speechGenerator.voice(obj=obj, string=sentence)
self.speakMessage(sentence, voice)
return True return True
def echoPreviousWord(self, obj, offset=None): def echoPreviousWord(self, obj, offset=None):
@@ -2297,9 +2304,12 @@ class Script(script.Script):
word = self.utilities.\ word = self.utilities.\
substring(obj, wordStartOffset + 1, wordEndOffset + 1) substring(obj, wordStartOffset + 1, wordEndOffset + 1)
voice = self.speechGenerator.voice(obj=obj, string=word)
word = self.utilities.adjustForRepeats(word) word = self.utilities.adjustForRepeats(word)
self.speakMessage(word, voice) if self._shouldUseCustomEchoVoice("word"):
speech.speakEchoText(word, self._getCustomEchoVoice())
else:
voice = self.speechGenerator.voice(obj=obj, string=word)
self.speakMessage(word, voice)
return True return True
def sayCharacter(self, obj): def sayCharacter(self, obj):
@@ -2901,6 +2911,9 @@ class Script(script.Script):
if not event.shouldEcho or event.isCthulhuModified(): if not event.shouldEcho or event.isCthulhuModified():
return False return False
if not cthulhu.cthulhuApp.settingsManager.getSetting('enableKeyEcho'):
return False
role = AXObject.get_role(cthulhu_state.locusOfFocus) role = AXObject.get_role(cthulhu_state.locusOfFocus)
if role in [Atspi.Role.DIALOG, Atspi.Role.FRAME, Atspi.Role.WINDOW]: if role in [Atspi.Role.DIALOG, Atspi.Role.FRAME, Atspi.Role.WINDOW]:
focusedObject = AXUtilities.get_focused_object(cthulhu_state.activeWindow) focusedObject = AXUtilities.get_focused_object(cthulhu_state.activeWindow)
@@ -3341,6 +3354,47 @@ class Script(script.Script):
# # # #
######################################################################## ########################################################################
def _shouldUseCustomEchoVoice(self, echoType):
"""Returns True if custom echo settings should be used for the given type."""
settingForType = {
"key": "useCustomEchoForKey",
"character": "useCustomEchoForCharacter",
"word": "useCustomEchoForWord",
"sentence": "useCustomEchoForSentence",
}
settingName = settingForType.get(echoType)
if not settingName:
return False
settingsManager = cthulhu.cthulhuApp.settingsManager
if not settingsManager.getSetting("enableKeyEcho"):
return False
if not settingsManager.getSetting("useCustomEchoVoice"):
return False
return settingsManager.getSetting(settingName)
def _getCustomEchoVoice(self):
"""Returns the effective ACSS voice for custom echo output."""
settingsManager = cthulhu.cthulhuApp.settingsManager
voices = settingsManager.getSetting("voices") or {}
defaultVoice = acss.ACSS(voices.get(settings.DEFAULT_VOICE, {}))
echoVoice = settingsManager.getSetting("echoVoice")
if not echoVoice:
return defaultVoice
try:
defaultVoice.update(acss.ACSS(echoVoice))
except Exception:
debug.printException(debug.LEVEL_INFO)
return defaultVoice
def speakKeyEvent(self, event): def speakKeyEvent(self, event):
"""Method to speak a keyboard event. Scripts should use this method """Method to speak a keyboard event. Scripts should use this method
rather than calling speech.speakKeyEvent directly.""" rather than calling speech.speakKeyEvent directly."""
@@ -3349,6 +3403,10 @@ class Script(script.Script):
if event.is_printable_key(): if event.is_printable_key():
string = event.event_string string = event.event_string
if self._shouldUseCustomEchoVoice("key"):
speech.speakEchoKeyEvent(event, self._getCustomEchoVoice())
return
voice = self.speechGenerator.voice(string=string) voice = self.speechGenerator.voice(string=string)
speech.speakKeyEvent(event, voice) speech.speakKeyEvent(event, voice)
@@ -3356,6 +3414,10 @@ class Script(script.Script):
"""Method to speak a single character. Scripts should use this """Method to speak a single character. Scripts should use this
method rather than calling speech.speakCharacter directly.""" method rather than calling speech.speakCharacter directly."""
if self._shouldUseCustomEchoVoice("character"):
speech.speakEchoCharacter(character, self._getCustomEchoVoice())
return
voice = self.speechGenerator.voice(string=character) voice = self.speechGenerator.voice(string=character)
speech.speakCharacter(character, voice) speech.speakCharacter(character, voice)
+16
View File
@@ -53,6 +53,14 @@ userCustomizableSettings = [
"enableEchoByWord", "enableEchoByWord",
"enableEchoBySentence", "enableEchoBySentence",
"enableKeyEcho", "enableKeyEcho",
"useCustomEchoVoice",
"useCustomEchoSpeechServer",
"echoSpeechServerInfo",
"echoVoice",
"useCustomEchoForKey",
"useCustomEchoForCharacter",
"useCustomEchoForWord",
"useCustomEchoForSentence",
"gameMode", "gameMode",
"nvda2cthulhuTranslateEnabled", "nvda2cthulhuTranslateEnabled",
"enableAlphabeticKeys", "enableAlphabeticKeys",
@@ -352,6 +360,14 @@ enableDiacriticalKeys = False
enableEchoByCharacter = False enableEchoByCharacter = False
enableEchoByWord = False enableEchoByWord = False
enableEchoBySentence = False enableEchoBySentence = False
useCustomEchoVoice = False
useCustomEchoSpeechServer = False
echoSpeechServerInfo = None
echoVoice = ACSS({})
useCustomEchoForKey = True
useCustomEchoForCharacter = True
useCustomEchoForWord = False
useCustomEchoForSentence = False
presentLockingKeys = None presentLockingKeys = None
# Mouse review # Mouse review
+17
View File
@@ -314,6 +314,13 @@ class SettingsManager(object):
converted_voices[voice_type] = voice_def converted_voices[voice_type] = voice_def
general["voices"] = converted_voices general["voices"] = converted_voices
echo_voice = general.get("echoVoice")
if isinstance(echo_voice, dict):
try:
general["echoVoice"] = ACSS(echo_voice)
except Exception:
general["echoVoice"] = echo_voice
return general return general
def getDefaultSetting(self, settingName: str) -> Any: def getDefaultSetting(self, settingName: str) -> Any:
@@ -632,6 +639,14 @@ class SettingsManager(object):
debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printMessage(debug.LEVEL_INFO, msg, True)
self.profileGeneral = {} self.profileGeneral = {}
alwaysPersistKeys = {
'useCustomEchoVoice',
'useCustomEchoSpeechServer',
'useCustomEchoForKey',
'useCustomEchoForCharacter',
'useCustomEchoForWord',
'useCustomEchoForSentence',
}
for key, value in general.items(): for key, value in general.items():
if key in ['startingProfile', 'activeProfile']: if key in ['startingProfile', 'activeProfile']:
@@ -640,6 +655,8 @@ class SettingsManager(object):
self.profileGeneral[key] = value self.profileGeneral[key] = value
elif key in ['activePlugins', 'pluginSources']: elif key in ['activePlugins', 'pluginSources']:
self.profileGeneral[key] = copy.deepcopy(value) self.profileGeneral[key] = copy.deepcopy(value)
elif key in alwaysPersistKeys:
self.profileGeneral[key] = copy.deepcopy(value)
elif value != self.defaultGeneral.get(key): elif value != self.defaultGeneral.get(key):
self.profileGeneral[key] = value self.profileGeneral[key] = value
elif self.general.get(key) != value: elif self.general.get(key) != value:
+129 -11
View File
@@ -69,6 +69,7 @@ def _ensureLogger() -> None:
# The speech server to use for all speech operations. # The speech server to use for all speech operations.
# #
_speechserver: Optional[SpeechServer] = None _speechserver: Optional[SpeechServer] = None
_echoSpeechserver: Optional[SpeechServer] = None
# The last time something was spoken. # The last time something was spoken.
_timestamp: float = 0.0 _timestamp: float = 0.0
@@ -76,12 +77,15 @@ _timestamp: float = 0.0
# Optional callback for live monitoring of spoken text. # Optional callback for live monitoring of spoken text.
_monitorWriteTextCallback: Optional[Callable[[str], None]] = None _monitorWriteTextCallback: Optional[Callable[[str], None]] = None
def _initSpeechServer(moduleName: Optional[str], speechServerInfo: Optional[Any]) -> None: def _isSpeechDispatcherFactory(moduleName: Optional[str]) -> bool:
if not moduleName:
return False
return moduleName.split(".")[-1] == "speechdispatcherfactory"
global _speechserver def _initSpeechServer(moduleName: Optional[str], speechServerInfo: Optional[Any]) -> SpeechServer:
if not moduleName: if not moduleName:
return raise Exception("ERROR: No speech server module name provided")
factory = None factory = None
try: try:
@@ -94,35 +98,58 @@ def _initSpeechServer(moduleName: Optional[str], speechServerInfo: Optional[Any]
# Now, get the speech server we care about. # Now, get the speech server we care about.
# #
speechServerInfo = settings.speechServerInfo speech_server = None
if factory: if factory:
if speechServerInfo: if speechServerInfo:
_speechserver = factory.SpeechServer.getSpeechServer(speechServerInfo) # type: ignore speech_server = factory.SpeechServer.getSpeechServer(speechServerInfo) # type: ignore
if not _speechserver: if not speech_server:
_speechserver = factory.SpeechServer.getSpeechServer() # type: ignore speech_server = factory.SpeechServer.getSpeechServer() # type: ignore
if speechServerInfo: if speechServerInfo:
tokens = ["SPEECH: Invalid speechServerInfo:", speechServerInfo] tokens = ["SPEECH: Invalid speechServerInfo:", speechServerInfo]
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
if not _speechserver: if not speech_server:
raise Exception(f"ERROR: No speech server for factory: {moduleName}") raise Exception(f"ERROR: No speech server for factory: {moduleName}")
return speech_server
def _refreshEchoSpeechServer() -> None:
global _echoSpeechserver
_echoSpeechserver = None
if not settings.enableSpeech or not settings.useCustomEchoSpeechServer:
return
if not _isSpeechDispatcherFactory(settings.speechServerFactory):
return
try:
_echoSpeechserver = _initSpeechServer(
settings.speechServerFactory,
settings.echoSpeechServerInfo
)
except Exception:
debug.printException(debug.LEVEL_INFO)
_echoSpeechserver = None
def init() -> None: def init() -> None:
global _speechserver
debug.printMessage(debug.LEVEL_INFO, 'SPEECH: Initializing', True) debug.printMessage(debug.LEVEL_INFO, 'SPEECH: Initializing', True)
if _speechserver: if _speechserver:
debug.printMessage(debug.LEVEL_INFO, 'SPEECH: Already initialized', True) debug.printMessage(debug.LEVEL_INFO, 'SPEECH: Already initialized', True)
_refreshEchoSpeechServer()
return return
chosenModuleName = settings.speechServerFactory chosenModuleName = settings.speechServerFactory
try: try:
_initSpeechServer(chosenModuleName, settings.speechServerInfo) _speechserver = _initSpeechServer(chosenModuleName, settings.speechServerInfo)
except Exception: except Exception:
moduleNames = settings.speechFactoryModules moduleNames = settings.speechFactoryModules
for moduleName in moduleNames: for moduleName in moduleNames:
if moduleName != settings.speechServerFactory: if moduleName != settings.speechServerFactory:
try: try:
_initSpeechServer(moduleName, None) _speechserver = _initSpeechServer(moduleName, None)
if _speechserver: if _speechserver:
chosenModuleName = moduleName chosenModuleName = moduleName
break break
@@ -144,6 +171,7 @@ def init() -> None:
msg = 'SPEECH: Not available' msg = 'SPEECH: Not available'
debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printMessage(debug.LEVEL_INFO, msg, True)
_refreshEchoSpeechServer()
debug.printMessage(debug.LEVEL_INFO, 'SPEECH: Initialized', True) debug.printMessage(debug.LEVEL_INFO, 'SPEECH: Initialized', True)
def checkSpeechSetting() -> None: def checkSpeechSetting() -> None:
@@ -157,6 +185,9 @@ def checkSpeechSetting() -> None:
if not _speechserver: if not _speechserver:
init() init()
return
_refreshEchoSpeechServer()
def getSpeechServer() -> Optional[SpeechServer]: def getSpeechServer() -> Optional[SpeechServer]:
"""Returns the speech server instance.""" """Returns the speech server instance."""
@@ -170,6 +201,7 @@ def setSpeechServer(speechServer: SpeechServer) -> None:
""" """
global _speechserver global _speechserver
_speechserver = speechServer _speechserver = speechServer
_refreshEchoSpeechServer()
def set_monitor_callbacks(writeText: Optional[Callable[[str], None]] = None) -> None: def set_monitor_callbacks(writeText: Optional[Callable[[str], None]] = None) -> None:
"""Sets runtime callbacks for live speech monitoring.""" """Sets runtime callbacks for live speech monitoring."""
@@ -418,6 +450,35 @@ def speakKeyEvent(event: Any, acss: Optional[Any] = None) -> None:
if _speechserver: if _speechserver:
_speechserver.speakKeyEvent(event, acss) # type: ignore _speechserver.speakKeyEvent(event, acss) # type: ignore
def speakEchoKeyEvent(event: Any, acss: Optional[Any] = None) -> None:
"""Speaks a key event for key/typing echo using echo speech settings."""
_ensureLogger()
if settings.silenceSpeech:
return
global _timestamp
if _timestamp:
msg = f"SPEECH: Last spoke {time.time() - _timestamp:.4f} seconds ago"
debug.printMessage(debug.LEVEL_INFO, msg, True)
_timestamp = time.time()
keyname = event.getKeyName()
lockingStateString = event.getLockingStateString()
acss = __resolveACSS(acss)
msg = f"{keyname} {lockingStateString}"
logLine = f"SPEECH ECHO OUTPUT: '{msg.strip()}' {acss}"
debug.printMessage(debug.LEVEL_INFO, logLine, True)
if log:
log.info(logLine)
_write_to_monitor(msg.strip())
server = _echoSpeechserver or _speechserver
if server:
server.speakKeyEvent(event, acss) # type: ignore
def speakCharacter(character: str, acss: Optional[Any] = None) -> None: def speakCharacter(character: str, acss: Optional[Any] = None) -> None:
"""Speaks a single character immediately. """Speaks a single character immediately.
@@ -452,6 +513,58 @@ def speakCharacter(character: str, acss: Optional[Any] = None) -> None:
if _speechserver: if _speechserver:
_speechserver.speakCharacter(character, acss=acss) # type: ignore _speechserver.speakCharacter(character, acss=acss) # type: ignore
def speakEchoCharacter(character: str, acss: Optional[Any] = None) -> None:
"""Speaks a single character for key/typing echo using echo speech settings."""
_ensureLogger()
if settings.silenceSpeech:
return
global _timestamp
if _timestamp:
msg = f"SPEECH: Last spoke {time.time() - _timestamp:.4f} seconds ago"
debug.printMessage(debug.LEVEL_INFO, msg, True)
_timestamp = time.time()
acss = __resolveACSS(acss)
tokens = [f"SPEECH ECHO OUTPUT: '{character}'", acss]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if log:
log.info(f"SPEECH ECHO OUTPUT: '{character}'")
_write_to_monitor(character)
server = _echoSpeechserver or _speechserver
if server:
server.speakCharacter(character, acss=acss) # type: ignore
def speakEchoText(text: str, acss: Optional[Any] = None, interrupt: bool = True) -> None:
"""Speaks text for typing echo using echo speech settings."""
_ensureLogger()
if settings.silenceSpeech:
return
global _timestamp
if _timestamp:
msg = f"SPEECH: Last spoke {time.time() - _timestamp:.4f} seconds ago"
debug.printMessage(debug.LEVEL_INFO, msg, True)
_timestamp = time.time()
acss = __resolveACSS(acss)
logLine = f"SPEECH ECHO OUTPUT: '{text}' {acss}"
debug.printMessage(debug.LEVEL_INFO, logLine, True)
if log:
log.info(logLine)
_write_to_monitor(text)
server = _echoSpeechserver or _speechserver
if server:
server.speak(text, acss, interrupt) # type: ignore
def getInfo() -> Optional[Any]: def getInfo() -> Optional[Any]:
info = None info = None
if _speechserver: if _speechserver:
@@ -462,13 +575,18 @@ def getInfo() -> Optional[Any]:
def stop() -> None: def stop() -> None:
if _speechserver: if _speechserver:
_speechserver.stop() # type: ignore _speechserver.stop() # type: ignore
if _echoSpeechserver and _echoSpeechserver != _speechserver:
_echoSpeechserver.stop() # type: ignore
def shutdown() -> None: def shutdown() -> None:
debug.printMessage(debug.LEVEL_INFO, 'SPEECH: Shutting down', True) debug.printMessage(debug.LEVEL_INFO, 'SPEECH: Shutting down', True)
global _speechserver global _speechserver, _echoSpeechserver
if _speechserver: if _speechserver:
_speechserver.shutdownActiveServers() # type: ignore _speechserver.shutdownActiveServers() # type: ignore
_speechserver = None _speechserver = None
elif _echoSpeechserver:
_echoSpeechserver.shutdownActiveServers() # type: ignore
_echoSpeechserver = None
def reset(text: Optional[str] = None, acss: Optional[Any] = None) -> None: def reset(text: Optional[str] = None, acss: Optional[Any] = None) -> None:
if _speechserver: if _speechserver: