243 lines
9.1 KiB
Python
243 lines
9.1 KiB
Python
|
#!/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()
|