7 Commits

Author SHA1 Message Date
Storm Dragon
c66a9ba9c2 Problems with voice selection fixed.: 2025-12-02 18:38:06 -05:00
Storm Dragon
2092a3e257 Fixed voice selection. 2025-12-02 18:36:46 -05:00
Storm Dragon
d46d8de3ee Updated sound driver to gstreamer by default. 2025-12-02 16:25:25 -05:00
Storm Dragon
75a8447759 One more feature addition before hopefully releasing the new version. 2025-12-02 16:13:15 -05:00
Storm Dragon
1650eec768 Add ability to switch speech-dispatcher module and voice to the speeach keys. 2025-12-02 16:11:47 -05:00
Storm Dragon
5bb786ef4c Bug fix in vmenu for keyboard layouts. 2025-11-27 22:44:51 -05:00
Storm Dragon
7f7faa17d3 keyboard layout fixed in vmenu. 2025-11-27 22:42:36 -05:00
10 changed files with 745 additions and 165 deletions

View File

@@ -3,9 +3,9 @@
enabled=True enabled=True
# Select the driver used to play sounds, choices are genericDriver and gstreamerDriver. # Select the driver used to play sounds, choices are genericDriver and gstreamerDriver.
# Sox is the default. # Gstreamer is the default.
#driver=gstreamerDriver driver=gstreamerDriver
driver=genericDriver #driver=genericDriver
# Sound themes. These are the pack of sounds used for sound alerts. # Sound themes. These are the pack of sounds used for sound alerts.
# Sound packs may be located at /usr/share/sounds # Sound packs may be located at /usr/share/sounds
@@ -217,6 +217,13 @@ list=
[menu] [menu]
vmenuPath= vmenuPath=
# quickMenu: Semicolon-separated list of settings for quick adjustment
# Format: section#setting;section#setting;...
# Supported settings:
# - speech#rate, speech#pitch, speech#volume (0.0-1.0)
# - speech#module, speech#voice (speechdDriver only, auto-added)
# Note: speech#module and speech#voice are automatically added when
# speechdDriver is active. Do not add them manually.
quickMenu=speech#rate;speech#pitch;speech#volume quickMenu=speech#rate;speech#pitch;speech#volume
[prompt] [prompt]

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
from fenrirscreenreader.core.i18n import _
from fenrirscreenreader.core import debug
class command:
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return _(
"TUI focus mode handler - suppresses screen update spam "
"for interactive TUI applications"
)
def run(self):
# Check if TUI mode is enabled
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"focus", "tui"
):
return
# TUI mode is active - set suppression flag for incoming handler
# This prevents the 70000-incoming.py command from announcing
# screen updates
self.env["commandBuffer"]["tuiSuppressIncoming"] = True
self.env["runtime"]["DebugManager"].write_debug_out(
"tui_focus_handler: TUI mode active, suppressing incoming text",
debug.DebugLevel.INFO
)
def set_callback(self, callback):
pass

View File

@@ -1,38 +0,0 @@
#!/usr/bin/env python3
import importlib.util
import os
from fenrirscreenreader.core.i18n import _
# Load base configuration class
_base_path = os.path.join(os.path.dirname(__file__), "..", "config_base.py")
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
_module = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_module)
config_command = _module.config_command
class command(config_command):
def __init__(self):
super().__init__()
def get_description(self):
return "Set keyboard layout to Desktop"
def run(self):
current_layout = self.get_setting("keyboard", "keyboardLayout", "desktop")
if current_layout.lower() == "desktop":
self.present_text("Keyboard layout already set to Desktop")
return
success = self.set_setting("keyboard", "keyboardLayout", "desktop")
if success:
self.present_text("Keyboard layout set to Desktop")
self.present_text("Please restart Fenrir for this change to take effect")
self.play_sound("Accept")
else:
self.present_text("Failed to change keyboard layout")
self.play_sound("Error")

View File

