latest testing code merged. Nothing major reported from testing branch, so if we get no reports, this will become the next stable release. I'm waiting a bit to tag because major new features introduced.
This commit is contained in:
commit
efb308ac72
@ -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,71 @@
|
||||
#!/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 (use setSetting to update settingArgDict)
|
||||
settingsManager.setSetting('speech', 'driver', 'speechdDriver')
|
||||
settingsManager.setSetting('speech', 'module', module)
|
||||
settingsManager.setSetting('speech', 'voice', voice)
|
||||
|
||||
# Apply to speech driver instance directly
|
||||
if 'speechDriver' in self.env['runtime']:
|
||||
speechDriver = self.env['runtime']['speechDriver']
|
||||
|
||||
# Set the module and voice on the driver instance
|
||||
speechDriver.setModule(module)
|
||||
speechDriver.setVoice(voice)
|
||||
|
||||
self.env['runtime']['outputManager'].presentText("Voice applied successfully!", interrupt=True)
|
||||
self.env['runtime']['outputManager'].presentText("Use save settings to make permanent", interrupt=True)
|
||||
self.env['runtime']['outputManager'].playSound('Accept')
|
||||
|
||||
except Exception as e:
|
||||
# Revert on failure
|
||||
settingsManager.setSetting('speech', 'driver', oldDriver)
|
||||
settingsManager.setSetting('speech', 'module', oldModule)
|
||||
settingsManager.setSetting('speech', 'voice', oldVoice)
|
||||
|
||||
self.env['runtime']['outputManager'].presentText(f"Failed to apply voice, reverted: {str(e)}", interrupt=True)
|
||||
self.env['runtime']['outputManager'].playSound('Error')
|
||||
|
||||
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,22 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributers.
|
||||
|
||||
|
||||
class command():
|
||||
def __init__(self):
|
||||
pass
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
def shutdown(self):
|
||||
pass
|
||||
def getDescription(self):
|
||||
return _('jump down 10% in v menu')
|
||||
def run(self):
|
||||
self.env['runtime']['vmenuManager'].pageDown()
|
||||
text = self.env['runtime']['vmenuManager'].getCurrentEntry()
|
||||
self.env['runtime']['outputManager'].presentText(text, interrupt=True)
|
||||
def setCallback(self, callback):
|
||||
pass
|
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributers.
|
||||
|
||||
|
||||
class command():
|
||||
def __init__(self):
|
||||
pass
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
def shutdown(self):
|
||||
pass
|
||||
def getDescription(self):
|
||||
return _('jump up 10% in v menu')
|
||||
def run(self):
|
||||
self.env['runtime']['vmenuManager'].pageUp()
|
||||
text = self.env['runtime']['vmenuManager'].getCurrentEntry()
|
||||
self.env['runtime']['outputManager'].presentText(text, interrupt=True)
|
||||
def setCallback(self, callback):
|
||||
pass
|
@ -0,0 +1 @@
|
||||
# Fenrir Configuration VMenu Profile
|
@ -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,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
|
270
src/fenrirscreenreader/core/dynamicVoiceMenu.py
Normal file
270
src/fenrirscreenreader/core/dynamicVoiceMenu.py
Normal file
@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
import importlib.util
|
||||
import os
|
||||
import time
|
||||
|
||||
class DynamicVoiceCommand:
|
||||
"""Dynamic command class for voice selection"""
|
||||
def __init__(self, module, voice, env):
|
||||
self.module = module
|
||||
self.voice = voice
|
||||
self.env = env
|
||||
self.testMessage = "This is a voice test. The quick brown fox jumps over the lazy dog."
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def getDescription(self):
|
||||
return f"Select voice: {self.voice}"
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.env['runtime']['outputManager'].presentText(f"Testing voice {self.voice} from {self.module}. Please wait.", interrupt=True)
|
||||
|
||||
# Brief pause before testing to avoid speech overlap
|
||||
time.sleep(0.5)
|
||||
|
||||
# Test voice
|
||||
testResult, errorMsg = self.testVoice()
|
||||
if testResult:
|
||||
self.env['runtime']['outputManager'].presentText("Voice test completed successfully. Navigate to Apply Tested Voice to use this voice.", interrupt=False, flush=False)
|
||||
|
||||
# Store for confirmation (use same variables as apply_tested_voice.py)
|
||||
self.env['commandBuffer']['lastTestedModule'] = self.module
|
||||
self.env['commandBuffer']['lastTestedVoice'] = self.voice
|
||||
self.env['commandBuffer']['pendingVoiceModule'] = self.module
|
||||
self.env['commandBuffer']['pendingVoiceVoice'] = self.voice
|
||||
self.env['commandBuffer']['voiceTestCompleted'] = True
|
||||
else:
|
||||
self.env['runtime']['outputManager'].presentText(f"Voice test failed: {errorMsg}", interrupt=False, flush=False)
|
||||
|
||||
except Exception as e:
|
||||
self.env['runtime']['outputManager'].presentText(f"Voice selection error: {str(e)}", interrupt=False, flush=False)
|
||||
|
||||
def testVoice(self):
|
||||
"""Test voice with spd-say"""
|
||||
try:
|
||||
cmd = ['spd-say', '-C', '-w', '-o', self.module, '-y', self.voice, self.testMessage]
|
||||
result = subprocess.run(cmd, timeout=8, capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
return True, "Voice test successful"
|
||||
else:
|
||||
error_msg = result.stderr.strip() if result.stderr else f"Command failed with return code {result.returncode}"
|
||||
return False, error_msg
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "Voice test timed out"
|
||||
except Exception as e:
|
||||
return False, f"Error running voice test: {str(e)}"
|
||||
|
||||
def setCallback(self, callback):
|
||||
pass
|
||||
|
||||
class DynamicApplyVoiceCommand:
|
||||
"""Command to apply the tested voice"""
|
||||
def __init__(self, env):
|
||||
self.env = env
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def getDescription(self):
|
||||
return "Apply tested voice to Fenrir"
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
if not self.env['commandBuffer'].get('voiceTestCompleted', False):
|
||||
self.env['runtime']['outputManager'].presentText("No voice has been tested yet", interrupt=True)
|
||||
return
|
||||
|
||||
module = self.env['commandBuffer']['pendingVoiceModule']
|
||||
voice = self.env['commandBuffer']['pendingVoiceVoice']
|
||||
|
||||
self.env['runtime']['outputManager'].presentText(f"Applying {voice} from {module}", interrupt=True)
|
||||
|
||||
# Debug: Show current settings
|
||||
settingsManager = self.env['runtime']['settingsManager']
|
||||
currentModule = settingsManager.getSetting('speech', 'module')
|
||||
currentVoice = settingsManager.getSetting('speech', 'voice')
|
||||
self.env['runtime']['outputManager'].presentText(f"Current: {currentVoice} from {currentModule}", interrupt=False, flush=False)
|
||||
|
||||
# Apply to runtime settings with fallback
|
||||
settingsManager = self.env['runtime']['settingsManager']
|
||||
|
||||
# Store old values for safety
|
||||
oldDriver = settingsManager.getSetting('speech', 'driver')
|
||||
oldModule = settingsManager.getSetting('speech', 'module')
|
||||
oldVoice = settingsManager.getSetting('speech', 'voice')
|
||||
|
||||
try:
|
||||
# Apply new settings to runtime only (use setSetting to update settingArgDict)
|
||||
settingsManager.setSetting('speech', 'driver', 'speechdDriver')
|
||||
settingsManager.setSetting('speech', 'module', module)
|
||||
settingsManager.setSetting('speech', 'voice', voice)
|
||||
|
||||
# Apply settings to speech driver directly
|
||||
if 'speechDriver' in self.env['runtime']:
|
||||
speechDriver = self.env['runtime']['speechDriver']
|
||||
|
||||
# Get current module to see if we're changing modules
|
||||
currentModule = settingsManager.getSetting('speech', 'module')
|
||||
moduleChanging = (currentModule != module)
|
||||
|
||||
# Set module and voice on driver instance first
|
||||
speechDriver.setModule(module)
|
||||
speechDriver.setVoice(voice)
|
||||
|
||||
if moduleChanging:
|
||||
# Module change requires reinitializing the speech driver
|
||||
self.env['runtime']['outputManager'].presentText(f"Switching from {currentModule} to {module} module", interrupt=True)
|
||||
speechDriver.shutdown()
|
||||
speechDriver.initialize(self.env)
|
||||
# Re-set after initialization
|
||||
speechDriver.setModule(module)
|
||||
speechDriver.setVoice(voice)
|
||||
self.env['runtime']['outputManager'].presentText("Speech driver reinitialized", interrupt=True)
|
||||
|
||||
# Debug: verify what was actually set
|
||||
self.env['runtime']['outputManager'].presentText(f"Speech driver now has module: {speechDriver.module}, voice: {speechDriver.voice}", interrupt=True)
|
||||
|
||||
# Force application by speaking a test message
|
||||
self.env['runtime']['outputManager'].presentText("Voice applied successfully! You should hear this in the new voice.", interrupt=True)
|
||||
|
||||
# Brief pause then more speech to test
|
||||
time.sleep(1)
|
||||
self.env['runtime']['outputManager'].presentText("Use save settings to make permanent", interrupt=True)
|
||||
|
||||
# Clear pending state
|
||||
self.env['commandBuffer']['voiceTestCompleted'] = False
|
||||
|
||||
# Exit vmenu after successful application
|
||||
self.env['runtime']['vmenuManager'].setActive(False)
|
||||
|
||||
except Exception as e:
|
||||
# Revert on failure
|
||||
settingsManager.settings['speech']['driver'] = oldDriver
|
||||
settingsManager.settings['speech']['module'] = oldModule
|
||||
settingsManager.settings['speech']['voice'] = oldVoice
|
||||
|
||||
# Try to reinitialize with old settings
|
||||
if 'speechDriver' in self.env['runtime']:
|
||||
try:
|
||||
speechDriver = self.env['runtime']['speechDriver']
|
||||
speechDriver.shutdown()
|
||||
speechDriver.initialize(self.env)
|
||||
except:
|
||||
pass # If this fails, at least we tried
|
||||
|
||||
self.env['runtime']['outputManager'].presentText(f"Failed to apply voice, reverted: {str(e)}", interrupt=False, flush=False)
|
||||
|
||||
except Exception as e:
|
||||
self.env['runtime']['outputManager'].presentText(f"Apply voice error: {str(e)}", interrupt=False, flush=False)
|
||||
|
||||
def setCallback(self, callback):
|
||||
pass
|
||||
|
||||
def addDynamicVoiceMenus(vmenuManager):
|
||||
"""Add dynamic voice menus to vmenu system"""
|
||||
try:
|
||||
env = vmenuManager.env
|
||||
|
||||
# Get speech modules
|
||||
modules = getSpeechdModules()
|
||||
if not modules:
|
||||
return
|
||||
|
||||
# Create voice browser submenu
|
||||
voiceBrowserMenu = {}
|
||||
|
||||
# Add apply voice command
|
||||
applyCommand = DynamicApplyVoiceCommand(env)
|
||||
voiceBrowserMenu['Apply Tested Voice Action'] = applyCommand
|
||||
|
||||
# Add modules as submenus
|
||||
for module in modules[:8]: # Limit to 8 modules to keep menu manageable
|
||||
moduleMenu = {}
|
||||
|
||||
# Get voices for this module
|
||||
voices = getModuleVoices(module)
|
||||
if voices:
|
||||
|
||||
# Add voice commands
|
||||
for voice in voices:
|
||||
voiceCommand = DynamicVoiceCommand(module, voice, env)
|
||||
moduleMenu[f"{voice} Action"] = voiceCommand
|
||||
else:
|
||||
moduleMenu['No voices available Action'] = createInfoCommand(f"No voices found for {module}", env)
|
||||
|
||||
voiceBrowserMenu[f"{module} Menu"] = moduleMenu
|
||||
|
||||
# Add to main menu dict
|
||||
vmenuManager.menuDict['Voice Browser Menu'] = voiceBrowserMenu
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating dynamic voice menus: {e}")
|
||||
|
||||
def createInfoCommand(message, env):
|
||||
"""Create a simple info command"""
|
||||
class InfoCommand:
|
||||
def __init__(self, message, env):
|
||||
self.message = message
|
||||
self.env = env
|
||||
def initialize(self, environment): pass
|
||||
def shutdown(self): pass
|
||||
def getDescription(self): return self.message
|
||||
def run(self):
|
||||
self.env['runtime']['outputManager'].presentText(self.message, interrupt=True)
|
||||
def setCallback(self, callback): pass
|
||||
|
||||
return InfoCommand(message, env)
|
||||
|
||||
def getSpeechdModules():
|
||||
"""Get available speech modules"""
|
||||
try:
|
||||
result = subprocess.run(['spd-say', '-O'], capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
lines = result.stdout.strip().split('\n')
|
||||
return [line.strip() for line in lines[1:] if line.strip()]
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
def getModuleVoices(module):
|
||||
"""Get voices for a module"""
|
||||
try:
|
||||
result = subprocess.run(['spd-say', '-o', module, '-L'], capture_output=True, text=True, timeout=8)
|
||||
if result.returncode == 0:
|
||||
lines = result.stdout.strip().split('\n')
|
||||
voices = []
|
||||
for line in lines[1:]:
|
||||
if not line.strip():
|
||||
continue
|
||||
if module.lower() == 'espeak-ng':
|
||||
voice = processEspeakVoice(line)
|
||||
if voice:
|
||||
voices.append(voice)
|
||||
else:
|
||||
voices.append(line.strip())
|
||||
return voices
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
def processEspeakVoice(voiceLine):
|
||||
"""Process espeak voice format"""
|
||||
try:
|
||||
parts = [p for p in voiceLine.split() if p]
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
langCode = parts[-2].lower()
|
||||
variant = parts[-1].lower()
|
||||
return f"{langCode}+{variant}" if variant and variant != 'none' else langCode
|
||||
except Exception:
|
||||
return None
|
@ -192,3 +192,12 @@ class outputManager():
|
||||
self.presentText(' review cursor ', interrupt=interrupt_p)
|
||||
else:
|
||||
self.presentText(' text cursor ', interrupt=interrupt_p)
|
||||
|
||||
def resetSpeechDriver(self):
|
||||
"""Reset speech driver to clean state - called by settingsManager"""
|
||||
if 'speechDriver' in self.env['runtime'] and self.env['runtime']['speechDriver']:
|
||||
try:
|
||||
self.env['runtime']['speechDriver'].reset()
|
||||
self.env['runtime']['debug'].writeDebugOut("Speech driver reset successfully", debug.debugLevel.INFO)
|
||||
except Exception as e:
|
||||
self.env['runtime']['debug'].writeDebugOut(f"resetSpeechDriver error: {e}", debug.debugLevel.ERROR)
|
||||
|
@ -138,6 +138,9 @@ class vmenuManager():
|
||||
self.env['bindings'][str([1, ['KEY_X']])] = 'SEARCH_X'
|
||||
self.env['bindings'][str([1, ['KEY_Y']])] = 'SEARCH_Y'
|
||||
self.env['bindings'][str([1, ['KEY_Z']])] = 'SEARCH_Z'
|
||||
# page navigation
|
||||
self.env['bindings'][str([1, ['KEY_PAGEUP']])] = 'PAGE_UP_VMENU'
|
||||
self.env['bindings'][str([1, ['KEY_PAGEDOWN']])] = 'PAGE_DOWN_VMENU'
|
||||
except Exception as e:
|
||||
print(e)
|
||||
else:
|
||||
@ -152,6 +155,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:
|
||||
@ -218,6 +229,32 @@ class vmenuManager():
|
||||
else:
|
||||
self.currIndex[len(self.currIndex) - 1] -= 1
|
||||
return True
|
||||
|
||||
def pageUp(self):
|
||||
if self.currIndex == None:
|
||||
return False
|
||||
menuSize = len(self.getNestedByPath(self.menuDict, self.currIndex[:-1]))
|
||||
if menuSize <= 1:
|
||||
return False
|
||||
jumpSize = max(1, int(menuSize * 0.1)) # 10% of menu size, minimum 1
|
||||
newIndex = self.currIndex[len(self.currIndex) - 1] - jumpSize
|
||||
if newIndex < 0:
|
||||
newIndex = 0
|
||||
self.currIndex[len(self.currIndex) - 1] = newIndex
|
||||
return True
|
||||
|
||||
def pageDown(self):
|
||||
if self.currIndex == None:
|
||||
return False
|
||||
menuSize = len(self.getNestedByPath(self.menuDict, self.currIndex[:-1]))
|
||||
if menuSize <= 1:
|
||||
return False
|
||||
jumpSize = max(1, int(menuSize * 0.1)) # 10% of menu size, minimum 1
|
||||
newIndex = self.currIndex[len(self.currIndex) - 1] + jumpSize
|
||||
if newIndex >= menuSize:
|
||||
newIndex = menuSize - 1
|
||||
self.currIndex[len(self.currIndex) - 1] = newIndex
|
||||
return True
|
||||
|
||||
def getCurrentEntry(self):
|
||||
return self.getKeysByPath(self.menuDict, self.currIndex)[self.currIndex[-1]]
|
||||
|
@ -4,5 +4,5 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributers.
|
||||
|
||||
version = "2025.06.07"
|
||||
version = "2025.06.17"
|
||||
codeName = "master"
|
||||
|
@ -16,9 +16,15 @@ class driver(speechDriver):
|
||||
self._sd = None
|
||||
self.env = environment
|
||||
self._isInitialized = False
|
||||
self.language = ''
|
||||
self.voice = ''
|
||||
self.module = ''
|
||||
|
||||
# Only set these if they haven't been set yet (preserve existing values)
|
||||
if not hasattr(self, 'language') or self.language is None:
|
||||
self.language = ''
|
||||
if not hasattr(self, 'voice') or self.voice is None:
|
||||
self.voice = ''
|
||||
if not hasattr(self, 'module') or self.module is None:
|
||||
self.module = ''
|
||||
|
||||
try:
|
||||
import speechd
|
||||
self._sd = speechd.SSIPClient('fenrir')
|
||||
|
Loading…
x
Reference in New Issue
Block a user