diff --git a/src/fenrirscreenreader/commands/commands/apply_tested_voice.py b/src/fenrirscreenreader/commands/commands/apply_tested_voice.py index 53fb91a3..91cbc047 100644 --- a/src/fenrirscreenreader/commands/commands/apply_tested_voice.py +++ b/src/fenrirscreenreader/commands/commands/apply_tested_voice.py @@ -37,16 +37,18 @@ class command(): oldVoice = settingsManager.getSetting('speech', 'voice') try: - # Apply new settings to runtime only - settingsManager.settings['speech']['driver'] = 'speechdDriver' - settingsManager.settings['speech']['module'] = module - settingsManager.settings['speech']['voice'] = voice + # Apply new settings to runtime only (use setSetting to update settingArgDict) + settingsManager.setSetting('speech', 'driver', 'speechdDriver') + settingsManager.setSetting('speech', 'module', module) + settingsManager.setSetting('speech', 'voice', voice) - # Try to reinitialize speech driver + # Apply to speech driver instance directly if 'speechDriver' in self.env['runtime']: speechDriver = self.env['runtime']['speechDriver'] - speechDriver.shutdown() - speechDriver.initialize(self.env) + + # Set the module and voice on the driver instance + speechDriver.setModule(module) + speechDriver.setVoice(voice) self.env['runtime']['outputManager'].presentText("Voice applied successfully!", interrupt=True) self.env['runtime']['outputManager'].presentText("Use save settings to make permanent", interrupt=True) @@ -54,9 +56,9 @@ class command(): except Exception as e: # Revert on failure - settingsManager.settings['speech']['driver'] = oldDriver - settingsManager.settings['speech']['module'] = oldModule - settingsManager.settings['speech']['voice'] = oldVoice + settingsManager.setSetting('speech', 'driver', oldDriver) + settingsManager.setSetting('speech', 'module', oldModule) + settingsManager.setSetting('speech', 'voice', oldVoice) self.env['runtime']['outputManager'].presentText(f"Failed to apply voice, reverted: {str(e)}", interrupt=True) self.env['runtime']['outputManager'].playSound('Error') diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/config_base.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/config_base.py deleted file mode 100644 index 135a1234..00000000 --- a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/config_base.py +++ /dev/null @@ -1,302 +0,0 @@ -#!/usr/bin/env python3 - -import os -import configparser -import subprocess -import tempfile -from typing import Dict, List, Optional, Tuple, Any - -class ConfigCommand: - """Base class for Fenrir configuration vmenu commands""" - - def __init__(self): - self.env = None - self.config = None - self.settingsFile = None - - # Configuration presets from original configure_fenrir.py - self.presetOptions = { - 'sound.driver': ['genericDriver', 'gstreamerDriver'], - 'speech.driver': ['speechdDriver', 'genericDriver'], - 'screen.driver': ['vcsaDriver', 'dummyDriver', 'ptyDriver', 'debugDriver'], - 'keyboard.driver': ['evdevDriver', 'dummyDriver'], - 'remote.driver': ['unixDriver', 'tcpDriver'], - 'keyboard.charEchoMode': ['0', '1', '2'], - 'general.punctuationLevel': ['none', 'some', 'most', 'all'], - 'general.debugMode': ['File', 'Print'], - 'keyboard.device': ['ALL', 'NOMICE'], - 'screen.encoding': ['auto', 'utf-8', 'cp1252', 'iso-8859-1'] - } - - # Help text for various 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)', - 'speech.pitch': 'Voice pitch from 0 (lowest) to 1.0 (highest)', - 'speech.capitalPitch': 'Pitch for capital letters from 0 to 1.0', - 'keyboard.charEchoMode': '0 = None, 1 = always, 2 = only while capslock', - 'keyboard.doubleTapTimeout': 'Timeout for double tap in seconds', - 'screen.screenUpdateDelay': 'Delay between screen updates in seconds', - 'general.punctuationLevel': 'none = no punctuation, some = basic, most = detailed, all = everything', - 'general.numberOfClipboards': 'Number of clipboard slots to maintain' - } - - def initialize(self, environment): - """Initialize with Fenrir environment""" - self.env = environment - self.settingsFile = self.env['runtime']['settingsManager'].settingsFile - self.config = configparser.ConfigParser(interpolation=None) - self.config.read(self.settingsFile) - - def shutdown(self): - """Cleanup resources""" - pass - - def getDescription(self): - """Return description for this configuration action""" - return "Base configuration command" - - def run(self): - """Execute the configuration action - to be overridden""" - self.presentText("Configuration base class - override run() method") - - def presentText(self, text: str, interrupt: bool = True): - """Present text using Fenrir's speech system""" - if self.env and 'runtime' in self.env and 'outputManager' in self.env['runtime']: - self.env['runtime']['outputManager'].presentText(text, interrupt=interrupt) - - def presentConfirmation(self, text: str): - """Present confirmation message that won't be interrupted by menu navigation""" - self.presentText(text, interrupt=False) - - def playSound(self, soundName: str): - """Play a sound using Fenrir's sound system (deprecated - sounds removed from vmenu)""" - # Sounds removed from vmenu commands to avoid configuration issues - pass - - def getSetting(self, section: str, option: str, default: str = "") -> str: - """Get a setting value from configuration""" - try: - if section in self.config and option in self.config[section]: - return self.config[section][option] - return default - except Exception: - return default - - def setSetting(self, section: str, option: str, value: str) -> bool: - """Set a setting value and save configuration""" - try: - if section not in self.config: - self.config[section] = {} - - oldValue = self.getSetting(section, option) - self.config[section][option] = str(value) - - # Write configuration file - with open(self.settingsFile, 'w') as configfile: - self.config.write(configfile) - - # Apply setting immediately if possible - self.applySettingImmediate(section, option, value, oldValue) - - return True - except Exception as e: - self.presentText(f"Error saving setting: {str(e)}") - return False - - def applySettingImmediate(self, section: str, option: str, newValue: str, oldValue: str): - """Apply setting immediately without restart where possible""" - try: - # Apply speech settings immediately - if section == 'speech': - if option in ['rate', 'pitch', 'volume']: - settingsManager = self.env['runtime']['settingsManager'] - settingsManager.setSetting(section, option, newValue) - - # Apply sound settings immediately - elif section == 'sound': - if option == 'volume': - settingsManager = self.env['runtime']['settingsManager'] - settingsManager.setSetting(section, option, newValue) - - except Exception as e: - # Silent fail - settings will apply on restart - pass - - def validateInput(self, section: str, option: str, value: str) -> Tuple[bool, str]: - """Validate user input based on option type and constraints""" - try: - # Validate float values (volume, rate, pitch) - if option in ['volume', 'rate', 'pitch', 'capitalPitch']: - floatVal = float(value) - if not 0 <= floatVal <= 1.0: - return False, "Value must be between 0 and 1.0" - return True, str(floatVal) - - # Validate integer values - elif option in ['numberOfClipboards', 'ignoreScreen']: - intVal = int(value) - if intVal < 0: - return False, "Value must be 0 or greater" - return True, str(intVal) - - # Validate float values with different ranges - elif option == 'doubleTapTimeout': - floatVal = float(value) - if not 0 <= floatVal <= 2.0: - return False, "Value must be between 0 and 2.0 seconds" - return True, str(floatVal) - - elif option == 'screenUpdateDelay': - floatVal = float(value) - if not 0 <= floatVal <= 1.0: - return False, "Value must be between 0 and 1.0 seconds" - return True, str(floatVal) - - # Validate boolean values - elif self.isBooleanOption(value): - if value.lower() in ['true', 'false']: - return True, value.capitalize() - return False, "Value must be True or False" - - # Validate preset options - key = f"{section}.{option}" - if key in self.presetOptions: - if value in self.presetOptions[key]: - return True, value - return False, f"Value must be one of: {', '.join(self.presetOptions[key])}" - - # Default validation - accept any string - return True, str(value).strip() - - except ValueError: - return False, "Invalid number format" - except Exception as e: - return False, f"Validation error: {str(e)}" - - def isBooleanOption(self, value: str) -> bool: - """Check if the current value is likely a boolean option""" - return value.lower() in ['true', 'false'] - - def getBooleanSetting(self, section: str, option: str, default: bool = False) -> bool: - """Get a boolean setting value""" - value = self.getSetting(section, option, str(default)).lower() - return value in ['true', '1', 'yes', 'on'] - - def setBooleanSetting(self, section: str, option: str, value: bool) -> bool: - """Set a boolean setting value""" - return self.setSetting(section, option, 'True' if value else 'False') - - def toggleBooleanSetting(self, section: str, option: str) -> bool: - """Toggle a boolean setting and return new value""" - currentValue = self.getBooleanSetting(section, option) - newValue = not currentValue - success = self.setBooleanSetting(section, option, newValue) - return newValue if success else currentValue - - def getFloatSetting(self, section: str, option: str, default: float = 0.0) -> float: - """Get a float setting value""" - try: - return float(self.getSetting(section, option, str(default))) - except ValueError: - return default - - def setFloatSetting(self, section: str, option: str, value: float) -> bool: - """Set a float setting value""" - return self.setSetting(section, option, str(value)) - - def adjustFloatSetting(self, section: str, option: str, delta: float, - minVal: float = 0.0, maxVal: float = 1.0) -> float: - """Adjust a float setting by delta amount""" - currentValue = self.getFloatSetting(section, option) - newValue = max(minVal, min(maxVal, currentValue + delta)) - success = self.setFloatSetting(section, option, newValue) - return newValue if success else currentValue - - def runCommand(self, cmd: List[str], timeout: int = 10) -> Optional[str]: - """Run a command and return output""" - try: - result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) - return result.stdout.strip() if result.returncode == 0 else None - except Exception: - return None - - def getSpeechdModules(self) -> List[str]: - """Get list of available speech-dispatcher modules""" - output = self.runCommand(['spd-say', '-O']) - if output: - lines = output.split('\n') - return [line.strip() for line in lines[1:] if line.strip()] - return [] - - def getModuleVoices(self, module: str) -> List[str]: - """Get list of voices for a specific speech module""" - output = self.runCommand(['spd-say', '-o', module, '-L']) - if output: - lines = output.split('\n') - voices = [] - for line in lines[1:]: - if not line.strip(): - continue - if module.lower() == 'espeak-ng': - voice = self.processEspeakVoice(line) - if voice: - voices.append(voice) - else: - voices.append(line.strip()) - return voices - return [] - - def processEspeakVoice(self, voiceLine: str) -> Optional[str]: - """Process espeak-ng voice line format""" - 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 testVoice(self, module: str, voice: str, testMessage: str = None) -> bool: - """Test a voice configuration""" - if not testMessage: - testMessage = "This is a voice test. If you can hear this clearly, the voice is working properly." - - try: - # Announce the test - self.presentText("Testing voice configuration. Listen for the test message.") - - # Run voice test - cmd = ['spd-say', '-o', module, '-y', voice, testMessage] - result = subprocess.run(cmd, timeout=10) - - return result.returncode == 0 - except Exception: - return False - - def backupConfig(self, announce: bool = True) -> tuple[bool, str]: - """Create a backup of current configuration""" - try: - import shutil - from datetime import datetime - - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - backupFile = f"{self.settingsFile}.backup_{timestamp}" - shutil.copy2(self.settingsFile, backupFile) - - message = f"Configuration backed up to {backupFile}" - if announce: - self.presentText(message, interrupt=False) - return True, message - except Exception as e: - error_msg = f"Error creating backup: {str(e)}" - if announce: - self.presentText(error_msg, interrupt=False) - return False, error_msg - - def reloadConfig(self): - """Reload configuration from file""" - try: - self.config.read(self.settingsFile) - return True - except Exception: - return False \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/apply_previewed_voice.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/apply_previewed_voice.py deleted file mode 100644 index 724424f6..00000000 --- a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/apply_previewed_voice.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 - -class command(): - def __init__(self): - pass - - def initialize(self, environment): - self.env = environment - - def shutdown(self): - pass - - def getDescription(self): - return "Apply last previewed voice to current session (temporary)" - - def run(self): - # Check if we have a tested voice stored (must be tested first) - if ('commandBuffer' not in self.env or - 'lastTestedModule' not in self.env['commandBuffer'] or - 'lastTestedVoice' not in self.env['commandBuffer']): - self.env['runtime']['outputManager'].presentText("No voice has been tested yet", interrupt=True) - self.env['runtime']['outputManager'].presentText("Use test voice command first to confirm the voice works", interrupt=True) - self.env['runtime']['outputManager'].playSound('Cancel') - return - - module = self.env['commandBuffer']['lastTestedModule'] - voice = self.env['commandBuffer']['lastTestedVoice'] - - self.env['runtime']['outputManager'].presentText(f"Applying tested voice: {voice} from module {module}", interrupt=True) - self.env['runtime']['outputManager'].presentText("This will change your current voice temporarily", interrupt=True) - - try: - # Apply to runtime settings only (temporary until saved) - settingsManager = self.env['runtime']['settingsManager'] - - # Store old values in case we need to revert - oldDriver = settingsManager.getSetting('speech', 'driver') - oldModule = settingsManager.getSetting('speech', 'module') - oldVoice = settingsManager.getSetting('speech', 'voice') - - # Apply new settings to runtime only - settingsManager.settings['speech']['driver'] = 'speechdDriver' - settingsManager.settings['speech']['module'] = module - settingsManager.settings['speech']['voice'] = voice - - # Try to reinitialize speech driver with new settings - if 'speechDriver' in self.env['runtime']: - try: - speechDriver = self.env['runtime']['speechDriver'] - speechDriver.shutdown() - speechDriver.initialize(self.env) - - # Test the new voice - self.env['runtime']['outputManager'].presentText("Voice applied successfully. This is how it sounds.", interrupt=False) - self.env['runtime']['outputManager'].presentText("Use save command to make this change permanent", interrupt=False) - self.env['runtime']['outputManager'].playSound('Accept') - - except Exception as e: - # Revert on failure - settingsManager.settings['speech']['driver'] = oldDriver - settingsManager.settings['speech']['module'] = oldModule - settingsManager.settings['speech']['voice'] = oldVoice - - self.env['runtime']['outputManager'].presentText(f"Failed to apply voice: {str(e)}", interrupt=False) - self.env['runtime']['outputManager'].presentText("Reverted to previous settings", interrupt=False) - self.env['runtime']['outputManager'].playSound('Error') - else: - self.env['runtime']['outputManager'].presentText("Speech driver not available", interrupt=False) - self.env['runtime']['outputManager'].playSound('Error') - - except Exception as e: - self.env['runtime']['outputManager'].presentText(f"Error applying voice: {str(e)}", interrupt=False) - self.env['runtime']['outputManager'].playSound('Error') - - def setCallback(self, callback): - pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/configure_voice.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/configure_voice.py deleted file mode 100644 index 734fa41e..00000000 --- a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/configure_voice.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env python3 - -import subprocess -import time - -class command(): - def __init__(self): - pass - - def initialize(self, environment): - self.env = environment - self.testMessage = "This is a voice test. If you can hear this message clearly, press Enter to accept this voice. Otherwise, wait 30 seconds to cancel." - - def shutdown(self): - pass - - def getDescription(self): - return "Configure speech module and voice with testing" - - def run(self): - self.env['runtime']['outputManager'].presentText("Starting voice configuration wizard", interrupt=True) - - # Step 1: Get available speech modules - modules = self.getSpeechdModules() - if not modules: - self.env['runtime']['outputManager'].presentText("No speech-dispatcher modules found. Please install speech-dispatcher.", interrupt=True) - self.env['runtime']['outputManager'].playSound('Error') - return - - # For this implementation, cycle through modules or use the first available one - # Get current module - currentModule = self.env['runtime']['settingsManager'].getSetting('speech', 'module') - if not currentModule: - currentModule = modules[0] - - # Find next module or cycle to first - try: - currentIndex = modules.index(currentModule) - nextIndex = (currentIndex + 1) % len(modules) - selectedModule = modules[nextIndex] - except ValueError: - selectedModule = modules[0] - - self.env['runtime']['outputManager'].presentText(f"Selected speech module: {selectedModule}", interrupt=True) - - # Step 2: Get available voices for the module - voices = self.getModuleVoices(selectedModule) - if not voices: - self.env['runtime']['outputManager'].presentText(f"No voices found for module {selectedModule}", interrupt=True) - self.env['runtime']['outputManager'].playSound('Error') - return - - # Get current voice and cycle to next, or use best voice - currentVoice = self.env['runtime']['settingsManager'].getSetting('speech', 'voice') - if currentVoice and currentVoice in voices: - try: - currentIndex = voices.index(currentVoice) - nextIndex = (currentIndex + 1) % len(voices) - selectedVoice = voices[nextIndex] - except ValueError: - selectedVoice = self.selectBestVoice(voices) - else: - selectedVoice = self.selectBestVoice(voices) - - self.env['runtime']['outputManager'].presentText(f"Testing voice: {selectedVoice}", interrupt=True) - - # Step 3: Test the voice configuration - if self.testVoiceWithConfirmation(selectedModule, selectedVoice): - # User confirmed - save the configuration - self.env['runtime']['settingsManager'].setSetting('speech', 'driver', 'speechdDriver') - self.env['runtime']['settingsManager'].setSetting('speech', 'module', selectedModule) - self.env['runtime']['settingsManager'].setSetting('speech', 'voice', selectedVoice) - - self.env['runtime']['outputManager'].presentText("Voice configuration saved successfully!", interrupt=True) - self.env['runtime']['outputManager'].presentText(f"Module: {selectedModule}, Voice: {selectedVoice}", interrupt=True) - self.env['runtime']['outputManager'].playSound('Accept') - else: - # User cancelled or test failed - self.env['runtime']['outputManager'].presentText("Voice configuration cancelled. Settings unchanged.", interrupt=True) - self.env['runtime']['outputManager'].playSound('Cancel') - - def getSpeechdModules(self): - """Get list of available speech-dispatcher modules""" - try: - result = subprocess.run(['spd-say', '-O'], capture_output=True, text=True, timeout=10) - if result.returncode == 0: - lines = result.stdout.strip().split('\n') - return [line.strip() for line in lines[1:] if line.strip()] - except Exception: - pass - return [] - - def getModuleVoices(self, module): - """Get list of voices for a specific speech module""" - try: - result = subprocess.run(['spd-say', '-o', module, '-L'], capture_output=True, text=True, timeout=10) - 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.processEspeakVoice(line) - if voice: - voices.append(voice) - else: - voices.append(line.strip()) - return voices - except Exception: - pass - return [] - - def processEspeakVoice(self, voiceLine): - """Process espeak-ng voice line format""" - 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 selectBestVoice(self, voices): - """Select the best voice from available voices, preferring English""" - # Look for English voices first - for voice in voices: - if any(lang in voice.lower() for lang in ['en', 'english', 'us', 'gb']): - return voice - - # If no English voice found, return the first available - return voices[0] if voices else "" - - def testVoiceWithConfirmation(self, module, voice): - """Test voice and wait for user confirmation""" - try: - # Start the voice test - self.env['runtime']['outputManager'].presentText("Starting voice test. Listen carefully.", interrupt=True) - time.sleep(1) # Brief pause - - # Use spd-say to test the voice - process = subprocess.Popen([ - 'spd-say', '-o', module, '-y', voice, self.testMessage - ]) - - # Wait for the test message to finish (give it some time) - time.sleep(2) - - # Now wait for user input - self.env['runtime']['outputManager'].presentText("Press Enter if you heard the test message and want to keep this voice, or wait 30 seconds to cancel.", interrupt=True) - - # Set up a simple confirmation system - # Since vmenu doesn't support input waiting natively, we'll use a simpler approach - # The user will need to run this command again to cycle through voices - # and the settings will be applied immediately for testing - - # Apply settings temporarily for immediate testing - oldModule = self.env['runtime']['settingsManager'].getSetting('speech', 'module') - oldVoice = self.env['runtime']['settingsManager'].getSetting('speech', 'voice') - - self.env['runtime']['settingsManager'].setSetting('speech', 'module', module) - self.env['runtime']['settingsManager'].setSetting('speech', 'voice', voice) - - # Test with Fenrir's own speech system - time.sleep(1) - self.env['runtime']['outputManager'].presentText("This is how the new voice sounds in Fenrir. Run this command again to try the next voice, or exit the menu to keep this voice.", interrupt=True) - - # For now, we'll auto-accept since we can't wait for input in vmenu - # The user can cycle through by running the command multiple times - return True - - except Exception as e: - self.env['runtime']['outputManager'].presentText(f"Error testing voice: {str(e)}", interrupt=True) - return False - finally: - # Clean up any running processes - try: - process.terminate() - except: - pass - - def setCallback(self, callback): - pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/preview_voices.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/preview_voices.py deleted file mode 100644 index 7855967a..00000000 --- a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/preview_voices.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python3 - -import subprocess - -class command(): - def __init__(self): - pass - - def initialize(self, environment): - self.env = environment - self.testMessage = "This is a voice preview. The quick brown fox jumps over the lazy dog." - - def shutdown(self): - pass - - def getDescription(self): - return "Cycle through available voices (run multiple times to browse)" - - def run(self): - # Get available modules - modules = self.getSpeechdModules() - if not modules: - self.env['runtime']['outputManager'].presentText("No speech-dispatcher modules found", interrupt=True) - self.env['runtime']['outputManager'].playSound('Error') - return - - # Get stored indexes or initialize - moduleIndex = self.env['commandBuffer'].get('voicePreviewModuleIndex', 0) - voiceIndex = self.env['commandBuffer'].get('voicePreviewVoiceIndex', 0) - - # Ensure indexes are valid - if moduleIndex >= len(modules): - moduleIndex = 0 - - selectedModule = modules[moduleIndex] - - # Get voices for current module - voices = self.getModuleVoices(selectedModule) - if not voices: - self.env['runtime']['outputManager'].presentText(f"No voices found for {selectedModule}, trying next module", interrupt=True) - moduleIndex = (moduleIndex + 1) % len(modules) - self.env['commandBuffer']['voicePreviewModuleIndex'] = moduleIndex - self.env['commandBuffer']['voicePreviewVoiceIndex'] = 0 - return - - # Ensure voice index is valid - if voiceIndex >= len(voices): - voiceIndex = 0 - - selectedVoice = voices[voiceIndex] - - # Present current selection - self.env['runtime']['outputManager'].presentText( - f"Module: {selectedModule} ({moduleIndex + 1}/{len(modules)})", interrupt=True - ) - self.env['runtime']['outputManager'].presentText( - f"Voice: {selectedVoice} ({voiceIndex + 1}/{len(voices)})", interrupt=True - ) - - # Test the voice - self.env['runtime']['outputManager'].presentText("Testing voice...", interrupt=True) - if self.previewVoice(selectedModule, selectedVoice): - self.env['runtime']['outputManager'].presentText("Voice test completed", interrupt=True) - self.env['runtime']['outputManager'].presentText("Run again for next voice, or use Apply Voice to make this active", interrupt=True) - - # Store for potential application - self.env['commandBuffer']['lastTestedModule'] = selectedModule - self.env['commandBuffer']['lastTestedVoice'] = selectedVoice - - self.env['runtime']['outputManager'].playSound('Accept') - else: - self.env['runtime']['outputManager'].presentText("Voice test failed", interrupt=True) - self.env['runtime']['outputManager'].playSound('Error') - - # Advance to next voice for next run - voiceIndex += 1 - if voiceIndex >= len(voices): - voiceIndex = 0 - moduleIndex = (moduleIndex + 1) % len(modules) - - # Store indexes for next run - self.env['commandBuffer']['voicePreviewModuleIndex'] = moduleIndex - self.env['commandBuffer']['voicePreviewVoiceIndex'] = voiceIndex - - def previewVoice(self, module, voice): - """Preview voice using spd-say without affecting Fenrir""" - try: - cmd = ['spd-say', '-o', module, '-y', voice, self.testMessage] - result = subprocess.run(cmd, timeout=15) - return result.returncode == 0 - except Exception: - return False - - def getSpeechdModules(self): - """Get list of available speech-dispatcher modules""" - try: - result = subprocess.run(['spd-say', '-O'], capture_output=True, text=True, timeout=10) - if result.returncode == 0: - lines = result.stdout.strip().split('\n') - return [line.strip() for line in lines[1:] if line.strip()] - except Exception: - pass - return [] - - def getModuleVoices(self, module): - """Get list of voices for a specific speech module""" - try: - result = subprocess.run(['spd-say', '-o', module, '-L'], capture_output=True, text=True, timeout=10) - 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.processEspeakVoice(line) - if voice: - voices.append(voice) - else: - voices.append(line.strip()) - return voices - except Exception: - pass - return [] - - def processEspeakVoice(self, voiceLine): - """Process espeak-ng voice line format""" - 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 setCallback(self, callback): - pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/test_current_voice.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/test_current_voice.py deleted file mode 100644 index f5c51d69..00000000 --- a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/test_current_voice.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python3 - -import subprocess - -class command(): - def __init__(self): - pass - - def initialize(self, environment): - self.env = environment - - def shutdown(self): - pass - - def getDescription(self): - return "Test current voice configuration" - - def run(self): - # Get current speech settings - driver = self.env['runtime']['settingsManager'].getSetting('speech', 'driver') - module = self.env['runtime']['settingsManager'].getSetting('speech', 'module') - voice = self.env['runtime']['settingsManager'].getSetting('speech', 'voice') - rate = self.env['runtime']['settingsManager'].getSetting('speech', 'rate') - pitch = self.env['runtime']['settingsManager'].getSetting('speech', 'pitch') - volume = self.env['runtime']['settingsManager'].getSetting('speech', 'volume') - - # Present current configuration - self.env['runtime']['outputManager'].presentText("Testing current voice configuration", interrupt=True) - - if driver == 'speechdDriver' and module: - self.env['runtime']['outputManager'].presentText(f"Driver: {driver}", interrupt=True) - self.env['runtime']['outputManager'].presentText(f"Module: {module}", interrupt=True) - if voice: - self.env['runtime']['outputManager'].presentText(f"Voice: {voice}", interrupt=True) - - try: - ratePercent = int(float(rate) * 100) - self.env['runtime']['outputManager'].presentText(f"Rate: {ratePercent} percent", interrupt=True) - except: - pass - - try: - pitchPercent = int(float(pitch) * 100) - self.env['runtime']['outputManager'].presentText(f"Pitch: {pitchPercent} percent", interrupt=True) - except: - pass - - try: - volumePercent = int(float(volume) * 100) - self.env['runtime']['outputManager'].presentText(f"Volume: {volumePercent} percent", interrupt=True) - except: - pass - - # Test message - testMessage = "This is a test of your current voice configuration. The quick brown fox jumps over the lazy dog. Numbers: one, two, three, four, five. If you can hear this message clearly, your voice settings are working properly." - - self.env['runtime']['outputManager'].presentText("Voice test:", interrupt=True) - self.env['runtime']['outputManager'].presentText(testMessage, interrupt=True) - self.env['runtime']['outputManager'].playSound('Accept') - - def setCallback(self, callback): - pass \ No newline at end of file diff --git a/src/fenrirscreenreader/core/dynamicVoiceMenu.py b/src/fenrirscreenreader/core/dynamicVoiceMenu.py index 972ec776..d23968a5 100644 --- a/src/fenrirscreenreader/core/dynamicVoiceMenu.py +++ b/src/fenrirscreenreader/core/dynamicVoiceMenu.py @@ -3,6 +3,7 @@ import subprocess import importlib.util import os +import time class DynamicVoiceCommand: """Dynamic command class for voice selection""" @@ -23,34 +24,42 @@ class DynamicVoiceCommand: def run(self): try: - self.env['runtime']['outputManager'].presentText(f"Testing {self.voice} from {self.module}", interrupt=True) + self.env['runtime']['outputManager'].presentText(f"Testing voice {self.voice} from {self.module}. Please wait.", interrupt=True) - # Test voice first - if self.testVoice(): - self.env['runtime']['outputManager'].presentText("Voice test completed. Press Enter again to apply.", interrupt=True) + # Brief pause before testing to avoid speech overlap + time.sleep(0.5) + + # Test voice + testResult, errorMsg = self.testVoice() + if testResult: + self.env['runtime']['outputManager'].presentText("Voice test completed successfully. Navigate to Apply Tested Voice to use this voice.", interrupt=False, flush=False) - # Store for confirmation + # Store for confirmation (use same variables as apply_tested_voice.py) + self.env['commandBuffer']['lastTestedModule'] = self.module + self.env['commandBuffer']['lastTestedVoice'] = self.voice self.env['commandBuffer']['pendingVoiceModule'] = self.module self.env['commandBuffer']['pendingVoiceVoice'] = self.voice self.env['commandBuffer']['voiceTestCompleted'] = True - - self.env['runtime']['outputManager'].playSound('Accept') else: - self.env['runtime']['outputManager'].presentText("Voice test failed", interrupt=True) - self.env['runtime']['outputManager'].playSound('Error') + self.env['runtime']['outputManager'].presentText(f"Voice test failed: {errorMsg}", interrupt=False, flush=False) except Exception as e: - self.env['runtime']['outputManager'].presentText(f"Voice selection error: {str(e)}", interrupt=True) - self.env['runtime']['outputManager'].playSound('Error') + self.env['runtime']['outputManager'].presentText(f"Voice selection error: {str(e)}", interrupt=False, flush=False) def testVoice(self): """Test voice with spd-say""" try: - cmd = ['spd-say', '-o', self.module, '-y', self.voice, self.testMessage] - result = subprocess.run(cmd, timeout=8) - return result.returncode == 0 - except Exception: - return False + cmd = ['spd-say', '-C', '-w', '-o', self.module, '-y', self.voice, self.testMessage] + result = subprocess.run(cmd, timeout=8, capture_output=True, text=True) + if result.returncode == 0: + return True, "Voice test successful" + else: + error_msg = result.stderr.strip() if result.stderr else f"Command failed with return code {result.returncode}" + return False, error_msg + except subprocess.TimeoutExpired: + return False, "Voice test timed out" + except Exception as e: + return False, f"Error running voice test: {str(e)}" def setCallback(self, callback): pass @@ -80,30 +89,83 @@ class DynamicApplyVoiceCommand: self.env['runtime']['outputManager'].presentText(f"Applying {voice} from {module}", interrupt=True) - # Apply to runtime settings + # Debug: Show current settings settingsManager = self.env['runtime']['settingsManager'] - settingsManager.settings['speech']['driver'] = 'speechdDriver' - settingsManager.settings['speech']['module'] = module - settingsManager.settings['speech']['voice'] = voice + currentModule = settingsManager.getSetting('speech', 'module') + currentVoice = settingsManager.getSetting('speech', 'voice') + self.env['runtime']['outputManager'].presentText(f"Current: {currentVoice} from {currentModule}", interrupt=False, flush=False) - # Reinitialize speech driver - if 'speechDriver' in self.env['runtime']: - speechDriver = self.env['runtime']['speechDriver'] - speechDriver.shutdown() - speechDriver.initialize(self.env) + # Apply to runtime settings with fallback + settingsManager = self.env['runtime']['settingsManager'] + + # Store old values for safety + oldDriver = settingsManager.getSetting('speech', 'driver') + oldModule = settingsManager.getSetting('speech', 'module') + oldVoice = settingsManager.getSetting('speech', 'voice') + + try: + # Apply new settings to runtime only (use setSetting to update settingArgDict) + settingsManager.setSetting('speech', 'driver', 'speechdDriver') + settingsManager.setSetting('speech', 'module', module) + settingsManager.setSetting('speech', 'voice', voice) - self.env['runtime']['outputManager'].presentText("Voice applied successfully!", interrupt=True) - self.env['runtime']['outputManager'].playSound('Accept') + # Apply settings to speech driver directly + if 'speechDriver' in self.env['runtime']: + speechDriver = self.env['runtime']['speechDriver'] + + # Get current module to see if we're changing modules + currentModule = settingsManager.getSetting('speech', 'module') + moduleChanging = (currentModule != module) + + # Set module and voice on driver instance first + speechDriver.setModule(module) + speechDriver.setVoice(voice) + + if moduleChanging: + # Module change requires reinitializing the speech driver + self.env['runtime']['outputManager'].presentText(f"Switching from {currentModule} to {module} module", interrupt=True) + speechDriver.shutdown() + speechDriver.initialize(self.env) + # Re-set after initialization + speechDriver.setModule(module) + speechDriver.setVoice(voice) + self.env['runtime']['outputManager'].presentText("Speech driver reinitialized", interrupt=True) + + # Debug: verify what was actually set + self.env['runtime']['outputManager'].presentText(f"Speech driver now has module: {speechDriver.module}, voice: {speechDriver.voice}", interrupt=True) + + # Force application by speaking a test message + self.env['runtime']['outputManager'].presentText("Voice applied successfully! You should hear this in the new voice.", interrupt=True) + + # Brief pause then more speech to test + time.sleep(1) + self.env['runtime']['outputManager'].presentText("Use save settings to make permanent", interrupt=True) # Clear pending state self.env['commandBuffer']['voiceTestCompleted'] = False # Exit vmenu after successful application self.env['runtime']['vmenuManager'].setActive(False) + + except Exception as e: + # Revert on failure + settingsManager.settings['speech']['driver'] = oldDriver + settingsManager.settings['speech']['module'] = oldModule + settingsManager.settings['speech']['voice'] = oldVoice + + # Try to reinitialize with old settings + if 'speechDriver' in self.env['runtime']: + try: + speechDriver = self.env['runtime']['speechDriver'] + speechDriver.shutdown() + speechDriver.initialize(self.env) + except: + pass # If this fails, at least we tried + + self.env['runtime']['outputManager'].presentText(f"Failed to apply voice, reverted: {str(e)}", interrupt=False, flush=False) except Exception as e: - self.env['runtime']['outputManager'].presentText(f"Apply failed: {str(e)}", interrupt=True) - self.env['runtime']['outputManager'].playSound('Error') + self.env['runtime']['outputManager'].presentText(f"Apply voice error: {str(e)}", interrupt=False, flush=False) def setCallback(self, callback): pass @@ -129,13 +191,9 @@ def addDynamicVoiceMenus(vmenuManager): for module in modules[:8]: # Limit to 8 modules to keep menu manageable moduleMenu = {} - # Get voices for this module (limit to prevent huge menus) + # Get voices for this module voices = getModuleVoices(module) if voices: - # Limit voices to keep menu usable - if len(voices) > 50: - voices = voices[:50] - moduleMenu['Note: Showing first 50 voices Action'] = createInfoCommand(f"Module {module} has {len(getModuleVoices(module))} voices, showing first 50", env) # Add voice commands for voice in voices: diff --git a/src/fenrirscreenreader/core/outputManager.py b/src/fenrirscreenreader/core/outputManager.py index 41a56456..b1651aae 100644 --- a/src/fenrirscreenreader/core/outputManager.py +++ b/src/fenrirscreenreader/core/outputManager.py @@ -192,3 +192,12 @@ class outputManager(): self.presentText(' review cursor ', interrupt=interrupt_p) else: self.presentText(' text cursor ', interrupt=interrupt_p) + + def resetSpeechDriver(self): + """Reset speech driver to clean state - called by settingsManager""" + if 'speechDriver' in self.env['runtime'] and self.env['runtime']['speechDriver']: + try: + self.env['runtime']['speechDriver'].reset() + self.env['runtime']['debug'].writeDebugOut("Speech driver reset successfully", debug.debugLevel.INFO) + except Exception as e: + self.env['runtime']['debug'].writeDebugOut(f"resetSpeechDriver error: {e}", debug.debugLevel.ERROR) diff --git a/src/fenrirscreenreader/speechDriver/speechdDriver.py b/src/fenrirscreenreader/speechDriver/speechdDriver.py index c87a86c0..57859e05 100644 --- a/src/fenrirscreenreader/speechDriver/speechdDriver.py +++ b/src/fenrirscreenreader/speechDriver/speechdDriver.py @@ -16,9 +16,15 @@ class driver(speechDriver): self._sd = None self.env = environment self._isInitialized = False - self.language = '' - self.voice = '' - self.module = '' + + # Only set these if they haven't been set yet (preserve existing values) + if not hasattr(self, 'language') or self.language is None: + self.language = '' + if not hasattr(self, 'voice') or self.voice is None: + self.voice = '' + if not hasattr(self, 'module') or self.module is None: + self.module = '' + try: import speechd self._sd = speechd.SSIPClient('fenrir')