Fixes to the voice driver. It should actually work completely now.

This commit is contained in:
Storm Dragon 2025-06-15 19:52:18 -04:00
parent 72bd334d65
commit 43871cea3c
9 changed files with 122 additions and 805 deletions

View File

@ -37,16 +37,18 @@ class command():
oldVoice = settingsManager.getSetting('speech', 'voice')
try:
# Apply new settings to runtime only
settingsManager.settings['speech']['driver'] = 'speechdDriver'
settingsManager.settings['speech']['module'] = module
settingsManager.settings['speech']['voice'] = voice
# 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)
# Try to reinitialize speech driver
# Apply to speech driver instance directly
if 'speechDriver' in self.env['runtime']:
speechDriver = self.env['runtime']['speechDriver']
speechDriver.shutdown()
speechDriver.initialize(self.env)
# Set the module and voice on the driver instance
speechDriver.setModule(module)
speechDriver.setVoice(voice)
self.env['runtime']['outputManager'].presentText("Voice applied successfully!", interrupt=True)
self.env['runtime']['outputManager'].presentText("Use save settings to make permanent", interrupt=True)
@ -54,9 +56,9 @@ class command():
except Exception as e:
# Revert on failure
settingsManager.settings['speech']['driver'] = oldDriver
settingsManager.settings['speech']['module'] = oldModule
settingsManager.settings['speech']['voice'] = oldVoice
settingsManager.setSetting('speech', 'driver', oldDriver)
settingsManager.setSetting('speech', 'module', oldModule)
settingsManager.setSetting('speech', 'voice', oldVoice)
self.env['runtime']['outputManager'].presentText(f"Failed to apply voice, reverted: {str(e)}", interrupt=True)
self.env['runtime']['outputManager'].playSound('Error')

View File

