diff --git a/home/stormux/Documents/change_log.md b/home/stormux/Documents/change_log.md index 7cfd958..eddc919 100644 --- a/home/stormux/Documents/change_log.md +++ b/home/stormux/Documents/change_log.md @@ -5,6 +5,8 @@ Dates are given for the image. All items listed are available for the listed ima ## September 1, 2025 +- Hopefully fix a bug where setting default voice would sometimes not save across reboots. +- Add pitch and volume parameters to the rate script - Initial battery monitoring setup - Make image size for x86_64 slightly smaller (whew, scary!) - Remove safety check for install to disk diff --git a/usr/local/bin/game_launcher.py b/usr/local/bin/game_launcher.py index 04dc082..7ad7992 100755 --- a/usr/local/bin/game_launcher.py +++ b/usr/local/bin/game_launcher.py @@ -1019,7 +1019,7 @@ if __name__ == "__main__": menu.add_item("System", "Internet Configuration", "GAME=\"Network Configuration\" /home/stormux/.clirc") menu.add_item("System", "Enable Screen", lambda: menu.toggle_screen("screen")) menu.add_item("System", "Disable Screen", lambda: menu.toggle_screen("headless")) - menu.add_item("System", "Set System Default Speech Rate", "/usr/local/bin/speechd_rate.py") + menu.add_item("System", "Set System Speech Settings", "/usr/local/bin/speechd_rate.py") menu.add_item("System", "Set Default Voice", "/usr/local/bin/set-voice.py") menu.add_item("System", "Set Timezone", "GAME='Set Timezone' /home/stormux/.clirc") menu.add_item("System", "Upload Files", "/home/stormux/.local/upload_server/uploader.py") diff --git a/usr/local/bin/set-voice.py b/usr/local/bin/set-voice.py index 0651045..15bc8fb 100755 --- a/usr/local/bin/set-voice.py +++ b/usr/local/bin/set-voice.py @@ -147,19 +147,48 @@ class VoiceSelectionMenu: # Clean up before executing system commands self.cleanup(full_cleanup=False) - # Use sed to update the DefaultModule in speechd.conf - sed_cmd = f"sudo sed -i '/^\\s*#\\?\\s*DefaultModule\\s\\+/c\\DefaultModule {module}' /etc/speech-dispatcher/speechd.conf" - subprocess.run(sed_cmd, shell=True, check=True) + # Read the current config file + import re + with open('/etc/speech-dispatcher/speechd.conf', 'r') as f: + content = f.read() - # Restart speech-dispatcher + # 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) + + # Restart speech-dispatcher more thoroughly + subprocess.run("sudo systemctl restart speech-dispatcher", shell=True, check=False) + # Also kill any remaining processes subprocess.run("sudo killall speech-dispatcher", shell=True, check=False) # Re-initialize speech after changes - time.sleep(1) # Give a moment for the service to restart + time.sleep(2) # Give more time for the service to restart self.init_speech() # Notify the user that the change is complete - should not be interrupted - self.speak(f"The {module} module is now being used for this system.", interrupt=False) + self.speak(f"The {module} module is now set as the default voice for this system.", interrupt=False) # Return to the menu after speech finishes # No sleep here - the next UI repaint doesn't depend on speech finishing diff --git a/usr/local/bin/speechd_rate.py b/usr/local/bin/speechd_rate.py index ddf32f0..f62d17d 100755 --- a/usr/local/bin/speechd_rate.py +++ b/usr/local/bin/speechd_rate.py @@ -12,55 +12,78 @@ import re import subprocess class SpeechRateMenu: - def __init__(self, title="Speech Rate Configuration"): + 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" - # Load current rate from config FIRST - self.load_current_rate() + # Load current settings from config FIRST + self.load_current_settings() - # Initialize speech client AFTER loading the rate + # 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_rate_menu") + self.speechClient = speechd.SSIPClient("speech_config_menu") self.speechClient.set_priority(speechd.Priority.IMPORTANT) self.speechClient.set_punctuation(speechd.PunctuationMode.SOME) - # Apply the loaded rate to the speech client + # 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_rate(self): - """Load the current default rate from speechd.conf""" + def load_current_settings(self): + """Load the current default settings from speechd.conf""" try: with open(self.configFile, 'r') as f: content = f.read() - # First check for uncommented DefaultRate with flexible whitespace + # Load Rate activeMatch = re.search(r'^\s*DefaultRate\s+(-?\d+)', content, re.MULTILINE) if activeMatch: self.currentRate = int(activeMatch.group(1)) else: - # If DefaultRate is commented out, get the value from commented line 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 value 0 + # If loading fails, we'll use default values pass - def save_rate_to_config(self): - """Save the current rate to the speech-dispatcher config file""" + def save_settings_to_config(self): + """Save the current settings to the speech-dispatcher config file""" try: # We need to use sudo to modify the system config file # This assumes the user has sudo privileges or the script is run as root @@ -69,21 +92,53 @@ class SpeechRateMenu: with open(self.configFile, 'r') as f: content = f.read() - # Check if DefaultRate is already uncommented - if re.search(r'^\s*DefaultRate\s+', content, re.MULTILINE): - # Replace the existing DefaultRate line, preserving leading whitespace + newContent = content + + # Handle DefaultRate + if re.search(r'^\s*DefaultRate\s+', newContent, re.MULTILINE): newContent = re.sub( r'^(\s*)DefaultRate\s+(-?\d+)', r'\1DefaultRate ' + str(self.currentRate), - content, + newContent, flags=re.MULTILINE ) else: - # Uncomment and update the DefaultRate line, preserving leading whitespace newContent = re.sub( r'^(\s*)#\s*DefaultRate\s+(-?\d+)', r'\1DefaultRate ' + str(self.currentRate), - content, + newContent, + flags=re.MULTILINE + ) + + # Handle DefaultVolume + if re.search(r'^\s*DefaultVolume\s+', newContent, re.MULTILINE): + newContent = re.sub( + r'^(\s*)DefaultVolume\s+(-?\d+)', + r'\1DefaultVolume ' + str(self.currentVolume), + newContent, + flags=re.MULTILINE + ) + else: + newContent = re.sub( + r'^(\s*)#\s*DefaultVolume\s+(-?\d+)', + r'\1DefaultVolume ' + str(self.currentVolume), + newContent, + flags=re.MULTILINE + ) + + # Handle DefaultPitch + if re.search(r'^\s*DefaultPitch\s+', newContent, re.MULTILINE): + newContent = re.sub( + r'^(\s*)DefaultPitch\s+(-?\d+)', + r'\1DefaultPitch ' + str(self.currentPitch), + newContent, + flags=re.MULTILINE + ) + else: + newContent = re.sub( + r'^(\s*)#\s*DefaultPitch\s+(-?\d+)', + r'\1DefaultPitch ' + str(self.currentPitch), + newContent, flags=re.MULTILINE ) @@ -100,19 +155,50 @@ class SpeechRateMenu: except Exception: return False - def adjust_rate(self, amount): - """Adjust the speech rate by the given amount""" - # Rate should be between -50 and 100 - newRate = max(-50, min(100, self.currentRate + amount)) - - if newRate != self.currentRate: - self.currentRate = newRate - if self.speechClient: - try: - self.speechClient.set_rate(self.currentRate) - self.speak(f"Speech rate {self.currentRate}") - except Exception: - pass + 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""" @@ -156,26 +242,55 @@ class SpeechRateMenu: self.stdscr.addstr(1, x, title, curses.A_BOLD) # Draw help line - helpText = "Up/Down: Adjust Rate | Enter: Save | Q/Esc: Quit" + 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 current rate - rateText = f"Current Rate: {self.currentRate}" - x = max(0, w // 2 - len(rateText) // 2) - self.stdscr.addstr(5, x, rateText, curses.A_REVERSE) + # 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) - # Draw rate visualization + # 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) - # Map rate (-50 to 100) to bar position (0 to barWidth) - rateRange = 150 # Total range (from -50 to 100) - normalizedRate = self.currentRate + 50 # Shift to 0-150 range - position = int((normalizedRate / rateRange) * barWidth) + # 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 = 7 + barY = 9 self.stdscr.addstr(barY, barX, "┌" + "─" * barWidth + "┐") self.stdscr.addstr(barY + 1, barX, "│" + " " * barWidth + "│") self.stdscr.addstr(barY + 2, barX, "└" + "─" * barWidth + "┘") @@ -185,11 +300,12 @@ class SpeechRateMenu: self.stdscr.addstr(barY + 1, barX + 1 + position, "█", curses.A_BOLD) # Add labels for min and max - self.stdscr.addstr(barY + 3, barX, "-50") - self.stdscr.addstr(barY + 3, barX + barWidth - 3, "100") + 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 the rate to system config" + 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) @@ -245,7 +361,8 @@ class SpeechRateMenu: self.draw_menu() # Welcome message - self.speak(f"The current rate for the default voice is {self.currentRate}.") + 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: @@ -256,30 +373,38 @@ class SpeechRateMenu: # Handle navigation if key == curses.KEY_UP: - # Increase rate by 10 - self.adjust_rate(10) + # Increase current value by 10 + self.adjust_current_value(10) self.draw_menu() elif key == curses.KEY_DOWN: - # Decrease rate by 10 - self.adjust_rate(-10) + # 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 the rate - self.speak("Saving speech rate to system configuration.") - success = self.save_rate_to_config() + # Save all settings + self.speak("Saving speech settings to system configuration.") + success = self.save_settings_to_config() if success: - self.speak(f"Speech rate {self.currentRate} has been saved successfully.") + self.speak(f"Speech settings saved successfully. Rate {self.currentRate}, Volume {self.currentVolume}, Pitch {self.currentPitch}.") else: - self.speak("Failed to save speech rate. You may need root privileges.") + self.speak("Failed to save speech settings. You may need root privileges.") # Wait briefly to allow speech to complete before exiting - time.sleep(3) + time.sleep(4) break # Exit the loop after saving elif key == 27 or key == ord('q') or key == ord('Q'): # Esc or Q - self.speak("Exiting speech rate configuration.") + self.speak("Exiting speech configuration.") break except Exception: