#!/usr/bin/env python3 import importlib.util import os import subprocess import time from fenrirscreenreader.core import debug 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 get_description(self): return f"Select voice: {self.voice}" def run(self): try: self.env["runtime"]["OutputManager"].present_text( 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.test_voice() if testResult: self.env["runtime"]["OutputManager"].present_text( "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"].present_text( f"Voice test failed: {errorMsg}", interrupt=False, flush=False, ) except Exception as e: self.env["runtime"]["OutputManager"].present_text( f"Voice selection error: {str(e)}", interrupt=False, flush=False, ) def test_voice(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 set_callback(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 get_description(self): return "Apply tested voice to Fenrir" def run(self): try: if not self.env["commandBuffer"].get("voiceTestCompleted", False): self.env["runtime"]["OutputManager"].present_text( "No voice has been tested yet", interrupt=True ) return module = self.env["commandBuffer"]["pendingVoiceModule"] voice = self.env["commandBuffer"]["pendingVoiceVoice"] self.env["runtime"]["OutputManager"].present_text( f"Applying {voice} from {module}", interrupt=True ) # Debug: Show current settings settings_manager = self.env["runtime"]["SettingsManager"] current_module = settings_manager.get_setting("speech", "module") current_voice = settings_manager.get_setting("speech", "voice") self.env["runtime"]["OutputManager"].present_text( f"Current: {current_voice} from {current_module}", interrupt=False, flush=False, ) # Apply to runtime settings with fallback settings_manager = self.env["runtime"]["SettingsManager"] # Store old values for safety old_driver = settings_manager.get_setting("speech", "driver") old_module = settings_manager.get_setting("speech", "module") old_voice = settings_manager.get_setting("speech", "voice") try: # Apply new settings to runtime only (use set_setting to update # settingArgDict) settings_manager.set_setting( "speech", "driver", "speechdDriver" ) settings_manager.set_setting("speech", "module", module) settings_manager.set_setting("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 current_module = settings_manager.get_setting( "speech", "module" ) module_changing = current_module != module # Set module and voice on driver instance first SpeechDriver.set_module(module) SpeechDriver.set_voice(voice) if module_changing: # Module change requires reinitializing the speech # driver self.env["runtime"]["OutputManager"].present_text( f"Switching from {current_module} to {module} module", interrupt=True, ) SpeechDriver.shutdown() SpeechDriver.initialize(self.env) # Re-set after initialization SpeechDriver.set_module(module) SpeechDriver.set_voice(voice) self.env["runtime"]["OutputManager"].present_text( "Speech driver reinitialized", interrupt=True ) # Debug: verify what was actually set self.env["runtime"]["OutputManager"].present_text( f"Speech driver now has module: { SpeechDriver.module}, voice: { SpeechDriver.voice}", interrupt=True, ) # Force application by speaking a test message self.env["runtime"]["OutputManager"].present_text( "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"].present_text( "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"].set_active(False) except Exception as e: # Revert on failure settings_manager.settings["speech"]["driver"] = old_driver settings_manager.settings["speech"]["module"] = old_module settings_manager.settings["speech"]["voice"] = old_voice # 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 Exception as e: self.env["runtime"]["DebugManager"].write_debug_out( "dynamicVoiceMenu: Error reinitializing speech driver: " + str(e), debug.DebugLevel.ERROR, ) self.env["runtime"]["OutputManager"].present_text( f"Failed to apply voice, reverted: { str(e)}", interrupt=False, flush=False, ) except Exception as e: self.env["runtime"]["OutputManager"].present_text( f"Apply voice error: {str(e)}", interrupt=False, flush=False ) def set_callback(self, callback): pass def add_dynamic_voice_menus(VmenuManager): """Add dynamic voice menus to vmenu system""" try: env = VmenuManager.env # Get speech modules modules = get_speechd_modules() if not modules: return # Create voice browser submenu voice_browser_menu = {} # Add apply voice command apply_command = DynamicApplyVoiceCommand(env) voice_browser_menu["Apply Tested Voice Action"] = apply_command # Add modules as submenus for module in modules[ :8 ]: # Limit to 8 modules to keep menu manageable module_menu = {} # Get voices for this module voices = get_module_voices(module) if voices: # Add voice commands for voice in voices: voice_command = DynamicVoiceCommand(module, voice, env) module_menu[f"{voice} Action"] = voice_command else: module_menu["No voices available Action"] = ( create_info_command(f"No voices found for {module}", env) ) voice_browser_menu[f"{module} Menu"] = module_menu # Add to main menu dict VmenuManager.menuDict["Voice Browser Menu"] = voice_browser_menu except Exception as e: # Use debug manager instead of print for error logging if "DebugManager" in env["runtime"]: env["runtime"]["DebugManager"].write_debug_out( f"Error creating dynamic voice menus: {e}", debug.DebugLevel.ERROR, ) else: print(f"Error creating dynamic voice menus: {e}") def create_info_command(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 get_description(self): return self.message def run(self): self.env["runtime"]["OutputManager"].present_text( self.message, interrupt=True ) def set_callback(self, callback): pass return InfoCommand(message, env) def get_speechd_modules(): """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 get_module_voices(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 = process_espeak_voice(line) if voice: voices.append(voice) else: voices.append(line.strip()) return voices except Exception: pass return [] def process_espeak_voice(voiceLine): """Process espeak voice format""" try: parts = [p for p in voiceLine.split() if p] if len(parts) < 2: return None lang_code = parts[-2].lower() variant = parts[-1].lower() return ( f"{lang_code}+{variant}" if variant and variant != "none" else lang_code ) except Exception: return None