Fixes to the voice driver. It should actually work completely now.
This commit is contained in:
parent
72bd334d65
commit
43871cea3c
@ -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')
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
||||
# 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)
|
||||
|
||||
self.env['runtime']['outputManager'].presentText("Voice applied successfully!", interrupt=True)
|
||||
self.env['runtime']['outputManager'].playSound('Accept')
|
||||
# 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
|
||||
@ -102,8 +148,24 @@ class DynamicApplyVoiceCommand:
|
||||
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')
|
||||
# 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
|
||||
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -16,9 +16,15 @@ class driver(speechDriver):
|
||||
self._sd = None
|
||||
self.env = environment
|
||||
self._isInitialized = False
|
||||
|
||||
# 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')
|
||||
|
Loading…
x
Reference in New Issue
Block a user