#!/usr/bin/env python3 import subprocess import importlib.util import os import time class DynamicVoiceCommand: """Dynamic command class for voice selection""" def __init__(self, module, voice, env): self.module = module self.voice = voice self.env = env self.testMessage = "This is a voice test. The quick brown fox jumps over the lazy dog." def initialize(self, environment): self.env = environment def shutdown(self): pass def getDescription(self): return f"Select voice: {self.voice}" def run(self): try: self.env['runtime']['outputManager'].presentText(f"Testing voice {self.voice} from {self.module}. Please wait.", 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 (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 else: 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=False, flush=False) def testVoice(self): """Test voice with spd-say""" try: 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 class DynamicApplyVoiceCommand: """Command to apply the tested voice""" def __init__(self, env): self.env = env def initialize(self, environment): self.env = environment def shutdown(self): pass def getDescription(self): return "Apply tested voice to Fenrir" def run(self): try: if not self.env['commandBuffer'].get('voiceTestCompleted', False): self.env['runtime']['outputManager'].presentText("No voice has been tested yet", interrupt=True) return module = self.env['commandBuffer']['pendingVoiceModule'] voice = self.env['commandBuffer']['pendingVoiceVoice'] self.env['runtime']['outputManager'].presentText(f"Applying {voice} from {module}", interrupt=True) # Debug: Show current settings settingsManager = self.env['runtime']['settingsManager'] currentModule = settingsManager.getSetting('speech', 'module') currentVoice = settingsManager.getSetting('speech', 'voice') self.env['runtime']['outputManager'].presentText(f"Current: {currentVoice} from {currentModule}", interrupt=False, flush=False) # 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) # 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 voice error: {str(e)}", interrupt=False, flush=False) def setCallback(self, callback): pass def addDynamicVoiceMenus(vmenuManager): """Add dynamic voice menus to vmenu system""" try: env = vmenuManager.env # Get speech modules modules = getSpeechdModules() if not modules: return # Create voice browser submenu voiceBrowserMenu = {} # Add apply voice command applyCommand = DynamicApplyVoiceCommand(env) voiceBrowserMenu['Apply Tested Voice Action'] = applyCommand # Add modules as submenus for module in modules[:8]: # Limit to 8 modules to keep menu manageable moduleMenu = {} # Get voices for this module voices = getModuleVoices(module) if voices: # Add voice commands for voice in voices: voiceCommand = DynamicVoiceCommand(module, voice, env) moduleMenu[f"{voice} Action"] = voiceCommand else: moduleMenu['No voices available Action'] = createInfoCommand(f"No voices found for {module}", env) voiceBrowserMenu[f"{module} Menu"] = moduleMenu # Add to main menu dict vmenuManager.menuDict['Voice Browser Menu'] = voiceBrowserMenu except Exception as e: print(f"Error creating dynamic voice menus: {e}") def createInfoCommand(message, env): """Create a simple info command""" class InfoCommand: def __init__(self, message, env): self.message = message self.env = env def initialize(self, environment): pass def shutdown(self): pass def getDescription(self): return self.message def run(self): self.env['runtime']['outputManager'].presentText(self.message, interrupt=True) def setCallback(self, callback): pass return InfoCommand(message, env) def getSpeechdModules(): """Get available speech modules""" try: result = subprocess.run(['spd-say', '-O'], capture_output=True, text=True, timeout=5) 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(module): """Get voices for a module""" 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 = processEspeakVoice(line) if voice: voices.append(voice) else: voices.append(line.strip()) return voices except Exception: pass return [] def processEspeakVoice(voiceLine): """Process espeak voice format""" try: 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 except Exception: return None