@ -1,302 +0,0 @@
#!/usr/bin/env python3
import os
import configparser
import subprocess
import tempfile
from typing import Dict, List, Optional, Tuple, Any
class ConfigCommand:
"""Base class for Fenrir configuration vmenu commands"""
def __init__(self):
self.env = None
self.config = None
self.settingsFile = None
# Configuration presets from original configure_fenrir.py
self.presetOptions = {
'sound.driver': ['genericDriver', 'gstreamerDriver'],
'speech.driver': ['speechdDriver', 'genericDriver'],
'screen.driver': ['vcsaDriver', 'dummyDriver', 'ptyDriver', 'debugDriver'],
'keyboard.driver': ['evdevDriver', 'dummyDriver'],
'remote.driver': ['unixDriver', 'tcpDriver'],
'keyboard.charEchoMode': ['0', '1', '2'],
'general.punctuationLevel': ['none', 'some', 'most', 'all'],
'general.debugMode': ['File', 'Print'],
'keyboard.device': ['ALL', 'NOMICE'],
'screen.encoding': ['auto', 'utf-8', 'cp1252', 'iso-8859-1']
}
# Help text for various options
self.helpText = {
'sound.volume': 'Volume level from 0 (quietest) to 1.0 (loudest)',
'speech.rate': 'Speech rate from 0 (slowest) to 1.0 (fastest)',
'speech.pitch': 'Voice pitch from 0 (lowest) to 1.0 (highest)',
'speech.capitalPitch': 'Pitch for capital letters from 0 to 1.0',
'keyboard.charEchoMode': '0 = None, 1 = always, 2 = only while capslock',
'keyboard.doubleTapTimeout': 'Timeout for double tap in seconds',
'screen.screenUpdateDelay': 'Delay between screen updates in seconds',
'general.punctuationLevel': 'none = no punctuation, some = basic, most = detailed, all = everything',
'general.numberOfClipboards': 'Number of clipboard slots to maintain'
}
def initialize(self, environment):
"""Initialize with Fenrir environment"""
self.env = environment
self.settingsFile = self.env['runtime']['settingsManager'].settingsFile
self.config = configparser.ConfigParser(interpolation=None)
self.config.read(self.settingsFile)
def shutdown(self):
"""Cleanup resources"""
pass
def getDescription(self):
"""Return description for this configuration action"""
return "Base configuration command"
def run(self):
"""Execute the configuration action - to be overridden"""
self.presentText("Configuration base class - override run() method")
def presentText(self, text: str, interrupt: bool = True):
"""Present text using Fenrir's speech system"""
if self.env and 'runtime' in self.env and 'outputManager' in self.env['runtime']:
self.env['runtime']['outputManager'].presentText(text, interrupt=interrupt)
def presentConfirmation(self, text: str):
"""Present confirmation message that won't be interrupted by menu navigation"""
self.presentText(text, interrupt=False)
def playSound(self, soundName: str):
"""Play a sound using Fenrir's sound system (deprecated - sounds removed from vmenu)"""
# Sounds removed from vmenu commands to avoid configuration issues
pass
def getSetting(self, section: str, option: str, default: str = "") -> str:
"""Get a setting value from configuration"""
try:
if section in self.config and option in self.config[section]:
return self.config[section][option]
return default
except Exception:
return default
def setSetting(self, section: str, option: str, value: str) -> bool:
"""Set a setting value and save configuration"""
try:
if section not in self.config:
self.config[section] = {}
oldValue = self.getSetting(section, option)
self.config[section][option] = str(value)
# Write configuration file
with open(self.settingsFile, 'w') as configfile:
self.config.write(configfile)
# Apply setting immediately if possible
self.applySettingImmediate(section, option, value, oldValue)
return True
except Exception as e:
self.presentText(f"Error saving setting: {str(e)}")
return False
def applySettingImmediate(self, section: str, option: str, newValue: str, oldValue: str):
"""Apply setting immediately without restart where possible"""
try:
# Apply speech settings immediately
if section == 'speech':
if option in ['rate', 'pitch', 'volume']:
settingsManager = self.env['runtime']['settingsManager']
settingsManager.setSetting(section, option, newValue)
# Apply sound settings immediately
elif section == 'sound':
if option == 'volume':
settingsManager = self.env['runtime']['settingsManager']
settingsManager.setSetting(section, option, newValue)
except Exception as e:
# Silent fail - settings will apply on restart
pass
def validateInput(self, section: str, option: str, value: str) -> Tuple[bool, str]:
"""Validate user input based on option type and constraints"""
try:
# Validate float values (volume, rate, pitch)
if option in ['volume', 'rate', 'pitch', 'capitalPitch']:
floatVal = float(value)
if not 0 <= floatVal <= 1.0:
return False, "Value must be between 0 and 1.0"
return True, str(floatVal)
# Validate integer values
elif option in ['numberOfClipboards', 'ignoreScreen']:
intVal = int(value)
if intVal < 0:
return False, "Value must be 0 or greater"
return True, str(intVal)
# Validate float values with different ranges
elif option == 'doubleTapTimeout':
floatVal = float(value)
if not 0 <= floatVal <= 2.0:
return False, "Value must be between 0 and 2.0 seconds"
return True, str(floatVal)
elif option == 'screenUpdateDelay':
floatVal = float(value)
if not 0 <= floatVal <= 1.0:
return False, "Value must be between 0 and 1.0 seconds"
return True, str(floatVal)
# Validate boolean values
elif self.isBooleanOption(value):
if value.lower() in ['true', 'false']:
return True, value.capitalize()
return False, "Value must be True or False"
# Validate preset options
key = f"{section}.{option}"
if key in self.presetOptions:
if value in self.presetOptions[key]:
return True, value
return False, f"Value must be one of: {', '.join(self.presetOptions[key])}"
# Default validation - accept any string
return True, str(value).strip()
except ValueError:
return False, "Invalid number format"
except Exception as e:
return False, f"Validation error: {str(e)}"
def isBooleanOption(self, value: str) -> bool:
"""Check if the current value is likely a boolean option"""
return value.lower() in ['true', 'false']
def getBooleanSetting(self, section: str, option: str, default: bool = False) -> bool:
"""Get a boolean setting value"""
value = self.getSetting(section, option, str(default)).lower()
return value in ['true', '1', 'yes', 'on']
def setBooleanSetting(self, section: str, option: str, value: bool) -> bool:
"""Set a boolean setting value"""
return self.setSetting(section, option, 'True' if value else 'False')
def toggleBooleanSetting(self, section: str, option: str) -> bool:
"""Toggle a boolean setting and return new value"""
currentValue = self.getBooleanSetting(section, option)
newValue = not currentValue
success = self.setBooleanSetting(section, option, newValue)
return newValue if success else currentValue
def getFloatSetting(self, section: str, option: str, default: float = 0.0) -> float:
"""Get a float setting value"""
try:
return float(self.getSetting(section, option, str(default)))
except ValueError:
return default
def setFloatSetting(self, section: str, option: str, value: float) -> bool:
"""Set a float setting value"""
return self.setSetting(section, option, str(value))
def adjustFloatSetting(self, section: str, option: str, delta: float,
minVal: float = 0.0, maxVal: float = 1.0) -> float:
"""Adjust a float setting by delta amount"""
currentValue = self.getFloatSetting(section, option)
newValue = max(minVal, min(maxVal, currentValue + delta))
success = self.setFloatSetting(section, option, newValue)
return newValue if success else currentValue
def runCommand(self, cmd: List[str], timeout: int = 10) -> Optional[str]:
"""Run a command and return output"""
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
return result.stdout.strip() if result.returncode == 0 else None
except Exception:
return None
def getSpeechdModules(self) -> List[str]:
"""Get list of available speech-dispatcher modules"""
output = self.runCommand(['spd-say', '-O'])
if output:
lines = output.split('\n')
return [line.strip() for line in lines[1:] if line.strip()]
return []
def getModuleVoices(self, module: str) -> List[str]:
"""Get list of voices for a specific speech module"""
output = self.runCommand(['spd-say', '-o', module, '-L'])
if output:
lines = output.split('\n')
voices = []
for line in lines[1:]:
if not line.strip():
continue
if module.lower() == 'espeak-ng':
voice = self.processEspeakVoice(line)
if voice:
voices.append(voice)
else:
voices.append(line.strip())
return voices
return []
def processEspeakVoice(self, voiceLine: str) -> Optional[str]:
"""Process espeak-ng voice line format"""
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
def testVoice(self, module: str, voice: str, testMessage: str = None) -> bool:
"""Test a voice configuration"""
if not testMessage:
testMessage = "This is a voice test. If you can hear this clearly, the voice is working properly."
try:
# Announce the test
self.presentText("Testing voice configuration. Listen for the test message.")
# Run voice test
cmd = ['spd-say', '-o', module, '-y', voice, testMessage]
result = subprocess.run(cmd, timeout=10)
return result.returncode == 0
except Exception:
return False
def backupConfig(self, announce: bool = True) -> tuple[bool, str]:
"""Create a backup of current configuration"""
try:
import shutil
from datetime import datetime
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backupFile = f"{self.settingsFile}.backup_{timestamp}"
shutil.copy2(self.settingsFile, backupFile)
message = f"Configuration backed up to {backupFile}"
if announce:
self.presentText(message, interrupt=False)
return True, message
except Exception as e:
error_msg = f"Error creating backup: {str(e)}"
if announce:
self.presentText(error_msg, interrupt=False)
return False, error_msg
def reloadConfig(self):
"""Reload configuration from file"""
try:
self.config.read(self.settingsFile)
return True
except Exception:
return False