@@ -1,38 +0,0 @@
#!/usr/bin/env python3
import importlib.util
import os
from fenrirscreenreader.core.i18n import _
# Load base configuration class
_base_path = os.path.join(os.path.dirname(__file__), "..", "config_base.py")
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
_module = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_module)
config_command = _module.config_command
class command(config_command):
def __init__(self):
super().__init__()
def get_description(self):
return "Set keyboard layout to Laptop"
def run(self):
current_layout = self.get_setting("keyboard", "keyboardLayout", "desktop")
if current_layout.lower() == "laptop":
self.present_text("Keyboard layout already set to Laptop")
return
success = self.set_setting("keyboard", "keyboardLayout", "laptop")
if success:
self.present_text("Keyboard layout set to Laptop")
self.present_text("Please restart Fenrir for this change to take effect")
self.play_sound("Accept")
else:
self.present_text("Failed to change keyboard layout")
self.play_sound("Error")

View File

@@ -1,38 +0,0 @@
#!/usr/bin/env python3
import importlib.util
import os
from fenrirscreenreader.core.i18n import _
# Load base configuration class
_base_path = os.path.join(os.path.dirname(__file__), "..", "config_base.py")
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
_module = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_module)
config_command = _module.config_command
class command(config_command):
def __init__(self):
super().__init__()
def get_description(self):
return "Set keyboard layout to PTY (terminal emulation)"
def run(self):
current_layout = self.get_setting("keyboard", "keyboardLayout", "desktop")
if current_layout.lower() == "pty":
self.present_text("Keyboard layout already set to PTY")
return
success = self.set_setting("keyboard", "keyboardLayout", "pty")
if success:
self.present_text("Keyboard layout set to PTY for terminal emulation")
self.present_text("Please restart Fenrir for this change to take effect")
self.play_sound("Accept")
else:
self.present_text("Failed to change keyboard layout")
self.play_sound("Error")

View File

@@ -1,38 +0,0 @@
#!/usr/bin/env python3
import importlib.util
import os
from fenrirscreenreader.core.i18n import _
# Load base configuration class
_base_path = os.path.join(os.path.dirname(__file__), "..", "config_base.py")
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
_module = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_module)
config_command = _module.config_command
class command(config_command):
def __init__(self):
super().__init__()
def get_description(self):
return "Set keyboard layout to PTY2 (alternative terminal layout)"
def run(self):
current_layout = self.get_setting("keyboard", "keyboardLayout", "desktop")
if current_layout.lower() == "pty2":
self.present_text("Keyboard layout already set to PTY2")
return
success = self.set_setting("keyboard", "keyboardLayout", "pty2")
if success:
self.present_text("Keyboard layout set to PTY2 alternative terminal layout")
self.present_text("Please restart Fenrir for this change to take effect")
self.play_sound("Accept")
else:
self.present_text("Failed to change keyboard layout")
self.play_sound("Error")

View File

