From af857d7976b87b5ed301c341d6858579f1ca51f3 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 5 Dec 2024 06:06:03 -0500 Subject: [PATCH] combined configure_fenrir and configure_speechd into a single script. This is the go-to for editing Fenrir settings without doing it by hand. --- tools/configure_fenrir.py | 190 +++++++++++++++++++++++------ tools/configure_speechd.py | 242 ------------------------------------- 2 files changed, 153 insertions(+), 279 deletions(-) delete mode 100755 tools/configure_speechd.py diff --git a/tools/configure_fenrir.py b/tools/configure_fenrir.py index 63b09dbb..3141a7a9 100755 --- a/tools/configure_fenrir.py +++ b/tools/configure_fenrir.py @@ -4,10 +4,13 @@ import os import sys import configparser import dialog -from typing import Dict, List, Optional import subprocess +import time +import select +import tempfile +from typing import Dict, List, Optional, Tuple -class FenrirConfig: +class FenrirConfigTool: def __init__(self): os.environ['DIALOGOPTS'] = '--no-lines --visit-items' self.tui = dialog.Dialog(dialog="dialog") @@ -17,24 +20,21 @@ class FenrirConfig: if not self.check_root(): self.escalate_privileges() sys.exit(0) - - # Navigation instructions for different dialog types + self.instructions = { 'menu': "\nNavigation: Use Up/Down arrows to move, Enter to select, Escape to go back", 'radiolist': "\nNavigation: Use Up/Down arrows to move, Space to select option, Enter to confirm, Escape to cancel", 'inputbox': "\nEnter your value and press Enter to confirm, or Escape to cancel" } - # Predefined options for certain settings + # Configuration presets and help text from original FenrirConfig self.presetOptions = { - # Drivers 'sound.driver': ['genericDriver', 'gstreamerDriver'], 'speech.driver': ['speechdDriver', 'genericDriver'], 'braille.driver': ['dummyDriver', 'brailttyDriver', 'brlapiDriver'], 'screen.driver': ['vcsaDriver', 'dummyDriver', 'ptyDriver', 'debugDriver'], 'keyboard.driver': ['evdevDriver', 'dummyDriver'], 'remote.driver': ['unixDriver', 'tcpDriver'], - # Other preset options 'braille.flushMode': ['word', 'char', 'fix', 'none'], 'braille.cursorFocusMode': ['page', 'fixCell'], 'braille.cursorFollowMode': ['review', 'last', 'none'], @@ -43,7 +43,6 @@ class FenrirConfig: 'general.debugMode': ['File', 'Print'] } - # Help text for certain options self.helpText = { 'sound.volume': 'Volume level from 0 (quietest) to 1.0 (loudest)', 'speech.rate': 'Speech rate from 0 (slowest) to 1.0 (fastest)', @@ -53,23 +52,20 @@ class FenrirConfig: } def check_root(self) -> bool: - """Check if the script is running with root privileges""" return os.geteuid() == 0 def find_privilege_escalation_tool(self) -> Optional[str]: - """Find available privilege escalation tool (sudo or doas)""" for tool in ['sudo', 'doas']: if subprocess.run(['which', tool], stdout=subprocess.PIPE).returncode == 0: return tool return None def escalate_privileges(self): - """Re-run the script with elevated privileges""" tool = self.find_privilege_escalation_tool() if not tool: self.tui.msgbox("Error: Neither sudo nor doas found. Please run this script as root.") sys.exit(1) - + try: scriptPath = os.path.abspath(sys.argv[0]) command = [tool, sys.executable, scriptPath] + sys.argv[1:] @@ -78,13 +74,6 @@ class FenrirConfig: self.tui.msgbox(f"Error escalating privileges: {str(e)}") sys.exit(1) - def check_permissions(self) -> bool: - """Check if we have write permissions to the settings file""" - if not os.access(self.settingsFile, os.W_OK): - self.tui.msgbox("Error: Insufficient permissions to modify the settings file even with root privileges.") - return False - return True - def is_boolean_option(self, value: str) -> bool: """Check if the current value is likely a boolean option""" return value.lower() in ['true', 'false'] @@ -144,52 +133,179 @@ class FenrirConfig: return value return None - def run(self): - if not self.check_permissions(): + def run_command(self, cmd: List[str], needsRoot: bool = False) -> Optional[str]: + try: + if needsRoot and not self.check_root(): + tool = self.find_privilege_escalation_tool() + if tool: + cmd = [tool] + cmd + result = subprocess.run(cmd, capture_output=True, text=True) + return result.stdout.strip() if result.returncode == 0 else None + except Exception as e: + self.tui.msgbox(f"Error running command {' '.join(cmd)}: {e}") + return None + + def get_speechd_modules(self) -> List[str]: + output = self.run_command(['spd-say', '-O'], True) + if output: + lines = output.split('\n') + return [line.strip() for line in lines[1:] if line.strip()] + return [] + + def process_espeak_voice(self, voiceLine: str) -> Optional[str]: + parts = [p for p in voiceLine.split() if p] + if len(parts) < 2: + return None + langCode = parts[-2].lower() + variant = parts[-1].lower() + return f"{langCode}+{variant}" if variant and variant != 'none' else langCode + + def get_module_voices(self, moduleName: str) -> List[str]: + output = self.run_command(['spd-say', '-o', moduleName, '-L'], True) + if output: + lines = output.split('\n') + voices = [] + for line in lines[1:]: + if not line.strip(): + continue + if moduleName.lower() == 'espeak-ng': + voice = self.process_espeak_voice(line) + if voice: + voices.append(voice) + else: + voices.append(line.strip()) + return voices + return [] + + def configure_speech(self) -> None: + moduleList = self.get_speechd_modules() + if not moduleList: + self.tui.msgbox("No speech-dispatcher modules found!") return + code, moduleChoice = self.tui.menu( + "Select speech module:" + self.instructions['menu'], + choices=[(module, "") for module in moduleList] + ) + + if code != self.tui.OK: + return + + voiceList = self.get_module_voices(moduleChoice) + if not voiceList: + self.tui.msgbox(f"No voices found for module {moduleChoice}") + return + + code, voice = self.tui.menu( + f"Select voice for {moduleChoice}:" + self.instructions['menu'], + choices=[(v, "") for v in voiceList] + ) + + if code != self.tui.OK: + return + + # Test voice configuration + if self.test_voice(moduleChoice, voice): + config = configparser.ConfigParser() + config.read(self.settingsFile) + + if 'speech' not in config: + config['speech'] = {} + + config['speech'].update({ + 'driver': 'speechdDriver', + 'module': moduleChoice, + 'voice': voice, + 'enabled': 'True', + 'rate': '0.25', + 'pitch': '0.5', + 'volume': '1.0' + }) + + with open(self.settingsFile, 'w') as configfile: + config.write(configfile) + + self.tui.msgbox("Speech configuration updated successfully!\nPlease restart Fenrir for changes to take effect.") + + def test_voice(self, moduleName: str, voiceName: str) -> bool: + testMessage = "If you hear this message, press Enter within 30 seconds to confirm." + try: + process = subprocess.Popen( + ['spd-say', '-o', moduleName, '-y', voiceName, testMessage] + ) + + code = self.tui.pause( + "Waiting for voice test...\n" + "Press Enter if you hear the message, or wait for timeout.", + 30 + ) + + process.terminate() + return code == self.tui.OK + + except Exception as e: + self.tui.msgbox(f"Error testing voice: {e}") + return False + + def edit_general_config(self) -> None: while True: config = configparser.ConfigParser() config.read(self.settingsFile) sections = config.sections() code, section = self.tui.menu( - "Select a section:" + self.instructions['menu'], - choices=[(s, "") for s in sections] + [("Exit", " ")] + "Select a section to configure:" + self.instructions['menu'], + choices=[(s, "") for s in sections] + [("Go Back", "")] ) - if section == "Exit": + if code != self.tui.OK or section == "Go Back": break while True: options = config.options(section) choices = [(o, f"Current: {config.get(section, o)}") for o in options] - choices.append(("Go Back", " ")) + choices.append(("Go Back", "")) code, option = self.tui.menu( f"Select option to edit in '{section}':" + self.instructions['menu'], choices=choices ) - if option == "Go Back": + if code != self.tui.OK or option == "Go Back": break - if code == self.tui.OK: - currentValue = config.get(section, option) - newValue = self.get_value_with_presets(section, option, currentValue) + currentValue = config.get(section, option) + newValue = self.get_value_with_presets(section, option, currentValue) - if newValue is not None and newValue != currentValue: - config.set(section, option, newValue) - with open(self.settingsFile, 'w') as configfile: - config.write(configfile) - self.tui.msgbox("Settings saved successfully.") + if newValue is not None and newValue != currentValue: + config.set(section, option, newValue) + with open(self.settingsFile, 'w') as configfile: + config.write(configfile) + self.tui.msgbox("Setting updated successfully.") + + def run(self): + while True: + code, choice = self.tui.menu( + "Fenrir Configuration Tool" + self.instructions['menu'], + choices=[ + ("speech-dispatcher", "Configure module and voice"), + ("Advanced", "Edit Fenrir settings"), + ("Exit", "") + ] + ) + + if code != self.tui.OK or choice == "Exit": + break + + if choice == "speech-dispatcher": + self.configure_speech() + elif choice == "Advanced": + self.edit_general_config() if __name__ == "__main__": - configTool = FenrirConfig() + configTool = FenrirConfigTool() try: configTool.run() - except (configparser.Error, dialog.error) as e: - sys.exit(0) except Exception as e: print(f"Unexpected error occurred: {str(e)}", file=sys.stderr) sys.exit(1) diff --git a/tools/configure_speechd.py b/tools/configure_speechd.py deleted file mode 100755 index e8eeaa31..00000000 --- a/tools/configure_speechd.py +++ /dev/null @@ -1,242 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# Voice configuration script -# By Storm Dragon - -import configparser -import subprocess -import os -import sys -import time -import select -import tempfile - -def check_root_access(): - """Check if script is running as root and determine available privilege escalation command""" - if os.geteuid() == 0: - return True, None - sudoPath = run_command(['which', 'sudo']) - if sudoPath: - return False, 'sudo' - doasPath = run_command(['which', 'doas']) - if doasPath: - return False, 'doas' - return False, None - -def run_command(cmd, needsRoot=False, privilegeCmd=None): - """Run a command with optional root privileges""" - try: - if needsRoot and privilegeCmd and os.geteuid() != 0: - fullCmd = [privilegeCmd] + cmd - else: - fullCmd = cmd - result = subprocess.run(fullCmd, capture_output=True, text=True) - if result.returncode != 0: - print(f"Command failed: {' '.join(fullCmd)}") - print(f"Error: {result.stderr}") - return None - return result.stdout.strip() - except Exception as e: - print(f"Error running command {' '.join(cmd)}: {e}") - return None - -def run_command_with_output(cmd, needsRoot=False, privilegeCmd=None): - """Run a command and return its content directly""" - try: - if needsRoot and privilegeCmd and os.geteuid() != 0: - cmd = [privilegeCmd] + cmd - with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) as process: - output, error = process.communicate() - if process.returncode != 0: - print(f"Error: {error}") - return None - return output - except Exception as e: - print(f"Error running command {' '.join(cmd)}: {e}") - return None - -def get_speechd_modules(privilegeCmd): - """Get list of available speech-dispatcher modules""" - output = run_command_with_output(['spd-say', '-O'], True, privilegeCmd) - if output: - lines = output.split('\n') - if len(lines) > 1: - return [line.strip() for line in lines[1:] if line.strip()] - return [] - -def process_espeak_voice(voiceLine): - """Process espeak-ng voice line to get voice name from last two columns""" - parts = [p for p in voiceLine.split() if p] - if len(parts) < 2: - return None - langCode = parts[-2].lower() - variant = parts[-1].lower() - if variant and variant != 'none': - return f"{langCode}+{variant}" - return langCode - -def get_module_voices(moduleName, privilegeCmd): - """Get available voices for a specific module""" - output = run_command_with_output(['spd-say', '-o', moduleName, '-L'], True, privilegeCmd) - if output: - lines = output.split('\n') - if len(lines) > 1: - voices = [] - for line in lines[1:]: - if not line.strip(): - continue - if moduleName.lower() == 'espeak-ng': - processedVoice = process_espeak_voice(line) - if processedVoice: - voices.append(processedVoice) - else: - voices.append(line.strip()) - return voices - return [] - -def display_paginated_voices(voiceList): - """Display voices in pages of 30""" - pageSize = 30 - totalVoices = len(voiceList) - totalPages = (totalVoices + pageSize - 1) // pageSize - currentPage = 0 - while currentPage < totalPages: - start = currentPage * pageSize - end = min(start + pageSize, totalVoices) - print(f"\nShowing voices {start + 1}-{end} of {totalVoices} (Page {currentPage + 1} of {totalPages})") - print("Available voices:") - for i, voice in enumerate(voiceList[start:end], start + 1): - print(f"{i}. {voice}") - if currentPage < totalPages - 1: - print("\nPress Enter for next page, 'b' for previous page, or enter a number to select a voice (0 to go back): ") - else: - print("\nPress 'b' for previous page, or enter a number to select a voice (0 to go back): ") - choice = input().strip().lower() - if choice == '': - if currentPage < totalPages - 1: - currentPage += 1 - elif choice == 'b': - if currentPage > 0: - currentPage -= 1 - elif choice.isdigit(): - choice = int(choice) - if choice == 0: - return None - if 1 <= choice <= totalVoices: - return voiceList[choice - 1] - print("Invalid selection!") - else: - print("Invalid input!") - return None - -def test_voice_config(moduleName, voiceName, privilegeCmd): - """Test if the selected voice works""" - print("\nTesting voice configuration...") - testMessage = "If you hear this message, press Enter within 30 seconds to confirm. Otherwise, wait for timeout to return to the menu." - try: - cmd = ['spd-say', '-o', moduleName, '-y', voiceName, testMessage] - if privilegeCmd and os.geteuid() != 0: - cmd = [privilegeCmd] + cmd - process = subprocess.Popen(cmd) - startTime = time.time() - while time.time() - startTime < 30: - if select.select([sys.stdin], [], [], 0.0)[0]: - userInput = input() - process.terminate() - return True - time.sleep(0.1) - process.terminate() - return False - except Exception as e: - print(f"Error testing voice: {e}") - return False - -def update_fenrir_config(configPath, moduleName, voiceName, privilegeCmd): - """Update Fenrir configuration with new speech settings""" - config = configparser.ConfigParser() - try: - configContent = run_command_with_output(['cat', configPath], True, privilegeCmd) - if not configContent: - return False - with tempfile.NamedTemporaryFile(mode='w', delete=False) as tempFile: - tempPath = tempFile.name - tempFile.write(configContent) - config.read(tempPath) - if 'speech' not in config: - config['speech'] = {} - config['speech']['driver'] = 'speechdDriver' - config['speech']['module'] = moduleName - config['speech']['voice'] = voiceName - config['speech']['enabled'] = 'True' - config['speech']['rate'] = '0.25' - config['speech']['pitch'] = '0.5' - config['speech']['volume'] = '1.0' - with open(tempPath, 'w') as configFile: - config.write(configFile) - result = run_command(['cp', tempPath, configPath], True, privilegeCmd) - os.unlink(tempPath) - return result is not None - except Exception as e: - print(f"Error updating configuration: {e}") - if 'tempPath' in locals(): - try: - os.unlink(tempPath) - except: - pass - return False - -def main(): - configPath = '/etc/fenrirscreenreader/settings/settings.conf' - isRoot, privilegeCmd = check_root_access() - if not isRoot and not privilegeCmd: - print("Error: This script needs root privileges. Please run with sudo or doas.") - return - configExists = run_command(['test', '-f', configPath], True, privilegeCmd) is not None - if not configExists: - print(f"Configuration file not found: {configPath}") - return - while True: - print("\nFenrir Speech-Dispatcher Configuration") - print("=====================================") - print("\nGetting available speech-dispatcher modules...") - moduleList = get_speechd_modules(privilegeCmd) - if not moduleList: - print("No speech-dispatcher modules found!") - return - print("\nAvailable modules:") - for i, moduleName in enumerate(moduleList, 1): - print(f"{i}. {moduleName}") - while True: - try: - moduleChoice = int(input("\nSelect module number (or 0 to exit): ")) - if moduleChoice == 0: - return - if 1 <= moduleChoice <= len(moduleList): - selectedModule = moduleList[moduleChoice - 1] - break - print("Invalid selection!") - except ValueError: - print("Please enter a number!") - print(f"\nGetting voices for {selectedModule}...") - voiceList = get_module_voices(selectedModule, privilegeCmd) - if not voiceList: - print(f"No voices found for module {selectedModule}") - continue - selectedVoice = display_paginated_voices(voiceList) - if not selectedVoice: - continue - print("\nTesting selected voice configuration...") - if test_voice_config(selectedModule, selectedVoice, privilegeCmd): - print("\nVoice test successful!") - if update_fenrir_config(configPath, selectedModule, selectedVoice, privilegeCmd): - print("\nConfiguration updated successfully!") - print("Please restart Fenrir for changes to take effect") - return - else: - print("Failed to update configuration!") - else: - print("\nVoice test failed or timed out. Please try again.") - -if __name__ == "__main__": - main()