Files
gaming-image-files/usr/local/bin/speechd_rate.py
2025-08-07 21:33:11 -04:00

429 lines
16 KiB
Python
Executable File

#!/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
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"
# 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:
# 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
# Create a temporary file with the modified content
with open(self.configFile, 'r') as f:
content = f.read()
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),
newContent,
flags=re.MULTILINE
)
else:
newContent = re.sub(
r'^(\s*)#\s*DefaultRate\s+(-?\d+)',
r'\1DefaultRate ' + str(self.currentRate),
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
)
# Write to a temporary file
tempFile = "/tmp/speechd.conf.new"
with open(tempFile, 'w') as f:
f.write(newContent)
# Use sudo to move the file to the correct location
cmd = f"sudo mv {tempFile} {self.configFile}"
subprocess.run(cmd, shell=True, 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 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.speak(f"Speech settings saved successfully. Rate {self.currentRate}, Volume {self.currentVolume}, Pitch {self.currentPitch}.")
else:
self.speak("Failed to save speech settings. You may need root privileges.")
# Wait briefly to allow speech to complete before exiting
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 configuration.")
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()