Files
fenrir/src/fenrirscreenreader/core/dynamicVoiceMenu.py

387 lines
13 KiB
Python

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