Reorganize files for new image. Add new paths in home directory. Start on game launchers and installers.
This commit is contained in:
Executable
+438
@@ -0,0 +1,438 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Self-voiced Speech Rate Configuration Menu
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import curses
|
||||
import speechd # Python bindings for Speech Dispatcher
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
def update_speechd_config_content(content, rate, volume, pitch):
|
||||
"""Return speechd.conf content with updated default voice parameters."""
|
||||
newContent = content
|
||||
|
||||
replacements = (
|
||||
("DefaultRate", rate),
|
||||
("DefaultVolume", volume),
|
||||
("DefaultPitch", pitch),
|
||||
)
|
||||
|
||||
for settingName, settingValue in replacements:
|
||||
activePattern = rf'^(\s*){settingName}\s+(-?\d+)'
|
||||
commentedPattern = rf'^(\s*)#\s*{settingName}\s+(-?\d+)'
|
||||
replacement = rf'\1{settingName} {settingValue}'
|
||||
|
||||
if re.search(activePattern, newContent, re.MULTILINE):
|
||||
newContent = re.sub(
|
||||
activePattern,
|
||||
replacement,
|
||||
newContent,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
else:
|
||||
newContent = re.sub(
|
||||
commentedPattern,
|
||||
replacement,
|
||||
newContent,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
return newContent
|
||||
|
||||
|
||||
def get_writable_config_targets(configTargets):
|
||||
"""Return existing config paths to update, preserving order."""
|
||||
return [configPath for configPath in configTargets if os.path.exists(configPath)]
|
||||
|
||||
|
||||
class SpeechRateMenu:
|
||||
def __init__(self, title="Speech Configuration"):
|
||||
self.title = title
|
||||
self.currentRate = 0 # Default rate
|
||||
self.currentVolume = 100 # Default volume
|
||||
self.currentPitch = 0 # Default pitch
|
||||
self.currentMode = 0 # 0=Rate, 1=Volume, 2=Pitch
|
||||
self.modes = ["Rate", "Volume", "Pitch"]
|
||||
self.stdscr = None
|
||||
self.cursesInitialized = False # Flag to track if curses has been initialized
|
||||
self.configFile = "/etc/speech-dispatcher/speechd.conf"
|
||||
self.configTargets = [
|
||||
self.configFile,
|
||||
os.path.expanduser("~/.fex-emu/RootFS/ArchLinux/etc/speech-dispatcher/speechd.conf"),
|
||||
]
|
||||
|
||||
# Load current settings from config FIRST
|
||||
self.load_current_settings()
|
||||
|
||||
# Initialize speech client AFTER loading the settings
|
||||
self.speechClient = None
|
||||
self.init_speech()
|
||||
|
||||
def init_speech(self):
|
||||
"""Initialize the speech client"""
|
||||
try:
|
||||
self.speechClient = speechd.SSIPClient("speech_config_menu")
|
||||
self.speechClient.set_priority(speechd.Priority.IMPORTANT)
|
||||
self.speechClient.set_punctuation(speechd.PunctuationMode.SOME)
|
||||
|
||||
# Apply the loaded settings to the speech client
|
||||
self.speechClient.set_rate(self.currentRate)
|
||||
self.speechClient.set_volume(self.currentVolume)
|
||||
self.speechClient.set_pitch(self.currentPitch)
|
||||
except Exception as e:
|
||||
# Fallback to None - the speak method will handle this
|
||||
pass
|
||||
|
||||
def load_current_settings(self):
|
||||
"""Load the current default settings from speechd.conf"""
|
||||
try:
|
||||
with open(self.configFile, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Load Rate
|
||||
activeMatch = re.search(r'^\s*DefaultRate\s+(-?\d+)', content, re.MULTILINE)
|
||||
if activeMatch:
|
||||
self.currentRate = int(activeMatch.group(1))
|
||||
else:
|
||||
commentedMatch = re.search(r'^\s*#\s*DefaultRate\s+(-?\d+)', content, re.MULTILINE)
|
||||
if commentedMatch:
|
||||
self.currentRate = int(commentedMatch.group(1))
|
||||
|
||||
# Load Volume
|
||||
activeMatch = re.search(r'^\s*DefaultVolume\s+(-?\d+)', content, re.MULTILINE)
|
||||
if activeMatch:
|
||||
self.currentVolume = int(activeMatch.group(1))
|
||||
else:
|
||||
commentedMatch = re.search(r'^\s*#\s*DefaultVolume\s+(-?\d+)', content, re.MULTILINE)
|
||||
if commentedMatch:
|
||||
self.currentVolume = int(commentedMatch.group(1))
|
||||
|
||||
# Load Pitch
|
||||
activeMatch = re.search(r'^\s*DefaultPitch\s+(-?\d+)', content, re.MULTILINE)
|
||||
if activeMatch:
|
||||
self.currentPitch = int(activeMatch.group(1))
|
||||
else:
|
||||
commentedMatch = re.search(r'^\s*#\s*DefaultPitch\s+(-?\d+)', content, re.MULTILINE)
|
||||
if commentedMatch:
|
||||
self.currentPitch = int(commentedMatch.group(1))
|
||||
|
||||
except Exception:
|
||||
# If loading fails, we'll use default values
|
||||
pass
|
||||
|
||||
def save_settings_to_config(self):
|
||||
"""Save the current settings to the speech-dispatcher config file"""
|
||||
try:
|
||||
for configPath in get_writable_config_targets(self.configTargets):
|
||||
with open(configPath, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
newContent = update_speechd_config_content(
|
||||
content,
|
||||
self.currentRate,
|
||||
self.currentVolume,
|
||||
self.currentPitch,
|
||||
)
|
||||
|
||||
tempFile = f"/tmp/{os.path.basename(configPath)}.new"
|
||||
with open(tempFile, 'w') as f:
|
||||
f.write(newContent)
|
||||
|
||||
subprocess.run(
|
||||
["sudo", "mv", tempFile, configPath],
|
||||
check=True,
|
||||
)
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_current_value(self):
|
||||
"""Get the current value for the active mode"""
|
||||
if self.currentMode == 0: # Rate
|
||||
return self.currentRate
|
||||
elif self.currentMode == 1: # Volume
|
||||
return self.currentVolume
|
||||
else: # Pitch
|
||||
return self.currentPitch
|
||||
|
||||
def adjust_current_value(self, amount):
|
||||
"""Adjust the current value by the given amount"""
|
||||
if self.currentMode == 0: # Rate
|
||||
# Rate should be between -50 and 100
|
||||
newValue = max(-50, min(100, self.currentRate + amount))
|
||||
if newValue != self.currentRate:
|
||||
self.currentRate = newValue
|
||||
if self.speechClient:
|
||||
try:
|
||||
self.speechClient.set_rate(self.currentRate)
|
||||
self.speak(f"Speech rate {self.currentRate}")
|
||||
except Exception:
|
||||
pass
|
||||
elif self.currentMode == 1: # Volume
|
||||
# Volume should be between -100 and 100
|
||||
newValue = max(-100, min(100, self.currentVolume + amount))
|
||||
if newValue != self.currentVolume:
|
||||
self.currentVolume = newValue
|
||||
if self.speechClient:
|
||||
try:
|
||||
self.speechClient.set_volume(self.currentVolume)
|
||||
self.speak(f"Volume {self.currentVolume}")
|
||||
except Exception:
|
||||
pass
|
||||
else: # Pitch
|
||||
# Pitch should be between -100 and 100
|
||||
newValue = max(-100, min(100, self.currentPitch + amount))
|
||||
if newValue != self.currentPitch:
|
||||
self.currentPitch = newValue
|
||||
if self.speechClient:
|
||||
try:
|
||||
self.speechClient.set_pitch(self.currentPitch)
|
||||
self.speak(f"Pitch {self.currentPitch}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def speak(self, text, interrupt=True):
|
||||
"""Speak the given text with option to interrupt existing speech"""
|
||||
if self.speechClient is None:
|
||||
return
|
||||
|
||||
try:
|
||||
if interrupt:
|
||||
self.stop_speech()
|
||||
|
||||
self.speechClient.speak(text)
|
||||
except Exception:
|
||||
# If speech fails, try to reinitialize and try once more
|
||||
try:
|
||||
self.init_speech()
|
||||
if self.speechClient:
|
||||
self.speechClient.speak(text)
|
||||
except:
|
||||
# If reinitializing fails, just give up silently
|
||||
pass
|
||||
|
||||
def stop_speech(self):
|
||||
"""Stop any ongoing speech"""
|
||||
if self.speechClient is None:
|
||||
return
|
||||
|
||||
try:
|
||||
self.speechClient.cancel()
|
||||
except Exception:
|
||||
# If cancel fails, try to reinitialize
|
||||
self.init_speech()
|
||||
|
||||
def confirm_saved_settings(self):
|
||||
"""Restart speechd, announce success, and wait for confirmation."""
|
||||
subprocess.run(
|
||||
["sudo", "killall", "speech-dispatcher"],
|
||||
check=False,
|
||||
)
|
||||
time.sleep(1)
|
||||
self.init_speech()
|
||||
self.speak("Speech settings applied. Press Enter to continue.", interrupt=False)
|
||||
|
||||
while True:
|
||||
key = self.stdscr.getch()
|
||||
if key == curses.KEY_ENTER or key == 10 or key == 13:
|
||||
break
|
||||
|
||||
def draw_menu(self):
|
||||
"""Draw the menu on the screen"""
|
||||
self.stdscr.clear()
|
||||
h, w = self.stdscr.getmaxyx()
|
||||
|
||||
# Draw title
|
||||
title = f" {self.title} "
|
||||
x = max(0, w // 2 - len(title) // 2)
|
||||
self.stdscr.addstr(1, x, title, curses.A_BOLD)
|
||||
|
||||
# Draw help line
|
||||
helpText = "Up/Down: Adjust | Tab: Switch Mode | Enter: Save | Q/Esc: Quit"
|
||||
x = max(0, w // 2 - len(helpText) // 2)
|
||||
self.stdscr.addstr(3, x, helpText)
|
||||
|
||||
# Draw all current values
|
||||
currentMode = self.modes[self.currentMode]
|
||||
|
||||
# Rate display
|
||||
rateText = f"Rate: {self.currentRate}"
|
||||
attr = curses.A_REVERSE if self.currentMode == 0 else curses.A_NORMAL
|
||||
x = max(0, w // 2 - 30)
|
||||
self.stdscr.addstr(5, x, rateText, attr)
|
||||
|
||||
# Volume display
|
||||
volumeText = f"Volume: {self.currentVolume}"
|
||||
attr = curses.A_REVERSE if self.currentMode == 1 else curses.A_NORMAL
|
||||
x = max(0, w // 2 - 5)
|
||||
self.stdscr.addstr(5, x, volumeText, attr)
|
||||
|
||||
# Pitch display
|
||||
pitchText = f"Pitch: {self.currentPitch}"
|
||||
attr = curses.A_REVERSE if self.currentMode == 2 else curses.A_NORMAL
|
||||
x = max(0, w // 2 + 20)
|
||||
self.stdscr.addstr(5, x, pitchText, attr)
|
||||
|
||||
# Current mode indicator
|
||||
modeText = f"Current Mode: {currentMode}"
|
||||
x = max(0, w // 2 - len(modeText) // 2)
|
||||
self.stdscr.addstr(7, x, modeText, curses.A_BOLD)
|
||||
|
||||
# Draw visualization bar for current parameter
|
||||
barWidth = 50 # Width of the visualization bar
|
||||
barX = max(0, w // 2 - barWidth // 2)
|
||||
|
||||
# Get current value and range
|
||||
currentValue = self.get_current_value()
|
||||
if self.currentMode == 0: # Rate
|
||||
minVal, maxVal = -50, 100
|
||||
totalRange = 150
|
||||
normalizedValue = currentValue + 50
|
||||
else: # Volume or Pitch
|
||||
minVal, maxVal = -100, 100
|
||||
totalRange = 200
|
||||
normalizedValue = currentValue + 100
|
||||
|
||||
position = int((normalizedValue / totalRange) * barWidth)
|
||||
|
||||
# Draw the bar
|
||||
barY = 9
|
||||
self.stdscr.addstr(barY, barX, "┌" + "─" * barWidth + "┐")
|
||||
self.stdscr.addstr(barY + 1, barX, "│" + " " * barWidth + "│")
|
||||
self.stdscr.addstr(barY + 2, barX, "└" + "─" * barWidth + "┘")
|
||||
|
||||
# Draw the position marker
|
||||
if 0 <= position < barWidth:
|
||||
self.stdscr.addstr(barY + 1, barX + 1 + position, "█", curses.A_BOLD)
|
||||
|
||||
# Add labels for min and max
|
||||
self.stdscr.addstr(barY + 3, barX, str(minVal))
|
||||
maxLabel = str(maxVal)
|
||||
self.stdscr.addstr(barY + 3, barX + barWidth - len(maxLabel), maxLabel)
|
||||
|
||||
# Note about saving
|
||||
note = "Press Enter to save all settings to system config"
|
||||
x = max(0, w // 2 - len(note) // 2)
|
||||
self.stdscr.addstr(h - 3, x, note, curses.A_DIM)
|
||||
|
||||
# Warning about system config
|
||||
warning = "Note: Saving requires sudo privileges"
|
||||
x = max(0, w // 2 - len(warning) // 2)
|
||||
self.stdscr.addstr(h - 2, x, warning, curses.A_DIM)
|
||||
|
||||
self.stdscr.refresh()
|
||||
|
||||
def cleanup(self, fullCleanup=False):
|
||||
"""Clean up resources before exiting
|
||||
|
||||
Args:
|
||||
fullCleanup: If True, also close curses. Used when exiting.
|
||||
"""
|
||||
# Stop any speech
|
||||
self.stop_speech()
|
||||
|
||||
# Close speech client
|
||||
if self.speechClient:
|
||||
try:
|
||||
self.speechClient.close()
|
||||
except:
|
||||
pass
|
||||
self.speechClient = None
|
||||
|
||||
# Restore terminal settings if curses was initialized
|
||||
if fullCleanup and self.cursesInitialized:
|
||||
try:
|
||||
curses.nocbreak()
|
||||
self.stdscr.keypad(False)
|
||||
curses.echo()
|
||||
curses.endwin()
|
||||
except:
|
||||
# If there's an error, just try a simple endwin
|
||||
try:
|
||||
curses.endwin()
|
||||
except:
|
||||
pass # Last resort, just continue
|
||||
|
||||
def run(self):
|
||||
"""Run the menu system"""
|
||||
try:
|
||||
# Initialize curses
|
||||
self.stdscr = curses.initscr()
|
||||
self.cursesInitialized = True
|
||||
curses.noecho()
|
||||
curses.cbreak()
|
||||
self.stdscr.keypad(True)
|
||||
|
||||
# Initial draw
|
||||
self.draw_menu()
|
||||
|
||||
# Welcome message
|
||||
currentMode = self.modes[self.currentMode]
|
||||
self.speak(f"Speech configuration menu. Currently adjusting {currentMode}. Rate {self.currentRate}, Volume {self.currentVolume}, Pitch {self.currentPitch}.")
|
||||
|
||||
# Main loop
|
||||
while True:
|
||||
key = self.stdscr.getch()
|
||||
|
||||
# Stop any speech when a key is pressed
|
||||
self.stop_speech()
|
||||
|
||||
# Handle navigation
|
||||
if key == curses.KEY_UP:
|
||||
# Increase current value by 10
|
||||
self.adjust_current_value(10)
|
||||
self.draw_menu()
|
||||
|
||||
elif key == curses.KEY_DOWN:
|
||||
# Decrease current value by 10
|
||||
self.adjust_current_value(-10)
|
||||
self.draw_menu()
|
||||
|
||||
elif key == ord('\t') or key == 9: # Tab key
|
||||
# Switch to next mode
|
||||
self.currentMode = (self.currentMode + 1) % len(self.modes)
|
||||
currentMode = self.modes[self.currentMode]
|
||||
currentValue = self.get_current_value()
|
||||
self.speak(f"Switching to {currentMode}. Current value: {currentValue}")
|
||||
self.draw_menu()
|
||||
|
||||
elif key == curses.KEY_ENTER or key == 10 or key == 13: # Enter key
|
||||
# Save all settings
|
||||
self.speak("Saving speech settings to system configuration.")
|
||||
success = self.save_settings_to_config()
|
||||
if success:
|
||||
self.confirm_saved_settings()
|
||||
else:
|
||||
self.speak("Failed to save speech settings. You may need root privileges.")
|
||||
|
||||
break # Exit the loop after saving
|
||||
|
||||
elif key == 27 or key == ord('q') or key == ord('Q'): # Esc or Q
|
||||
self.speak("Speech settings discarded.")
|
||||
time.sleep(3)
|
||||
break
|
||||
|
||||
except Exception:
|
||||
# End curses in case of error
|
||||
if self.cursesInitialized:
|
||||
try:
|
||||
curses.endwin()
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
# Clean up - safe to call even if curses wasn't initialized
|
||||
self.cleanup(fullCleanup=True)
|
||||
|
||||
|
||||
# Run the menu
|
||||
if __name__ == "__main__":
|
||||
# Create the menu
|
||||
menu = SpeechRateMenu()
|
||||
|
||||
# Run the menu
|
||||
menu.run()
|
||||
Reference in New Issue
Block a user