@@ -0,0 +1,203 @@
#!/usr/bin/env python3
import glob
import os
from fenrirscreenreader.core import debug
class DynamicKeyboardLayoutCommand:
"""Dynamic command class for keyboard layout selection"""
def __init__(self, layoutName, layoutPath, env):
self.layoutName = layoutName
self.layoutPath = layoutPath
# Extract just the base name without extension for comparison
self.layoutBaseName = layoutName
self.env = env
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return f"Set keyboard layout to {self.layoutName}"
def run(self):
try:
settingsManager = self.env["runtime"]["SettingsManager"]
currentLayout = settingsManager.get_setting(
"keyboard", "keyboardLayout"
)
# Check if already set (compare both full path and base name)
currentBaseName = os.path.splitext(os.path.basename(currentLayout))[0] if currentLayout else ""
if currentBaseName.lower() == self.layoutBaseName.lower() or currentLayout.lower() == self.layoutPath.lower():
self.env["runtime"]["OutputManager"].present_text(
f"Keyboard layout already set to {self.layoutName}"
)
return
# Set the new layout in the config file using full path
try:
# Update the setting in memory
settingsManager.set_setting(
"keyboard", "keyboardLayout", self.layoutPath
)
# Save to the actual config file
configFilePath = settingsManager.get_settings_file()
settingsManager.save_settings(configFilePath)
self.env["runtime"]["OutputManager"].present_text(
f"Keyboard layout set to {self.layoutName}. Please restart Fenrir for this change to take effect."
)
# Play accept sound
self.env["runtime"]["OutputManager"].present_text(
"", sound_icon="Accept", interrupt=False
)
except Exception as e:
self.env["runtime"]["OutputManager"].present_text(
f"Failed to change keyboard layout to {self.layoutName}"
)
self.env["runtime"]["DebugManager"].write_debug_out(
f"DynamicKeyboardLayout: Error setting layout {self.layoutName}: {e}",
debug.DebugLevel.ERROR,
)
# Play error sound
self.env["runtime"]["OutputManager"].present_text(
"", sound_icon="ErrorSound", interrupt=False
)
except Exception as e:
self.env["runtime"]["OutputManager"].present_text(
f"Keyboard layout change error: {str(e)}", interrupt=True
)
self.env["runtime"]["DebugManager"].write_debug_out(
f"DynamicKeyboardLayout: Unexpected error for {self.layoutName}: {e}",
debug.DebugLevel.ERROR,
)
def set_callback(self, callback):
pass
def add_dynamic_keyboard_layout_menus(VmenuManager):
"""Add dynamic keyboard layout menus to vmenu system"""
try:
env = VmenuManager.env
# Get keyboard layout files
layouts = get_keyboard_layouts(env)
if not layouts:
return
# Create keyboard layouts submenu
layoutMenu = {}
# Add layout commands
for layoutName, layoutPath in layouts:
layoutCommand = DynamicKeyboardLayoutCommand(
layoutName, layoutPath, env
)
layoutMenu[f"{layoutName} Action"] = layoutCommand
# Find keyboard menu in existing vmenu structure
# If fenrir menu exists, add layouts under it
if "fenrir Menu" in VmenuManager.menuDict:
fenrirMenu = VmenuManager.menuDict["fenrir Menu"]
if "keyboard Menu" in fenrirMenu:
# Add dynamic layouts to existing keyboard menu
keyboardMenu = fenrirMenu["keyboard Menu"]
keyboardMenu["Keyboard Layouts Menu"] = layoutMenu
else:
# Create keyboard menu with layouts
fenrirMenu["keyboard Menu"] = {
"Keyboard Layouts Menu": layoutMenu
}
else:
# Create standalone keyboard layouts menu
VmenuManager.menuDict["Keyboard Layouts Menu"] = layoutMenu
except Exception as e:
# Use debug manager for error logging
if "DebugManager" in env.get("runtime", {}):
env["runtime"]["DebugManager"].write_debug_out(
f"Error creating dynamic keyboard layout menus: {e}",
debug.DebugLevel.ERROR,
)
else:
print(f"Error creating dynamic keyboard layout menus: {e}")
def get_keyboard_layouts(env):
"""Get available keyboard layouts from keyboard directory"""
layouts = []
try:
# Get keyboard directory paths
keyboardDirs = []
# Check system installation path
systemKeyboardPath = "/etc/fenrirscreenreader/keyboard/"
if os.path.exists(systemKeyboardPath):
keyboardDirs.append(systemKeyboardPath)
# Check source/development path
try:
import fenrirscreenreader
fenrirPath = os.path.dirname(fenrirscreenreader.__file__)
devKeyboardPath = os.path.join(
fenrirPath, "..", "..", "config", "keyboard"
)
devKeyboardPath = os.path.abspath(devKeyboardPath)
if os.path.exists(devKeyboardPath):
keyboardDirs.append(devKeyboardPath)
except Exception:
pass
# Get current layout setting path
try:
currentLayoutSetting = env["runtime"]["SettingsManager"].get_setting(
"keyboard", "keyboardLayout"
)
if currentLayoutSetting and os.path.exists(currentLayoutSetting):
currentLayoutDir = os.path.dirname(currentLayoutSetting)
if currentLayoutDir not in keyboardDirs:
keyboardDirs.append(currentLayoutDir)
except Exception:
pass
# Scan for .conf files
seenLayouts = set()
for keyboardDir in keyboardDirs:
try:
confFiles = glob.glob(os.path.join(keyboardDir, "*.conf"))
for confFile in confFiles:
layoutName = os.path.splitext(os.path.basename(confFile))[
0
]
if layoutName not in seenLayouts:
seenLayouts.add(layoutName)
layouts.append((layoutName, confFile))
except Exception as e:
if "DebugManager" in env.get("runtime", {}):
env["runtime"]["DebugManager"].write_debug_out(
f"Error scanning keyboard directory {keyboardDir}: {e}",
debug.DebugLevel.WARNING,
)
# Sort layouts alphabetically
layouts.sort(key=lambda x: x[0].lower())
except Exception as e:
if "DebugManager" in env.get("runtime", {}):
env["runtime"]["DebugManager"].write_debug_out(
f"Error getting keyboard layouts: {e}",
debug.DebugLevel.ERROR,
)
return layouts