View File

@ -1,76 +0,0 @@
#!/usr/bin/env python3
class command():
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def getDescription(self):
return "Apply last previewed voice to current session (temporary)"
def run(self):
# Check if we have a tested voice stored (must be tested first)
if ('commandBuffer' not in self.env or
'lastTestedModule' not in self.env['commandBuffer'] or
'lastTestedVoice' not in self.env['commandBuffer']):
self.env['runtime']['outputManager'].presentText("No voice has been tested yet", interrupt=True)
self.env['runtime']['outputManager'].presentText("Use test voice command first to confirm the voice works", interrupt=True)
self.env['runtime']['outputManager'].playSound('Cancel')
return
module = self.env['commandBuffer']['lastTestedModule']
voice = self.env['commandBuffer']['lastTestedVoice']
self.env['runtime']['outputManager'].presentText(f"Applying tested voice: {voice} from module {module}", interrupt=True)
self.env['runtime']['outputManager'].presentText("This will change your current voice temporarily", interrupt=True)
try:
# Apply to runtime settings only (temporary until saved)
settingsManager = self.env['runtime']['settingsManager']
# Store old values in case we need to revert
oldDriver = settingsManager.getSetting('speech', 'driver')
oldModule = settingsManager.getSetting('speech', 'module')
oldVoice = settingsManager.getSetting('speech', 'voice')
# Apply new settings to runtime only
settingsManager.settings['speech']['driver'] = 'speechdDriver'
settingsManager.settings['speech']['module'] = module
settingsManager.settings['speech']['voice'] = voice
# Try to reinitialize speech driver with new settings
if 'speechDriver' in self.env['runtime']:
try:
speechDriver = self.env['runtime']['speechDriver']
speechDriver.shutdown()
speechDriver.initialize(self.env)
# Test the new voice
self.env['runtime']['outputManager'].presentText("Voice applied successfully. This is how it sounds.", interrupt=False)
self.env['runtime']['outputManager'].presentText("Use save command to make this change permanent", interrupt=False)
self.env['runtime']['outputManager'].playSound('Accept')
except Exception as e:
# Revert on failure
settingsManager.settings['speech']['driver'] = oldDriver
settingsManager.settings['speech']['module'] = oldModule
settingsManager.settings['speech']['voice'] = oldVoice
self.env['runtime']['outputManager'].presentText(f"Failed to apply voice: {str(e)}", interrupt=False)
self.env['runtime']['outputManager'].presentText("Reverted to previous settings", interrupt=False)
self.env['runtime']['outputManager'].playSound('Error')
else:
self.env['runtime']['outputManager'].presentText("Speech driver not available", interrupt=False)
self.env['runtime']['outputManager'].playSound('Error')
except Exception as e:
self.env['runtime']['outputManager'].presentText(f"Error applying voice: {str(e)}", interrupt=False)
self.env['runtime']['outputManager'].playSound('Error')
def setCallback(self, callback):
pass

