Very experimental attempt to move the configure_fenrir script into fenrir itself using the vmenu system. Lots of testing please.
This commit is contained in:
212
src/fenrirscreenreader/core/dynamicVoiceMenu.py
Normal file
212
src/fenrirscreenreader/core/dynamicVoiceMenu.py
Normal file
@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
import importlib.util
|
||||
import os
|
||||
|
||||
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 {self.voice} from {self.module}", interrupt=True)
|
||||
|
||||
# Test voice first
|
||||
if self.testVoice():
|
||||
self.env['runtime']['outputManager'].presentText("Voice test completed. Press Enter again to apply.", interrupt=True)
|
||||
|
||||
# Store for confirmation
|
||||
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')
|
||||
|
||||
except Exception as e:
|
||||
self.env['runtime']['outputManager'].presentText(f"Voice selection error: {str(e)}", interrupt=True)
|
||||
self.env['runtime']['outputManager'].playSound('Error')
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
# Apply to runtime settings
|
||||
settingsManager = self.env['runtime']['settingsManager']
|
||||
settingsManager.settings['speech']['driver'] = 'speechdDriver'
|
||||
settingsManager.settings['speech']['module'] = module
|
||||
settingsManager.settings['speech']['voice'] = voice
|
||||
|
||||
# Reinitialize speech driver
|
||||
if 'speechDriver' in self.env['runtime']:
|
||||
speechDriver = self.env['runtime']['speechDriver']
|
||||
speechDriver.shutdown()
|
||||
speechDriver.initialize(self.env)
|
||||
|
||||
self.env['runtime']['outputManager'].presentText("Voice applied successfully!", interrupt=True)
|
||||
self.env['runtime']['outputManager'].playSound('Accept')
|
||||
|
||||
# Clear pending state
|
||||
self.env['commandBuffer']['voiceTestCompleted'] = False
|
||||
|
||||
# Exit vmenu after successful application
|
||||
self.env['runtime']['vmenuManager'].setActive(False)
|
||||
|
||||
except Exception as e:
|
||||
self.env['runtime']['outputManager'].presentText(f"Apply failed: {str(e)}", interrupt=True)
|
||||
self.env['runtime']['outputManager'].playSound('Error')
|
||||
|
||||
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 (limit to prevent huge menus)
|
||||
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:
|
||||
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
|
@ -152,6 +152,14 @@ class vmenuManager():
|
||||
menu = self.fs_tree_to_dict( self.defaultVMenuPath)
|
||||
if menu:
|
||||
self.menuDict = menu
|
||||
|
||||
# Add dynamic voice menus
|
||||
try:
|
||||
from fenrirscreenreader.core.dynamicVoiceMenu import addDynamicVoiceMenus
|
||||
addDynamicVoiceMenus(self)
|
||||
except Exception as e:
|
||||
print(f"Error adding dynamic voice menus: {e}")
|
||||
|
||||
# index still valid?
|
||||
if self.currIndex != None:
|
||||
try:
|
||||
|
Reference in New Issue
Block a user