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:
@@ -114,6 +114,8 @@ KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_X=remove_marks
|
|||||||
KEY_FENRIR,KEY_X=set_mark
|
KEY_FENRIR,KEY_X=set_mark
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_X=marked_text
|
KEY_FENRIR,KEY_SHIFT,KEY_X=marked_text
|
||||||
KEY_FENRIR,KEY_F10=toggle_vmenu_mode
|
KEY_FENRIR,KEY_F10=toggle_vmenu_mode
|
||||||
|
KEY_FENRIR,KEY_SHIFT,KEY_F10=voice_browser_safe
|
||||||
|
KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_F10=apply_tested_voice
|
||||||
KEY_FENRIR,KEY_SPACE=current_quick_menu_entry
|
KEY_FENRIR,KEY_SPACE=current_quick_menu_entry
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_SPACE=current_quick_menu_value
|
KEY_FENRIR,KEY_CTRL,KEY_SPACE=current_quick_menu_value
|
||||||
KEY_FENRIR,KEY_RIGHT=next_quick_menu_entry
|
KEY_FENRIR,KEY_RIGHT=next_quick_menu_entry
|
||||||
|
@@ -113,6 +113,8 @@ KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_X=remove_marks
|
|||||||
KEY_FENRIR,KEY_X=set_mark
|
KEY_FENRIR,KEY_X=set_mark
|
||||||
KEY_FENRIR,KEY_SHIFT,KEY_X=marked_text
|
KEY_FENRIR,KEY_SHIFT,KEY_X=marked_text
|
||||||
KEY_FENRIR,KEY_F10=toggle_vmenu_mode
|
KEY_FENRIR,KEY_F10=toggle_vmenu_mode
|
||||||
|
KEY_FENRIR,KEY_SHIFT,KEY_F10=voice_browser_safe
|
||||||
|
KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_F10=apply_tested_voice
|
||||||
KEY_FENRIR,KEY_SPACE=current_quick_menu_entry
|
KEY_FENRIR,KEY_SPACE=current_quick_menu_entry
|
||||||
KEY_FENRIR,KEY_CTRL,KEY_SPACE=current_quick_menu_value
|
KEY_FENRIR,KEY_CTRL,KEY_SPACE=current_quick_menu_value
|
||||||
KEY_FENRIR,KEY_RIGHT=next_quick_menu_entry
|
KEY_FENRIR,KEY_RIGHT=next_quick_menu_entry
|
||||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -30,9 +30,6 @@ ContentChanged='ContentChanged.wav'
|
|||||||
# Speech has turned On or Off
|
# Speech has turned On or Off
|
||||||
SpeechOn='SpeechOn.wav'
|
SpeechOn='SpeechOn.wav'
|
||||||
SpeechOff='SpeechOff.wav'
|
SpeechOff='SpeechOff.wav'
|
||||||
# Braille has turned On or Off
|
|
||||||
BrailleOn='BrailleOn.wav'
|
|
||||||
BrailleOff='BrailleOff.wav'
|
|
||||||
# SoundIcons has turned On or Off
|
# SoundIcons has turned On or Off
|
||||||
SoundOn='SoundOn.wav'
|
SoundOn='SoundOn.wav'
|
||||||
SoundOff='SoundOff.wav'
|
SoundOff='SoundOff.wav'
|
||||||
@@ -44,9 +41,8 @@ PlaceEndMark='PlaceEndMark.wav'
|
|||||||
CopyToClipboard='CopyToClipboard.wav'
|
CopyToClipboard='CopyToClipboard.wav'
|
||||||
# Pasted on the screen
|
# Pasted on the screen
|
||||||
PasteClipboardOnScreen='PasteClipboardOnScreen.wav'
|
PasteClipboardOnScreen='PasteClipboardOnScreen.wav'
|
||||||
# An error accoured while speech or braille output or reading the screen
|
# An error accoured while speech output or reading the screen
|
||||||
ErrorSpeech='ErrorSpeech.wav'
|
ErrorSpeech='ErrorSpeech.wav'
|
||||||
ErrorBraille='ErrorBraille.wav'
|
|
||||||
ErrorScreen='ErrorScreen.wav'
|
ErrorScreen='ErrorScreen.wav'
|
||||||
# If you cursor over an text that has attributs (like color)
|
# If you cursor over an text that has attributs (like color)
|
||||||
HasAttributes='has_attribute.wav'
|
HasAttributes='has_attribute.wav'
|
||||||
|
@@ -0,0 +1,69 @@
|
|||||||
|
#!/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 the last tested voice from safe voice browser"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
# Check if we have a tested voice
|
||||||
|
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 voice browser first", interrupt=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
module = self.env['commandBuffer']['lastTestedModule']
|
||||||
|
voice = self.env['commandBuffer']['lastTestedVoice']
|
||||||
|
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Applying {voice} from {module}", interrupt=True)
|
||||||
|
|
||||||
|
# Apply to runtime settings only (temporary until saved)
|
||||||
|
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
|
||||||
|
settingsManager.settings['speech']['driver'] = 'speechdDriver'
|
||||||
|
settingsManager.settings['speech']['module'] = module
|
||||||
|
settingsManager.settings['speech']['voice'] = voice
|
||||||
|
|
||||||
|
# Try to 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'].presentText("Use save settings to make permanent", interrupt=True)
|
||||||
|
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, reverted: {str(e)}", interrupt=True)
|
||||||
|
self.env['runtime']['outputManager'].playSound('Error')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Apply voice error: {str(e)}", interrupt=True)
|
||||||
|
self.env['runtime']['outputManager'].playSound('Error')
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
255
src/fenrirscreenreader/commands/commands/voice_browser.py
Normal file
255
src/fenrirscreenreader/commands/commands/voice_browser.py
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
#!/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. The quick brown fox jumps over the lazy dog."
|
||||||
|
self.modules = []
|
||||||
|
self.voices = []
|
||||||
|
self.moduleIndex = 0
|
||||||
|
self.voiceIndex = 0
|
||||||
|
self.browserActive = False
|
||||||
|
self.originalBindings = {}
|
||||||
|
self.lastAnnounceTime = 0
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Interactive voice browser with arrow key navigation"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if self.browserActive:
|
||||||
|
self.exitVoiceBrowser()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.env['runtime']['outputManager'].presentText("Starting voice browser", interrupt=True)
|
||||||
|
|
||||||
|
# Load modules
|
||||||
|
self.modules = self.getSpeechdModules()
|
||||||
|
if not self.modules:
|
||||||
|
self.env['runtime']['outputManager'].presentText("No speech modules found", interrupt=True)
|
||||||
|
self.env['runtime']['outputManager'].playSound('Error')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set current module
|
||||||
|
currentModule = self.env['runtime']['settingsManager'].getSetting('speech', 'module')
|
||||||
|
if currentModule and currentModule in self.modules:
|
||||||
|
self.moduleIndex = self.modules.index(currentModule)
|
||||||
|
|
||||||
|
# Load voices
|
||||||
|
self.loadVoicesForCurrentModule()
|
||||||
|
|
||||||
|
# Set current voice
|
||||||
|
currentVoice = self.env['runtime']['settingsManager'].getSetting('speech', 'voice')
|
||||||
|
if currentVoice and currentVoice in self.voices:
|
||||||
|
self.voiceIndex = self.voices.index(currentVoice)
|
||||||
|
|
||||||
|
# Enter browser mode
|
||||||
|
self.enterVoiceBrowser()
|
||||||
|
self.announceCurrentSelection()
|
||||||
|
|
||||||
|
def enterVoiceBrowser(self):
|
||||||
|
"""Enter voice browser mode with custom key bindings"""
|
||||||
|
self.browserActive = True
|
||||||
|
|
||||||
|
# Store original bindings
|
||||||
|
self.originalBindings = self.env['bindings'].copy()
|
||||||
|
|
||||||
|
# Override navigation keys for voice browsing
|
||||||
|
# Use lambda to capture self and create bound methods
|
||||||
|
self.env['bindings'][str([1, ['KEY_UP']])] = 'VOICE_BROWSER_PREV_VOICE'
|
||||||
|
self.env['bindings'][str([1, ['KEY_DOWN']])] = 'VOICE_BROWSER_NEXT_VOICE'
|
||||||
|
self.env['bindings'][str([1, ['KEY_LEFT']])] = 'VOICE_BROWSER_PREV_MODULE'
|
||||||
|
self.env['bindings'][str([1, ['KEY_RIGHT']])] = 'VOICE_BROWSER_NEXT_MODULE'
|
||||||
|
self.env['bindings'][str([1, ['KEY_ENTER']])] = 'VOICE_BROWSER_TEST'
|
||||||
|
self.env['bindings'][str([1, ['KEY_SPACE']])] = 'VOICE_BROWSER_APPLY'
|
||||||
|
self.env['bindings'][str([1, ['KEY_ESC']])] = 'VOICE_BROWSER_EXIT'
|
||||||
|
|
||||||
|
# Register browser commands
|
||||||
|
self.registerBrowserCommands()
|
||||||
|
|
||||||
|
self.env['runtime']['outputManager'].presentText("Voice browser active", interrupt=True)
|
||||||
|
self.env['runtime']['outputManager'].presentText("Up/Down=voices, Left/Right=modules, Enter=test, Space=apply, Esc=exit", interrupt=True)
|
||||||
|
|
||||||
|
def registerBrowserCommands(self):
|
||||||
|
"""Register voice browser commands with the command manager"""
|
||||||
|
# Create command objects for voice browser actions
|
||||||
|
commandManager = self.env['runtime']['commandManager']
|
||||||
|
|
||||||
|
# This is a hack - we'll store references to our methods in the environment
|
||||||
|
# so the key handlers can call them
|
||||||
|
if 'voiceBrowserInstance' not in self.env['runtime']:
|
||||||
|
self.env['runtime']['voiceBrowserInstance'] = self
|
||||||
|
|
||||||
|
def exitVoiceBrowser(self):
|
||||||
|
"""Exit voice browser and restore normal key bindings"""
|
||||||
|
if not self.browserActive:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.browserActive = False
|
||||||
|
|
||||||
|
# Restore original bindings
|
||||||
|
self.env['bindings'] = self.originalBindings
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
if 'voiceBrowserInstance' in self.env['runtime']:
|
||||||
|
del self.env['runtime']['voiceBrowserInstance']
|
||||||
|
|
||||||
|
self.env['runtime']['outputManager'].presentText("Voice browser exited", interrupt=True)
|
||||||
|
|
||||||
|
def loadVoicesForCurrentModule(self):
|
||||||
|
"""Load voices for current module"""
|
||||||
|
if self.moduleIndex < len(self.modules):
|
||||||
|
module = self.modules[self.moduleIndex]
|
||||||
|
self.voices = self.getModuleVoices(module)
|
||||||
|
self.voiceIndex = 0 # Reset to first voice when changing modules
|
||||||
|
|
||||||
|
def announceCurrentSelection(self):
|
||||||
|
"""Announce current module and voice"""
|
||||||
|
# Throttle announcements to avoid spam
|
||||||
|
now = time.time()
|
||||||
|
if now - self.lastAnnounceTime < 0.3:
|
||||||
|
return
|
||||||
|
self.lastAnnounceTime = now
|
||||||
|
|
||||||
|
if not self.modules or self.moduleIndex >= len(self.modules):
|
||||||
|
return
|
||||||
|
|
||||||
|
module = self.modules[self.moduleIndex]
|
||||||
|
if self.voices and self.voiceIndex < len(self.voices):
|
||||||
|
voice = self.voices[self.voiceIndex]
|
||||||
|
self.env['runtime']['outputManager'].presentText(
|
||||||
|
f"{module}: {voice} ({self.voiceIndex + 1}/{len(self.voices)})", interrupt=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"{module}: No voices", interrupt=True)
|
||||||
|
|
||||||
|
def nextVoice(self):
|
||||||
|
"""Move to next voice"""
|
||||||
|
if not self.voices:
|
||||||
|
return
|
||||||
|
self.voiceIndex = (self.voiceIndex + 1) % len(self.voices)
|
||||||
|
self.announceCurrentSelection()
|
||||||
|
|
||||||
|
def prevVoice(self):
|
||||||
|
"""Move to previous voice"""
|
||||||
|
if not self.voices:
|
||||||
|
return
|
||||||
|
self.voiceIndex = (self.voiceIndex - 1) % len(self.voices)
|
||||||
|
self.announceCurrentSelection()
|
||||||
|
|
||||||
|
def nextModule(self):
|
||||||
|
"""Move to next module"""
|
||||||
|
self.moduleIndex = (self.moduleIndex + 1) % len(self.modules)
|
||||||
|
self.loadVoicesForCurrentModule()
|
||||||
|
self.announceCurrentSelection()
|
||||||
|
|
||||||
|
def prevModule(self):
|
||||||
|
"""Move to previous module"""
|
||||||
|
self.moduleIndex = (self.moduleIndex - 1) % len(self.modules)
|
||||||
|
self.loadVoicesForCurrentModule()
|
||||||
|
self.announceCurrentSelection()
|
||||||
|
|
||||||
|
def testVoice(self):
|
||||||
|
"""Test current voice"""
|
||||||
|
if not self.voices or self.voiceIndex >= len(self.voices):
|
||||||
|
self.env['runtime']['outputManager'].presentText("No voice selected", interrupt=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
module = self.modules[self.moduleIndex]
|
||||||
|
voice = self.voices[self.voiceIndex]
|
||||||
|
|
||||||
|
self.env['runtime']['outputManager'].presentText("Testing...", interrupt=True)
|
||||||
|
if self.previewVoice(module, voice):
|
||||||
|
# Store for apply command
|
||||||
|
self.env['commandBuffer']['lastTestedModule'] = module
|
||||||
|
self.env['commandBuffer']['lastTestedVoice'] = voice
|
||||||
|
self.env['runtime']['outputManager'].playSound('Accept')
|
||||||
|
else:
|
||||||
|
self.env['runtime']['outputManager'].playSound('Error')
|
||||||
|
|
||||||
|
def applyVoice(self):
|
||||||
|
"""Apply current voice to Fenrir"""
|
||||||
|
if not self.voices or self.voiceIndex >= len(self.voices):
|
||||||
|
return
|
||||||
|
|
||||||
|
module = self.modules[self.moduleIndex]
|
||||||
|
voice = self.voices[self.voiceIndex]
|
||||||
|
|
||||||
|
try:
|
||||||
|
settingsManager = self.env['runtime']['settingsManager']
|
||||||
|
settingsManager.settings['speech']['driver'] = 'speechdDriver'
|
||||||
|
settingsManager.settings['speech']['module'] = module
|
||||||
|
settingsManager.settings['speech']['voice'] = voice
|
||||||
|
|
||||||
|
if 'speechDriver' in self.env['runtime']:
|
||||||
|
speechDriver = self.env['runtime']['speechDriver']
|
||||||
|
speechDriver.shutdown()
|
||||||
|
speechDriver.initialize(self.env)
|
||||||
|
|
||||||
|
self.env['runtime']['outputManager'].presentText("Voice applied!", interrupt=True)
|
||||||
|
self.env['runtime']['outputManager'].playSound('Accept')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.env['runtime']['outputManager'].presentText("Apply failed", interrupt=True)
|
||||||
|
self.env['runtime']['outputManager'].playSound('Error')
|
||||||
|
|
||||||
|
def previewVoice(self, module, voice):
|
||||||
|
"""Test voice with spd-say"""
|
||||||
|
try:
|
||||||
|
cmd = ['spd-say', '-o', module, '-y', voice, self.testMessage]
|
||||||
|
result = subprocess.run(cmd, timeout=10)
|
||||||
|
return result.returncode == 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getSpeechdModules(self):
|
||||||
|
"""Get available speech 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 voices for 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 voice 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
|
@@ -0,0 +1,21 @@
|
|||||||
|
#!/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 "Voice browser: apply current voice"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if 'voiceBrowserInstance' in self.env['runtime']:
|
||||||
|
self.env['runtime']['voiceBrowserInstance'].applyVoice()
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
@@ -0,0 +1,21 @@
|
|||||||
|
#!/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 "Voice browser: exit browser mode"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if 'voiceBrowserInstance' in self.env['runtime']:
|
||||||
|
self.env['runtime']['voiceBrowserInstance'].exitVoiceBrowser()
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
@@ -0,0 +1,21 @@
|
|||||||
|
#!/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 "Voice browser: next module"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if 'voiceBrowserInstance' in self.env['runtime']:
|
||||||
|
self.env['runtime']['voiceBrowserInstance'].nextModule()
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
@@ -0,0 +1,21 @@
|
|||||||
|
#!/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 "Voice browser: next voice"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if 'voiceBrowserInstance' in self.env['runtime']:
|
||||||
|
self.env['runtime']['voiceBrowserInstance'].nextVoice()
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
@@ -0,0 +1,21 @@
|
|||||||
|
#!/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 "Voice browser: previous module"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if 'voiceBrowserInstance' in self.env['runtime']:
|
||||||
|
self.env['runtime']['voiceBrowserInstance'].prevModule()
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
@@ -0,0 +1,21 @@
|
|||||||
|
#!/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 "Voice browser: previous voice"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if 'voiceBrowserInstance' in self.env['runtime']:
|
||||||
|
self.env['runtime']['voiceBrowserInstance'].prevVoice()
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
170
src/fenrirscreenreader/commands/commands/voice_browser_safe.py
Normal file
170
src/fenrirscreenreader/commands/commands/voice_browser_safe.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
class command():
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def initialize(self, environment):
|
||||||
|
self.env = environment
|
||||||
|
self.testMessage = "Voice test: The quick brown fox jumps over the lazy dog."
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Safe voice browser - cycles through voices without hanging"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
self.env['runtime']['outputManager'].presentText("Starting safe voice browser", interrupt=True)
|
||||||
|
|
||||||
|
# Get modules with timeout protection
|
||||||
|
modules = self.getSpeechdModulesWithTimeout()
|
||||||
|
if not modules:
|
||||||
|
self.env['runtime']['outputManager'].presentText("No speech modules found", interrupt=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get current position from commandBuffer or start fresh
|
||||||
|
moduleIndex = self.env['commandBuffer'].get('safeBrowserModuleIndex', 0)
|
||||||
|
voiceIndex = self.env['commandBuffer'].get('safeBrowserVoiceIndex', 0)
|
||||||
|
|
||||||
|
# Ensure valid module index
|
||||||
|
if moduleIndex >= len(modules):
|
||||||
|
moduleIndex = 0
|
||||||
|
|
||||||
|
currentModule = modules[moduleIndex]
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Loading voices for {currentModule}...", interrupt=True)
|
||||||
|
|
||||||
|
# Get voices with timeout protection
|
||||||
|
voices = self.getModuleVoicesWithTimeout(currentModule)
|
||||||
|
if not voices:
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"No voices in {currentModule}, trying next module", interrupt=True)
|
||||||
|
moduleIndex = (moduleIndex + 1) % len(modules)
|
||||||
|
self.env['commandBuffer']['safeBrowserModuleIndex'] = moduleIndex
|
||||||
|
self.env['commandBuffer']['safeBrowserVoiceIndex'] = 0
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ensure valid voice index
|
||||||
|
if voiceIndex >= len(voices):
|
||||||
|
voiceIndex = 0
|
||||||
|
|
||||||
|
currentVoice = voices[voiceIndex]
|
||||||
|
|
||||||
|
# Announce current selection
|
||||||
|
self.env['runtime']['outputManager'].presentText(
|
||||||
|
f"Module: {currentModule} ({moduleIndex + 1}/{len(modules)})", interrupt=True
|
||||||
|
)
|
||||||
|
self.env['runtime']['outputManager'].presentText(
|
||||||
|
f"Voice: {currentVoice} ({voiceIndex + 1}/{len(voices)})", interrupt=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test voice in background thread to avoid blocking
|
||||||
|
self.env['runtime']['outputManager'].presentText("Testing voice...", interrupt=True)
|
||||||
|
|
||||||
|
# Use threading to prevent freezing
|
||||||
|
testThread = threading.Thread(target=self.testVoiceAsync, args=(currentModule, currentVoice))
|
||||||
|
testThread.daemon = True
|
||||||
|
testThread.start()
|
||||||
|
|
||||||
|
# Store tested voice for apply command
|
||||||
|
self.env['commandBuffer']['lastTestedModule'] = currentModule
|
||||||
|
self.env['commandBuffer']['lastTestedVoice'] = currentVoice
|
||||||
|
|
||||||
|
# Advance to next voice for next run
|
||||||
|
voiceIndex += 1
|
||||||
|
if voiceIndex >= len(voices):
|
||||||
|
voiceIndex = 0
|
||||||
|
moduleIndex = (moduleIndex + 1) % len(modules)
|
||||||
|
|
||||||
|
# Store position for next run
|
||||||
|
self.env['commandBuffer']['safeBrowserModuleIndex'] = moduleIndex
|
||||||
|
self.env['commandBuffer']['safeBrowserVoiceIndex'] = voiceIndex
|
||||||
|
|
||||||
|
# Give instructions
|
||||||
|
self.env['runtime']['outputManager'].presentText("Run again for next voice, or use apply voice command", interrupt=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Voice browser error: {str(e)}", interrupt=True)
|
||||||
|
self.env['runtime']['outputManager'].playSound('Error')
|
||||||
|
|
||||||
|
def testVoiceAsync(self, module, voice):
|
||||||
|
"""Test voice in background thread to avoid blocking"""
|
||||||
|
try:
|
||||||
|
# Run with strict timeout
|
||||||
|
cmd = ['spd-say', '-o', module, '-y', voice, self.testMessage]
|
||||||
|
result = subprocess.run(cmd, timeout=5, capture_output=True)
|
||||||
|
|
||||||
|
# Schedule success sound for main thread
|
||||||
|
if result.returncode == 0:
|
||||||
|
# We can't call outputManager from background thread safely
|
||||||
|
# So we'll just let the main thread handle feedback
|
||||||
|
pass
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
# Voice test timed out - this is okay, don't crash
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
# Any other error - also okay, don't crash
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getSpeechdModulesWithTimeout(self):
|
||||||
|
"""Get speech modules with timeout protection"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['spd-say', '-O'], capture_output=True, text=True, timeout=3)
|
||||||
|
if result.returncode == 0:
|
||||||
|
lines = result.stdout.strip().split('\n')
|
||||||
|
modules = [line.strip() for line in lines[1:] if line.strip()]
|
||||||
|
return modules[:10] # Limit to first 10 modules to prevent overload
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
self.env['runtime']['outputManager'].presentText("Module detection timed out", interrupt=True)
|
||||||
|
except Exception as e:
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Module detection failed: {str(e)}", interrupt=True)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def getModuleVoicesWithTimeout(self, module):
|
||||||
|
"""Get voices with timeout and limits"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['spd-say', '-o', module, '-L'], capture_output=True, text=True, timeout=5)
|
||||||
|
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())
|
||||||
|
|
||||||
|
# Limit voice count to prevent memory issues
|
||||||
|
if len(voices) > 1000:
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Found {len(voices)} voices, limiting to first 1000", interrupt=True)
|
||||||
|
voices = voices[:1000]
|
||||||
|
|
||||||
|
return voices
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Voice detection for {module} timed out", interrupt=True)
|
||||||
|
except Exception as e:
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Voice detection failed: {str(e)}", interrupt=True)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def processEspeakVoice(self, 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
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
@@ -0,0 +1,21 @@
|
|||||||
|
#!/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 "Voice browser: test current voice"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if 'voiceBrowserInstance' in self.env['runtime']:
|
||||||
|
self.env['runtime']['voiceBrowserInstance'].testVoice()
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
@@ -0,0 +1 @@
|
|||||||
|
# Fenrir Configuration VMenu Profile
|
@@ -0,0 +1,302 @@
|
|||||||
|
#!/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
|
@@ -0,0 +1 @@
|
|||||||
|
# Fenrir general configuration
|
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Load base configuration class
|
||||||
|
_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py')
|
||||||
|
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||||
|
_module = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_module)
|
||||||
|
ConfigCommand = _module.ConfigCommand
|
||||||
|
|
||||||
|
class command(ConfigCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Set punctuation verbosity level"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
currentLevel = self.getSetting('general', 'punctuationLevel', 'some')
|
||||||
|
|
||||||
|
# Present current level
|
||||||
|
levelDescriptions = {
|
||||||
|
'none': 'None - no punctuation spoken',
|
||||||
|
'some': 'Some - basic punctuation only',
|
||||||
|
'most': 'Most - detailed punctuation',
|
||||||
|
'all': 'All - every punctuation mark'
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDescription = levelDescriptions.get(currentLevel, 'Unknown')
|
||||||
|
self.presentText(f"Current punctuation level: {currentDescription}")
|
||||||
|
|
||||||
|
# Cycle through the four levels
|
||||||
|
levels = ['none', 'some', 'most', 'all']
|
||||||
|
try:
|
||||||
|
currentIndex = levels.index(currentLevel)
|
||||||
|
nextIndex = (currentIndex + 1) % len(levels)
|
||||||
|
newLevel = levels[nextIndex]
|
||||||
|
except ValueError:
|
||||||
|
newLevel = 'some' # Default to some
|
||||||
|
|
||||||
|
success = self.setSetting('general', 'punctuationLevel', newLevel)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
newDescription = levelDescriptions[newLevel]
|
||||||
|
self.presentText(f"Punctuation level set to: {newDescription}")
|
||||||
|
self.playSound('Accept')
|
||||||
|
else:
|
||||||
|
self.presentText("Failed to change punctuation level")
|
||||||
|
self.playSound('Error')
|
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Load base configuration class
|
||||||
|
_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py')
|
||||||
|
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||||
|
_module = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_module)
|
||||||
|
ConfigCommand = _module.ConfigCommand
|
||||||
|
|
||||||
|
class command(ConfigCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Toggle debug mode"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
currentLevel = self.getSetting('general', 'debugLevel', '0')
|
||||||
|
|
||||||
|
# Present current debug level
|
||||||
|
if currentLevel == '0':
|
||||||
|
self.presentText("Current debug mode: disabled")
|
||||||
|
newLevel = '1'
|
||||||
|
stateText = "enabled"
|
||||||
|
else:
|
||||||
|
self.presentText(f"Current debug level: {currentLevel}")
|
||||||
|
newLevel = '0'
|
||||||
|
stateText = "disabled"
|
||||||
|
|
||||||
|
success = self.setSetting('general', 'debugLevel', newLevel)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.presentText(f"Debug mode {stateText}")
|
||||||
|
if newLevel != '0':
|
||||||
|
debugMode = self.getSetting('general', 'debugMode', 'File')
|
||||||
|
if debugMode == 'File':
|
||||||
|
self.presentText("Debug output will be written to log file")
|
||||||
|
else:
|
||||||
|
self.presentText("Debug output will be printed to console")
|
||||||
|
self.playSound('Accept')
|
||||||
|
else:
|
||||||
|
self.presentText("Failed to change debug mode")
|
||||||
|
self.playSound('Error')
|
@@ -0,0 +1 @@
|
|||||||
|
# Fenrir keyboard configuration
|
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Load base configuration class
|
||||||
|
_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py')
|
||||||
|
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||||
|
_module = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_module)
|
||||||
|
ConfigCommand = _module.ConfigCommand
|
||||||
|
|
||||||
|
class command(ConfigCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Select keyboard layout (desktop or laptop)"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
currentLayout = self.getSetting('keyboard', 'keyboardLayout', 'desktop')
|
||||||
|
|
||||||
|
# Present current layout
|
||||||
|
self.presentText(f"Current keyboard layout: {currentLayout}")
|
||||||
|
|
||||||
|
# Find available keyboard layouts
|
||||||
|
keyboardPath = '/etc/fenrirscreenreader/keyboard'
|
||||||
|
if not os.path.isdir(keyboardPath):
|
||||||
|
# Development path
|
||||||
|
keyboardPath = os.path.join(os.path.dirname(self.settingsFile), '..', 'keyboard')
|
||||||
|
|
||||||
|
availableLayouts = self.getAvailableLayouts(keyboardPath)
|
||||||
|
|
||||||
|
if len(availableLayouts) > 1:
|
||||||
|
# Cycle through available layouts
|
||||||
|
try:
|
||||||
|
currentIndex = availableLayouts.index(currentLayout)
|
||||||
|
nextIndex = (currentIndex + 1) % len(availableLayouts)
|
||||||
|
newLayout = availableLayouts[nextIndex]
|
||||||
|
except ValueError:
|
||||||
|
# Current layout not found, use first available
|
||||||
|
newLayout = availableLayouts[0]
|
||||||
|
|
||||||
|
success = self.setSetting('keyboard', 'keyboardLayout', newLayout)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.presentText(f"Keyboard layout changed to: {newLayout}")
|
||||||
|
self.presentText("Please restart Fenrir for this change to take effect.")
|
||||||
|
self.playSound('Accept')
|
||||||
|
else:
|
||||||
|
self.presentText("Failed to change keyboard layout")
|
||||||
|
self.playSound('Error')
|
||||||
|
else:
|
||||||
|
self.presentText("Only default keyboard layout is available")
|
||||||
|
self.playSound('Cancel')
|
||||||
|
|
||||||
|
def getAvailableLayouts(self, keyboardPath):
|
||||||
|
"""Find available keyboard layout files"""
|
||||||
|
layouts = []
|
||||||
|
|
||||||
|
if os.path.isdir(keyboardPath):
|
||||||
|
try:
|
||||||
|
for file in os.listdir(keyboardPath):
|
||||||
|
if file.endswith('.conf') and not file.startswith('.'):
|
||||||
|
layoutName = file[:-5] # Remove .conf extension
|
||||||
|
layouts.append(layoutName)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Ensure we have at least the default layouts
|
||||||
|
if not layouts:
|
||||||
|
layouts = ['desktop', 'laptop']
|
||||||
|
|
||||||
|
return sorted(layouts)
|
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Load base configuration class
|
||||||
|
_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py')
|
||||||
|
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||||
|
_module = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_module)
|
||||||
|
ConfigCommand = _module.ConfigCommand
|
||||||
|
|
||||||
|
class command(ConfigCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Set character echo mode"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
currentMode = self.getSetting('keyboard', 'charEchoMode', '1')
|
||||||
|
|
||||||
|
# Present current mode
|
||||||
|
modeDescriptions = {
|
||||||
|
'0': 'None - no character echo',
|
||||||
|
'1': 'Always - echo all typed characters',
|
||||||
|
'2': 'Caps Lock - echo only when caps lock is on'
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDescription = modeDescriptions.get(currentMode, 'Unknown')
|
||||||
|
self.presentText(f"Current character echo mode: {currentDescription}")
|
||||||
|
|
||||||
|
# Cycle through the three modes
|
||||||
|
modes = ['0', '1', '2']
|
||||||
|
try:
|
||||||
|
currentIndex = modes.index(currentMode)
|
||||||
|
nextIndex = (currentIndex + 1) % len(modes)
|
||||||
|
newMode = modes[nextIndex]
|
||||||
|
except ValueError:
|
||||||
|
newMode = '1' # Default to always
|
||||||
|
|
||||||
|
success = self.setSetting('keyboard', 'charEchoMode', newMode)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
newDescription = modeDescriptions[newMode]
|
||||||
|
self.presentText(f"Character echo mode set to: {newDescription}")
|
||||||
|
self.playSound('Accept')
|
||||||
|
else:
|
||||||
|
self.presentText("Failed to change character echo mode")
|
||||||
|
self.playSound('Error')
|
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Load base configuration class
|
||||||
|
_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py')
|
||||||
|
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||||
|
_module = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_module)
|
||||||
|
ConfigCommand = _module.ConfigCommand
|
||||||
|
|
||||||
|
class command(ConfigCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Toggle exclusive keyboard access"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
currentState = self.getBooleanSetting('keyboard', 'grabDevices', True)
|
||||||
|
newState = self.toggleBooleanSetting('keyboard', 'grabDevices')
|
||||||
|
|
||||||
|
if newState != currentState:
|
||||||
|
stateText = "enabled" if newState else "disabled"
|
||||||
|
self.presentText(f"Exclusive keyboard access {stateText}")
|
||||||
|
if newState:
|
||||||
|
self.presentText("Fenrir will have exclusive control of keyboard input")
|
||||||
|
else:
|
||||||
|
self.presentText("Fenrir will share keyboard input with other applications")
|
||||||
|
self.presentText("Please restart Fenrir for this change to take effect")
|
||||||
|
self.playSound('Accept')
|
||||||
|
else:
|
||||||
|
self.presentText("Failed to change keyboard grab setting")
|
||||||
|
self.playSound('Error')
|
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Load base configuration class
|
||||||
|
_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py')
|
||||||
|
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||||
|
_module = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_module)
|
||||||
|
ConfigCommand = _module.ConfigCommand
|
||||||
|
|
||||||
|
class command(ConfigCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Toggle word echo when pressing space"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
currentState = self.getBooleanSetting('keyboard', 'wordEcho', False)
|
||||||
|
newState = self.toggleBooleanSetting('keyboard', 'wordEcho')
|
||||||
|
|
||||||
|
if newState != currentState:
|
||||||
|
stateText = "enabled" if newState else "disabled"
|
||||||
|
self.presentText(f"Word echo {stateText}")
|
||||||
|
if newState:
|
||||||
|
self.presentText("Words will be spoken when you press space")
|
||||||
|
else:
|
||||||
|
self.presentText("Words will not be spoken when you press space")
|
||||||
|
self.playSound('Accept')
|
||||||
|
else:
|
||||||
|
self.presentText("Failed to change word echo setting")
|
||||||
|
self.playSound('Error')
|
@@ -0,0 +1 @@
|
|||||||
|
# Fenrir management configuration
|
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Load base configuration class
|
||||||
|
_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py')
|
||||||
|
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||||
|
_module = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_module)
|
||||||
|
ConfigCommand = _module.ConfigCommand
|
||||||
|
|
||||||
|
class command(ConfigCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Create backup of current configuration"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.presentText("Creating configuration backup...")
|
||||||
|
|
||||||
|
success, message = self.backupConfig(announce=False)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# Force the message to be queued and spoken
|
||||||
|
self.env['runtime']['outputManager'].presentText("Configuration backup created successfully", interrupt=False, flush=False)
|
||||||
|
else:
|
||||||
|
self.env['runtime']['outputManager'].presentText("Failed to create configuration backup", interrupt=False, flush=False)
|
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Load base configuration class
|
||||||
|
_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py')
|
||||||
|
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||||
|
_module = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_module)
|
||||||
|
ConfigCommand = _module.ConfigCommand
|
||||||
|
|
||||||
|
class command(ConfigCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Reload configuration from file"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.presentText("Reloading configuration from file...")
|
||||||
|
|
||||||
|
success = self.reloadConfig()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.presentText("Configuration reloaded successfully")
|
||||||
|
self.playSound('Accept')
|
||||||
|
else:
|
||||||
|
self.presentText("Failed to reload configuration")
|
||||||
|
self.playSound('Error')
|
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# Load base configuration class
|
||||||
|
_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py')
|
||||||
|
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||||
|
_module = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_module)
|
||||||
|
ConfigCommand = _module.ConfigCommand
|
||||||
|
|
||||||
|
class command(ConfigCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Reset configuration to default settings"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.presentText("WARNING: This will reset all settings to defaults")
|
||||||
|
self.presentText("Creating backup before reset...")
|
||||||
|
|
||||||
|
# Create backup first
|
||||||
|
backupSuccess, backupMessage = self.backupConfig(announce=False)
|
||||||
|
if not backupSuccess:
|
||||||
|
self.presentText("Failed to create backup. Reset cancelled for safety.", interrupt=False)
|
||||||
|
self.playSound('Error')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find default configuration file
|
||||||
|
defaultConfigPath = self.findDefaultConfig()
|
||||||
|
|
||||||
|
if defaultConfigPath and os.path.isfile(defaultConfigPath):
|
||||||
|
try:
|
||||||
|
# Copy default configuration over current
|
||||||
|
shutil.copy2(defaultConfigPath, self.settingsFile)
|
||||||
|
|
||||||
|
# Reload the configuration
|
||||||
|
self.reloadConfig()
|
||||||
|
|
||||||
|
self.presentText("Configuration reset to defaults successfully", interrupt=False, flush=False)
|
||||||
|
self.presentText("Please restart Fenrir for all changes to take effect", interrupt=False, flush=False)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.presentText(f"Failed to reset configuration: {str(e)}", interrupt=False, flush=False)
|
||||||
|
else:
|
||||||
|
# Manually create basic default configuration
|
||||||
|
self.createBasicDefaults()
|
||||||
|
|
||||||
|
def findDefaultConfig(self):
|
||||||
|
"""Find the default configuration file"""
|
||||||
|
possiblePaths = [
|
||||||
|
'/usr/share/fenrirscreenreader/config/settings/settings.conf',
|
||||||
|
'/etc/fenrirscreenreader/settings/settings.conf.default',
|
||||||
|
os.path.join(os.path.dirname(self.settingsFile), 'settings.conf.default'),
|
||||||
|
# Development path
|
||||||
|
os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', '..', '..', 'config', 'settings', 'settings.conf')
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in possiblePaths:
|
||||||
|
if os.path.isfile(path):
|
||||||
|
return path
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def createBasicDefaults(self):
|
||||||
|
"""Create basic default configuration manually"""
|
||||||
|
try:
|
||||||
|
self.config.clear()
|
||||||
|
|
||||||
|
# Basic speech defaults
|
||||||
|
self.config['speech'] = {
|
||||||
|
'enabled': 'True',
|
||||||
|
'driver': 'speechdDriver',
|
||||||
|
'rate': '0.5',
|
||||||
|
'pitch': '0.5',
|
||||||
|
'volume': '1.0',
|
||||||
|
'autoReadIncoming': 'True'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Basic sound defaults
|
||||||
|
self.config['sound'] = {
|
||||||
|
'enabled': 'True',
|
||||||
|
'driver': 'genericDriver',
|
||||||
|
'theme': 'default',
|
||||||
|
'volume': '0.7'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Basic keyboard defaults
|
||||||
|
self.config['keyboard'] = {
|
||||||
|
'driver': 'evdevDriver',
|
||||||
|
'device': 'ALL',
|
||||||
|
'keyboardLayout': 'desktop',
|
||||||
|
'charEchoMode': '1',
|
||||||
|
'wordEcho': 'False',
|
||||||
|
'charDeleteEcho': 'True'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Basic screen defaults
|
||||||
|
self.config['screen'] = {
|
||||||
|
'driver': 'vcsaDriver',
|
||||||
|
'encoding': 'auto'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Basic general defaults
|
||||||
|
self.config['general'] = {
|
||||||
|
'punctuationLevel': 'some',
|
||||||
|
'debugLevel': '0',
|
||||||
|
'numberOfClipboards': '50'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write the configuration
|
||||||
|
with open(self.settingsFile, 'w') as configfile:
|
||||||
|
self.config.write(configfile)
|
||||||
|
|
||||||
|
self.presentText("Basic default configuration created", interrupt=False, flush=False)
|
||||||
|
self.presentText("Please restart Fenrir for changes to take effect", interrupt=False, flush=False)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.presentText(f"Failed to create default configuration: {str(e)}", interrupt=False, flush=False)
|
@@ -0,0 +1,50 @@
|
|||||||
|
#!/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 "Discard temporary changes and revert to saved settings"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.env['runtime']['outputManager'].presentText("Reverting to saved configuration...", interrupt=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Reload settings from file, discarding runtime changes
|
||||||
|
settingsManager = self.env['runtime']['settingsManager']
|
||||||
|
settingsManager.loadSettings()
|
||||||
|
|
||||||
|
# Reinitialize speech system with restored settings
|
||||||
|
if 'speechDriver' in self.env['runtime']:
|
||||||
|
try:
|
||||||
|
speechDriver = self.env['runtime']['speechDriver']
|
||||||
|
speechDriver.shutdown()
|
||||||
|
speechDriver.initialize(self.env)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Reinitialize sound system with restored settings
|
||||||
|
if 'soundDriver' in self.env['runtime']:
|
||||||
|
try:
|
||||||
|
soundDriver = self.env['runtime']['soundDriver']
|
||||||
|
soundDriver.shutdown()
|
||||||
|
soundDriver.initialize(self.env)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.env['runtime']['outputManager'].presentText("Successfully reverted to saved settings", interrupt=False, flush=False)
|
||||||
|
self.env['runtime']['outputManager'].presentText("All temporary changes have been discarded", interrupt=False, flush=False)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Error reverting settings: {str(e)}", interrupt=False, flush=False)
|
||||||
|
self.env['runtime']['outputManager'].presentText("You may need to restart Fenrir", interrupt=False, flush=False)
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Load base configuration class
|
||||||
|
_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py')
|
||||||
|
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||||
|
_module = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_module)
|
||||||
|
ConfigCommand = _module.ConfigCommand
|
||||||
|
|
||||||
|
class command(ConfigCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Save current configuration to file"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.presentText("Saving current configuration...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Force reload and save of current configuration
|
||||||
|
self.reloadConfig()
|
||||||
|
|
||||||
|
# Write the configuration file
|
||||||
|
with open(self.settingsFile, 'w') as configfile:
|
||||||
|
self.config.write(configfile)
|
||||||
|
|
||||||
|
self.presentText("Configuration saved successfully")
|
||||||
|
self.playSound('Accept')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.presentText(f"Failed to save configuration: {str(e)}")
|
||||||
|
self.playSound('Error')
|
@@ -0,0 +1,30 @@
|
|||||||
|
#!/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 "Save current session settings to configuration file"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.env['runtime']['outputManager'].presentText("Saving current session settings to configuration file...", interrupt=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# This calls the settings manager's save method which writes current runtime settings to file
|
||||||
|
self.env['runtime']['settingsManager'].saveSettings()
|
||||||
|
|
||||||
|
self.env['runtime']['outputManager'].presentText("Session settings saved successfully!", interrupt=False, flush=False)
|
||||||
|
self.env['runtime']['outputManager'].presentText("All temporary changes are now permanent.", interrupt=False, flush=False)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Failed to save settings: {str(e)}", interrupt=False, flush=False)
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
class command():
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def initialize(self, environment):
|
||||||
|
self.env = environment
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Show temporary changes not yet saved to file"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.env['runtime']['outputManager'].presentText("Checking for unsaved changes...", interrupt=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read the current config file
|
||||||
|
settingsFile = self.env['runtime']['settingsManager'].settingsFile
|
||||||
|
fileConfig = configparser.ConfigParser(interpolation=None)
|
||||||
|
fileConfig.read(settingsFile)
|
||||||
|
|
||||||
|
# Compare with runtime settings
|
||||||
|
runtimeSettings = self.env['runtime']['settingsManager'].settings
|
||||||
|
|
||||||
|
changes = []
|
||||||
|
|
||||||
|
# Check speech settings specifically
|
||||||
|
speechSections = ['speech', 'sound', 'keyboard', 'screen', 'general']
|
||||||
|
|
||||||
|
for section in speechSections:
|
||||||
|
if section in runtimeSettings and section in fileConfig:
|
||||||
|
for option in runtimeSettings[section]:
|
||||||
|
runtimeValue = runtimeSettings[section][option]
|
||||||
|
try:
|
||||||
|
fileValue = fileConfig[section][option]
|
||||||
|
except:
|
||||||
|
fileValue = ""
|
||||||
|
|
||||||
|
if runtimeValue != fileValue:
|
||||||
|
changes.append(f"{section}.{option}: {fileValue} → {runtimeValue}")
|
||||||
|
|
||||||
|
if changes:
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Found {len(changes)} unsaved changes:", interrupt=True)
|
||||||
|
for change in changes[:5]: # Limit to first 5 changes
|
||||||
|
self.env['runtime']['outputManager'].presentText(change, interrupt=True)
|
||||||
|
|
||||||
|
if len(changes) > 5:
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"... and {len(changes) - 5} more changes", interrupt=True)
|
||||||
|
|
||||||
|
self.env['runtime']['outputManager'].presentText("Use save command to make these changes permanent", interrupt=True)
|
||||||
|
self.env['runtime']['outputManager'].playSound('Accept')
|
||||||
|
else:
|
||||||
|
self.env['runtime']['outputManager'].presentText("No unsaved changes found", interrupt=True)
|
||||||
|
self.env['runtime']['outputManager'].playSound('Cancel')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Error checking for changes: {str(e)}", interrupt=True)
|
||||||
|
self.env['runtime']['outputManager'].playSound('Error')
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
@@ -0,0 +1 @@
|
|||||||
|
# Fenrir screen configuration
|
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Load base configuration class
|
||||||
|
_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py')
|
||||||
|
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||||
|
_module = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_module)
|
||||||
|
ConfigCommand = _module.ConfigCommand
|
||||||
|
|
||||||
|
class command(ConfigCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Select screen driver"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
currentDriver = self.getSetting('screen', 'driver', 'vcsaDriver')
|
||||||
|
|
||||||
|
# Present current driver
|
||||||
|
driverDescriptions = {
|
||||||
|
'vcsaDriver': 'VCSA Driver - Linux TTY console access',
|
||||||
|
'ptyDriver': 'PTY Driver - terminal emulation',
|
||||||
|
'dummyDriver': 'Dummy Driver - for testing only',
|
||||||
|
'debugDriver': 'Debug Driver - development/debugging'
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDescription = driverDescriptions.get(currentDriver, 'Unknown driver')
|
||||||
|
self.presentText(f"Current screen driver: {currentDescription}")
|
||||||
|
|
||||||
|
# Cycle through the available drivers
|
||||||
|
drivers = ['vcsaDriver', 'ptyDriver', 'dummyDriver', 'debugDriver']
|
||||||
|
try:
|
||||||
|
currentIndex = drivers.index(currentDriver)
|
||||||
|
nextIndex = (currentIndex + 1) % len(drivers)
|
||||||
|
newDriver = drivers[nextIndex]
|
||||||
|
except ValueError:
|
||||||
|
newDriver = 'vcsaDriver' # Default to VCSA
|
||||||
|
|
||||||
|
success = self.setSetting('screen', 'driver', newDriver)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
newDescription = driverDescriptions[newDriver]
|
||||||
|
self.presentText(f"Screen driver changed to: {newDescription}")
|
||||||
|
self.presentText("Please restart Fenrir for this change to take effect.")
|
||||||
|
self.playSound('Accept')
|
||||||
|
else:
|
||||||
|
self.presentText("Failed to change screen driver")
|
||||||
|
self.playSound('Error')
|
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Load base configuration class
|
||||||
|
_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py')
|
||||||
|
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||||
|
_module = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_module)
|
||||||
|
ConfigCommand = _module.ConfigCommand
|
||||||
|
|
||||||
|
class command(ConfigCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Set screen text encoding"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
currentEncoding = self.getSetting('screen', 'encoding', 'auto')
|
||||||
|
|
||||||
|
# Present current encoding
|
||||||
|
self.presentText(f"Current screen encoding: {currentEncoding}")
|
||||||
|
|
||||||
|
# Cycle through available encodings
|
||||||
|
encodings = ['auto', 'utf-8', 'cp1252', 'iso-8859-1']
|
||||||
|
try:
|
||||||
|
currentIndex = encodings.index(currentEncoding)
|
||||||
|
nextIndex = (currentIndex + 1) % len(encodings)
|
||||||
|
newEncoding = encodings[nextIndex]
|
||||||
|
except ValueError:
|
||||||
|
newEncoding = 'auto' # Default to auto
|
||||||
|
|
||||||
|
success = self.setSetting('screen', 'encoding', newEncoding)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.presentText(f"Screen encoding set to: {newEncoding}")
|
||||||
|
if newEncoding == 'auto':
|
||||||
|
self.presentText("Fenrir will automatically detect text encoding")
|
||||||
|
else:
|
||||||
|
self.presentText(f"Fenrir will use {newEncoding} encoding")
|
||||||
|
self.playSound('Accept')
|
||||||
|
else:
|
||||||
|
self.presentText("Failed to change screen encoding")
|
||||||
|
self.playSound('Error')
|
@@ -0,0 +1 @@
|
|||||||
|
# Fenrir sound configuration
|
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Load base configuration class
|
||||||
|
_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py')
|
||||||
|
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||||
|
_module = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_module)
|
||||||
|
ConfigCommand = _module.ConfigCommand
|
||||||
|
|
||||||
|
class command(ConfigCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Adjust sound volume"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
currentVolume = self.getFloatSetting('sound', 'volume', 0.7)
|
||||||
|
|
||||||
|
# Present current volume
|
||||||
|
volumePercent = int(currentVolume * 100)
|
||||||
|
self.presentText(f"Current sound volume: {volumePercent} percent")
|
||||||
|
|
||||||
|
# Adjust volume by 10%
|
||||||
|
newVolume = self.adjustFloatSetting('sound', 'volume', 0.1, 0.0, 1.0)
|
||||||
|
|
||||||
|
if newVolume != currentVolume:
|
||||||
|
newPercent = int(newVolume * 100)
|
||||||
|
self.presentText(f"Sound volume set to {newPercent} percent", interrupt=False)
|
||||||
|
self.playSound('Accept')
|
||||||
|
else:
|
||||||
|
# If we hit the maximum, try decreasing instead
|
||||||
|
newVolume = self.adjustFloatSetting('sound', 'volume', -0.1, 0.0, 1.0)
|
||||||
|
if newVolume != currentVolume:
|
||||||
|
newPercent = int(newVolume * 100)
|
||||||
|
self.presentText(f"Sound volume set to {newPercent} percent", interrupt=False)
|
||||||
|
self.playSound('Accept')
|
||||||
|
else:
|
||||||
|
self.presentText("Sound volume unchanged", interrupt=False)
|
||||||
|
self.playSound('Cancel')
|
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Load base configuration class
|
||||||
|
_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py')
|
||||||
|
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||||
|
_module = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_module)
|
||||||
|
ConfigCommand = _module.ConfigCommand
|
||||||
|
|
||||||
|
class command(ConfigCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Select sound driver (genericDriver or gstreamerDriver)"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
currentDriver = self.getSetting('sound', 'driver', 'genericDriver')
|
||||||
|
|
||||||
|
# Present current driver
|
||||||
|
self.presentText(f"Current sound driver: {currentDriver}")
|
||||||
|
|
||||||
|
# Toggle between the two available drivers
|
||||||
|
if currentDriver == 'genericDriver':
|
||||||
|
newDriver = 'gstreamerDriver'
|
||||||
|
driverDescription = "GStreamer Driver - advanced multimedia framework"
|
||||||
|
else:
|
||||||
|
newDriver = 'genericDriver'
|
||||||
|
driverDescription = "Generic Driver - uses SOX for audio playback"
|
||||||
|
|
||||||
|
success = self.setSetting('sound', 'driver', newDriver)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.presentText(f"Sound driver changed to {newDriver}. {driverDescription}")
|
||||||
|
self.presentText("Please restart Fenrir for this change to take effect.")
|
||||||
|
self.playSound('Accept')
|
||||||
|
else:
|
||||||
|
self.presentText("Failed to change sound driver")
|
||||||
|
self.playSound('Error')
|
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Load base configuration class
|
||||||
|
_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py')
|
||||||
|
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||||
|
_module = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_module)
|
||||||
|
ConfigCommand = _module.ConfigCommand
|
||||||
|
|
||||||
|
class command(ConfigCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Select sound theme"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
currentTheme = self.getSetting('sound', 'theme', 'default')
|
||||||
|
|
||||||
|
# Present current theme
|
||||||
|
self.presentText(f"Current sound theme: {currentTheme}")
|
||||||
|
|
||||||
|
# Look for available sound themes
|
||||||
|
soundPaths = [
|
||||||
|
'/usr/share/sounds',
|
||||||
|
'/usr/share/fenrirscreenreader/sounds',
|
||||||
|
os.path.expanduser('~/.local/share/fenrirscreenreader/sounds')
|
||||||
|
]
|
||||||
|
|
||||||
|
availableThemes = self.getAvailableThemes(soundPaths)
|
||||||
|
|
||||||
|
if len(availableThemes) > 1:
|
||||||
|
# For this implementation, cycle through available themes
|
||||||
|
try:
|
||||||
|
currentIndex = availableThemes.index(currentTheme)
|
||||||
|
nextIndex = (currentIndex + 1) % len(availableThemes)
|
||||||
|
newTheme = availableThemes[nextIndex]
|
||||||
|
except ValueError:
|
||||||
|
# Current theme not found in available themes, use first one
|
||||||
|
newTheme = availableThemes[0]
|
||||||
|
|
||||||
|
success = self.setSetting('sound', 'theme', newTheme)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.presentText(f"Sound theme changed to: {newTheme}")
|
||||||
|
self.playSound('Accept')
|
||||||
|
else:
|
||||||
|
self.presentText("Failed to change sound theme")
|
||||||
|
self.playSound('Error')
|
||||||
|
else:
|
||||||
|
self.presentText("Only default sound theme is available")
|
||||||
|
self.playSound('Cancel')
|
||||||
|
|
||||||
|
def getAvailableThemes(self, searchPaths):
|
||||||
|
"""Find available sound themes in the search paths"""
|
||||||
|
themes = ['default'] # Always include default
|
||||||
|
|
||||||
|
for path in searchPaths:
|
||||||
|
if os.path.isdir(path):
|
||||||
|
try:
|
||||||
|
for item in os.listdir(path):
|
||||||
|
fullPath = os.path.join(path, item)
|
||||||
|
if os.path.isdir(fullPath) and item != 'default' and item not in themes:
|
||||||
|
# Check if it looks like a sound theme (contains sound files)
|
||||||
|
if self.isValidSoundTheme(fullPath):
|
||||||
|
themes.append(item)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return themes
|
||||||
|
|
||||||
|
def isValidSoundTheme(self, themePath):
|
||||||
|
"""Check if a directory contains sound files"""
|
||||||
|
soundExtensions = ['.wav', '.ogg', '.mp3', '.flac']
|
||||||
|
try:
|
||||||
|
for file in os.listdir(themePath):
|
||||||
|
if any(file.lower().endswith(ext) for ext in soundExtensions):
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Load base configuration class
|
||||||
|
_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py')
|
||||||
|
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||||
|
_module = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_module)
|
||||||
|
ConfigCommand = _module.ConfigCommand
|
||||||
|
|
||||||
|
class command(ConfigCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return "Toggle sound output on or off"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
currentState = self.getBooleanSetting('sound', 'enabled', True)
|
||||||
|
newState = self.toggleBooleanSetting('sound', 'enabled')
|
||||||
|
|
||||||
|
if newState != currentState:
|
||||||
|
stateText = "enabled" if newState else "disabled"
|
||||||
|
self.presentText(f"Sound {stateText}")
|
||||||
|
# Only play sound if we enabled sound
|
||||||
|
if newState:
|
||||||
|
self.playSound('Accept')
|
||||||
|
else:
|
||||||
|
self.presentText("Failed to change sound setting")
|
@@ -0,0 +1 @@
|
|||||||
|
# Fenrir speech configuration
|
@@ -0,0 +1,40 @@
|
|||||||
|
#!/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 "Adjust speech pitch (voice height)"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
# Get current pitch
|
||||||
|
currentPitch = float(self.env['runtime']['settingsManager'].getSetting('speech', 'pitch'))
|
||||||
|
except:
|
||||||
|
currentPitch = 0.5
|
||||||
|
|
||||||
|
# Present current pitch
|
||||||
|
pitchPercent = int(currentPitch * 100)
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Current speech pitch: {pitchPercent} percent", interrupt=True)
|
||||||
|
|
||||||
|
# Increase by 10%, wrap around if at max
|
||||||
|
newPitch = currentPitch + 0.1
|
||||||
|
if newPitch > 1.0:
|
||||||
|
newPitch = 0.1 # Wrap to minimum
|
||||||
|
|
||||||
|
# Apply the new pitch
|
||||||
|
self.env['runtime']['settingsManager'].setSetting('speech', 'pitch', str(newPitch))
|
||||||
|
|
||||||
|
newPercent = int(newPitch * 100)
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Speech pitch set to {newPercent} percent", interrupt=True)
|
||||||
|
self.env['runtime']['outputManager'].playSound('Accept')
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
@@ -0,0 +1,39 @@
|
|||||||
|
#!/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 "Adjust speech rate (speed)"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
# Get current rate
|
||||||
|
currentRate = float(self.env['runtime']['settingsManager'].getSetting('speech', 'rate'))
|
||||||
|
except:
|
||||||
|
currentRate = 0.5
|
||||||
|
|
||||||
|
# Present current rate
|
||||||
|
ratePercent = int(currentRate * 100)
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Current speech rate: {ratePercent} percent", interrupt=True)
|
||||||
|
|
||||||
|
# Increase by 10%, wrap around if at max
|
||||||
|
newRate = currentRate + 0.1
|
||||||
|
if newRate > 1.0:
|
||||||
|
newRate = 0.1 # Wrap to minimum
|
||||||
|
|
||||||
|
# Apply the new rate
|
||||||
|
self.env['runtime']['settingsManager'].setSetting('speech', 'rate', str(newRate))
|
||||||
|
|
||||||
|
newPercent = int(newRate * 100)
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Speech rate set to {newPercent} percent", interrupt=False, flush=False)
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
@@ -0,0 +1,49 @@
|
|||||||
|
#!/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 "Adjust speech rate (temporary - use save to make permanent)"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
# Get current rate from runtime settings (may be temporary)
|
||||||
|
settingsManager = self.env['runtime']['settingsManager']
|
||||||
|
currentRate = float(settingsManager.getSetting('speech', 'rate'))
|
||||||
|
except:
|
||||||
|
currentRate = 0.5
|
||||||
|
|
||||||
|
# Present current rate
|
||||||
|
ratePercent = int(currentRate * 100)
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Current speech rate: {ratePercent} percent", interrupt=True)
|
||||||
|
|
||||||
|
# Increase by 10%, wrap around if at max
|
||||||
|
newRate = currentRate + 0.1
|
||||||
|
if newRate > 1.0:
|
||||||
|
newRate = 0.1 # Wrap to minimum
|
||||||
|
|
||||||
|
# Apply ONLY to runtime - this is temporary until saved
|
||||||
|
settingsManager = self.env['runtime']['settingsManager']
|
||||||
|
settingsManager.settings['speech']['rate'] = str(newRate)
|
||||||
|
|
||||||
|
# Apply to speech system immediately for this session
|
||||||
|
if 'speechDriver' in self.env['runtime']:
|
||||||
|
try:
|
||||||
|
self.env['runtime']['speechDriver'].setRate(newRate)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
newPercent = int(newRate * 100)
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Speech rate temporarily set to {newPercent} percent", interrupt=False, flush=False)
|
||||||
|
self.env['runtime']['outputManager'].presentText("Use save command to make permanent", interrupt=False, flush=False)
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
@@ -0,0 +1,40 @@
|
|||||||
|
#!/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 "Adjust speech volume (loudness)"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
# Get current volume
|
||||||
|
currentVolume = float(self.env['runtime']['settingsManager'].getSetting('speech', 'volume'))
|
||||||
|
except:
|
||||||
|
currentVolume = 1.0
|
||||||
|
|
||||||
|
# Present current volume
|
||||||
|
volumePercent = int(currentVolume * 100)
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Current speech volume: {volumePercent} percent", interrupt=True)
|
||||||
|
|
||||||
|
# Increase by 10%, wrap around if at max
|
||||||
|
newVolume = currentVolume + 0.1
|
||||||
|
if newVolume > 1.0:
|
||||||
|
newVolume = 0.1 # Wrap to minimum
|
||||||
|
|
||||||
|
# Apply the new volume
|
||||||
|
self.env['runtime']['settingsManager'].setSetting('speech', 'volume', str(newVolume))
|
||||||
|
|
||||||
|
newPercent = int(newVolume * 100)
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Speech volume set to {newPercent} percent", interrupt=True)
|
||||||
|
self.env['runtime']['outputManager'].playSound('Accept')
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
@@ -0,0 +1,76 @@
|
|||||||
|
#!/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
|
@@ -0,0 +1,182 @@
|
|||||||
|
#!/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
|
@@ -0,0 +1,136 @@
|
|||||||
|
#!/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
|
@@ -0,0 +1,59 @@
|
|||||||
|
#!/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 "Show current speech settings"
|
||||||
|
|
||||||
|
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')
|
||||||
|
enabled = self.env['runtime']['settingsManager'].getSetting('speech', 'enabled')
|
||||||
|
|
||||||
|
self.env['runtime']['outputManager'].presentText("Current speech settings:", interrupt=True)
|
||||||
|
|
||||||
|
# Present all settings
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Speech enabled: {enabled}", interrupt=True)
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Driver: {driver}", interrupt=True)
|
||||||
|
|
||||||
|
if module:
|
||||||
|
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:
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Rate: {rate}", interrupt=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pitchPercent = int(float(pitch) * 100)
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Pitch: {pitchPercent} percent", interrupt=True)
|
||||||
|
except:
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Pitch: {pitch}", interrupt=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
volumePercent = int(float(volume) * 100)
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Volume: {volumePercent} percent", interrupt=True)
|
||||||
|
except:
|
||||||
|
self.env['runtime']['outputManager'].presentText(f"Volume: {volume}", interrupt=True)
|
||||||
|
|
||||||
|
self.env['runtime']['outputManager'].playSound('Accept')
|
||||||
|
|
||||||
|
def setCallback(self, callback):
|
||||||
|
pass
|
@@ -0,0 +1,62 @@
|
|||||||
|
#!/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
|
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)
|
menu = self.fs_tree_to_dict( self.defaultVMenuPath)
|
||||||
if menu:
|
if menu:
|
||||||
self.menuDict = 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?
|
# index still valid?
|
||||||
if self.currIndex != None:
|
if self.currIndex != None:
|
||||||
try:
|
try:
|
||||||
|
@@ -4,5 +4,5 @@
|
|||||||
# Fenrir TTY screen reader
|
# Fenrir TTY screen reader
|
||||||
# By Chrys, Storm Dragon, and contributers.
|
# By Chrys, Storm Dragon, and contributers.
|
||||||
|
|
||||||
version = "2025.06.12"
|
version = "2025.06.15"
|
||||||
codeName = "testing"
|
codeName = "testing"
|
||||||
|
Reference in New Issue
Block a user