View File

@ -1,182 +0,0 @@
#!/usr/bin/env python3
import subprocess
import time
class command():
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
self.testMessage = "This is a voice test. If you can hear this message clearly, press Enter to accept this voice. Otherwise, wait 30 seconds to cancel."
def shutdown(self):
pass
def getDescription(self):
return "Configure speech module and voice with testing"
def run(self):
self.env['runtime']['outputManager'].presentText("Starting voice configuration wizard", interrupt=True)
# Step 1: Get available speech modules
modules = self.getSpeechdModules()
if not modules:
self.env['runtime']['outputManager'].presentText("No speech-dispatcher modules found. Please install speech-dispatcher.", interrupt=True)
self.env['runtime']['outputManager'].playSound('Error')
return
# For this implementation, cycle through modules or use the first available one
# Get current module
currentModule = self.env['runtime']['settingsManager'].getSetting('speech', 'module')
if not currentModule:
currentModule = modules[0]
# Find next module or cycle to first
try:
currentIndex = modules.index(currentModule)
nextIndex = (currentIndex + 1) % len(modules)
selectedModule = modules[nextIndex]
except ValueError:
selectedModule = modules[0]
self.env['runtime']['outputManager'].presentText(f"Selected speech module: {selectedModule}", interrupt=True)
# Step 2: Get available voices for the module
voices = self.getModuleVoices(selectedModule)
if not voices:
self.env['runtime']['outputManager'].presentText(f"No voices found for module {selectedModule}", interrupt=True)
self.env['runtime']['outputManager'].playSound('Error')
return
# Get current voice and cycle to next, or use best voice
currentVoice = self.env['runtime']['settingsManager'].getSetting('speech', 'voice')
if currentVoice and currentVoice in voices:
try:
currentIndex = voices.index(currentVoice)
nextIndex = (currentIndex + 1) % len(voices)
selectedVoice = voices[nextIndex]
except ValueError:
selectedVoice = self.selectBestVoice(voices)
else:
selectedVoice = self.selectBestVoice(voices)
self.env['runtime']['outputManager'].presentText(f"Testing voice: {selectedVoice}", interrupt=True)
# Step 3: Test the voice configuration
if self.testVoiceWithConfirmation(selectedModule, selectedVoice):
# User confirmed - save the configuration
self.env['runtime']['settingsManager'].setSetting('speech', 'driver', 'speechdDriver')
self.env['runtime']['settingsManager'].setSetting('speech', 'module', selectedModule)
self.env['runtime']['settingsManager'].setSetting('speech', 'voice', selectedVoice)
self.env['runtime']['outputManager'].presentText("Voice configuration saved successfully!", interrupt=True)
self.env['runtime']['outputManager'].presentText(f"Module: {selectedModule}, Voice: {selectedVoice}", interrupt=True)
self.env['runtime']['outputManager'].playSound('Accept')
else:
# User cancelled or test failed
self.env['runtime']['outputManager'].presentText("Voice configuration cancelled. Settings unchanged.", interrupt=True)
self.env['runtime']['outputManager'].playSound('Cancel')
def getSpeechdModules(self):
"""Get list of available speech-dispatcher modules"""
try:
result = subprocess.run(['spd-say', '-O'], capture_output=True, text=True, timeout=10)
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(self, module):
"""Get list of voices for a specific speech module"""
try:
result = subprocess.run(['spd-say', '-o', module, '-L'], capture_output=True, text=True, timeout=10)
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 = self.processEspeakVoice(line)
if voice:
voices.append(voice)
else:
voices.append(line.strip())
return voices
except Exception:
pass
return []
def processEspeakVoice(self, voiceLine):
"""Process espeak-ng voice line format"""
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
def selectBestVoice(self, voices):
"""Select the best voice from available voices, preferring English"""
# Look for English voices first
for voice in voices:
if any(lang in voice.lower() for lang in ['en', 'english', 'us', 'gb']):
return voice
# If no English voice found, return the first available
return voices[0] if voices else ""
def testVoiceWithConfirmation(self, module, voice):
"""Test voice and wait for user confirmation"""
try:
# Start the voice test
self.env['runtime']['outputManager'].presentText("Starting voice test. Listen carefully.", interrupt=True)
time.sleep(1) # Brief pause
# Use spd-say to test the voice
process = subprocess.Popen([
'spd-say', '-o', module, '-y', voice, self.testMessage
])
# Wait for the test message to finish (give it some time)
time.sleep(2)
# Now wait for user input
self.env['runtime']['outputManager'].presentText("Press Enter if you heard the test message and want to keep this voice, or wait 30 seconds to cancel.", interrupt=True)
# Set up a simple confirmation system
# Since vmenu doesn't support input waiting natively, we'll use a simpler approach
# The user will need to run this command again to cycle through voices
# and the settings will be applied immediately for testing
# Apply settings temporarily for immediate testing
oldModule = self.env['runtime']['settingsManager'].getSetting('speech', 'module')
oldVoice = self.env['runtime']['settingsManager'].getSetting('speech', 'voice')
self.env['runtime']['settingsManager'].setSetting('speech', 'module', module)
self.env['runtime']['settingsManager'].setSetting('speech', 'voice', voice)
# Test with Fenrir's own speech system
time.sleep(1)
self.env['runtime']['outputManager'].presentText("This is how the new voice sounds in Fenrir. Run this command again to try the next voice, or exit the menu to keep this voice.", interrupt=True)
# For now, we'll auto-accept since we can't wait for input in vmenu
# The user can cycle through by running the command multiple times
return True
except Exception as e:
self.env['runtime']['outputManager'].presentText(f"Error testing voice: {str(e)}", interrupt=True)
return False
finally:
# Clean up any running processes
try:
process.terminate()
except:
pass
def setCallback(self, callback):
pass

