Initial pass at getting all the scripts and menus working with the new everything in home setup. Also unify speech settings. Not sure why I thought separate speech settings for all menus was a good idea lol.

This commit is contained in:
Storm Dragon
2026-04-28 18:21:43 -04:00
parent 20811bba1e
commit 1063f21c43
13 changed files with 294 additions and 305 deletions
+12 -42
View File
@@ -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()
+4 -14
View File
@@ -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:
+1 -1
View File
@@ -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
+29 -29
View File
@@ -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)
+16 -15
View File
@@ -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()
+12 -42
View File
@@ -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()
+12 -4
View File
@@ -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
set_timezone
+34 -33
View File
@@ -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
+28 -91
View File
@@ -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)
@@ -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
@@ -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)"
@@ -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
+19 -24
View File
@@ -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():