270 lines
11 KiB
Python
270 lines
11 KiB
Python
#!/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 |