View File

@ -1,136 +0,0 @@
#!/usr/bin/env python3
import subprocess
class command():
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
self.testMessage = "This is a voice preview. The quick brown fox jumps over the lazy dog."
def shutdown(self):
pass
def getDescription(self):
return "Cycle through available voices (run multiple times to browse)"
def run(self):
# Get available modules
modules = self.getSpeechdModules()
if not modules:
self.env['runtime']['outputManager'].presentText("No speech-dispatcher modules found", interrupt=True)
self.env['runtime']['outputManager'].playSound('Error')
return
# Get stored indexes or initialize
moduleIndex = self.env['commandBuffer'].get('voicePreviewModuleIndex', 0)
voiceIndex = self.env['commandBuffer'].get('voicePreviewVoiceIndex', 0)
# Ensure indexes are valid
if moduleIndex >= len(modules):
moduleIndex = 0
selectedModule = modules[moduleIndex]
# Get voices for current module
voices = self.getModuleVoices(selectedModule)
if not voices:
self.env['runtime']['outputManager'].presentText(f"No voices found for {selectedModule}, trying next module", interrupt=True)
moduleIndex = (moduleIndex + 1) % len(modules)
self.env['commandBuffer']['voicePreviewModuleIndex'] = moduleIndex
self.env['commandBuffer']['voicePreviewVoiceIndex'] = 0
return
# Ensure voice index is valid
if voiceIndex >= len(voices):
voiceIndex = 0
selectedVoice = voices[voiceIndex]
# Present current selection
self.env['runtime']['outputManager'].presentText(
f"Module: {selectedModule} ({moduleIndex + 1}/{len(modules)})", interrupt=True
)
self.env['runtime']['outputManager'].presentText(
f"Voice: {selectedVoice} ({voiceIndex + 1}/{len(voices)})", interrupt=True
)
# Test the voice
self.env['runtime']['outputManager'].presentText("Testing voice...", interrupt=True)
if self.previewVoice(selectedModule, selectedVoice):
self.env['runtime']['outputManager'].presentText("Voice test completed", interrupt=True)
self.env['runtime']['outputManager'].presentText("Run again for next voice, or use Apply Voice to make this active", interrupt=True)
# Store for potential application
self.env['commandBuffer']['lastTestedModule'] = selectedModule
self.env['commandBuffer']['lastTestedVoice'] = selectedVoice
self.env['runtime']['outputManager'].playSound('Accept')
else:
self.env['runtime']['outputManager'].presentText("Voice test failed", interrupt=True)
self.env['runtime']['outputManager'].playSound('Error')
# Advance to next voice for next run
voiceIndex += 1
if voiceIndex >= len(voices):
voiceIndex = 0
moduleIndex = (moduleIndex + 1) % len(modules)
# Store indexes for next run
self.env['commandBuffer']['voicePreviewModuleIndex'] = moduleIndex
self.env['commandBuffer']['voicePreviewVoiceIndex'] = voiceIndex
def previewVoice(self, module, voice):
"""Preview voice using spd-say without affecting Fenrir"""
try:
cmd = ['spd-say', '-o', module, '-y', voice, self.testMessage]
result = subprocess.run(cmd, timeout=15)
return result.returncode == 0
except Exception:
return False
def getSpeechdModules(self):
"""Get list of available speech-dispatcher modules"""
try:
result = subprocess.run(['spd-say', '-O'], capture_output=True, text=True, timeout=10)
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(self, module):
"""Get list of voices for a specific speech module"""
try:
result = subprocess.run(['spd-say', '-o', module, '-L'], capture_output=True, text=True, timeout=10)
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 = self.processEspeakVoice(line)
if voice:
voices.append(voice)
else:
voices.append(line.strip())
return voices
except Exception:
pass
return []
def processEspeakVoice(self, voiceLine):
"""Process espeak-ng voice line format"""
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
def setCallback(self, callback):
pass

