Speech settings added to cthulhu+arrow keys.

This commit is contained in:
Storm Dragon
2026-01-02 10:46:03 -05:00
parent 5b446000b8
commit 42a984b32f
4 changed files with 496 additions and 2 deletions
+15
View File
@@ -450,6 +450,21 @@ INCREASE_SPEECH_VOLUME = _("Increase the speech volume")
# speech synthesis engine will generate speech.
DECREASE_SPEECH_VOLUME = _("Decrease the speech volume")
# Translators: This string describes the command to select the previous speech setting.
SPEECH_SETTING_PREVIOUS = _("Select the previous speech setting")
# Translators: This string describes the command to select the next speech setting.
SPEECH_SETTING_NEXT = _("Select the next speech setting")
# Translators: This string describes the command to decrease the current speech setting.
SPEECH_SETTING_DECREASE = _("Decrease the current speech setting")
# Translators: This string describes the command to increase the current speech setting.
SPEECH_SETTING_INCREASE = _("Increase the current speech setting")
# Translators: This string describes the command to save speech settings.
SPEECH_SETTINGS_SAVE = _("Save speech settings")
# Translators: Cthulhu allows the user to turn speech synthesis on or off.
# We call it 'silencing'.
TOGGLE_SPEECH = _("Toggle the silencing of speech")
+27
View File
@@ -2275,6 +2275,33 @@ SPEECH_LOUDER = _("louder.")
# Translators: This string announces speech volume change.
SPEECH_SOFTER = _("softer.")
# Translators: This string announces the current speech rate value.
SPEECH_RATE_VALUE = _("Rate %d")
# Translators: This string announces the current speech pitch value.
SPEECH_PITCH_VALUE = _("Pitch %s")
# Translators: This string announces the current speech volume value.
SPEECH_VOLUME_VALUE = _("Volume %s")
# Translators: This string announces the current speech-dispatcher module.
SPEECH_MODULE_VALUE = _("Speech-dispatcher module %s")
# Translators: This string announces the current speech-dispatcher voice.
SPEECH_VOICE_VALUE = _("Speech-dispatcher voice %s")
# Translators: This string is presented when speech-dispatcher modules are unavailable.
SPEECH_MODULES_UNAVAILABLE = _("No speech-dispatcher modules available")
# Translators: This string is presented when speech-dispatcher voices are unavailable.
SPEECH_VOICES_UNAVAILABLE = _("No speech-dispatcher voices available")
# Translators: This string confirms speech settings have been saved.
SPEECH_SETTINGS_SAVED = _("Speech settings saved")
# Translators: This string is presented when speech settings cannot be saved.
SPEECH_SETTINGS_SAVE_FAILED = _("Speech settings could not be saved")
# Translators: Cthulhu's verbosity levels control how much (or how little)
# Cthulhu will speak when presenting objects as the user navigates within
# applications and reads content. The two levels are "brief" and "verbose".
+447
View File
@@ -39,6 +39,8 @@ from . import input_event
from . import keybindings
from . import messages
from . import cthulhu_state
from . import script_manager
from . import speechserver
from . import settings
from . import settings_manager
from . import speech
@@ -49,6 +51,17 @@ class SpeechAndVerbosityManager:
"""Configures speech and verbosity settings."""
def __init__(self):
self._speech_settings_order = [
"rate",
"pitch",
"volume",
"module",
"voice",
]
self._current_speech_setting_index = 0
self._rate_step = 1
self._pitch_step = 0.1
self._volume_step = 0.1
self._handlers = self._setup_handlers()
self._bindings = self._setup_bindings()
@@ -149,6 +162,31 @@ class SpeechAndVerbosityManager:
self.increase_volume,
cmdnames.INCREASE_SPEECH_VOLUME)
handlers["selectPreviousSpeechSettingHandler"] = \
input_event.InputEventHandler(
self.select_previous_speech_setting,
cmdnames.SPEECH_SETTING_PREVIOUS)
handlers["selectNextSpeechSettingHandler"] = \
input_event.InputEventHandler(
self.select_next_speech_setting,
cmdnames.SPEECH_SETTING_NEXT)
handlers["decreaseCurrentSpeechSettingHandler"] = \
input_event.InputEventHandler(
self.decrease_current_speech_setting,
cmdnames.SPEECH_SETTING_DECREASE)
handlers["increaseCurrentSpeechSettingHandler"] = \
input_event.InputEventHandler(
self.increase_current_speech_setting,
cmdnames.SPEECH_SETTING_INCREASE)
handlers["saveSpeechSettingsHandler"] = \
input_event.InputEventHandler(
self.save_speech_settings,
cmdnames.SPEECH_SETTINGS_SAVE)
return handlers
def _setup_bindings(self):
@@ -233,6 +271,41 @@ class SpeechAndVerbosityManager:
keybindings.NO_MODIFIER_MASK,
self._handlers.get("increaseSpeechVolumeHandler")))
bindings.add(
keybindings.KeyBinding(
"Left",
keybindings.defaultModifierMask,
keybindings.CTHULHU_MODIFIER_MASK,
self._handlers.get("selectPreviousSpeechSettingHandler")))
bindings.add(
keybindings.KeyBinding(
"Right",
keybindings.defaultModifierMask,
keybindings.CTHULHU_MODIFIER_MASK,
self._handlers.get("selectNextSpeechSettingHandler")))
bindings.add(
keybindings.KeyBinding(
"Down",
keybindings.defaultModifierMask,
keybindings.CTHULHU_MODIFIER_MASK,
self._handlers.get("decreaseCurrentSpeechSettingHandler")))
bindings.add(
keybindings.KeyBinding(
"Up",
keybindings.defaultModifierMask,
keybindings.CTHULHU_MODIFIER_MASK,
self._handlers.get("increaseCurrentSpeechSettingHandler")))
bindings.add(
keybindings.KeyBinding(
"s",
keybindings.defaultModifierMask,
keybindings.CTHULHU_CTRL_MODIFIER_MASK | keybindings.SHIFT_MODIFIER_MASK,
self._handlers.get("saveSpeechSettingsHandler")))
bindings.add(
keybindings.KeyBinding(
"",
@@ -266,6 +339,380 @@ class SpeechAndVerbosityManager:
def _get_server(self):
return speech.getSpeechServer()
def _format_number(self, value):
if isinstance(value, float):
if value.is_integer():
return str(int(value))
return f"{value:.1f}".rstrip("0").rstrip(".")
return str(value)
def _present_message(self, script, message):
if script:
script.presentMessage(message)
else:
speech.speak(message)
def _get_default_voice(self):
from . import acss
default_voice = settings.voices.get(settings.DEFAULT_VOICE)
if default_voice is None:
default_voice = acss.ACSS({})
settings.voices[settings.DEFAULT_VOICE] = default_voice
return default_voice
def _set_default_voice_family(self, family):
from . import acss
default_voice = self._get_default_voice()
if family is None:
default_voice.pop(acss.ACSS.FAMILY, None)
else:
default_voice[acss.ACSS.FAMILY] = dict(family)
default_voice['established'] = True
def _get_current_speech_setting(self):
return self._speech_settings_order[self._current_speech_setting_index]
def _get_rate_value(self):
from . import acss
default_voice = self._get_default_voice()
return int(default_voice.get(acss.ACSS.RATE, 50))
def _set_rate_value(self, value):
from . import acss
default_voice = self._get_default_voice()
default_voice[acss.ACSS.RATE] = int(value)
default_voice['established'] = True
def _get_pitch_value(self):
from . import acss
default_voice = self._get_default_voice()
return float(default_voice.get(acss.ACSS.AVERAGE_PITCH, 5.0))
def _set_pitch_value(self, value):
from . import acss
default_voice = self._get_default_voice()
default_voice[acss.ACSS.AVERAGE_PITCH] = float(value)
default_voice['established'] = True
def _get_volume_value(self):
from . import acss
default_voice = self._get_default_voice()
return float(default_voice.get(acss.ACSS.GAIN, 10.0))
def _set_volume_value(self, value):
from . import acss
default_voice = self._get_default_voice()
default_voice[acss.ACSS.GAIN] = float(value)
default_voice['established'] = True
def _get_current_voice_name(self, server=None):
from . import acss
default_voice = self._get_default_voice()
family = default_voice.get(acss.ACSS.FAMILY, {}) or {}
name = family.get(speechserver.VoiceFamily.NAME)
if name:
return name
if server:
voices = self._get_available_voices(server)
if voices:
return voices[0].get(speechserver.VoiceFamily.NAME)
return ""
def _get_available_modules(self, server):
if server is None or not hasattr(server, 'list_output_modules'):
return []
try:
modules = list(server.list_output_modules() or [])
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error getting output modules: {e}", True)
return []
return modules
def _get_available_voices(self, server):
if server is None or not hasattr(server, 'getVoiceFamilies'):
return []
try:
voices = server.getVoiceFamilies() or []
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error getting voices: {e}", True)
return []
filtered = []
for voice in voices:
if voice.get(speechserver.VoiceFamily.NAME):
filtered.append(voice)
if not filtered:
return []
if not self._should_filter_voices_by_locale(server):
return filtered
language, dialect = self._get_system_language_and_dialect()
if not language:
return filtered
language_filtered = []
for voice in filtered:
voice_lang = (voice.get(speechserver.VoiceFamily.LANG) or "").lower()
voice_dialect = (voice.get(speechserver.VoiceFamily.DIALECT) or "").lower()
if voice_lang != language:
continue
if dialect and voice_dialect and voice_dialect != dialect:
continue
language_filtered.append(voice)
if language_filtered:
msg = (
"SPEECH AND VERBOSITY MANAGER: "
f"Filtered voices to locale {language}"
f"{('-' + dialect) if dialect else ''}: "
f"{len(language_filtered)} of {len(filtered)}"
)
debug.printMessage(debug.LEVEL_INFO, msg, True)
return language_filtered
msg = (
"SPEECH AND VERBOSITY MANAGER: "
f"No voices for locale {language}"
f"{('-' + dialect) if dialect else ''}; using all voices."
)
debug.printMessage(debug.LEVEL_INFO, msg, True)
return filtered
def _should_filter_voices_by_locale(self, server):
if server is None or not hasattr(server, "getOutputModule"):
return False
try:
module = (server.getOutputModule() or "").lower()
except Exception:
return False
return "espeak-ng" in module
def _get_system_language_and_dialect(self):
import locale
locale_value = None
try:
locale_value = locale.getlocale(locale.LC_MESSAGES)[0]
except Exception:
locale_value = None
if not locale_value:
try:
locale_value = locale.getdefaultlocale()[0]
except Exception:
locale_value = None
if not locale_value:
return "", ""
if "_" in locale_value:
language, dialect = locale_value.split("_", 1)
elif "-" in locale_value:
language, dialect = locale_value.split("-", 1)
else:
language, dialect = locale_value, ""
return language.lower(), dialect.lower()
def _announce_current_speech_setting(self, script=None):
setting = self._get_current_speech_setting()
debug.printMessage(debug.LEVEL_INFO,
f"SPEECH AND VERBOSITY MANAGER: Current setting {setting}",
True)
if setting == "rate":
value = self._get_rate_value()
message = messages.SPEECH_RATE_VALUE % value
elif setting == "pitch":
value = self._format_number(self._get_pitch_value())
message = messages.SPEECH_PITCH_VALUE % value
elif setting == "volume":
value = self._format_number(self._get_volume_value())
message = messages.SPEECH_VOLUME_VALUE % value
elif setting == "module":
server = self._get_server()
if server is None or not hasattr(server, 'getOutputModule'):
message = messages.SPEECH_MODULES_UNAVAILABLE
else:
module = server.getOutputModule() or ""
if module:
message = messages.SPEECH_MODULE_VALUE % module
else:
message = messages.SPEECH_MODULES_UNAVAILABLE
elif setting == "voice":
server = self._get_server()
voices = self._get_available_voices(server)
if not voices:
message = messages.SPEECH_VOICES_UNAVAILABLE
else:
name = self._get_current_voice_name(server)
message = messages.SPEECH_VOICE_VALUE % name
else:
message = ""
if message:
self._present_message(script, message)
@dbus_service.command
def select_previous_speech_setting(self, script=None, event=None):
if self._current_speech_setting_index > 0:
self._current_speech_setting_index -= 1
self._announce_current_speech_setting(script)
return True
@dbus_service.command
def select_next_speech_setting(self, script=None, event=None):
if self._current_speech_setting_index < len(self._speech_settings_order) - 1:
self._current_speech_setting_index += 1
self._announce_current_speech_setting(script)
return True
@dbus_service.command
def decrease_current_speech_setting(self, script=None, event=None):
return self._adjust_current_speech_setting(script, decrease=True)
@dbus_service.command
def increase_current_speech_setting(self, script=None, event=None):
return self._adjust_current_speech_setting(script, decrease=False)
def _adjust_current_speech_setting(self, script, decrease=False):
setting = self._get_current_speech_setting()
if setting == "rate":
return self._adjust_rate(script, decrease)
if setting == "pitch":
return self._adjust_pitch(script, decrease)
if setting == "volume":
return self._adjust_volume(script, decrease)
if setting == "module":
return self._adjust_module(script, decrease)
if setting == "voice":
return self._adjust_voice(script, decrease)
return True
def _adjust_rate(self, script, decrease):
delta = -self._rate_step if decrease else self._rate_step
value = self._get_rate_value() + delta
value = max(0, min(100, value))
self._set_rate_value(value)
msg = f"SPEECH AND VERBOSITY MANAGER: Rate set to {value}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._announce_current_speech_setting(script)
return True
def _adjust_pitch(self, script, decrease):
delta = -self._pitch_step if decrease else self._pitch_step
value = round(self._get_pitch_value() + delta, 1)
value = max(0.0, min(10.0, value))
self._set_pitch_value(value)
msg = f"SPEECH AND VERBOSITY MANAGER: Pitch set to {value}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._announce_current_speech_setting(script)
return True
def _adjust_volume(self, script, decrease):
delta = -self._volume_step if decrease else self._volume_step
value = round(self._get_volume_value() + delta, 1)
value = max(0.0, min(10.0, value))
self._set_volume_value(value)
msg = f"SPEECH AND VERBOSITY MANAGER: Volume set to {value}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._announce_current_speech_setting(script)
return True
def _adjust_module(self, script, decrease):
server = self._get_server()
if server is None:
msg = "SPEECH AND VERBOSITY MANAGER: Cannot get speech server."
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
modules = self._get_available_modules(server)
if not modules:
self._present_message(script, messages.SPEECH_MODULES_UNAVAILABLE)
return True
current = server.getOutputModule() if hasattr(server, 'getOutputModule') else ""
try:
index = modules.index(current)
except ValueError:
index = 0
delta = -1 if decrease else 1
new_index = max(0, min(len(modules) - 1, index + delta))
new_module = modules[new_index]
if new_module != current:
try:
server.setOutputModule(new_module)
msg = f"SPEECH AND VERBOSITY MANAGER: Output module set to {new_module}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error setting output module: {e}", True)
return True
settings.speechServerInfo = [new_module, new_module]
msg = f"SPEECH AND VERBOSITY MANAGER: speechServerInfo set to {settings.speechServerInfo}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
voices = self._get_available_voices(server)
if voices:
self._set_default_voice_family(voices[0])
else:
self._set_default_voice_family({})
self._present_message(script, messages.SPEECH_MODULE_VALUE % new_module)
return True
def _adjust_voice(self, script, decrease):
server = self._get_server()
voices = self._get_available_voices(server)
if not voices:
self._present_message(script, messages.SPEECH_VOICES_UNAVAILABLE)
return True
current_name = self._get_current_voice_name(server)
index = 0
for idx, voice in enumerate(voices):
if voice.get(speechserver.VoiceFamily.NAME) == current_name:
index = idx
break
delta = -1 if decrease else 1
new_index = max(0, min(len(voices) - 1, index + delta))
new_voice = voices[new_index]
self._set_default_voice_family(new_voice)
name = new_voice.get(speechserver.VoiceFamily.NAME, "")
msg = f"SPEECH AND VERBOSITY MANAGER: Voice set to {name}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._present_message(script, messages.SPEECH_VOICE_VALUE % name)
return True
@dbus_service.command
def save_speech_settings(self, script=None, event=None):
try:
server = self._get_server()
if server and hasattr(server, "getOutputModule"):
module = server.getOutputModule() or ""
if module:
settings.speechServerInfo = [module, module]
msg = f"SPEECH AND VERBOSITY MANAGER: Saving speechServerInfo {settings.speechServerInfo}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
general = {}
for key in settings.userCustomizableSettings:
general[key] = getattr(settings, key)
current_profile = _settings_manager.profile
pronunciations = _settings_manager.getPronunciations(current_profile)
keybindings = _settings_manager.getKeybindings(current_profile)
default_script = script_manager.get_manager().get_default_script()
_settings_manager.saveSettings(default_script,
general,
pronunciations,
keybindings)
self._present_message(script, messages.SPEECH_SETTINGS_SAVED)
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error saving speech settings: {e}", True)
self._present_message(script, messages.SPEECH_SETTINGS_SAVE_FAILED)
return True
# ========================================================================
# D-Bus Speech Server Controls
# ========================================================================
+7 -2
View File
@@ -589,7 +589,13 @@ class SpeechServer(speechserver.SpeechServer):
if not default_lang:
default_lang = locale_language
voices = ((self._default_voice_name, default_lang, None),) + voices
current_module = ""
try:
current_module = self.getOutputModule() or ""
except Exception:
current_module = ""
default_name = guilabels.SPEECH_DEFAULT_VOICE % (current_module or self._id)
voices = ((default_name, default_lang, None),) + voices
families = []
for name, lang, variant in voices:
@@ -798,4 +804,3 @@ class SpeechServer(speechserver.SpeechServer):
return ()
except speechd.SSIPCommandError:
return ()