#!/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()