View File

@ -1,62 +0,0 @@
#!/usr/bin/env python3
import subprocess
class command():
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def getDescription(self):
return "Test current voice configuration"
def run(self):
# Get current speech settings
driver = self.env['runtime']['settingsManager'].getSetting('speech', 'driver')
module = self.env['runtime']['settingsManager'].getSetting('speech', 'module')
voice = self.env['runtime']['settingsManager'].getSetting('speech', 'voice')
rate = self.env['runtime']['settingsManager'].getSetting('speech', 'rate')
pitch = self.env['runtime']['settingsManager'].getSetting('speech', 'pitch')
volume = self.env['runtime']['settingsManager'].getSetting('speech', 'volume')
# Present current configuration
self.env['runtime']['outputManager'].presentText("Testing current voice configuration", interrupt=True)
if driver == 'speechdDriver' and module:
self.env['runtime']['outputManager'].presentText(f"Driver: {driver}", interrupt=True)
self.env['runtime']['outputManager'].presentText(f"Module: {module}", interrupt=True)
if voice:
self.env['runtime']['outputManager'].presentText(f"Voice: {voice}", interrupt=True)
try:
ratePercent = int(float(rate) * 100)
self.env['runtime']['outputManager'].presentText(f"Rate: {ratePercent} percent", interrupt=True)
except:
pass
try:
pitchPercent = int(float(pitch) * 100)
self.env['runtime']['outputManager'].presentText(f"Pitch: {pitchPercent} percent", interrupt=True)
except:
pass
try:
volumePercent = int(float(volume) * 100)
self.env['runtime']['outputManager'].presentText(f"Volume: {volumePercent} percent", interrupt=True)
except:
pass
# Test message
testMessage = "This is a test of your current voice configuration. The quick brown fox jumps over the lazy dog. Numbers: one, two, three, four, five. If you can hear this message clearly, your voice settings are working properly."
self.env['runtime']['outputManager'].presentText("Voice test:", interrupt=True)
self.env['runtime']['outputManager'].presentText(testMessage, interrupt=True)
self.env['runtime']['outputManager'].playSound('Accept')
def setCallback(self, callback):
pass

