387 lines
13 KiB
Python
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
|