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:
parent
e76b914d6e
commit
72bd334d65
@ -114,6 +114,8 @@ KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_X=remove_marks
|
||||
KEY_FENRIR,KEY_X=set_mark
|
||||
KEY_FENRIR,KEY_SHIFT,KEY_X=marked_text
|
||||
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_CTRL,KEY_SPACE=current_quick_menu_value
|
||||
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_SHIFT,KEY_X=marked_text
|
||||
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_CTRL,KEY_SPACE=current_quick_menu_value
|
||||
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
|
||||
SpeechOn='SpeechOn.wav'
|
||||
SpeechOff='SpeechOff.wav'
|
||||
# Braille has turned On or Off
|
||||
BrailleOn='BrailleOn.wav'
|
||||
BrailleOff='BrailleOff.wav'
|
||||
# SoundIcons has turned On or Off
|
||||
SoundOn='SoundOn.wav'
|
||||
SoundOff='SoundOff.wav'
|
||||
@ -44,9 +41,8 @@ PlaceEndMark='PlaceEndMark.wav'
|
||||
CopyToClipboard='CopyToClipboard.wav'
|
||||
# Pasted on the screen
|
||||
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'
|
||||
ErrorBraille='ErrorBraille.wav'
|
||||
ErrorScreen='ErrorScreen.wav'
|
||||
# If you cursor over an text that has attributs (like color)
|
||||
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)
|
||||
if menu:
|
||||
self.menuDict = menu
|
||||
|
||||
# Add dynamic voice menus
|
||||
try:
|
||||
from fenrirscreenreader.core.dynamicVoiceMenu import addDynamicVoiceMenus
|
||||
addDynamicVoiceMenus(self)
|
||||
except Exception as e:
|
||||
print(f"Error adding dynamic voice menus: {e}")
|
||||
|
||||
# index still valid?
|
||||
if self.currIndex != None:
|
||||
try:
|
||||
|
@ -4,5 +4,5 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributers.
|
||||
|
||||
version = "2025.06.12"
|
||||
version = "2025.06.15"
|
||||
codeName = "testing"
|
||||
|
Loading…
x
Reference in New Issue
Block a user