View File

@ -3,6 +3,7 @@
import subprocess
import importlib.util
import os
import time
class DynamicVoiceCommand:
"""Dynamic command class for voice selection"""
@ -23,34 +24,42 @@ class DynamicVoiceCommand:
def run(self):
try:
self.env['runtime']['outputManager'].presentText(f"Testing {self.voice} from {self.module}", interrupt=True)
self.env['runtime']['outputManager'].presentText(f"Testing voice {self.voice} from {self.module}. Please wait.", interrupt=True)
# Test voice first
if self.testVoice():
self.env['runtime']['outputManager'].presentText("Voice test completed. Press Enter again to apply.", interrupt=True)
# Brief pause before testing to avoid speech overlap
time.sleep(0.5)
# Store for confirmation
# 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
self.env['runtime']['outputManager'].playSound('Accept')
else:
self.env['runtime']['outputManager'].presentText("Voice test failed", interrupt=True)
self.env['runtime']['outputManager'].playSound('Error')
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=True)
self.env['runtime']['outputManager'].playSound('Error')
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', '-o', self.module, '-y', self.voice, self.testMessage]
result = subprocess.run(cmd, timeout=8)
return result.returncode == 0
except Exception:
return False
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
@ -80,20 +89,57 @@ class DynamicApplyVoiceCommand:
self.env['runtime']['outputManager'].presentText(f"Applying {voice} from {module}", interrupt=True)
# Apply to runtime settings
# Debug: Show current settings
settingsManager = self.env['runtime']['settingsManager']
settingsManager.settings['speech']['driver'] = 'speechdDriver'
settingsManager.settings['speech']['module'] = module
settingsManager.settings['speech']['voice'] = voice
currentModule = settingsManager.getSetting('speech', 'module')
currentVoice = settingsManager.getSetting('speech', 'voice')
self.env['runtime']['outputManager'].presentText(f"Current: {currentVoice} from {currentModule}", interrupt=False, flush=False)
# Reinitialize speech driver
if 'speechDriver' in self.env['runtime']:
speechDriver = self.env['runtime']['speechDriver']
speechDriver.shutdown()
speechDriver.initialize(self.env)
# Apply to runtime settings with fallback
settingsManager = self.env['runtime']['settingsManager']
self.env['runtime']['outputManager'].presentText("Voice applied successfully!", interrupt=True)
self.env['runtime']['outputManager'].playSound('Accept')
# 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
@ -101,9 +147,25 @@ class DynamicApplyVoiceCommand:
# 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 failed: {str(e)}", interrupt=True)
self.env['runtime']['outputManager'].playSound('Error')
self.env['runtime']['outputManager'].presentText(f"Apply voice error: {str(e)}", interrupt=False, flush=False)
def setCallback(self, callback):
pass
@ -129,13 +191,9 @@ def addDynamicVoiceMenus(vmenuManager):
for module in modules[:8]: # Limit to 8 modules to keep menu manageable
moduleMenu = {}
# Get voices for this module (limit to prevent huge menus)
# Get voices for this module
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:

View File

@ -192,3 +192,12 @@ class outputManager():
self.presentText(' review cursor ', interrupt=interrupt_p)
else:
self.presentText(' text cursor ', interrupt=interrupt_p)
def resetSpeechDriver(self):
"""Reset speech driver to clean state - called by settingsManager"""
if 'speechDriver' in self.env['runtime'] and self.env['runtime']['speechDriver']:
try:
self.env['runtime']['speechDriver'].reset()
self.env['runtime']['debug'].writeDebugOut("Speech driver reset successfully", debug.debugLevel.INFO)
except Exception as e:
self.env['runtime']['debug'].writeDebugOut(f"resetSpeechDriver error: {e}", debug.debugLevel.ERROR)

View File

@ -16,9 +16,15 @@ class driver(speechDriver):
self._sd = None
self.env = environment
self._isInitialized = False
self.language = ''
self.voice = ''
self.module = ''
# Only set these if they haven't been set yet (preserve existing values)
if not hasattr(self, 'language') or self.language is None:
self.language = ''
if not hasattr(self, 'voice') or self.voice is None:
self.voice = ''
if not hasattr(self, 'module') or self.module is None:
self.module = ''
try:
import speechd
self._sd = speechd.SSIPClient('fenrir')