diff --git a/home/stormux/.local/bin/apple_2e.py b/home/stormux/.local/bin/apple_2e.py index 0ebf75c..189dc99 100755 --- a/home/stormux/.local/bin/apple_2e.py +++ b/home/stormux/.local/bin/apple_2e.py @@ -8,7 +8,7 @@ import sys import time import curses import speechd # Python bindings for Speech Dispatcher -import configparser +from stormux_speech_settings import load_speech_settings, save_speech_settings class VoicedDiskMenu: def __init__(self, title="Apple 2e Disk Menu"): @@ -18,11 +18,6 @@ class VoicedDiskMenu: self.stdscr = None self.curses_initialized = False # Flag to track if curses has been initialized - # Config settings - self.config_dir = os.path.expanduser("~/.config/stormux") - self.config_file = os.path.join(self.config_dir, "apple2e_menu.conf") - self.config = configparser.ConfigParser() - # Default settings self.speech_rate = 0 # Normal speech rate (0 is default in speechd) @@ -47,39 +42,14 @@ class VoicedDiskMenu: # Fallback to None - the speak method will handle this def load_settings(self): - """Load settings from config file""" - # Create default settings if they don't exist - if not os.path.exists(self.config_file): - self.save_settings() - return - - try: - self.config.read(self.config_file) - - # Load speech settings - if 'Speech' in self.config: - self.speech_rate = self.config.getint('Speech', 'rate', fallback=0) - except Exception as e: - print(f"Error loading settings: {e}") - # If loading fails, we'll use default values + """Load speech settings from the shared Speech Dispatcher config.""" + self.speech_rate = load_speech_settings().rate def save_settings(self): - """Save settings to config file""" - # Ensure config directory exists - os.makedirs(self.config_dir, exist_ok=True) - - # Update config object - if 'Speech' not in self.config: - self.config['Speech'] = {} - - self.config['Speech']['rate'] = str(self.speech_rate) - - # Write to file - try: - with open(self.config_file, 'w') as f: - self.config.write(f) - except Exception as e: - print(f"Error saving settings: {e}") + """Save speech settings to the shared Speech Dispatcher config.""" + speechSettings = load_speech_settings() + speechSettings.rate = self.speech_rate + save_speech_settings(speechSettings) def increase_speech_rate(self): """Increase speech rate""" @@ -87,7 +57,7 @@ class VoicedDiskMenu: if self.speech_client: try: self.speech_client.set_rate(self.speech_rate) - self.speak(f"Speech rate: {self.speech_rate}") + self.speak(f"System speech rate: {self.speech_rate}") except Exception as e: print(f"Error adjusting speech rate: {e}") @@ -100,7 +70,7 @@ class VoicedDiskMenu: if self.speech_client: try: self.speech_client.set_rate(self.speech_rate) - self.speak(f"Speech rate: {self.speech_rate}") + self.speak(f"System speech rate: {self.speech_rate}") except Exception as e: print(f"Error adjusting speech rate: {e}") @@ -173,8 +143,8 @@ class VoicedDiskMenu: Down arrow: Next disk. Enter: Launch selected disk. H key: Hear these instructions again. - Left bracket: Decrease speech rate. - Right bracket: Increase speech rate. + Left bracket: Decrease system speech rate. + Right bracket: Increase system speech rate. Escape or Q: Exit the menu. Any key will interrupt speech. """ @@ -230,7 +200,7 @@ class VoicedDiskMenu: self.stdscr.addstr(y, x, text, attr) # Draw speech rate indicator - rateText = f"Speech Rate: {self.speech_rate}" + rateText = f"System Speech Rate: {self.speech_rate}" self.stdscr.addstr(h-2, 2, rateText) self.stdscr.refresh() diff --git a/home/stormux/.local/bin/audio_manager.py b/home/stormux/.local/bin/audio_manager.py index 8af8331..1dd1ff8 100755 --- a/home/stormux/.local/bin/audio_manager.py +++ b/home/stormux/.local/bin/audio_manager.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import curses -import configparser import json import os import re @@ -10,6 +9,7 @@ import subprocess import sys import time from dataclasses import dataclass +from stormux_speech_settings import load_speech_settings try: import simpleaudio as sa @@ -257,9 +257,6 @@ class AudioManagerApp: self.backend = AudioBackend() self.feedback = AudioFeedback() self.speech_client = None - self.config_dir = os.path.expanduser("~/.config/stormux") - self.config_file = os.path.join(self.config_dir, "game_launcher.conf") - self.config = configparser.ConfigParser() self.speech_rate = 0 self.speech_pitch = 0 self.stdscr = None @@ -285,16 +282,9 @@ class AudioManagerApp: self.load_speech_settings() def load_speech_settings(self): - if not os.path.exists(self.config_file): - return - - try: - self.config.read(self.config_file) - if "Speech" in self.config: - self.speech_rate = self.config.getint("Speech", "rate", fallback=0) - self.speech_pitch = self.config.getint("Speech", "pitch", fallback=0) - except Exception: - pass + speech_settings = load_speech_settings() + self.speech_rate = speech_settings.rate + self.speech_pitch = speech_settings.pitch def init_speech(self): if speechd is None: diff --git a/home/stormux/.local/bin/diagnostics.sh b/home/stormux/.local/bin/diagnostics.sh index cfca14f..75d44a7 100755 --- a/home/stormux/.local/bin/diagnostics.sh +++ b/home/stormux/.local/bin/diagnostics.sh @@ -28,7 +28,7 @@ fi # File system trim rootPartition=$(findmnt / -o SOURCE -n) -if [[ $(lsblk -b --discard $rootPartition | awk 'NR==2 { print $NF }') -gt 0 ]]; then +if [[ $(lsblk -b --discard "$rootPartition" | awk 'NR==2 { print $NF }') -gt 0 ]]; then if ! systemctl is-enabled fstrim.timer ; then sudo systemctl enable fstrim.timer fi diff --git a/home/stormux/.local/bin/game_launcher.py b/home/stormux/.local/bin/game_launcher.py index 066d87c..ba54b70 100755 --- a/home/stormux/.local/bin/game_launcher.py +++ b/home/stormux/.local/bin/game_launcher.py @@ -20,6 +20,7 @@ import json import shlex from dataclasses import dataclass from pathlib import Path +from stormux_speech_settings import load_speech_settings, save_speech_settings INSTALL_GAME_RUNNER = Path("/home/stormux/.local/.functions/install_game.sh") @@ -219,6 +220,10 @@ class VoicedMenu: def load_settings(self): """Load settings from config file""" + speechSettings = load_speech_settings() + self.speechRate = speechSettings.rate + self.speechPitch = speechSettings.pitch + # Create default settings if they don't exist if not os.path.exists(self.configFile): self.save_settings() @@ -227,11 +232,6 @@ class VoicedMenu: try: self.config.read(self.configFile) - # Load speech settings - if 'Speech' in self.config: - self.speechRate = self.config.getint('Speech', 'rate', fallback=0) - self.speechPitch = self.config.getint('Speech', 'pitch', fallback=0) - # Load volume settings if 'Volume' in self.config: self.volume = self.config.getint('Volume', 'level', fallback=50) @@ -240,17 +240,10 @@ class VoicedMenu: # If loading fails, we'll use default values def save_settings(self): - """Save settings to config file""" + """Save menu-local settings to config file.""" # Ensure config directory exists os.makedirs(self.configDir, exist_ok=True) - # Update config object - if 'Speech' not in self.config: - self.config['Speech'] = {} - - self.config['Speech']['rate'] = str(self.speechRate) - self.config['Speech']['pitch'] = str(self.speechPitch) - # Save volume settings if 'Volume' not in self.config: self.config['Volume'] = {} @@ -264,18 +257,25 @@ class VoicedMenu: except Exception as e: print(f"Error saving settings: {e}") + def save_speech_settings(self): + """Save speech settings to the shared Speech Dispatcher config.""" + speechSettings = load_speech_settings() + speechSettings.rate = self.speechRate + speechSettings.pitch = self.speechPitch + save_speech_settings(speechSettings) + def increase_speech_rate(self): """Increase speech rate""" self.speechRate = min(100, self.speechRate + 10) # Max is 100 if self.speechClient: try: self.speechClient.set_rate(self.speechRate) - self.speak(f"Speech rate: {self.speechRate}") + self.speak(f"System speech rate: {self.speechRate}") except Exception as e: print(f"Error adjusting speech rate: {e}") # Save the new setting - self.save_settings() + self.save_speech_settings() def decrease_speech_rate(self): """Decrease speech rate""" @@ -283,12 +283,12 @@ class VoicedMenu: if self.speechClient: try: self.speechClient.set_rate(self.speechRate) - self.speak(f"Speech rate: {self.speechRate}") + self.speak(f"System speech rate: {self.speechRate}") except Exception as e: print(f"Error adjusting speech rate: {e}") # Save the new setting - self.save_settings() + self.save_speech_settings() def increase_speech_pitch(self): """Increase speech pitch""" @@ -296,12 +296,12 @@ class VoicedMenu: if self.speechClient: try: self.speechClient.set_pitch(self.speechPitch) - self.speak(f"Speech pitch: {self.speechPitch}") + self.speak(f"System speech pitch: {self.speechPitch}") except Exception as e: print(f"Error adjusting speech pitch: {e}") # Save the new setting - self.save_settings() + self.save_speech_settings() def decrease_speech_pitch(self): """Decrease speech pitch""" @@ -309,12 +309,12 @@ class VoicedMenu: if self.speechClient: try: self.speechClient.set_pitch(self.speechPitch) - self.speak(f"Speech pitch: {self.speechPitch}") + self.speak(f"System speech pitch: {self.speechPitch}") except Exception as e: print(f"Error adjusting speech pitch: {e}") # Save the new setting - self.save_settings() + self.save_speech_settings() def get_current_volume(self): """Get the current system volume percentage""" @@ -846,8 +846,8 @@ class VoicedMenu: # Now clean up resources self.cleanup() - # Try to kill speech-dispatcher if needed - subprocess.run(["sudo", "killall", "speech-dispatcher"], check=False) + # Try to kill the user's speech-dispatcher if needed + subprocess.run(["killall", "speech-dispatcher"], check=False) # Restart the application os.execv(sys.argv[0], sys.argv) @@ -1144,10 +1144,10 @@ class VoicedMenu: Enter: Launch selected item. Control H: Hear these instructions again. Control B: Report battery status. - Left bracket: Decrease speech rate. - Right bracket: Increase speech rate. - Left brace: Decrease speech pitch. - Right brace: Increase speech pitch. + Left bracket: Decrease system speech rate. + Right bracket: Increase system speech rate. + Left brace: Decrease system speech pitch. + Right brace: Increase system speech pitch. 9 key: Decrease volume. 0 key: Increase volume. Escape: Go back from submenus or refresh the main menu. @@ -1198,11 +1198,11 @@ class VoicedMenu: self.stdscr.addstr(y, x, text, attr) # Draw speech rate indicator - rateText = f"Speech Rate: {self.speechRate}" + rateText = f"System Speech Rate: {self.speechRate}" self.stdscr.addstr(h-2, 2, rateText) # Draw speech pitch indicator - pitchText = f"Pitch: {self.speechPitch}" + pitchText = f"System Pitch: {self.speechPitch}" pitchX = max(0, w // 2 - len(pitchText) // 2) self.stdscr.addstr(h-2, pitchX, pitchText) diff --git a/home/stormux/.local/bin/music_player.py b/home/stormux/.local/bin/music_player.py index 8b7cf14..905f78f 100755 --- a/home/stormux/.local/bin/music_player.py +++ b/home/stormux/.local/bin/music_player.py @@ -13,6 +13,7 @@ import configparser import pathlib import re import random +from stormux_speech_settings import load_speech_settings, save_speech_settings class VoicedMusicPlayer: def __init__(self, title="Stormux Music Player"): @@ -55,15 +56,14 @@ class VoicedMusicPlayer: print(f"Could not initialize speech: {e}") def load_settings(self): + self.speechRate = load_speech_settings().rate + if not os.path.exists(self.configFile): self.save_settings() return try: self.config.read(self.configFile) - - if 'Speech' in self.config: - self.speechRate = self.config.getint('Speech', 'rate', fallback=0) if 'Player' in self.config: self.randomize = self.config.getboolean('Player', 'randomize', fallback=False) @@ -73,13 +73,9 @@ class VoicedMusicPlayer: def save_settings(self): os.makedirs(self.configDir, exist_ok=True) - if 'Speech' not in self.config: - self.config['Speech'] = {} - if 'Player' not in self.config: self.config['Player'] = {} - self.config['Speech']['rate'] = str(self.speechRate) self.config['Player']['randomize'] = str(self.randomize) try: @@ -88,6 +84,11 @@ class VoicedMusicPlayer: except Exception as e: print(f"Error saving settings: {e}") + def save_speech_settings(self): + speechSettings = load_speech_settings() + speechSettings.rate = self.speechRate + save_speech_settings(speechSettings) + def toggle_randomize(self): self.randomize = not self.randomize self.save_settings() @@ -101,22 +102,22 @@ class VoicedMusicPlayer: if self.speechClient: try: self.speechClient.set_rate(self.speechRate) - self.speak(f"Speech rate: {self.speechRate}") + self.speak(f"System speech rate: {self.speechRate}") except Exception as e: print(f"Error adjusting speech rate: {e}") - self.save_settings() + self.save_speech_settings() def decrease_speech_rate(self): self.speechRate = max(-100, self.speechRate - 10) if self.speechClient: try: self.speechClient.set_rate(self.speechRate) - self.speak(f"Speech rate: {self.speechRate}") + self.speak(f"System speech rate: {self.speechRate}") except Exception as e: print(f"Error adjusting speech rate: {e}") - self.save_settings() + self.save_speech_settings() def add_section(self, sectionName): if sectionName not in self.menuSections: @@ -337,8 +338,8 @@ class VoicedMusicPlayer: Backspace: Go back to previous view. R key: Toggle random playback. H key: Hear these instructions again. - Left bracket: Decrease speech rate. - Right bracket: Increase speech rate. + Left bracket: Decrease system speech rate. + Right bracket: Increase system speech rate. Escape or Q: Exit the menu. Any key will interrupt speech. """ @@ -352,7 +353,7 @@ class VoicedMusicPlayer: x = max(0, w // 2 - len(title) // 2) self.stdscr.addstr(1, x, title, curses.A_BOLD) - helpText = "Navigate | Enter: Select | R: Random | H: Help | [ ] Rate | Q/Esc: Quit" + helpText = "Navigate | Enter: Select | R: Random | H: Help | [ ] System Rate | Q/Esc: Quit" x = max(0, w // 2 - len(helpText) // 2) self.stdscr.addstr(3, x, helpText) @@ -395,7 +396,7 @@ class VoicedMusicPlayer: x = max(0, w // 2 - len(text) // 2) self.stdscr.addstr(y, x, text, attr) - rateText = f"Speech Rate: {self.speechRate}" + rateText = f"System Speech Rate: {self.speechRate}" self.stdscr.addstr(h-2, 2, rateText) self.stdscr.refresh() diff --git a/home/stormux/.local/bin/rom_launcher.py b/home/stormux/.local/bin/rom_launcher.py index 6caf87e..b1b6a25 100755 --- a/home/stormux/.local/bin/rom_launcher.py +++ b/home/stormux/.local/bin/rom_launcher.py @@ -11,9 +11,9 @@ import signal import curses import subprocess import speechd # Python bindings for Speech Dispatcher -import configparser import pathlib import re +from stormux_speech_settings import load_speech_settings, save_speech_settings class VoicedMenu: def __init__(self, title="Stormux Game Menu"): @@ -26,11 +26,6 @@ class VoicedMenu: self.curses_initialized = False # Flag to track if curses has been initialized self.has_items = False # Flag to track if any section has items - # Config settings - self.configDir = os.path.expanduser("~/.config/stormux") - self.configFile = os.path.join(self.configDir, "game_launcher.conf") - self.config = configparser.ConfigParser() - # Default settings self.speechRate = 0 # Normal speech rate (0 is default in speechd) @@ -56,39 +51,14 @@ class VoicedMenu: # Fallback to None - the speak method will handle this def load_settings(self): - """Load settings from config file""" - # Create default settings if they don't exist - if not os.path.exists(self.configFile): - self.save_settings() - return - - try: - self.config.read(self.configFile) - - # Load speech settings - if 'Speech' in self.config: - self.speechRate = self.config.getint('Speech', 'rate', fallback=0) - except Exception as e: - print(f"Error loading settings: {e}") - # If loading fails, we'll use default values + """Load speech settings from the shared Speech Dispatcher config.""" + self.speechRate = load_speech_settings().rate def save_settings(self): - """Save settings to config file""" - # Ensure config directory exists - os.makedirs(self.configDir, exist_ok=True) - - # Update config object - if 'Speech' not in self.config: - self.config['Speech'] = {} - - self.config['Speech']['rate'] = str(self.speechRate) - - # Write to file - try: - with open(self.configFile, 'w') as f: - self.config.write(f) - except Exception as e: - print(f"Error saving settings: {e}") + """Save speech settings to the shared Speech Dispatcher config.""" + speechSettings = load_speech_settings() + speechSettings.rate = self.speechRate + save_speech_settings(speechSettings) def increase_speech_rate(self): """Increase speech rate""" @@ -96,7 +66,7 @@ class VoicedMenu: if self.speechClient: try: self.speechClient.set_rate(self.speechRate) - self.speak(f"Speech rate: {self.speechRate}") + self.speak(f"System speech rate: {self.speechRate}") except Exception as e: print(f"Error adjusting speech rate: {e}") @@ -109,7 +79,7 @@ class VoicedMenu: if self.speechClient: try: self.speechClient.set_rate(self.speechRate) - self.speak(f"Speech rate: {self.speechRate}") + self.speak(f"System speech rate: {self.speechRate}") except Exception as e: print(f"Error adjusting speech rate: {e}") @@ -226,8 +196,8 @@ class VoicedMenu: Right arrow: Next section. Enter: Launch selected item. H key: Hear these instructions again. - Left bracket: Decrease speech rate. - Right bracket: Increase speech rate. + Left bracket: Decrease system speech rate. + Right bracket: Increase system speech rate. Escape or Q: Exit the menu. Any key will interrupt speech. """ @@ -280,7 +250,7 @@ class VoicedMenu: self.stdscr.addstr(y, x, text, attr) # Draw speech rate indicator - rateText = f"Speech Rate: {self.speechRate}" + rateText = f"System Speech Rate: {self.speechRate}" self.stdscr.addstr(h-2, 2, rateText) self.stdscr.refresh() diff --git a/home/stormux/.local/bin/set-timezone.sh b/home/stormux/.local/bin/set-timezone.sh index 04d2153..9081d2b 100755 --- a/home/stormux/.local/bin/set-timezone.sh +++ b/home/stormux/.local/bin/set-timezone.sh @@ -8,12 +8,16 @@ export DIALOGOPTS='--insecure --no-lines --visit-items' set_timezone() { # Get the list of timezones mapfile -t regions < <(timedatectl --no-pager list-timezones | cut -d '/' -f1 | sort -u) + regionOptions=() + for regionName in "${regions[@]}"; do + regionOptions+=("$regionName" "$regionName") + done # Use the same text twice here and just hide the tag field. region=$(dialog --backtitle "Please select your Region" \ --no-tags \ --menu "Use up and down arrows or page-up and page-down to navigate the list, and press 'Enter' to make your selection." 0 0 0 \ - $(for i in ${regions[@]} ; do echo "$i";echo "$i";done) --stdout) + "${regionOptions[@]}" --stdout) if [[ -z "$region" ]]; then echo "No region selected" @@ -21,12 +25,16 @@ set_timezone() { fi mapfile -t cities < <(timedatectl --no-pager list-timezones | grep "$region" | cut -d '/' -f2 | sort -u) + cityOptions=() + for cityName in "${cities[@]}"; do + cityOptions+=("$cityName" "$cityName") + done # Use the same text twice here and just hide the tag field. city=$(dialog --backtitle "Please select a city near you" \ --no-tags \ --menu "Use up and down arrow or page-up and page-down to navigate the list." 0 0 10 \ - $(for i in ${cities[@]} ; do echo "$i";echo "$i";done) --stdout) + "${cityOptions[@]}" --stdout) if [[ -z "$city" ]]; then echo "No city selected" @@ -37,7 +45,7 @@ set_timezone() { if [[ -f /etc/localtime ]]; then rm /etc/localtime fi - ln -sf /usr/share/zoneinfo/${region}/${city} /etc/localtime + ln -sf "/usr/share/zoneinfo/${region}/${city}" /etc/localtime timedatectl set-ntp true echo "Timezone set to ${region}/${city}" @@ -49,4 +57,4 @@ if [[ $EUID -ne 0 ]]; then exit 1 fi -set_timezone \ No newline at end of file +set_timezone diff --git a/home/stormux/.local/bin/set-voice.py b/home/stormux/.local/bin/set-voice.py index 15bc8fb..0ca8e29 100755 --- a/home/stormux/.local/bin/set-voice.py +++ b/home/stormux/.local/bin/set-voice.py @@ -10,6 +10,37 @@ import curses import speechd import subprocess import configparser +import re +from pathlib import Path + + +SPEECHD_CONFIG = Path.home() / ".local" / "etc" / "speech-dispatcher" / "speechd.conf" + + +def update_default_module_content(content, module): + """Return speechd.conf content with DefaultModule set to module.""" + if re.search(r'^\s*DefaultModule\s+', content, re.MULTILINE): + return re.sub( + r'^(\s*)DefaultModule\s+\S+', + f'\\1DefaultModule {module}', + content, + flags=re.MULTILINE, + ) + + return re.sub( + r'^(\s*)#\s*DefaultModule\s+\S*', + f'\\1DefaultModule {module}', + content, + flags=re.MULTILINE, + ) + + +def set_default_module_in_config(configPath, module): + """Update DefaultModule without replacing a symlink at configPath.""" + configPath = Path(configPath) + content = configPath.read_text() + configPath.write_text(update_default_module_content(content, module)) + return True class VoiceSelectionMenu: def __init__(self, title="Speech Dispatcher Voice Selection"): @@ -146,42 +177,12 @@ class VoiceSelectionMenu: try: # Clean up before executing system commands self.cleanup(full_cleanup=False) - - # Read the current config file - import re - with open('/etc/speech-dispatcher/speechd.conf', 'r') as f: - content = f.read() - - # Check if DefaultModule is already uncommented - if re.search(r'^\s*DefaultModule\s+', content, re.MULTILINE): - # Replace existing DefaultModule line - new_content = re.sub( - r'^(\s*)DefaultModule\s+\S+', - f'\\1DefaultModule {module}', - content, - flags=re.MULTILINE - ) - else: - # Uncomment and set DefaultModule line - new_content = re.sub( - r'^(\s*)#\s*DefaultModule\s+\S*', - f'\\1DefaultModule {module}', - content, - flags=re.MULTILINE - ) - - # Write to a temporary file - temp_file = "/tmp/speechd.conf.new" - with open(temp_file, 'w') as f: - f.write(new_content) - - # Use sudo to move the file to the correct location - subprocess.run(f"sudo mv {temp_file} /etc/speech-dispatcher/speechd.conf", shell=True, check=True) + set_default_module_in_config(SPEECHD_CONFIG, module) # Restart speech-dispatcher more thoroughly - subprocess.run("sudo systemctl restart speech-dispatcher", shell=True, check=False) + subprocess.run(["systemctl", "--user", "restart", "speech-dispatcher.socket"], check=False) # Also kill any remaining processes - subprocess.run("sudo killall speech-dispatcher", shell=True, check=False) + subprocess.run(["killall", "speech-dispatcher"], check=False) # Re-initialize speech after changes time.sleep(2) # Give more time for the service to restart diff --git a/home/stormux/.local/bin/speechd_rate.py b/home/stormux/.local/bin/speechd_rate.py index 50c0ecd..cf7b39a 100755 --- a/home/stormux/.local/bin/speechd_rate.py +++ b/home/stormux/.local/bin/speechd_rate.py @@ -3,51 +3,28 @@ # 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 +from stormux_speech_settings import ( + SPEECHD_CONFIG, + FEX_SPEECHD_CONFIG, + SpeechSettings, + load_speech_settings, + save_speech_settings, + update_speechd_config_content as update_shared_speechd_config_content, + write_speechd_config, +) + 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), + return update_shared_speechd_config_content( + content, + SpeechSettings(rate=rate, volume=volume, pitch=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"): @@ -59,11 +36,8 @@ class SpeechRateMenu: 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"), - ] + self.configFile = str(SPEECHD_CONFIG) + self.configTargets = [SPEECHD_CONFIG, FEX_SPEECHD_CONFIG] # Load current settings from config FIRST self.load_current_settings() @@ -90,35 +64,10 @@ class SpeechRateMenu: 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)) + settings = load_speech_settings(self.configFile) + self.currentRate = settings.rate + self.currentVolume = settings.volume + self.currentPitch = settings.pitch except Exception: # If loading fails, we'll use default values @@ -127,26 +76,14 @@ class SpeechRateMenu: 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, - ) - + save_speech_settings( + SpeechSettings( + rate=self.currentRate, + volume=self.currentVolume, + pitch=self.currentPitch, + ), + self.configTargets, + ) return True except Exception: return False @@ -230,7 +167,7 @@ class SpeechRateMenu: def confirm_saved_settings(self): """Restart speechd, announce success, and wait for confirmation.""" subprocess.run( - ["sudo", "killall", "speech-dispatcher"], + ["killall", "speech-dispatcher"], check=False, ) time.sleep(1) diff --git a/home/stormux/.local/bin/stormux_speech_settings.py b/home/stormux/.local/bin/stormux_speech_settings.py new file mode 100644 index 0000000..21b062f --- /dev/null +++ b/home/stormux/.local/bin/stormux_speech_settings.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 + +import re +from dataclasses import dataclass +from pathlib import Path + + +SPEECHD_CONFIG = Path.home() / ".local" / "etc" / "speech-dispatcher" / "speechd.conf" +FEX_SPEECHD_CONFIG = ( + Path.home() + / ".fex-emu" + / "RootFS" + / "ArchLinux" + / "etc" + / "speech-dispatcher" + / "speechd.conf" +) + + +@dataclass +class SpeechSettings: + rate: int = 0 + volume: int = 100 + pitch: int = 0 + + +def _read_int_setting(content, settingName, fallback): + activeMatch = re.search(rf'^\s*{settingName}\s+(-?\d+)', content, re.MULTILINE) + if activeMatch: + return int(activeMatch.group(1)) + + commentedMatch = re.search(rf'^\s*#\s*{settingName}\s+(-?\d+)', content, re.MULTILINE) + if commentedMatch: + return int(commentedMatch.group(1)) + + return fallback + + +def load_speech_settings(configPath=None): + """Load speech-dispatcher default rate, volume, and pitch.""" + if configPath is None: + configPath = SPEECHD_CONFIG + + try: + content = Path(configPath).read_text() + except OSError: + return SpeechSettings() + + return SpeechSettings( + rate=_read_int_setting(content, "DefaultRate", 0), + volume=_read_int_setting(content, "DefaultVolume", 100), + pitch=_read_int_setting(content, "DefaultPitch", 0), + ) + + +def update_speechd_config_content(content, settings): + """Return speechd.conf content with updated default voice parameters.""" + newContent = content + + replacements = ( + ("DefaultRate", settings.rate), + ("DefaultVolume", settings.volume), + ("DefaultPitch", settings.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 write_speechd_config(configPath, updateContent): + """Update a speechd.conf file without replacing a symlink at configPath.""" + configPath = Path(configPath) + content = configPath.read_text() + configPath.write_text(updateContent(content)) + return True + + +def get_existing_config_targets(configTargets=None): + """Return speech-dispatcher config targets that currently exist.""" + if configTargets is None: + configTargets = (SPEECHD_CONFIG, FEX_SPEECHD_CONFIG) + + return [Path(configPath) for configPath in configTargets if Path(configPath).exists()] + + +def save_speech_settings(settings, configTargets=None): + """Persist speech settings to home speechd.conf and existing optional targets.""" + for configPath in get_existing_config_targets(configTargets): + write_speechd_config( + configPath, + lambda content: update_speechd_config_content(content, settings), + ) + return True diff --git a/home/stormux/.local/files/clipboard_translator.sh b/home/stormux/.local/files/clipboard_translator.sh index 7e1675b..0a77c9e 100755 --- a/home/stormux/.local/files/clipboard_translator.sh +++ b/home/stormux/.local/files/clipboard_translator.sh @@ -5,15 +5,13 @@ # The next line has been commented because if there's nothing in clipboard the script breaks. # set -Eeuo pipefail # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/#:~:text=set%20%2Du,is%20often%20highly%20desirable%20behavior. -shopt -s expand_aliases - if [[ $# -ne 2 ]]; then echo "Usage: $0 \"application name\" \"file name\"." exit 1 fi # Wait for the application to start -while ! pgrep -u "$USER" ^$1 &> /dev/null ; do +while ! pgrep -u "$USER" "^$1" &> /dev/null ; do sleep 0.05 done @@ -56,7 +54,7 @@ insert_database() { } # Read so long as the application is running -while pgrep -u "$USER" ^$1 &> /dev/null ; do +while pgrep -u "$USER" "^$1" &> /dev/null ; do sleep 0.05 text="$(xclip -d "${DISPLAY:-:0}" -selection clipboard -o 2> /dev/null)" if [[ -f ~/.agmsilent ]]; then @@ -73,12 +71,19 @@ while pgrep -u "$USER" ^$1 &> /dev/null ; do # https://en.wikipedia.org/wiki/Unicode_equivalence#Combining_and_precomposed_characters # https://www.effectiveperlprogramming.com/2011/09/normalize-your-perl-source/ - alias nfc="perl -MUnicode::Normalize -CS -ne 'print NFC(\$_)'" # composed characters + nfc() { + perl -MUnicode::Normalize -CS -ne 'print NFC($_)' + } # Normalize different unicode space characters to the same space # https://stackoverflow.com/a/43640405 - alias normalize_spaces="perl -CSDA -plE 's/[^\\S\\t]/ /g'" - alias normalize_unicode="normalize_spaces | nfc" + normalize_spaces() { + perl -CSDA -plE 's/[^\S\t]/ /g' + } + + normalize_unicode() { + normalize_spaces | nfc + } # Normalize text normalized_text="$(echo "$text" | normalize_unicode)" diff --git a/home/stormux/.local/files/speak_window_title.sh b/home/stormux/.local/files/speak_window_title.sh index eb73553..d869a60 100755 --- a/home/stormux/.local/files/speak_window_title.sh +++ b/home/stormux/.local/files/speak_window_title.sh @@ -3,18 +3,18 @@ # https://bbs.archlinux.org/viewtopic.php?id=117031 # Wait for the application to start -while ! pgrep -u "$USER" ^$1 &> /dev/null ; do +while ! pgrep -u "$USER" "^$1" &> /dev/null ; do sleep 0.05 done # Read so long as the application is running -while pgrep -u "$USER" ^$1 &> /dev/null ; do +while pgrep -u "$USER" "^$1" &> /dev/null ; do sleep 0.05 if [[ -f ~/.agmsilent ]]; then continue fi wnd_focus=$(xdotool getwindowfocus) - wnd_title=$(xprop -id $wnd_focus WM_NAME) + wnd_title=$(xprop -id "$wnd_focus" WM_NAME) lookfor='"(.*)"' if [[ "$wnd_title" =~ $lookfor ]]; then diff --git a/home/stormux/.local/upload_server/uploader.py b/home/stormux/.local/upload_server/uploader.py index 2ad3d13..90be4f8 100755 --- a/home/stormux/.local/upload_server/uploader.py +++ b/home/stormux/.local/upload_server/uploader.py @@ -13,6 +13,7 @@ import threading import termios import tty import select +import tempfile from threading import Timer from werkzeug.utils import secure_filename @@ -391,6 +392,15 @@ def is_voxin_package(filename): """Check if the filename starts with 'voxin-' and ends with '.tgz'.""" return filename.lower().startswith('voxin-') and filename.lower().endswith('.tgz') + +def find_voxin_installer(extract_dir): + """Return the Voxin installer path from an extracted package, if present.""" + for root, dirs, files in os.walk(extract_dir): + if 'voxin-installer.sh' in files: + return os.path.join(root, 'voxin-installer.sh') + return None + + # Add function to handle voxin installation def handle_voxin_package(file_path): """ @@ -401,26 +411,18 @@ def handle_voxin_package(file_path): Returns: dict with status and message """ + extract_dir = None try: filename = os.path.basename(file_path) logger.info(f"Starting voxin package processing: {filename}") - extract_dir = os.path.join('/tmp', 'voxin_extract') - - # Create fresh extraction directory - if os.path.exists(extract_dir): - shutil.rmtree(extract_dir) - os.makedirs(extract_dir) + extract_dir = tempfile.mkdtemp(prefix='voxin_extract_') # Extract the tarball logger.info(f"Extracting voxin package: {filename}") subprocess.run(['tar', 'xf', file_path, '-C', extract_dir], check=True) # Find the installer script - installer_path = None - for root, dirs, files in os.walk(extract_dir): - if 'voxin-installer.sh' in files: - installer_path = os.path.join(root, 'voxin-installer.sh') - break + installer_path = find_voxin_installer(extract_dir) if not installer_path: logger.error(f"Voxin installer script not found in package: {filename}") @@ -432,18 +434,11 @@ def handle_voxin_package(file_path): # Make the installer executable os.chmod(installer_path, 0o755) - # Run the installer with -l option for root's speech-dispatcher + # Run the installer with -l option. /etc/speech-dispatcher/speechd.conf + # is a symlink into /home/stormux/.local/etc on Stormux images. logger.info(f"Running voxin installer for: {filename}") install_result = subprocess.run( - ['sudo', installer_path, '-l'], - capture_output=True, - text=True, - timeout=180 # 3 minute timeout - ) - - # Run the installer with -l option for RootFS's speech-dispatcher - install_result = subprocess.run( - ['sudo', installer_path, '-l'], + ['sudo', str(installer_path), '-l'], capture_output=True, text=True, timeout=180 # 3 minute timeout @@ -472,9 +467,6 @@ def handle_voxin_package(file_path): text=True ) - # Clean up extraction directory - shutil.rmtree(extract_dir) - logger.info(f"Voxin package installation completed successfully: {filename}") return { 'success': True, @@ -496,6 +488,9 @@ def handle_voxin_package(file_path): 'message': 'Voice installation error.', 'details': str(e) } + finally: + if extract_dir and os.path.exists(extract_dir): + shutil.rmtree(extract_dir) @app.route('/classify', methods=['POST']) def classify_files():