View File

@@ -7,25 +7,285 @@
from fenrirscreenreader.core import debug from fenrirscreenreader.core import debug
from fenrirscreenreader.core.i18n import _ from fenrirscreenreader.core.i18n import _
from fenrirscreenreader.core.settingsData import settings_data from fenrirscreenreader.core.settingsData import settings_data
import subprocess
import time
class QuickMenuManager: class SpeechHelperMixin:
"""Helper methods for querying speech-dispatcher modules and voices.
Provides caching and query functionality for speech-dispatcher module
and voice enumeration, reusing proven logic from voice_browser.py.
"""
def __init__(self): def __init__(self):
self._modules_cache = None
self._voices_cache = {} # {module_name: [voice_list]}
self._cache_timestamp = 0
self._cache_timeout = 300 # 5 minutes
def get_speechd_modules(self):
"""Get available speech-dispatcher modules (cached).
Returns:
list: Available module names (e.g., ['espeak-ng', 'festival'])
"""
now = time.time()
# Return cached if valid
if (self._modules_cache and
(now - self._cache_timestamp) < self._cache_timeout):
return self._modules_cache
# Query spd-say
try:
result = subprocess.run(
["spd-say", "-O"],
capture_output=True,
text=True,
timeout=8
)
if result.returncode == 0:
lines = result.stdout.strip().split("\n")
self._modules_cache = [
line.strip() for line in lines[1:]
if line.strip() and line.strip().lower() != "dummy"
]
self._cache_timestamp = now
return self._modules_cache
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
(f"QuickMenuManager get_speechd_modules: "
f"Error querying modules: {e}"),
debug.DebugLevel.ERROR
)
return []
def get_module_voices(self, module):
"""Get voices for a specific module (cached per-module).
Args:
module (str): Module name (e.g., 'espeak-ng')
Returns:
list: Available voice names for this module
"""
# Return cached if available
if module in self._voices_cache:
return self._voices_cache[module]
# Query spd-say
try:
result = subprocess.run(
["spd-say", "-o", module, "-L"],
capture_output=True,
text=True,
timeout=8
)
if result.returncode == 0:
lines = result.stdout.strip().split("\n")
voices = []
for line in lines[1:]:
if not line.strip():
continue
if module.lower() == "espeak-ng":
voice = self._process_espeak_voice(line)
if voice:
voices.append(voice)
elif module.lower() == "voxin":
# For Voxin, store voice name with language
voice_data = self._process_voxin_voice(line)
if voice_data:
voices.append(voice_data)
else:
# For other modules, extract first field (voice name)
parts = line.strip().split()
if parts:
voices.append(parts[0])
self._voices_cache[module] = voices
return voices
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
(f"QuickMenuManager get_module_voices: "
f"Error querying voices for {module}: {e}"),
debug.DebugLevel.ERROR
)
return []
def _process_espeak_voice(self, voice_line):
"""Process espeak-ng voice format into usable voice name.
Args:
voice_line (str): Raw line from spd-say -L output
Returns:
str: Processed voice name (e.g., 'en-us' or 'en-us+f3')
"""
parts = [p for p in voice_line.split() if p]
if len(parts) < 2:
return None
lang_code = parts[-2].lower()
variant = parts[-1].lower()
return (f"{lang_code}+{variant}"
if variant and variant != "none" else lang_code)
def _process_voxin_voice(self, voice_line):
"""Process Voxin voice format with language information.
Args:
voice_line (str): Raw line from spd-say -o voxin -L output
Format: NAME LANGUAGE VARIANT
Returns:
str: Voice name with language encoded (e.g., 'daniel-embedded-high|en-GB')
"""
parts = [p for p in voice_line.split() if p]
if len(parts) < 2:
return None
voice_name = parts[0]
language = parts[1]
# Encode language with voice for later extraction
return f"{voice_name}|{language}"
def _select_default_voice(self, voices):
"""Select a sensible default voice from list, preferring user's
language.
Args:
voices (list): List of available voice names
Returns:
str: Selected default voice (matches user language if possible)
"""
if not voices:
return ""
# Get current voice to preserve language preference
current_voice = self.env["runtime"]["SettingsManager"].get_setting(
"speech", "voice"
)
# Get configured language from settings
configured_lang = self.env["runtime"]["SettingsManager"].get_setting(
"speech", "language"
)
# Extract language code from current voice if available
current_lang = None
if current_voice:
# Extract language code (e.g., 'en-gb' from 'en-gb+male')
current_lang = current_voice.split('+')[0].lower()
# Build preference list: current language, configured language, English
preferences = []
if current_lang:
preferences.append(current_lang)
if configured_lang:
preferences.append(configured_lang.lower())
preferences.extend(['en-gb', 'en-us', 'en'])
# Remove duplicates while preserving order
seen = set()
preferences = [x for x in preferences
if not (x in seen or seen.add(x))]
# Try exact matches for preferred languages
for pref in preferences:
for voice in voices:
# Extract language if voice is in "name|lang" format
voice_to_check = voice
if "|" in voice:
_, voice_lang = voice.split("|", 1)
voice_to_check = voice_lang
if voice_to_check.lower() == pref:
return voice
# Try voices starting with preferred language codes
for pref in preferences:
for voice in voices:
# Extract language if voice is in "name|lang" format
voice_to_check = voice
if "|" in voice:
_, voice_lang = voice.split("|", 1)
voice_to_check = voice_lang
if voice_to_check.lower().startswith(pref):
return voice
# Fall back to first available voice
return voices[0]
def invalidate_speech_cache(self):
"""Clear cached module and voice data."""
self._modules_cache = None
self._voices_cache = {}
self._cache_timestamp = 0
class QuickMenuManager(SpeechHelperMixin):
def __init__(self):
SpeechHelperMixin.__init__(self)
self.position = 0 self.position = 0
self.quickMenu = [] self.quickMenu = []
self.settings = settings_data self.settings = settings_data
def initialize(self, environment): def initialize(self, environment):
self.env = environment self.env = environment
self.load_menu(
self.env["runtime"]["SettingsManager"].get_setting( # Load base menu from config
"menu", "quickMenu" menu_string = self.env["runtime"]["SettingsManager"].get_setting(
) "menu", "quickMenu"
) )
# Dynamically add speech-dispatcher specific items
if self._is_speechd_driver_active():
menu_string = self._add_speechd_menu_items(menu_string)
self.load_menu(menu_string)
def shutdown(self): def shutdown(self):
pass pass
def _is_speechd_driver_active(self):
"""Check if speechdDriver is currently active.
Returns:
bool: True if speech driver is speechdDriver
"""
try:
driver = self.env["runtime"]["SettingsManager"].get_setting(
"speech", "driver"
)
return driver == "speechdDriver"
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
(f"QuickMenuManager _is_speechd_driver_active: "
f"Error checking driver: {e}"),
debug.DebugLevel.ERROR
)
return False
def _add_speechd_menu_items(self, menu_string):
"""Add speech-dispatcher module and voice to quick menu.
Args:
menu_string (str): Existing menu string from config
Returns:
str: Updated menu string with module and voice added
"""
if not menu_string:
return "speech#module;speech#voice"
# Check if already present (user manually added)
if "speech#module" in menu_string or "speech#voice" in menu_string:
return menu_string
# Add module and voice after existing items
return f"{menu_string};speech#module;speech#voice"
def load_menu(self, menuString): def load_menu(self, menuString):
self.position = 0 self.position = 0
self.quickMenu = [] self.quickMenu = []
@@ -86,8 +346,15 @@ class QuickMenuManager:
try: try:
if isinstance(self.settings[section][setting], str): if isinstance(self.settings[section][setting], str):
value = str(value_string) # Check for special string cycling cases
return False if section == "speech" and setting == "module":
return self.cycle_speech_module("next")
elif section == "speech" and setting == "voice":
return self.cycle_speech_voice("next")
else:
# Generic strings not supported for cycling
value = str(value_string)
return False
elif isinstance(self.settings[section][setting], bool): elif isinstance(self.settings[section][setting], bool):
if value_string not in ["True", "False"]: if value_string not in ["True", "False"]:
return False return False
@@ -132,8 +399,15 @@ class QuickMenuManager:
return False return False
try: try:
if isinstance(self.settings[section][setting], str): if isinstance(self.settings[section][setting], str):
value = str(value_string) # Check for special string cycling cases
return False if section == "speech" and setting == "module":
return self.cycle_speech_module("prev")
elif section == "speech" and setting == "voice":
return self.cycle_speech_voice("prev")
else:
# Generic strings not supported for cycling
value = str(value_string)
return False
elif isinstance(self.settings[section][setting], bool): elif isinstance(self.settings[section][setting], bool):
if value_string not in ["True", "False"]: if value_string not in ["True", "False"]:
return False return False
@@ -161,6 +435,199 @@ class QuickMenuManager:
return False return False
return True return True
def cycle_speech_module(self, direction):
"""Cycle to next/previous speech-dispatcher module.
Args:
direction (str): 'next' or 'prev'
Returns:
bool: True if successful, False otherwise
"""
try:
# Get available modules
modules = self.get_speechd_modules()
if not modules:
self.env["runtime"]["OutputManager"].present_text(
"No modules available", interrupt=True
)
return False
# Get current module
current_module = self.env["runtime"]["SettingsManager"].get_setting(
"speech", "module"
)
# Find current index
try:
current_index = (modules.index(current_module)
if current_module else 0)
except ValueError:
current_index = 0
# Cycle to next/previous
if direction == "next":
new_index = (current_index + 1) % len(modules)
else: # prev
new_index = (current_index - 1) % len(modules)
new_module = modules[new_index]
# Update setting (runtime only)
self.env["runtime"]["SettingsManager"].set_setting(
"speech", "module", new_module
)
# Select sensible default voice for new module
voices = self.get_module_voices(new_module)
if voices:
default_voice = self._select_default_voice(voices)
# Parse voice name and language for modules like Voxin
voice_name = default_voice
voice_lang = None
if "|" in default_voice:
voice_name, voice_lang = default_voice.split("|", 1)
self.env["runtime"]["SettingsManager"].set_setting(
"speech", "voice", voice_name
)
# Apply voice to speech driver immediately
if "SpeechDriver" in self.env["runtime"]:
try:
self.env["runtime"]["SpeechDriver"].set_module(
new_module
)
# Set language first if available
if voice_lang:
self.env["runtime"]["SpeechDriver"].set_language(
voice_lang
)
# Then set voice
self.env["runtime"]["SpeechDriver"].set_voice(
voice_name
)
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
(f"QuickMenuManager cycle_speech_module: "
f"Error applying voice: {e}"),
debug.DebugLevel.ERROR
)
# Announce new module
self.env["runtime"]["OutputManager"].present_text(
new_module, interrupt=True
)
return True
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
f"QuickMenuManager cycle_speech_module: Error: {e}",
debug.DebugLevel.ERROR
)
return False
def cycle_speech_voice(self, direction):
"""Cycle to next/previous voice for current module.
Args:
direction (str): 'next' or 'prev'
Returns:
bool: True if successful, False otherwise
"""
try:
# Get current module
current_module = self.env["runtime"]["SettingsManager"].get_setting(
"speech", "module"
)
if not current_module:
self.env["runtime"]["OutputManager"].present_text(
"No module selected", interrupt=True
)
return False
# Get available voices for this module
voices = self.get_module_voices(current_module)
if not voices:
self.env["runtime"]["OutputManager"].present_text(
f"No voices for module {current_module}", interrupt=True
)
return False
# Get current voice
current_voice = self.env["runtime"]["SettingsManager"].get_setting(
"speech", "voice"
)
# Find current index (handle Voxin voice|language format)
current_index = 0
if current_voice:
try:
# Try exact match first
current_index = voices.index(current_voice)
except ValueError:
# For Voxin, compare just the voice name part
for i, voice in enumerate(voices):
voice_name = voice.split("|")[0] if "|" in voice else voice
if voice_name == current_voice:
current_index = i
break
# Cycle to next/previous
if direction == "next":
new_index = (current_index + 1) % len(voices)
else: # prev
new_index = (current_index - 1) % len(voices)
new_voice = voices[new_index]
# Parse voice name and language for modules like Voxin
voice_name = new_voice
voice_lang = None
if "|" in new_voice:
# Format: "voicename|language" (e.g., "daniel-embedded-high|en-GB")
voice_name, voice_lang = new_voice.split("|", 1)
# Update setting (runtime only) - store the voice name only
self.env["runtime"]["SettingsManager"].set_setting(
"speech", "voice", voice_name
)
# Apply voice to speech driver immediately
if "SpeechDriver" in self.env["runtime"]:
try:
# Set language first if available
if voice_lang:
self.env["runtime"]["SpeechDriver"].set_language(
voice_lang
)
# Then set voice
self.env["runtime"]["SpeechDriver"].set_voice(voice_name)
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
(f"QuickMenuManager cycle_speech_voice: "
f"Error applying voice: {e}"),
debug.DebugLevel.ERROR
)
# Announce new voice (voice name only, not language)
self.env["runtime"]["OutputManager"].present_text(
voice_name, interrupt=True
)
return True
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
f"QuickMenuManager cycle_speech_voice: Error: {e}",
debug.DebugLevel.ERROR
)
return False
def get_current_entry(self): def get_current_entry(self):
if len(self.quickMenu) == 0: if len(self.quickMenu) == 0:
return "" return ""

View File

@@ -217,6 +217,16 @@ class VmenuManager:
except Exception as e: except Exception as e:
print(f"Error adding dynamic voice menus: {e}") print(f"Error adding dynamic voice menus: {e}")
# Add dynamic keyboard layout menus
try:
from fenrirscreenreader.core.dynamicKeyboardLayoutMenu import (
add_dynamic_keyboard_layout_menus,
)
add_dynamic_keyboard_layout_menus(self)
except Exception as e:
print(f"Error adding dynamic keyboard layout menus: {e}")
# index still valid? # index still valid?
if self.curr_index is not None: if self.curr_index is not None:
try: try:

View File

@@ -4,5 +4,5 @@
# Fenrir TTY screen reader # Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors. # By Chrys, Storm Dragon, and contributors.
version = "2025.11.24" version = "2025.12.02"
code_name = "master" code_name = "master"